Skip to content

Fix annotated attribute injection #889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from 14 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@ src/**/*.html
.workspace/

.vscode/

# Cursor project files
.cursor
8 changes: 8 additions & 0 deletions docs/main/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_

Develop
-------

- Add support for ``Annotated`` type for module and class attribute injection in wiring,
with updated documentation and examples.
See discussion:
https://github.com/ets-labs/python-dependency-injector/pull/721#issuecomment-2025263718

4.46.0
------

Expand Down
32 changes: 31 additions & 1 deletion docs/wiring.rst
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,43 @@ To inject a container use special identifier ``<container>``:
Making injections into modules and class attributes
---------------------------------------------------

You can use wiring to make injections into modules and class attributes.
You can use wiring to make injections into modules and class attributes. Both the classic marker
syntax and the ``Annotated`` form are supported.

Classic marker syntax:

.. code-block:: python

service: Service = Provide[Container.service]

class Main:
service: Service = Provide[Container.service]

Full example of the classic marker syntax:

.. literalinclude:: ../examples/wiring/example_attribute.py
:language: python
:lines: 3-
:emphasize-lines: 14,19

Annotated form (Python 3.9+):

.. code-block:: python

from typing import Annotated

service: Annotated[Service, Provide[Container.service]]

class Main:
service: Annotated[Service, Provide[Container.service]]

Full example of the annotated form:

.. literalinclude:: ../examples/wiring/example_attribute_annotated.py
:language: python
:lines: 3-
:emphasize-lines: 16,21

You could also use string identifiers to avoid a dependency on a container:

.. code-block:: python
Expand Down
31 changes: 31 additions & 0 deletions examples/wiring/example_attribute_annotated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Wiring attribute example with Annotated."""

from typing import Annotated

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


class Service:
...


class Container(containers.DeclarativeContainer):

service = providers.Factory(Service)


service: Annotated[Service, Provide[Container.service]]


class Main:

service: Annotated[Service, Provide[Container.service]]


if __name__ == "__main__":
container = Container()
container.wire(modules=[__name__])

assert isinstance(service, Service)
assert isinstance(Main.service, Service)
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ per-file-ignores =
examples/containers/traverse.py: E501
examples/providers/async.py: F841
examples/providers/async_overriding.py: F841
examples/wiring/*: F841
examples/wiring/*: F821,F841

[pydocstyle]
ignore = D100,D101,D102,D105,D106,D107,D203,D213
29 changes: 26 additions & 3 deletions src/dependency_injector/wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ def wire( # noqa: C901
providers_map = ProvidersMap(container)

for module in modules:
for member_name, member in inspect.getmembers(module):
for member_name, member in _get_members_and_annotated(module):
if _inspect_filter.is_excluded(member):
continue

Expand All @@ -426,7 +426,7 @@ def wire( # noqa: C901
elif inspect.isclass(member):
cls = member
try:
cls_members = inspect.getmembers(cls)
cls_members = _get_members_and_annotated(cls)
except Exception: # noqa
# Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/441
continue
Expand Down Expand Up @@ -579,7 +579,11 @@ def _unpatch_attribute(patched: PatchedAttribute) -> None:

def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]:
if get_origin(parameter.annotation) is Annotated:
marker = get_args(parameter.annotation)[1]
args = get_args(parameter.annotation)
if len(args) > 1:
marker = args[1]
else:
marker = None
else:
marker = parameter.default

Expand Down Expand Up @@ -1025,3 +1029,22 @@ def _patched(*args, **kwargs):
patched.closing,
)
return cast(F, _patched)


def _get_annotations(obj: Any) -> Dict[str, Any]:
if sys.version_info >= (3, 10):
return inspect.get_annotations(obj)
else:
return getattr(obj, "__annotations__", {})


def _get_members_and_annotated(obj: Any) -> Iterable[Tuple[str, Any]]:
members = inspect.getmembers(obj)
annotations = _get_annotations(obj)
for annotation_name, annotation in annotations.items():
if get_origin(annotation) is Annotated:
args = get_args(annotation)
if len(args) > 1:
member = args[1]
members.append((annotation_name, member))
return members
126 changes: 126 additions & 0 deletions tests/unit/samples/wiring/module_annotated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""Test module for wiring with Annotated."""

