injector icon indicating copy to clipboard operation
injector copied to clipboard

@noninjectable seems to not work with dataclasses and auto_bind

Open bnorick opened this issue 4 years ago • 2 comments

The interaction between @dataclass, @noninjectable, and auto_bind=True seems to be broken. I would not expect the following to work, because foo shouldn't be injected due to the decorator,

from dataclasses import dataclass
from injector import Injector, inject

class Persistence:
    pass

@inject
@noninjectable('foo')
@dataclass
class Parent:
    p: Persistence
    foo: int

injector = Injector()
parent = injector.get(Parent)
print(type(parent))
print(parent.foo)

but it actually runs and produces

<class '__main__.Parent'>
0

The same implementation without @dataclass

from injector import Injector, inject

class Persistence:
    pass

class Parent:
    @inject
    @noninjectable('foo')
    def __init__(self, persistence: Persistence, foo: int):
        self.p = persistence
        self.foo = foo

injector = Injector()
parent = injector.get(Parent)
print(type(parent))
print(parent.foo)

throws an exception, as expected

Traceback (most recent call last):
  File "examples\injector_test.py", line 116, in <module>
    parent = injector.get(Parent)
  File "[USER_DIR]\AppData\Local\pypoetry\Cache\virtualenvs\[VENV]\lib\site-packages\injector\__init__.py", line 963, in get
    result = scope_instance.get(interface, binding.provider).get(self)
  File "[USER_DIR]\AppData\Local\pypoetry\Cache\virtualenvs\[VENV]\lib\site-packages\injector\__init__.py", line 291, in get
    return injector.create_object(self._cls)
  File "[USER_DIR]\AppData\Local\pypoetry\Cache\virtualenvs\[VENV]\lib\site-packages\injector\__init__.py", line 990, in create_object
    self.call_with_injection(cls.__init__, self_=instance, kwargs=additional_kwargs)
  File "[USER_DIR]\AppData\Local\pypoetry\Cache\virtualenvs\[VENV]\lib\site-packages\injector\__init__.py", line 1032, in call_with_injection
    reraise(e, CallError(self_, callable, args, dependencies, e, self._stack))
  File "[USER_DIR]\AppData\Local\pypoetry\Cache\virtualenvs\[VENV]\lib\site-packages\injector\__init__.py", line 211, in reraise
    raise exception.with_traceback(tb)
  File "[USER_DIR]\AppData\Local\pypoetry\Cache\virtualenvs\[VENV]\lib\site-packages\injector\__init__.py", line 1030, in call_with_injection
    return callable(*full_args, **dependencies)
injector.CallError: Call to Parent.__init__(persistence=<__main__.Persistence object at 0x0000023D47334940>) failed: __init__() missing 1 required positional argument: 'foo' (injection stack: [])

bnorick avatar Dec 08 '20 00:12 bnorick

I did some debugging:

from dataclasses import dataclass
from injector import Injector, inject, noninjectable

@inject
@noninjectable("foo")
@dataclass
class Parent:
    p: Persistence
    foo: int

injector = Injector()
parent = injector.get(Parent)
  1. @dataclass does its dynamic code generation and attaches, amongst other things, a new __init__(p:Persistence,foo:int) method onto Parent.
  2. @noninjectable expects an initializer (!) function, checks for existing function.__noninjectables__ and assigns "foo" to that attribute. However it gets called on the whole Parent, so now Parent.__noninjectables__ is set.
  3. @inject can work with either a class or an __init__. It finds a type Parent with an initializer and continues, everything good.
  4. Somewhere along the way it gets decided that Parent is to be constructed using a ClassProvider (which makes sense) and subsequently the free injector.get_bindings function is invoked with Parent.__init__. That's when it tries to find a __noninjectables__ attribute on the initializer, which doesn't exist (as it is set on Parent itself).

So seems to me like @noinjectable is the culprit as it doesn't have the

    if isinstance(initializer_or_class, type) and hasattr(initializer_or_class, '__init__'):

check that @inject uses to determine the decorated type.

JosXa avatar Dec 08 '20 01:12 JosXa

Yeah, it's a bug.

jstasiak avatar Dec 08 '20 10:12 jstasiak