Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update andi.plan to support a function/class signature in dict #23

Open
BurnzZ opened this issue Nov 25, 2022 · 3 comments
Open

Update andi.plan to support a function/class signature in dict #23

BurnzZ opened this issue Nov 25, 2022 · 3 comments

Comments

@BurnzZ
Copy link
Member

BurnzZ commented Nov 25, 2022

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.

@BurnzZ BurnzZ changed the title Creating an equivalent to andi.plan which supports dict Update andi.plan to support a function/class signature in dict Nov 25, 2022
@BurnzZ
Copy link
Member Author

BurnzZ commented Nov 29, 2022

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})]

@BurnzZ
Copy link
Member Author

BurnzZ commented Nov 30, 2022

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})]

@kmike
Copy link
Member

kmike commented Jan 9, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants