From ebf49a60a3d1f52e75a1ebc64ff66914a120822d Mon Sep 17 00:00:00 2001 From: Deepak Ganesh Date: Sun, 2 Aug 2026 16:52:01 +0530 Subject: [PATCH 1/3] Fix @inject on classmethod returning wrong cls in subclass hierarchy When _patch_method processed inherited classmethods on subclasses, it would wrap the already-bound method (which has cls baked in) and set it as a plain function on the subclass. This broke the classmethod descriptor protocol, causing deeper subclasses to receive the wrong cls. The fix skips re-patching inherited methods whose underlying __func__ is already patched by the parent class, preserving correct cls binding through normal Python inheritance. Fixes #947 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/dependency_injector/wiring.py | 7 +++ ...est_classmethod_inject_inheritance_py36.py | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/unit/wiring/test_classmethod_inject_inheritance_py36.py diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 62267c47..68066813 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -625,6 +625,13 @@ def _patch_method( method = cls.__dict__[name] fn = method.__func__ else: + # For inherited methods, check if the underlying function is already + # patched on a parent class. If so, skip to preserve the classmethod + # descriptor protocol (cls binding) for subclasses. + # See: https://github.com/ets-labs/python-dependency-injector/issues/947 + underlying = getattr(method, "__func__", None) + if underlying is not None and _is_patched(underlying): + return fn = method if not _is_patched(fn): diff --git a/tests/unit/wiring/test_classmethod_inject_inheritance_py36.py b/tests/unit/wiring/test_classmethod_inject_inheritance_py36.py new file mode 100644 index 00000000..c499daff --- /dev/null +++ b/tests/unit/wiring/test_classmethod_inject_inheritance_py36.py @@ -0,0 +1,57 @@ +"""Test that @inject on classmethods preserves correct cls in subclasses. + +See issue for details: https://github.com/ets-labs/python-dependency-injector/issues/947 +""" + +import sys + +from dependency_injector import providers +from dependency_injector.containers import DeclarativeContainer +from dependency_injector.wiring import inject, Provide +from pytest import fixture + + +class Container(DeclarativeContainer): + singleton = providers.Singleton(lambda: object()) + + +class Base: + @classmethod + @inject + def injected_factory(cls, container: Container = Provide[Container]): + return cls() + + +class Sub1(Base): + pass + + +class Sub2(Sub1): + pass + + +@fixture +def container(): + container = Container() + container.wire(modules=[sys.modules[__name__]]) + yield container + container.unwire() + + +def test_base_injected_classmethod_returns_base(container): + result = Base.injected_factory() + assert isinstance(result, Base) + assert type(result) is Base + + +def test_sub1_injected_classmethod_returns_sub1(container): + result = Sub1.injected_factory() + assert isinstance(result, Sub1) + assert type(result) is Sub1 + + +def test_sub2_injected_classmethod_returns_sub2(container): + """Regression: Sub2.injected_factory() must return Sub2, not Sub1.""" + result = Sub2.injected_factory() + assert isinstance(result, Sub2) + assert type(result) is Sub2 From c069de8901aaa4a197871ec156fb8dad3f236241 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Tue, 4 Aug 2026 18:27:59 +0000 Subject: [PATCH 2/3] Simplify test cases --- ...est_classmethod_inject_inheritance_py36.py | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/tests/unit/wiring/test_classmethod_inject_inheritance_py36.py b/tests/unit/wiring/test_classmethod_inject_inheritance_py36.py index c499daff..60910e3b 100644 --- a/tests/unit/wiring/test_classmethod_inject_inheritance_py36.py +++ b/tests/unit/wiring/test_classmethod_inject_inheritance_py36.py @@ -5,10 +5,12 @@ import sys +from pytest import fixture +from typing_extensions import Annotated + from dependency_injector import providers from dependency_injector.containers import DeclarativeContainer -from dependency_injector.wiring import inject, Provide -from pytest import fixture +from dependency_injector.wiring import Provide, inject class Container(DeclarativeContainer): @@ -18,8 +20,8 @@ class Container(DeclarativeContainer): class Base: @classmethod @inject - def injected_factory(cls, container: Container = Provide[Container]): - return cls() + def injected_factory(cls, singleton: Annotated[object, Provide["singleton"]]): + return cls, singleton class Sub1(Base): @@ -38,20 +40,9 @@ def container(): container.unwire() -def test_base_injected_classmethod_returns_base(container): - result = Base.injected_factory() - assert isinstance(result, Base) - assert type(result) is Base - - -def test_sub1_injected_classmethod_returns_sub1(container): - result = Sub1.injected_factory() - assert isinstance(result, Sub1) - assert type(result) is Sub1 - +def test_base_injected_classmethod(container): + sentinel = container.singleton() -def test_sub2_injected_classmethod_returns_sub2(container): - """Regression: Sub2.injected_factory() must return Sub2, not Sub1.""" - result = Sub2.injected_factory() - assert isinstance(result, Sub2) - assert type(result) is Sub2 + for cls in [Sub2, Sub1, Base]: + result = cls.injected_factory() + assert result == (cls, sentinel) From 186d71ae7b1f96ed05602f166ad76217e262a032 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Tue, 4 Aug 2026 18:30:02 +0000 Subject: [PATCH 3/3] Add note on complex class hierarchy wiring --- docs/wiring.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/wiring.rst b/docs/wiring.rst index e0cbed5f..6188d703 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -55,6 +55,11 @@ the wiring works appropriately. This will also contribute to the performance of Specifying the ``@inject`` as a first decorator is also crucial for FastAPI, other frameworks using decorators similarly, for closures, and for any types of custom decorators with the injections. +.. note:: Note on complex class hierarchies + + If you have complex class hierarchies with ``@inject``, when wiring modules, make sure to include all + modules with decorator. Otherwise partially wired classes might violate Liskov Substitution Principle. + FastAPI example: .. code-block:: python