gin/config.py
def _make_configurable(fn_or_cls,
                       name=None,
                       module=None,
                       whitelist=None,
                       blacklist=None,
                       subclass=False):
  """Wraps `fn_or_cls` to make it configurable.

  Infers the configurable name from `fn_or_cls.__name__` if necessary, and
  updates global state to keep track of configurable name <-> function
  mappings, as well as whitelisted and blacklisted parameters.

  Args:
    fn_or_cls: The function or class to decorate.
    name: A name for the configurable. If `None`, the name will be inferred from
      from `fn_or_cls`. The `name` may also include module components to be used
      for disambiguation (these will be appended to any components explicitly
      specified by `module`).
    module: The module to associate with the configurable, to help handle naming
      collisions. If `None`, `fn_or_cls.__module__` will be used (if no module
      is specified as part of `name`).
    whitelist: A whitelisted set of parameter names to supply values for.
    blacklist: A blacklisted set of parameter names not to supply values for.
    subclass: If `fn_or_cls` is a class and `subclass` is `True`, decorate by
      subclassing `fn_or_cls` and overriding its `__init__` method. If `False`,
      replace the existing `__init__` with a decorated version.

  Returns:
    A wrapped version of `fn_or_cls` that will take parameter values from the
    global configuration.

  Raises:
    RuntimeError: If the config is locked.
    ValueError: If a configurable with `name` (or the name of `fn_or_cls`)
      already exists, or if both a whitelist and blacklist are specified.
  """
  if config_is_locked():
    err_str = 'Attempted to add a new configurable after the config was locked.'
    raise RuntimeError(err_str)

  name = fn_or_cls.__name__ if name is None else name
  if config_parser.IDENTIFIER_RE.match(name):
    default_module = getattr(fn_or_cls, '__module__', None)
    module = default_module if module is None else module
  elif not config_parser.MODULE_RE.match(name):
    raise ValueError("Configurable name '{}' is invalid.".format(name))

  if module is not None and not config_parser.MODULE_RE.match(module):
    raise ValueError("Module '{}' is invalid.".format(module))

  selector = module + '.' + name if module else name
  if not _INTERACTIVE_MODE and selector in _REGISTRY:
    err_str = "A configurable matching '{}' already exists."
    raise ValueError(err_str.format(selector))

  if whitelist and blacklist:
    err_str = 'A whitelist or a blacklist can be specified, but not both.'
    raise ValueError(err_str)

  if whitelist and not isinstance(whitelist, (list, tuple)):
    raise TypeError('Whitelist should be a list or tuple.')

  if blacklist and not isinstance(blacklist, (list, tuple)):
    raise TypeError('Blacklist should be a list or tuple.')

  _validate_parameters(fn_or_cls, whitelist, 'whitelist')
  _validate_parameters(fn_or_cls, blacklist, 'blacklist')

  def apply_config(fn):
    """Wraps `fn` so that it obtains parameters from the configuration."""

    @six.wraps(fn)
    def wrapper(*args, **kwargs):
      """Supplies fn with parameter values from the configuration."""
      scope_components = current_scope()
      new_kwargs = {}
      for i in range(len(scope_components) + 1):
        partial_scope_str = '/'.join(scope_components[:i])
        new_kwargs.update(_CONFIG.get((partial_scope_str, selector), {}))
      gin_bound_args = list(new_kwargs.keys())
      scope_str = partial_scope_str

      arg_names = _get_supplied_positional_parameter_names(fn, args)

      for arg in args[len(arg_names):]:
        if arg is REQUIRED:
          raise ValueError(
              'gin.REQUIRED is not allowed for unnamed (vararg) parameters. If '
              'the function being called is wrapped by a non-Gin decorator, '
              'try explicitly providing argument names for positional '
              'parameters.')

      required_arg_names = []
      required_arg_indexes = []
      for i, arg in enumerate(args[:len(arg_names)]):
        if arg is REQUIRED:
          required_arg_names.append(arg_names[i])
          required_arg_indexes.append(i)

      required_kwargs = []
      for kwarg, value in six.iteritems(kwargs):
        if value is REQUIRED:
          required_kwargs.append(kwarg)

      # If the caller passed arguments as positional arguments that correspond
      # to a keyword arg in new_kwargs, remove the keyword argument from
      # new_kwargs to let the caller win and avoid throwing an error. Unless it
      # is an arg marked as REQUIRED.
      for arg_name in arg_names:
        if arg_name not in required_arg_names:
          new_kwargs.pop(arg_name, None)

      # Get default values for configurable parameters.
      operative_parameter_values = _get_default_configurable_parameter_values(
          fn, whitelist, blacklist)
      # Update with the values supplied via configuration.
      operative_parameter_values.update(new_kwargs)

      # Remove any values from the operative config that are overridden by the
      # caller. These can't be configured, so they won't be logged. We skip
      # values that are marked as REQUIRED.
      for k in arg_names:
        if k not in required_arg_names:
          operative_parameter_values.pop(k, None)
      for k in kwargs:
        if k not in required_kwargs:
          operative_parameter_values.pop(k, None)

      # An update is performed in case another caller of this same configurable
      # object has supplied a different set of arguments. By doing an update, a
      # Gin-supplied or default value will be present if it was used (not
      # overridden by the caller) at least once.
      _OPERATIVE_CONFIG.setdefault((scope_str, selector), {}).update(
          operative_parameter_values)

      # We call deepcopy for two reasons: First, to prevent the called function
      # from modifying any of the values in `_CONFIG` through references passed
      # in via `new_kwargs`; Second, to facilitate evaluation of any
      # `ConfigurableReference` instances buried somewhere inside
      # `new_kwargs`. See the docstring on `ConfigurableReference.__deepcopy__`
      # above for more details on the dark magic happening here.
      new_kwargs = copy.deepcopy(new_kwargs)

      # Validate args marked as REQUIRED have been bound in the Gin config.
      missing_required_params = []
      new_args = list(args)
      for i, arg_name in zip(required_arg_indexes, required_arg_names):
        if arg_name not in new_kwargs:
          missing_required_params.append(arg_name)
        else:
          new_args[i] = new_kwargs.pop(arg_name)

      # Validate kwargs marked as REQUIRED have been bound in the Gin config.
      for kwarg in required_kwargs:
        if kwarg not in new_kwargs:
          missing_required_params.append(kwarg)
        else:
          # Remove from kwargs and let the new_kwargs value be used.
          kwargs.pop(kwarg)

      if missing_required_params:
        err_str = 'Required bindings for `{}` not provided in config: {}'
        minimal_selector = _REGISTRY.minimal_selector(selector)
        err_str = err_str.format(minimal_selector, missing_required_params)
        raise RuntimeError(err_str)

      # Now, update with the caller-supplied `kwargs`, allowing the caller to
      # have the final say on keyword argument values.
      new_kwargs.update(kwargs)

      try:
        return fn(*new_args, **new_kwargs)
      except Exception as e:  # pylint: disable=broad-except
        err_str = ''
        if isinstance(e, TypeError):
          all_arg_names = _get_all_positional_parameter_names(fn)
          if len(new_args) < len(all_arg_names):
            unbound_positional_args = list(
                set(all_arg_names[len(new_args):]) - set(new_kwargs))
            if unbound_positional_args:
              caller_supplied_args = list(
                  set(arg_names + list(kwargs)) -
                  set(required_arg_names + list(required_kwargs)))
              fmt = ('\n  No values supplied by Gin or caller for arguments: {}'
                     '\n  Gin had values bound for: {gin_bound_args}'
                     '\n  Caller supplied values for: {caller_supplied_args}')
              canonicalize = lambda x: list(map(str, sorted(x)))
              err_str += fmt.format(
                  canonicalize(unbound_positional_args),
                  gin_bound_args=canonicalize(gin_bound_args),
                  caller_supplied_args=canonicalize(caller_supplied_args))
        err_str += "\n  In call to configurable '{}' ({}){}"
        scope_info = " in scope '{}'".format(scope_str) if scope_str else ''
        err_str = err_str.format(name, fn, scope_info)
        utils.augment_exception_message_and_reraise(e, err_str)

    return wrapper

  decorated_fn_or_cls = _decorate_fn_or_cls(
      apply_config, fn_or_cls, subclass=subclass)

  _REGISTRY[selector] = Configurable(
      decorated_fn_or_cls,
      name=name,
      module=module,
      whitelist=whitelist,
      blacklist=blacklist,
      selector=selector)
  return decorated_fn_or_cls
