Update `andi.plan` to support a function/class signature in `dict`
BurnzZ opened this issue · 3 comments
Currently, andi.plan()
works by passing a function or class. This requires them to have their signatures already established:
class Valves:
pass
class Engine:
def __init__(self, valves: Valves):
self.valves = valves
class Wheels:
pass
class Car:
def __init__(self, engine: Engine, wheels: Wheels):
self.engine = engine
self.wheels = wheels
However, there are use cases where we might want to create different types of cars which means that the car signatures could be dynamic which is only determined during runtime.
For example, during runtime the car's wheels could be electric, or the wheels could become tank treads. We could solve this by defining an ElectricCar
or TankCar
. Yet, this solution doesn't cover all the other permutation of car types, especially when its signature changes if it adds an arbitrary amount of attachments:
engine: Engine, wheels: Wheels, top_carrier: RoofCarrier
engine: Engine, wheels: Wheels, top_carrier: BikeRack
engine: Engine, wheels: Wheels, back_carrier: BikeRack, top_carrier: RoofCarrier
We could list down all of the possible dependencies in the signature but that wouldn't be efficient since it takes some effort to fulfill all of them but at the end, only a handful of them will be used.
I'm proposing to update the API to allow such arbitrary signatures to used. This allows something like this to be possible:
def get_blueprint(customer_request):
results: Dict[str, Any] = {}
for i, dependency in enumerate(read_request(customer_request)):
results[f"arg_{i}"] = results
return results # something like {"arg_1": Engine, "arg_2": Wheels, "arg_3": BikeRack}
signature = get_blueprint(customer_request)
plan = andi.plan(
signature,
is_injectable=is_injectable,
externally_provided=externally_provided,
)
andi.plan()
largely remains the same except that it now supports a mapping representing any arbitrary function/class signature.
I think another way to solve this problem without modifying andi would be something like:
import attrs
import andi
class BaseDep:
pass
@attrs.define
class Foo:
dep: BaseDep
@attrs.define
class Bar:
dep: BaseDep
and then calling andi.plan()
for each dependency:
>>> is_injectable = lambda x: True
>>> andi.plan(Foo, is_injectable=is_injectable)
[(__main__.BaseDep, {}), (__main__.Foo, {'dep': __main__.BaseDep})]
>>> andi.plan(Bar, is_injectable=is_injectable)
[(__main__.BaseDep, {}), (__main__.Bar, {'dep': __main__.BaseDep})]
But we have to be careful to deduplicate any base dependencies that are shared.
Having a modified andi.plan()
would still be great since it could easily render:
[(__main__.BaseDep, {}),
(__main__.Foo, {'dep': __main__.BaseDep}),
(__main__.Bar, {'dep': __main__.BaseDep}),
(<function __main__.combine(foo: __main__.Foo, bar: __main__.Bar)>,
{'foo': __main__.Foo, 'bar': __main__.Bar})]
I found a way to prevent modifying andi but still create dynamic class signature using dataclasses.make_dataclass. 🎉
import andi
import attrs
from dataclasses import make_dataclass
class BaseDep:
pass
@attrs.define
class Foo:
dep: BaseDep
@attrs.define
class Bar:
dep: BaseDep
Tmp = make_dataclass("Tmp", [('f', Foo), ('b', Bar)])
is_injectable = lambda x: True
andi.plan(Tmp, is_injectable=is_injectable)
#[(__main__.BaseDep, {}),
# (__main__.Foo, {'dep': __main__.BaseDep}),
# (__main__.Bar, {'dep': __main__.BaseDep}),
# (types.Tmp, {'f': __main__.Foo, 'b': __main__.Bar})]
Allowing andi to accept {"arg_1": Engine, "arg_2": Wheels, "arg_3": BikeRack}
dicts instead of callables or classes makes sense to me; +1 to implement this.
I'd probably avoid calling it "signature" or "blueprint", because it enables other possibilities, not tied to signatures. Just an API to create a plan on how to create dependencies, which you can use anywhere.