Finistere / antidote

Dependency injection for Python
MIT License
90 stars 9 forks source link

Multiple dependencies #19

Closed bernoussim closed 4 years ago

bernoussim commented 4 years ago

Hello,

I am opening an issue but it is actually more a support request. I would like to know how to implement multiple dependencies. I tried what was specified in the documentation but I still can't understand how to change a state via the enum. thanks in advance

Finistere commented 4 years ago

Hello,

Feel free to open as many issues for questions as you need ! :) If I understood you correctly, you're talking about this part: How to / use-interfaces. For the context :

from antidote import register, implements, world
from enum import Flag, auto

class Profile(Flag):
    POSTGRES = auto()
    MYSQL = auto()

class Database:
    pass

@implements(Database, state=Profile.POSTGRES)
@register
class PostgresDB(Database):
    pass

@implements(Database, state=Profile.MYSQL)
@register
class MySQLDB(Database):
    pass

world.update_singletons({Profile: Profile.POSTGRES})
world.get(Database)  # Postgres

State in this example is a singleton, so it's meant to be used as a constant switch which would typically depend on the environment. For example being deployed on GCP or AWS, or a change of environment during a migration... Changing the state afterwards leads to undefined behavior in this case as Antidote caches singletons. So what you would get as the Database implementation would depend on what has been done before the change.

However, currently it doesn't need to be a singleton. Instead of defining it with:

from antidote import world
world.update_singletons({Profile: Profile.POSTGRES})

You could use a factory which returns the current state:

from antidote import factory
horrible_global_state = Profile.POSTGRES

@factory(singleton=False)
def get_global_profile() -> Profile:
    return horrible_global_state

world.get(Database) # Postgres

horrible_global_state = Profile.MYSQL

world.get(Database) # MYSQL

Note that it's @register that defines whether the PostgresDB and MySQLDB are singletons or not. Now whether being able to change dynamically the state is a good idea or not, I'm not sure. :)

To be honest this is a feature, the state switch, I'm not entirely satisfied with. I imitated it from a smaller dependency injection library (don't remember its name) because I found it to be an interesting concept. But I didn't have a concrete use case in mind, contrary to most of the library. So I would love to hear about your use case to understand how I could improve it !

bernoussim commented 4 years ago

thank you @Finistere , using the factory worked. And thanks for the explanation, I understand better for which purpose this feature was designed. My use case is the following:

Finistere commented 4 years ago

Does your device vendor really change at runtime ? If not I would define the state during initialization.

If it does, or if you want more flexibility, using a @factory would be a good option. If a function is not enough, you can decorate a class instead. __call__() will be used to generate the dependency. This allows you to keep some state within the factory. Injections will be done on __init__() and __call__() by default. Weirdly I don't document this anymore in the tutorial/how to.

I'll give you an example and update the documentation as soon as possible. :)

On Thu, Aug 20, 2020, 16:20 bernoussim notifications@github.com wrote:

thank you @Finistere https://github.com/Finistere , using the factory worked. And thanks for the explanation, I understand better for which purpose this feature was designed. My use case is the following:

  • I have a service layer that interacts with network devices orchestrators, depending on the device vendor, I would either call the API for vendor A or vendor B. Therefore I wanted to have two implementation, that would be called based on the device vendor. But from your explanation, not sure anymore if switching dynamically between implementations is a good thing. Appreciate your feedback.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Finistere/antidote/issues/19#issuecomment-677695387, or unsubscribe https://github.com/notifications/unsubscribe-auth/AB4E47JBO4AO2G5PT2Z3PXDSBUWJ5ANCNFSM4QFMDJYA .

bernoussim commented 4 years ago

unfortunately, it does change at runtime, this service will be exposed to users that don't need to know if the network device is from vendor A or vendor B. they will provide inputs that could be sitting within devices belonging to vendor A or vendor B. I have another service on top that first define where the input is sitting. I wanted to inject different services depending on where the input is sitting. Hope it's clear. Looking forward for the example. Thanks!

Finistere commented 4 years ago

Here is the stateful factory documentation: https://antidote.readthedocs.io/en/latest/how_to.html#create-a-stateful-factory

In your case you could do something like:

from antidote import factory

class Vendor:
    pass

@factory(singleton=False)  # singleton applies for the dependency provided by the factory, Vendor
class VendorFactory:
    def __call__(self) -> Vendor:
        pass  # return current vendor

Now there are two ways to handle the vendors themselves. Either you're handling everything yourself within the factory and cache them if necessary. Or you can rely on Antidote to instantiate the vendors. For example you could do the following:

from antidote import register, world

@register
class VendorA(Vendor):
    pass

@register
class VendorB(Vendor):
    pass

@factory(singleton=False)
class VendorFactory:
    def __init__(self, vendorA: VendorA):
        self._vendorA = vendorA

    def __call__(self) -> Vendor:
        if "vendorA":
            return self._vendorA
        else if "vendorB":
            return world.get(VendorB)  # retrieved dynamically only if necessary

Does this solve your need ?

bernoussim commented 4 years ago

thank you! closing it.

Finistere commented 3 years ago

Hello,

Just wanted to say that I added a cleaner way to do this with the v0.8:

from antidote import implementation

# permanent meaning whether the implementation is chosen permanently or not.
@implementation(Vendor, permanent=False)
def choose_vendor():
    """ Expected to return a dependency """
    if "vendorA":
        return VendorA
    else:
        return VendorB

It should be cleaner that the previous @implements.