ivankorobkov / python-inject

Python dependency injection
Apache License 2.0
671 stars 77 forks source link

More documentation or examples? #104

Open javierseixas opened 1 month ago

javierseixas commented 1 month ago

Hello! I'm interested in using this library, but it's being difficult to understand how it works, just reading the README file. Is there more information or samples so I can understand better how configure the container.

Thanks.

ivankorobkov commented 1 month ago

Hello.

First, start by configuring an injector. Usually, it is done at the start of your application.

inject.configure(my_config)

Specify optional providers in the config, or let inject automatically construct singletons for you:

def my_config(binder):
  binder.bind(Cache, RedisCache('localhost:1234'))
  binder.bind_to_provider(DB, SQLDatabase)

Use inject to inject instances:

class User:
  db = inject.attr(DB)

  @classmethod
  def get_by_id(cls, id):
    row = cls.db.fetch('users', id)
    return ...

Use other ways to inject dependencies, i.e. inject.autoparams, etc.

--

See something like https://stackoverflow.com/questions/130794/what-is-dependency-injection to better understand concepts.

javierseixas commented 1 month ago

Thanks for your response, it's the same that appears in the README, though :sweat_smile:

Can I build a decorator pattern using inject? This pattern uses the same interface in several classes, so for instance, I have this interface:

from abc import ABC, abstractmethod

class Interface(ABC):
    @abstractmethod
    def say_hello(self):
        pass

This class implementing the previous interface:

import inject

class Decorator(Interface):
    decorated: Decorated = inject.attr(Decorated)

    def say_hello(self):
        print("Hello")
        self.decorated.say_hello()

And this other implementation of the interface:

class Decorated(Interface):
    def say_hello(self):
        print("Hello World!")

Having that, when I set my config like this:

def my_config(binder):
    binder.bind(Interface, Decorator())
    binder.bind(Interface, Decorated())

I get this error: inject.InjectorException: Duplicate binding, key=<class 'interface.Interface'>

What I expect from your examples is that I can define several singletons based on the same Interface: Cache is the interface and RedisCache its implementation.

Anyhow, how I could implement decorator or strategy patterns, where I can have more than one singleton implementing the same interface?

Thanks for your help.

ivankorobkov commented 1 month ago

Anyhow, how I could implement decorator or strategy patterns, where I can have more than one singleton implementing the same interface?

Well, you can't.

The injector gives you the exact error here:

    binder.bind(Interface, Decorator())
    binder.bind(Interface, Decorated())

You can't bind multiple values to the same type.

What instance, Decorator or Decorated, should be injected here?

@inject.param(Interface)
def function(iface):
  pass
javierseixas commented 1 month ago

I see your point. So, answering my initial question: the way of doing this that comes to my mind would be something injecting the configuration, like this:

import inject

from abc import ABC, abstractmethod

class Interface(ABC):
    @abstractmethod
    def say_hello(self):
        pass

class NerdDecorator(Interface):
    def say_hello(self):
        print("Hello World!")

class FrenchDecorator(Interface):
    decorated: Interface = inject.attr("nerd")

    def say_hello(self):
        print("Salut!")
        self.decorated.say_hello()

class EnglishDecorator(Interface):
    decorated: Interface = inject.attr("french")

    def say_hello(self):
        print("Hello")
        self.decorated.say_hello()

class CatalanDecorator(Interface):
    decorated: Interface = inject.attr("english")

    def say_hello(self):
        print("Hola")
        self.decorated.say_hello()

def my_config(binder):
    binder.bind("catalan", CatalanDecorator())
    binder.bind("english", EnglishDecorator())
    binder.bind("french", FrenchDecorator())
    binder.bind("nerd", NerdDecorator())
    pass

inject.configure(my_config)

decorator = CatalanDecorator()
decorator.say_hello()

Resulting on:

Hola
Hello
Salut!
Hello World!

So I'm manually creating the singletons and indicating where to inject them. Is there a better way you may suggest, @ivankorobkov ?

ivankorobkov commented 1 month ago

Yes, you are right.

I would also mention that it's better to use types, not strings. I think it is easier to reason about the application structure as a typed object graph.

Also using types usually allows to omit binding specifications.

class NerdDecorator(Interface):
    def say_hello(self):
        print("Hello World!")

class FrenchDecorator(Interface):
    decorated: Interface = inject.attr(NerdDecorator)
    def say_hello(self):
        print("Salut!")
        self.decorated.say_hello()

class EnglishDecorator(Interface):
    decorated: Interface = inject.attr(FrenchDecorator)
    def say_hello(self):
        print("Hello")
        self.decorated.say_hello()

class CatalanDecorator(Interface):
    decorated: Interface = inject.attr(EnglishDecorator)
    def say_hello(self):
        print("Hola")
        self.decorated.say_hello()

def my_config(binder):
    pass

inject.configure(my_config)

decorator = CatalanDecorator()
decorator.say_hello()

also prints:

Hola
Hello
Salut!
Hello World!
javierseixas commented 3 weeks ago

Thanks for all your explanations. What do you think about including some more samples in the README? Can I propose a PR with that?

ivankorobkov commented 3 weeks ago

Sure, I you make a PR, I will merge it.