def _make_configurable(fn_or_cls,
                       name=None,
                       module=None,
                       whitelist=None,
                       blacklist=None,
                       subclass=False):
  """Wraps `fn_or_cls` to make it configurable.

  Infers the configurable name from `fn_or_cls.__name__` if necessary, and
  updates global state to keep track of configurable name <-> function
  mappings, as well as whitelisted and blacklisted parameters.

  Args:
    fn_or_cls: The function or class to decorate.
    name: A name for the configurable. If `None`, the name will be inferred from
      from `fn_or_cls`. The `name` may also include module components to be used
      for disambiguation (these will be appended to any components explicitly
      specified by `module`).
    module: The module to associate with the configurable, to help handle naming
      collisions. If `None`, `fn_or_cls.__module__` will be used (if no module
      is specified as part of `name`).
    whitelist: A whitelisted set of parameter names to supply values for.
    blacklist: A blacklisted set of parameter names not to supply values for.
    subclass: If `fn_or_cls` is a class and `subclass` is `True`, decorate by
      subclassing `fn_or_cls` and overriding its `__init__` method. If `False`,
      replace the existing `__init__` with a decorated version.

  Returns:
    A wrapped version of `fn_or_cls` that will take parameter values from the
    global configuration.

  Raises:
    RuntimeError: If the config is locked.
    ValueError: If a configurable with `name` (or the name of `fn_or_cls`)
      already exists, or if both a whitelist and blacklist are specified.
  """
  if config_is_locked():
    err_str = 'Attempted to add a new configurable after the config was locked.'
    raise RuntimeError(err_str)

  name = fn_or_cls.__name__ if name is None else name
  if config_parser.IDENTIFIER_RE.match(name):
    default_module = getattr(fn_or_cls, '__module__', None)
    module = default_module if module is None else module
  elif not config_parser.MODULE_RE.match(name):
    raise ValueError("Configurable name '{}' is invalid.".format(name))

  if module is not None and not config_parser.MODULE_RE.match(module):
    raise ValueError("Module '{}' is invalid.".format(module))

  selector = module + '.' + name if module else name
  if not _INTERACTIVE_MODE and selector in _REGISTRY:
    err_str = "A configurable matching '{}' already exists."
    raise ValueError(err_str.format(selector))

  if whitelist and blacklist:
    err_str = 'A whitelist or a blacklist can be specified, but not both.'
    raise ValueError(err_str)

  if whitelist and not isinstance(whitelist, (list, tuple)):
    raise TypeError('Whitelist should be a list or tuple.')

  if blacklist and not isinstance(blacklist, (list, tuple)):
    raise TypeError('Blacklist should be a list or tuple.')

  _validate_parameters(fn_or_cls, whitelist, 'whitelist')
  _validate_parameters(fn_or_cls, blacklist, 'blacklist')

  def apply_config(fn):
    """Wraps `fn` so that it obtains parameters from the configuration."""

    @six.wraps(fn)
    def wrapper(*args, **kwargs):
      """Supplies fn with parameter values from the configuration."""
      scope_components = current_scope()
      new_kwargs = {}
      for i in range(len(scope_components) + 1):
        partial_scope_str = '/'.join(scope_components[:i])
        new_kwargs.update(_CONFIG.get((partial_scope_str, selector), {}))
      gin_bound_args = list(new_kwargs.keys())
      scope_str = partial_scope_str

      arg_names = _get_supplied_positional_parameter_names(fn, args)

      for arg in args[len(arg_names):]:
        if arg is REQUIRED:
          raise ValueError(
              'gin.REQUIRED is not allowed for unnamed (vararg) parameters. If '
              'the function being called is wrapped by a non-Gin decorator, '
              'try explicitly providing argument names for positional '
              'parameters.')

      required_arg_names = []
      required_arg_indexes = []
      for i, arg in enumerate(args[:len(arg_names)]):
        if arg is REQUIRED:
          required_arg_names.append(arg_names[i])
          required_arg_indexes.append(i)

      required_kwargs = []
      for kwarg, value in six.iteritems(kwargs):
        if value is REQUIRED:
          required_kwargs.append(kwarg)

      # If the caller passed arguments as positional arguments that correspond
      # to a keyword arg in new_kwargs, remove the keyword argument from
      # new_kwargs to let the caller win and avoid throwing an error. Unless it
      # is an arg marked as REQUIRED.
      for arg_name in arg_names:
        if arg_name not in required_arg_names:
          new_kwargs.pop(arg_name, None)

      # Get default values for configurable parameters.
      operative_parameter_values = _get_default_configurable_parameter_values(
          fn, whitelist, blacklist)
      # Update with the values supplied via configuration.
      operative_parameter_values.update(new_kwargs)

      # Remove any values from the operative config that are overridden by the
      # caller. These can't be configured, so they won't be logged. We skip
      # values that are marked as REQUIRED.
      for k in arg_names:
        if k not in required_arg_names:
          operative_parameter_values.pop(k, None)
      for k in kwargs:
        if k not in required_kwargs:
          operative_parameter_values.pop(k, None)

      # An update is performed in case another caller of this same configurable
      # object has supplied a different set of arguments. By doing an update, a
      # Gin-supplied or default value will be present if it was used (not
      # overridden by the caller) at least once.
      _OPERATIVE_CONFIG.setdefault((scope_str, selector), {}).update(
          operative_parameter_values)

      # We call deepcopy for two reasons: First, to prevent the called function
      # from modifying any of the values in `_CONFIG` through references passed
      # in via `new_kwargs`; Second, to facilitate evaluation of any
      # `ConfigurableReference` instances buried somewhere inside
      # `new_kwargs`. See the docstring on `ConfigurableReference.__deepcopy__`
      # above for more details on the dark magic happening here.
      new_kwargs = copy.deepcopy(new_kwargs)

      # Validate args marked as REQUIRED have been bound in the Gin config.
      missing_required_params = []
      new_args = list(args)
      for i, arg_name in zip(required_arg_indexes, required_arg_names):
        if arg_name not in new_kwargs:
          missing_required_params.append(arg_name)
        else:
          new_args[i] = new_kwargs.pop(arg_name)

      # Validate kwargs marked as REQUIRED have been bound in the Gin config.
      for kwarg in required_kwargs:
        if kwarg not in new_kwargs:
          missing_required_params.append(kwarg)
        else:
          # Remove from kwargs and let the new_kwargs value be used.
          kwargs.pop(kwarg)

      if missing_required_params:
        err_str = 'Required bindings for `{}` not provided in config: {}'
        minimal_selector = _REGISTRY.minimal_selector(selector)
        err_str = err_str.format(minimal_selector, missing_required_params)
        raise RuntimeError(err_str)

      # Now, update with the caller-supplied `kwargs`, allowing the caller to
      # have the final say on keyword argument values.
      new_kwargs.update(kwargs)

      try:
        return fn(*new_args, **new_kwargs)
      except Exception as e:  # pylint: disable=broad-except
        err_str = ''
        if isinstance(e, TypeError):
          all_arg_names = _get_all_positional_parameter_names(fn)
          if len(new_args) < len(all_arg_names):
            unbound_positional_args = list(
                set(all_arg_names[len(new_args):]) - set(new_kwargs))
            if unbound_positional_args:
              caller_supplied_args = list(
                  set(arg_names + list(kwargs)) -
                  set(required_arg_names + list(required_kwargs)))
              fmt = ('\n  No values supplied by Gin or caller for arguments: {}'
                     '\n  Gin had values bound for: {gin_bound_args}'
                     '\n  Caller supplied values for: {caller_supplied_args}')
              canonicalize = lambda x: list(map(str, sorted(x)))
              err_str += fmt.format(
                  canonicalize(unbound_positional_args),
                  gin_bound_args=canonicalize(gin_bound_args),
                  caller_supplied_args=canonicalize(caller_supplied_args))
        err_str += "\n  In call to configurable '{}' ({}){}"
        scope_info = " in scope '{}'".format(scope_str) if scope_str else ''
        err_str = err_str.format(name, fn, scope_info)
        utils.augment_exception_message_and_reraise(e, err_str)

    return wrapper

  decorated_fn_or_cls = _decorate_fn_or_cls(
      apply_config, fn_or_cls, subclass=subclass)

  _REGISTRY[selector] = Configurable(
      decorated_fn_or_cls,
      name=name,
      module=module,
      whitelist=whitelist,
      blacklist=blacklist,
      selector=selector)
  return decorated_fn_or_cls
