A starting point could be something like this, where we create a decorator that allows us to preempt any calls to the function and resolve any dependencies.
from typing import Dict, Callable, Any
from functools import wraps
import inspect
# Our decorator which inspects the function and resolves any
# dependencies when called
def resolve_dependencies(func):
# based on https://stackoverflow.com/a/69170441/
f_sig = inspect.signature(func)
@wraps(func)
def resolve_nice_to_have(*args, **kwargs):
bound = f_sig.bind(*args, **kwargs)
bound.apply_defaults()
for k, arg in bound.arguments.items():
if type(arg) == ItWouldBeNice:
bound.arguments[k] = arg()
return func(*bound.args, **bound.kwargs)
return resolve_nice_to_have
# Our actual dependency wrapper, with a simple cache to avoid
# invocating an already resolved dependency.
# Slightly friendlier named than actually depending on something.
class ItWouldBeNice:
cache: Dict[Callable, Any] = {}
def __init__(self, dependency: Callable):
self.dependency = dependency
def __call__(self) -> Any:
if self.dependency in ItWouldBeNice.cache:
return ItWouldBeNice.cache[self.dependency]
result = self.dependency()
ItWouldBeNice.cache[self.dependency] = result
return result
Example of usage:
from iwant import ItWouldBeNice, resolve_dependencies
def late_eval():
print("late was called")
return "static string"
@resolve_dependencies
def i_want_it(s: str = ItWouldBeNice(late_eval)):
print(s)
@resolve_dependencies
def i_want_it_again(s: str = ItWouldBeNice(late_eval)):
print(s)
i_want_it()
i_want_it_again()
This doesn't support hierarchical dependencies etc., but should at least illustrate a concept you could apply to do something similar.