ivankorobkov / python-inject

Python dependency injection
Apache License 2.0
694 stars 79 forks source link

Overwrite existing bindings #38

Closed Saabertooth closed 5 years ago

Saabertooth commented 5 years ago

Currently, if you create a binding where a binding already exists for the for the given key, an exception is thrown: inject.InjectorException: Duplicate binding, key=<class 'someValue'>

In many DI frameworks, it's common to allow binding of an existing key to a new value, the behavior for which is to replace the existing binding. For example, Angular uses a hierarchical injector that allows children to override their parent bindings. Autofac uses the last registration/binding for a given key, value.

An example use case:

I have a set of binding configurations that my app uses. I then have a separate test configuration that inherits from my app bindings. If I'm using provider functions, its simple to override them, however if I'm doing simple binding of a Class/value to a value, then I don't have easy way to override the binding.

I realize that there are workarounds for this, but I think this would be simple to implement and provide value to the framework as well as align the duplicate binding behavior to other DI frameworks.

ivankorobkov commented 5 years ago

Hi!

Could you post some code how you intend to override bindings? Thanks.

But for now, I do not think overriding is good. Inject has one global injector. It means that with overriding in a big application it will be very difficult to reason about its state.

Saabertooth commented 5 years ago

Certainly.

class ContainerConfig(object):

    def __init__(self):
        injector = inject.configure_once(self.create_bindings)

    def provide_my_class(self) -> MyClass:
        # Construct and return MyClass instance
    return MyClass()

    def create_bindings(self, binder):
        binder.bind(MyClass, self.provide_my_class())
        binder.bind(MyAbstractClass, MyConcreteClass) # this binding style is difficult to override

class TestContainerConfig(ContainerConfig):

    # overriding provider functions in a derived class works fine
    def provide_my_class(self) -> MyClass:
        # Construct and return MyClassStub instance
        return MyClassStub()

    def create_bindings(self, binder):
        super().create_bindings()
        binder.bind(MyAbstractClass, MyStubClass) # cannot do this, results in error

Now, I realize that I could have simply created a provider function for my example that I could have then overriden in my TestContainerConfig class, it's just additional/unnecessary steps to accomplish the end goal. The goal here is to allow for a single base configuration that can be overridden as necessary.

An alternative would be to allow for runtime bindings or explicit clearing of bindings. I know you're not very keen on runtime bindings either, however, this would allow for individual tests to easily swap a dependency with a stub. Calling clear_and_reconfigure wipes out the entire configuration which is far too tedious to reconstruct in individual tests.

At the end of the day, what I'm searching for is a simple method to allow for swapping of dependencies in my tests without having to rebuild the entire configuration or maintain a separate configuration.

ivankorobkov commented 5 years ago

Sorry for the delay. I'm travelling right now.

I think, no changes are needed for python-inject. You can always implement your own configuration the way you want. For example, you can create a callable class with provider-methods (as you have already mentioned), inherit it and override the methods you need. Or you can group all your providers in a map then iterate over it and call binder.bind(cls, provider). Etc.

However, I think injector should be immutable and simple. Immutability equals thread-safety. Simplicity allows to easily reason about its state.

If you don't mind, please, close the issue.