# inspired by: # https://github.com/neogeny/late/blob/master/late/__init__.py # and # href="https://peps.python.org/pep-0671/ from dataclasses import dataclass from typing import Callable, Any import functools import inspect @dataclass class LateBound: resolver: Callable def _invoke_late_bound(callable: Callable, arg_name_to_value: dict[str, Any]) -> Any: """ invokes a callable passing over to it the parameters defined in its signature we obtain those values from the arg_name_to_value dictionary """ expected_params = inspect.signature(callable).parameters.keys() kwargs = {name: arg_name_to_value[name] for name in expected_params } return callable(**kwargs) def _add_late_bounds(arg_name_to_value: dict[str, Any], late_bounds: list[str, Callable]): """resolves late-bound values and adds them to the arg_name_to_value dictionary""" for name, callable in late_bounds: val = _invoke_late_bound(callable, arg_name_to_value) #this way one late bound can depend on a previous late boud arg_name_to_value[name] = val def _resolve_args(target_fn: Callable, *args, **kwargs) -> dict[str, Any]: """returns a dictionary with the name and value all the parameters (the ones already provided, the calculated latebounds and the normal defaults)""" # dictionary of the arguments and values received by the function at runtime # we use it to be able to calculate late_bound values based on other parameters arg_name_to_value: dict[str, Any] = {} arg_names = list(inspect.signature(target_fn).parameters.keys()) for index, arg in enumerate(args): arg_name_to_value[arg_names[index]] = arg arg_name_to_value = {**arg_name_to_value, **kwargs} # obtain the values for all default parameters that have not been provided # we obtain them all here so that late_bounds can depend on other (compile-time or late-bound) default parameters #late bounds to calculate (were not provided in args-kwargs) not_late_bounds = {name: param.default for name, param in inspect.signature(target_fn).parameters.items() if not isinstance(param.default, LateBound) and not name in arg_name_to_value } arg_name_to_value = {**arg_name_to_value, **not_late_bounds} # list rather than dictionary as order matters (so that a late-bound can depend on a previous late-bound) late_bounds = [(name, param.default.resolver) for name, param in inspect.signature(target_fn).parameters.items() if isinstance(param.default, LateBound) and not name in arg_name_to_value ] _add_late_bounds(arg_name_to_value, late_bounds) return arg_name_to_value #decorator function def late_bind(target_fn: Callable | type) -> Callable | type: """decorates a function enabling late-binding of default parameters for it""" @functools.wraps(target_fn) def wrapper(*args, **kwargs): kwargs = _resolve_args(target_fn, *args, **kwargs) return target_fn(**kwargs) return wrapper