Skip to content

Instantly share code, notes, and snippets.

@XoseLluis
Last active February 1, 2024 23:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save XoseLluis/0c388e8a736ccb723b0d1477ec936ccb to your computer and use it in GitHub Desktop.
Save XoseLluis/0c388e8a736ccb723b0d1477ec936ccb to your computer and use it in GitHub Desktop.
late-bound default parameters for python. see more here: https://deploytonenyures.blogspot.com/2024/02/python-late-bound-default-parameters.html
# 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
from datetime import datetime
from dataclasses import dataclass
from late_bound_default_args import late_bind, LateBound
@late_bind
def say_hi(source: str, target: str, greet: str,
extra = LateBound(lambda: f"[{datetime.now():%Y-%m-%d_%H%M%S}]"),
):
""""""
return f"{greet} from {source} to {target}. {extra}"
@late_bind
def say_hi2(source: str, target: str, greet: str,
extra = LateBound(lambda greet: f"[{greet.upper()}!]"),
):
""""""
return f"{greet} from {source} to {target}. {extra}"
print(say_hi("Xuan", "Francois", "Bonjour"))
print(say_hi2("Xuan", "Francois", "Bonjour"))
# Bonjour from Xuan to Francois. [2024-02-02_002939]
# Bonjour from Xuan to Francois. [BONJOUR!]
print("------------")
@dataclass
class Person:
name: str
birth_place: str
@late_bind
def travel(self, by: str,
start_city: str = LateBound(lambda self: self.birth_place),
to: str = "Paris"
):
""""""
return(f"{self.name} is travelling from {start_city} to {to} by {by}")
p1 = Person("Xuan", "Xixon")
print(p1.travel("train"))
# Xuan is travelling from Xixon to Paris by train
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment