def to_str(func: ConfigKey): """Get a unique string for each function. Example: >>> to_str(to_str) 'config.config.to_str' >>> import random >>> to_str(random.randint) 'random.Random.randint' """ try: func = _unwrap(func) absolute_filename = Path(inspect.getfile(func)) # NOTE: `relative_filename` is the longest filename relative to `sys.path` paths but # shorter than a absolute filename. relative_filename = None for path in sys.path: try: new_filename = str( absolute_filename.relative_to(Path(path).absolute())) if relative_filename is None: relative_filename = new_filename elif len(new_filename) > len(relative_filename): relative_filename = new_filename except ValueError: pass filename = str(relative_filename if relative_filename is not None else absolute_filename) return filename.replace("/", ".")[:-3] + "." + func.__qualname__ except TypeError: return "#" + func.__qualname__
def partial(func: typing.Callable[..., _PartialReturnType], *args, **kwargs) -> typing.Callable[..., _PartialReturnType]: """Get a `partial` for `func` using the global configuration.""" key = _unwrap(func) if key not in _config: raise KeyError(f"`{key.__qualname__}` has not been configured.") return functools.partial(func, *args, **kwargs, **_config[key])
def add(config: Config, overwrite: bool = False): """Add to the global configuration. Args: config overwrite: Iff `True` then configurations can be overwritten. """ global _config global _code_to_func [_check_args(func, args) for func, args in config.items()] for key_, value in config.items(): key = _unwrap(key_) if not _is_builtin(key): setattr(key, _orginal_key, key_) _get_funcs(key) # NOTE: Check `key` before adding it if key in _config: update = Args({**_config[key], **value}) if not overwrite and len(update) != len(_config[key]) + len(value): for arg, val in _config[key].items(): if not _is_equal(update[arg], val): message = f"Trying to overwrite `{key.__qualname__}#{arg}` configuration." raise ValueError(message) _config[key] = update else: _config[key] = value.copy() _update_trace_globals()
def _get_func_and_arg( arg: typing.Optional[str] = None, func: typing.Optional[ConfigKey] = None, stack: int = 1, ) -> tuple[ConfigKey, str]: """Get the calling function and argument that executes this code to get it's input. NOTE: This may not work with PyTest, learn more: https://github.com/alexmojaki/executing/issues/2 NOTE: Use `executing` until Python 3.11, learn more: https://github.com/alexmojaki/executing/issues/24 https://www.python.org/dev/peps/pep-0657/ """ if func is not None and arg is not None: return func, arg frame = sys._getframe(stack) # NOTE: This was inspired by `executing.Source.__executing_cache`. key = (frame.f_code, id(frame.f_code), frame.f_lasti, func, arg, stack) try: return _get_func_and_arg_cache[key] except KeyError: pass exec_ = executing.Source.executing(frame) if frame.f_code.co_filename == "<stdin>": raise NotImplementedError("REPL is not supported.") assert len(exec_.statements) == 1, "Invariant failure." tree = next(iter(exec_.statements)) parents = _get_child_to_parent_map(tree) if exec_.node is None and "pytest" in sys.modules: raise RuntimeError( "This issue may have been triggered:\n" "https://github.com/alexmojaki/executing/issues/2\n\n" "If so, please don't run this in a PyTest `assert` statement.") parent = parents[exec_.node] if arg is None: if isinstance(parent, ast.keyword): arg = parent.arg parent = parents[parent] if func is None: if not isinstance(parent, ast.Call): raise SyntaxError("Unable to find calling function.") func = _resolve_func(frame, parent.func) if func == functools.partial: if len(parent.args) == 0: raise SyntaxError("Partial doesn't have arguments.") func = _resolve_func(frame, parent.args[0]) func = _unwrap(func) _get_func_and_arg_cache[key] = (func, arg) return func, arg
def _get_funcs(func: typing.Callable) -> typing.List[typing.Callable]: """Get a list of functions `func` may be referring to.""" if inspect.isclass(func): funcs = [] if func.__init__ not in (b.__init__ for b in func.__bases__): funcs.append(func.__init__) if func.__new__ not in (b.__new__ for b in func.__bases__): funcs.append(func.__new__) if len(funcs) == 0: # NOTE: An implicit configuration like this may lead to duplicate configurations. # This happens when the user defines a seperate configuration for a child and parent # object that share the same initiation. Since there is no difference between their # initiation functions, the configuration for them is duplicated. raise KeyError( f"The initiation for `{func}` is only implicitly defined. " "Please use only explicit configurations.") else: funcs = [func] return [_unwrap(f) for f in funcs]