Source code for fiddle._src.mutate_buildable

# coding=utf-8
# Copyright 2022 The Fiddle-Config Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Library for mutating Buildable instances."""

from typing import Callable, Type, TypeVar, Union

from fiddle._src import config as config_lib
from fiddle._src import signatures


T = TypeVar('T')
TypeOrCallableProducingT = Union[Callable[..., T], Type[T]]


_buildable_internals_keys = (
    '__fn_or_cls__',
    '__arguments__',
    '__argument_tags__',
    '__argument_history__',
    '__signature_info__',
)


def move_buildable_internals(
    *, source: config_lib.Buildable, destination: config_lib.Buildable
):
  """Changes the internals of `destination` to be equivalent to `source`.

  Currently, this results in aliasing behavior, but this should not be relied
  upon. All the internals of `source` are aliased into `destination`.

  Args:
    source: The source configuration to pull internals from.
    destination: One of the buildables to swap.
  """
  if type(source) is not type(destination):
    # TODO(saeta): Relax this constraint such that both types merely need to be
    # buildable's.
    raise TypeError(f'types must match exactly: {type(source)} vs '
                    f'{type(destination)}.')

  # Update in-place using object.__setattr__ to bypass argument checking.
  for attr in _buildable_internals_keys:
    object.__setattr__(destination, attr, getattr(source, attr))


[docs] def update_callable( buildable: config_lib.Buildable, new_callable: TypeOrCallableProducingT, drop_invalid_args: bool = False, ) -> None: """Updates ``config`` to build ``new_callable`` instead. When extending a base configuration, it can often be useful to swap one class for another. For example, an experiment may want to swap in a subclass that has augmented functionality. ``update_callable`` updates ``config`` in-place (preserving argument history). Args: buildable: A ``Buildable`` (e.g. a ``fdl.Config``) to mutate. new_callable: The new callable ``config`` should call when built. drop_invalid_args: If True, arguments that don't exist in the new callable will be removed from buildable. If False, raise an exception for such arguments. Raises: TypeError: if ``new_callable`` has varargs, or if there are arguments set on ``config`` that are invalid to pass to ``new_callable``. """ for key in buildable.__arguments__.keys(): if isinstance(key, int): raise NotImplementedError( 'Update callable for configs with positional arguments is not' ' supported yet.' ) # Note: can't just call config.__init__(new_callable, **config.__arguments__) # to preserve history. # # Note: can't call `setattr` on all the args to validate them, because that # will result in duplicate history entries. new_signature = signatures.get_signature(new_callable) # Update the signature early so that we can set arguments by position. # Otherwise, parameter validation logic would complain about argument # name not exists. new_signature_info = signatures.SignatureInfo(signature=new_signature) object.__setattr__(buildable, '__signature__', new_signature) object.__setattr__(buildable, '__signature_info__', new_signature_info) if not new_signature_info.has_var_keyword: invalid_args = [ arg for arg in buildable.__arguments__.keys() if arg not in new_signature.parameters and isinstance(arg, str) ] if invalid_args: if drop_invalid_args: for arg in invalid_args: delattr(buildable, arg) else: raise TypeError( f'Cannot switch to {new_callable} (from ' f'{buildable.__fn_or_cls__}) because the Buildable would ' f'have invalid arguments {invalid_args}.' ) object.__setattr__(buildable, '__fn_or_cls__', new_callable) buildable.__argument_history__.add_new_value('__fn_or_cls__', new_callable)
[docs] def assign(buildable: config_lib.Buildable, /, **kwargs): """Assigns multiple arguments to ``buildable``. Although this function does not enable a caller to do something they can't already do with other syntax, this helper function can be useful when manipulating deeply nested configs. Example:: cfg = # ... fdl.assign(cfg.my.deeply.nested.child.object, arg_a=1, arg_b='b') The above code snippet is equivalent to:: cfg = # ... cfg.my.deeply.nested.child.object.arg_a = 1 cfg.my.deeply.nested.child.object.arg_b = 'b' Args: buildable: A ``Buildable`` (e.g. a ``fdl.Config``) to set values upon. **kwargs: The arguments and values to assign. """ for name, value in kwargs.items(): setattr(buildable, name, value)