import sys
import pytest

if sys.version_info < (3, 9):
pytest.skip("Annotated is only available in Python 3.9+", allow_module_level=True)

from decimal import Decimal
from typing import Callable, Annotated

from dependency_injector import providers
from dependency_injector.wiring import inject, Provide, Provider

from .container import Container, SubContainer
from .service import Service

service: Annotated[Service, Provide[Container.service]]
service_provider: Annotated[Callable[..., Service], Provider[Container.service]]
undefined: Annotated[Callable, Provide[providers.Provider()]]

class TestClass:
service: Annotated[Service, Provide[Container.service]]
service_provider: Annotated[Callable[..., Service], Provider[Container.service]]
undefined: Annotated[Callable, Provide[providers.Provider()]]

@inject
def __init__(self, service: Annotated[Service, Provide[Container.service]]):
self.service = service

@inject
def method(self, service: Annotated[Service, Provide[Container.service]]):
return service

@classmethod
@inject
def class_method(cls, service: Annotated[Service, Provide[Container.service]]):
return service

@staticmethod
@inject
def static_method(service: Annotated[Service, Provide[Container.service]]):
return service

@inject
def test_function(service: Annotated[Service, Provide[Container.service]]):
return service

@inject
def test_function_provider(service_provider: Annotated[Callable[..., Service], Provider[Container.service]]):
service = service_provider()
return service

@inject
def test_config_value(
value_int: Annotated[int, Provide[Container.config.a.b.c.as_int()]],
value_float: Annotated[float, Provide[Container.config.a.b.c.as_float()]],
value_str: Annotated[str, Provide[Container.config.a.b.c.as_(str)]],
value_decimal: Annotated[Decimal, Provide[Container.config.a.b.c.as_(Decimal)]],
value_required: Annotated[str, Provide[Container.config.a.b.c.required()]],
value_required_int: Annotated[int, Provide[Container.config.a.b.c.required().as_int()]],
value_required_float: Annotated[float, Provide[Container.config.a.b.c.required().as_float()]],
value_required_str: Annotated[str, Provide[Container.config.a.b.c.required().as_(str)]],
value_required_decimal: Annotated[str, Provide[Container.config.a.b.c.required().as_(Decimal)]],
):
return (
value_int,
value_float,
value_str,
value_decimal,
value_required,
value_required_int,
value_required_float,
value_required_str,
value_required_decimal,
)

@inject
def test_config_value_required_undefined(
value_required: Annotated[int, Provide[Container.config.a.b.c.required()]],
):
return value_required

@inject
def test_provide_provider(service_provider: Annotated[Callable[..., Service], Provide[Container.service.provider]]):
service = service_provider()
return service

@inject
def test_provider_provider(service_provider: Annotated[Callable[..., Service], Provider[Container.service.provider]]):
service = service_provider()
return service

@inject
def test_provided_instance(some_value: Annotated[int, Provide[Container.service.provided.foo["bar"].call()]]):
return some_value

@inject
def test_subcontainer_provider(some_value: Annotated[int, Provide[Container.sub.int_object]]):
return some_value

@inject
def test_config_invariant(some_value: Annotated[int, Provide[Container.config.option[Container.config.switch]]]):
return some_value

@inject
def test_provide_from_different_containers(
service: Annotated[Service, Provide[Container.service]],
some_value: Annotated[int, Provide[SubContainer.int_object]],
):
return service, some_value

class ClassDecorator:
def __init__(self, fn):
self._fn = fn

def __call__(self, *args, **kwargs):
return self._fn(*args, **kwargs)

@ClassDecorator
@inject
def test_class_decorator(service: Annotated[Service, Provide[Container.service]]):
return service

def test_container(container: Annotated[Container, Provide[Container]]):
return container.service()
Loading