-
-
Notifications
You must be signed in to change notification settings - Fork 343
Description
Injected classmethod returns instance of wrong subclass when using @Inject
Description
When decorating a @classmethod with @Inject in dependency‑injector and calling it through multiple levels of inheritance, the injected method is bound to the wrong class.
In the provided reproducer, a base class defines a simple @classmethod factory method and an injected factory method called through @classmethod *_creator method. The injected version returns an instance of a different subclass when invoked on a deeper subclass. This breaks polymorphic factories and violates the semantics of class methods.
Affected versions
The issue was observed on:
• Python: 3.14.0
• pytest: 9.0.2
>> uv pip show dependency-injector
Name: dependency-injector
Version: 4.48.3
Location: /Users/danielchiliaev/PyCharmMiscProject/.venv/lib/python3.14/site-packages
Requires:
Required-by:Older issues (#318) related to classmethod injection were fixed in version 4.3.3, but those addressed extra parameters being injected. The current problem concerns the binding of cls when subclasses are involved.
Steps to reproduce
test_inject.py
# Reproducer for bug in dependency-injector where classmethod factories do not
# receive the correct cls when called from subclasses if the factory is injected.
from typing import Self
import pytest
from dependency_injector import providers
from dependency_injector.containers import DeclarativeContainer
from dependency_injector.wiring import inject, Provide
# 1. Define a container with a trivial provider:
class Container(DeclarativeContainer):
singleton = providers.Singleton(lambda: object())
# 2. Define a base class with a plain factory, an injected factory, and creator methods:
class Base:
@classmethod
def factory(cls, repo_cls: type | None = None) -> Self:
instance = cls()
return instance
@classmethod
def creator(cls, repo_cls: type) -> Self:
# sub factory is not injected -> cls is always the current class
instance = cls.factory(repo_cls=repo_cls)
assert repo_cls is cls, cls
assert isinstance(
instance,
repo_cls
), f"Expected instance of {repo_cls.__name__}, got {type(instance).__name__}"
return instance
@classmethod
@inject
def injected_factory(cls, container: Container = Provide[Container], repo_cls: type | None = None) -> Self:
instance = cls()
return instance
@classmethod
def inject_creator(cls, repo_cls: type) -> Self:
# sub factory is injected -> cls is Sub1 when called from Sub2
instance = cls.injected_factory(repo_cls=repo_cls)
assert repo_cls is cls, cls
assert isinstance(
instance,
repo_cls
), f"Expected instance of {repo_cls.__name__}, got {type(instance).__name__}"
return instance
class Sub1(Base):
"""
Subclass of Base to test factory methods.
"""
pass
class Sub2(Sub1):
"""
Subclass of Sub1 to test factory methods.
"""
pass
@pytest.fixture
def container():
_container = Container()
_container.wire(modules=[__name__])
yield _container
_container.unwire()
# Tests for direct factories
def test_sub1_factory(container: Container):
# Passes correctly (1 layer of inheritance)
sub1_instance = Sub1.factory(repo_cls=Sub1)
assert isinstance(sub1_instance, Sub1)
def test_sub2_factory(container: Container):
# Passes correctly (2 layers of inheritance)
sub2_instance = Sub2.creator(repo_cls=Sub2)
assert isinstance(sub2_instance, Sub2)
# Tests for injected factories
def test_sub1_factory_inject(container: Container):
# Passes correctly (1 layer of inheritance)
sub1_instance = Sub1.injected_factory(repo_cls=Sub1)
assert isinstance(sub1_instance, Sub1)
def test_sub2_factory_inject(container: Container):
# Fails (2 layers of inheritance)
sub2_instance = Sub2.inject_creator(repo_cls=Sub2)
assert isinstance(sub2_instance, Sub2)- Run
>> pytest test_inject.py
================================================================= test session starts =================================================================
platform darwin -- Python 3.14.0, pytest-9.0.2, pluggy-1.6.0
rootdir: /Users/danielchiliaev/PyCharmMiscProject
configfile: pyproject.toml
plugins: anyio-4.12.0
collected 4 items
reproduces/dependency-injector/test_inject.py ...F [100%]
====================================================================== FAILURES =======================================================================
______________________________________________________________ test_sub2_factory_inject _______________________________________________________________
container = <dependency_injector.containers.DynamicContainer object at 0x108a05350>
def test_sub2_factory_inject(container: Container):
# Fails (2 layers of inheritance)
> sub2_instance = Sub2.inject_creator(repo_cls=Sub2)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
reproduces/dependency-injector/test_inject.py:99:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
cls = <class 'test_inject.Sub2'>, repo_cls = <class 'test_inject.Sub2'>
@classmethod
def inject_creator(cls, repo_cls: type) -> Self:
# sub factory is injected -> cls is Sub1 when called from Sub2
instance = cls.injected_factory(repo_cls=repo_cls)
assert repo_cls is cls, cls
> assert isinstance(
instance,
repo_cls
), f"Expected instance of {repo_cls.__name__}, got {type(instance).__name__}"
E AssertionError: Expected instance of Sub2, got Sub1
E assert False
E + where False = isinstance(<test_inject.Sub1 object at 0x107c2a0d0>, <class 'test_inject.Sub2'>)
reproduces/dependency-injector/test_inject.py:46: AssertionError
=============================================================== short test summary info ===============================================================
FAILED reproduces/dependency-injector/test_inject.py::test_sub2_factory_inject - AssertionError: Expected instance of Sub2, got Sub1
============================================================= 1 failed, 3 passed in 0.15s =============================================================Expected behavior
Inheritance:
Base -> Sub1 -> Sub2
Calls:
Sub2.inject_creator(cls, ...) -> Sub2.inject_factory(cls, ...)
Each class method should receive its own class as cls regardless of whether it is decorated with @Inject or how deep the inheritance chain is. Calling the injected factory on Sub2 should produce an instance of Sub2, just as the plain factory does.
Actual behavior
When a @inject‑decorated classmethod is inherited and called in another classmethod, it receives wrong cls param