Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/wiring.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/dependency_injector/wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/wiring/test_classmethod_inject_inheritance_py36.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""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 pytest import fixture
from typing_extensions import Annotated

from dependency_injector import providers
from dependency_injector.containers import DeclarativeContainer
from dependency_injector.wiring import Provide, inject


class Container(DeclarativeContainer):
singleton = providers.Singleton(lambda: object())


class Base:
@classmethod
@inject
def injected_factory(cls, singleton: Annotated[object, Provide["singleton"]]):
return cls, singleton


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(container):
sentinel = container.singleton()

for cls in [Sub2, Sub1, Base]:
result = cls.injected_factory()
assert result == (cls, sentinel)