Neoteroi / rodi

Implementation of dependency injection for Python 3
MIT License
174 stars 17 forks source link

Question about how rodi handles __init__ #43

Open lucas-labs opened 10 months ago

lucas-labs commented 10 months ago

Hi!

I was working on a project using rodi and I noticed that when a dependency that depends on another dependency has an __init__, it fails to resolve. Ok, I'm not good explaing things with human words, so as Linus said... talk is cheap, show me the code:

This works just fine:

class Foo:
    def bar(self) -> str:
        return 'bar'

class Baz:
    foo: Foo

    def call_foo(self) -> str:
        return self.foo.bar()

container = Container()
container.register(Foo)
container.register(Baz)
baz = container.resolve(Baz)

# this one works
print(baz.call_foo()) # >>> bar 

But if we add a init, it gets angry

class Foo:
    def bar(self) -> str:
        return 'bar'

class Baz:
    foo: Foo

+    def __init__(self) -> None:
+        pass

    def call_foo(self) -> str:
        return self.foo.bar()

container = Container()
container.register(Foo)
container.register(Baz)
baz = container.resolve(Baz)

# 💥
print(baz.call_foo())

# Traceback (most recent call last):
#   File "whatever\test.py", blahblah
#     return self.foo.bar()
#            ^^^^^^^^
# AttributeError: 'Baz' object has no attribute 'foo'

And this works too, but I love the idea of not having to use an __init__ if I don't need it (I know there's a whole activist movement out there claiming that constructor based injection is the new heaven. But I prefer property-based injection if that option is available, especially in python):

...
class Baz:
   def __init__(self, foo: Foo) -> None:
        self.foo = foo
...

So, here's my question. Is this behaviour by design?

RobertoPrevato commented 10 months ago

Hi @lucas-labs Thank You for opening this issue. rodi currently works this way: if a class defines a constructor, it resolves dependencies inspecting it (the __init__); otherwise it tries to resolve by class annotations. In the second case, it must be possible to create an instance of the class by simple calling it (this particular detail is by design) because dependencies are set right after instantiating the class.

As a side note, all inspections happen once for best performance: rodi creates activator functions that are used to resolve objects.

But now that you are asking about this, rodi might be modified to support both cases: resolving both __init__ and class properties in such cases.

I wouldn't care about the activists you mention: I also prefer to not define an __init__ method when possible and use only class annotations to describe dependencies (the code is less verbose). And I didn't think that supporting both situations at the same time would be useful. By the way, I worked on rodi not because I am a "dependency injection activist" myself, simply because I worked for years with ASP.NET Core and its DI, and I know how comfortable it is. I kept intentionally rodi abstracted away from BlackSheep because I want to be able to use it also in other kinds of projects, especially CLI apps.

lucas-labs commented 10 months ago

@RobertoPrevato Yeah! I've worked with groovy on grails (ruby on rails brother but using the groovy lang) for many years and that's heavy based on DI. It's quite useful, especially for web frameworks like grails or blacksheep.

The only useful situation that I can think of supporting both, having an __init__ method and also defining properties to be injected by its type annotations is the case when you need to do some initialization tasks that doesn't depend on the injected stuff, like so:

class Baz:
    foo: Foo

    def __init__(self) -> None:
        self.whatever = ':)'

    def call_foo(self) -> str:
        return self.foo.bar()

In this case self.foo would not be injected by rodi. Not a big issue tho. It might even be ok from a design point of view. I was just doing something similar to the above and it didn't work so I came here to ask haha.

Thanks for the answer!! Cheers