Wraps `fn_or_cls` to make it configurable.

  Infers the configurable name from `fn_or_cls.__name__` if necessary, and
  updates global state to keep track of configurable name <-> function
  mappings, as well as whitelisted and blacklisted parameters.

  Args:
    fn_or_cls: The function or class to decorate.
    name: A name for the configurable. If `None`, the name will be inferred from
      from `fn_or_cls`. The `name` may also include module components to be used
      for disambiguation (these will be appended to any components explicitly
      specified by `module`).
    module: The module to associate with the configurable, to help handle naming
      collisions. If `None`, `fn_or_cls.__module__` will be used (if no module
      is specified as part of `name`).
    whitelist: A whitelisted set of parameter names to supply values for.
    blacklist: A blacklisted set of parameter names not to supply values for.
    subclass: If `fn_or_cls` is a class and `subclass` is `True`, decorate by
      subclassing `fn_or_cls` and overriding its `__init__` method. If `False`,
      replace the existing `__init__` with a decorated version.

  Returns:
    A wrapped version of `fn_or_cls` that will take parameter values from the
    global configuration.

  Raises:
    RuntimeError: If the config is locked.
    ValueError: If a configurable with `name` (or the name of `fn_or_cls`)
      already exists, or if both a whitelist and blacklist are specified.