Skip to content

Commit 8814db3

Browse files
rmk135ZipFile
andauthored
Fix annotated attribute injection (#889)
* Add example for Annotated attribute injection for module/class attributes * Fix attribute injection with Annotated types * Add unit tests for Annotated attribute and argument injection in wiring * Add .cursor to .gitignore * Style: add blank lines between class definitions and attributes in annotated attribute example * Docs: clarify and format module/class attribute injection for classic and Annotated forms * Changelog: add note and discussion link for Annotated attribute injection support * Fix nls * Fix CI checks and Python 3.8 tests * Fix PR issues * Fix Python 3.8 tests * Fix flake8 issues * Fix: robust Annotated detection for wiring across Python versions * Refactor: extract annotation retrieval and improve typing for Python 3.9 compatibility * Update src/dependency_injector/wiring.py Co-authored-by: ZipFile <[email protected]> --------- Co-authored-by: ZipFile <[email protected]>
1 parent 8bf9ed0 commit 8814db3

File tree

9 files changed

+405
-7
lines changed

9 files changed

+405
-7
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,6 @@ src/**/*.html
7373
.workspace/
7474

7575
.vscode/
76+
77+
# Cursor project files
78+
.cursor

docs/main/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ that were made in every particular version.
77
From version 0.7.6 *Dependency Injector* framework strictly
88
follows `Semantic versioning`_
99

10+
Develop
11+
-------
12+
13+
- Add support for ``Annotated`` type for module and class attribute injection in wiring,
14+
with updated documentation and examples.
15+
See discussion:
16+
https://github.com/ets-labs/python-dependency-injector/pull/721#issuecomment-2025263718
17+
1018
4.46.0
1119
------
1220

docs/wiring.rst

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,13 +254,43 @@ To inject a container use special identifier ``<container>``:
254254
Making injections into modules and class attributes
255255
---------------------------------------------------
256256

257-
You can use wiring to make injections into modules and class attributes.
257+
You can use wiring to make injections into modules and class attributes. Both the classic marker
258+
syntax and the ``Annotated`` form are supported.
259+
260+
Classic marker syntax:
261+
262+
.. code-block:: python
263+
264+
service: Service = Provide[Container.service]
265+
266+
class Main:
267+
service: Service = Provide[Container.service]
268+
269+
Full example of the classic marker syntax:
258270

259271
.. literalinclude:: ../examples/wiring/example_attribute.py
260272
:language: python
261273
:lines: 3-
262274
:emphasize-lines: 14,19
263275

276+
Annotated form (Python 3.9+):
277+
278+
.. code-block:: python
279+
280+
from typing import Annotated
281+
282+
service: Annotated[Service, Provide[Container.service]]
283+
284+
class Main:
285+
service: Annotated[Service, Provide[Container.service]]
286+
287+
Full example of the annotated form:
288+
289+
.. literalinclude:: ../examples/wiring/example_attribute_annotated.py
290+
:language: python
291+
:lines: 3-
292+
:emphasize-lines: 16,21
293+
264294
You could also use string identifiers to avoid a dependency on a container:
265295

266296
.. code-block:: python
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Wiring attribute example with Annotated."""
2+
3+
from typing import Annotated
4+
5+
from dependency_injector import containers, providers
6+
from dependency_injector.wiring import Provide
7+
8+
9+
class Service:
10+
...
11+
12+
13+
class Container(containers.DeclarativeContainer):
14+
15+
service = providers.Factory(Service)
16+
17+
18+
service: Annotated[Service, Provide[Container.service]]
19+
20+
21+
class Main:
22+
23+
service: Annotated[Service, Provide[Container.service]]
24+
25+
26+
if __name__ == "__main__":
27+
container = Container()
28+
container.wire(modules=[__name__])
29+
30+
assert isinstance(service, Service)
31+
assert isinstance(Main.service, Service)

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ per-file-ignores =
88
examples/containers/traverse.py: E501
99
examples/providers/async.py: F841
1010
examples/providers/async_overriding.py: F841
11-
examples/wiring/*: F841
11+
examples/wiring/*: F821,F841
1212

1313
[pydocstyle]
1414
ignore = D100,D101,D102,D105,D106,D107,D203,D213

src/dependency_injector/wiring.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ def wire( # noqa: C901
415415
providers_map = ProvidersMap(container)
416416

417417
for module in modules:
418-
for member_name, member in inspect.getmembers(module):
418+
for member_name, member in _get_members_and_annotated(module):
419419
if _inspect_filter.is_excluded(member):
420420
continue
421421

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

580580
def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]:
581581
if get_origin(parameter.annotation) is Annotated:
582-
marker = get_args(parameter.annotation)[1]
582+
args = get_args(parameter.annotation)
583+
if len(args) > 1:
584+
marker = args[1]
585+
else:
586+
marker = None
583587
else:
584588
marker = parameter.default
585589

@@ -1025,3 +1029,23 @@ def _patched(*args, **kwargs):
10251029
patched.closing,
10261030
)
10271031
return cast(F, _patched)
1032+
1033+
1034+
if sys.version_info >= (3, 10):
1035+
def _get_annotations(obj: Any) -> Dict[str, Any]:
1036+
return inspect.get_annotations(obj)
1037+
else:
1038+
def _get_annotations(obj: Any) -> Dict[str, Any]:
1039+
return getattr(obj, "__annotations__", {})
1040+
1041+
1042+
def _get_members_and_annotated(obj: Any) -> Iterable[Tuple[str, Any]]:
1043+
members = inspect.getmembers(obj)
1044+
annotations = _get_annotations(obj)
1045+
for annotation_name, annotation in annotations.items():
1046+
if get_origin(annotation) is Annotated:
1047+
args = get_args(annotation)
1048+
if len(args) > 1:
1049+
member = args[1]
1050+
members.append((annotation_name, member))
1051+
return members
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""Test module for wiring with Annotated."""
2+
3+
import sys
4+
import pytest
5+
6+
if sys.version_info < (3, 9):
7+
pytest.skip("Annotated is only available in Python 3.9+", allow_module_level=True)
8+
9+
from decimal import Decimal
10+
from typing import Callable, Annotated
11+
12+
from dependency_injector import providers
13+
from dependency_injector.wiring import inject, Provide, Provider
14+
15+
from .container import Container, SubContainer
16+
from .service import Service
17+
18+
service: Annotated[Service, Provide[Container.service]]
19+
service_provider: Annotated[Callable[..., Service], Provider[Container.service]]
20+
undefined: Annotated[Callable, Provide[providers.Provider()]]
21+
22+
class TestClass:
23+
service: Annotated[Service, Provide[Container.service]]
24+
service_provider: Annotated[Callable[..., Service], Provider[Container.service]]
25+
undefined: Annotated[Callable, Provide[providers.Provider()]]
26+
27+
@inject
28+
def __init__(self, service: Annotated[Service, Provide[Container.service]]):
29+
self.service = service
30+
31+
@inject
32+
def method(self, service: Annotated[Service, Provide[Container.service]]):
33+
return service
34+
35+
@classmethod
36+
@inject
37+
def class_method(cls, service: Annotated[Service, Provide[Container.service]]):
38+
return service
39+
40+
@staticmethod
41+
@inject
42+
def static_method(service: Annotated[Service, Provide[Container.service]]):
43+
return service
44+
45+
@inject
46+
def test_function(service: Annotated[Service, Provide[Container.service]]):
47+
return service
48+
49+
@inject
50+
def test_function_provider(service_provider: Annotated[Callable[..., Service], Provider[Container.service]]):
51+
service = service_provider()
52+
return service
53+
54+
@inject
55+
def test_config_value(
56+
value_int: Annotated[int, Provide[Container.config.a.b.c.as_int()]],
57+
value_float: Annotated[float, Provide[Container.config.a.b.c.as_float()]],
58+
value_str: Annotated[str, Provide[Container.config.a.b.c.as_(str)]],
59+
value_decimal: Annotated[Decimal, Provide[Container.config.a.b.c.as_(Decimal)]],
60+
value_required: Annotated[str, Provide[Container.config.a.b.c.required()]],
61+
value_required_int: Annotated[int, Provide[Container.config.a.b.c.required().as_int()]],
62+
value_required_float: Annotated[float, Provide[Container.config.a.b.c.required().as_float()]],
63+
value_required_str: Annotated[str, Provide[Container.config.a.b.c.required().as_(str)]],
64+
value_required_decimal: Annotated[str, Provide[Container.config.a.b.c.required().as_(Decimal)]],
65+
):
66+
return (
67+
value_int,
68+
value_float,
69+
value_str,
70+
value_decimal,
71+
value_required,
72+
value_required_int,
73+
value_required_float,
74+
value_required_str,
75+
value_required_decimal,
76+
)
77+
78+
@inject
79+
def test_config_value_required_undefined(
80+
value_required: Annotated[int, Provide[Container.config.a.b.c.required()]],
81+
):
82+
return value_required
83+
84+
@inject
85+
def test_provide_provider(service_provider: Annotated[Callable[..., Service], Provide[Container.service.provider]]):
86+
service = service_provider()
87+
return service
88+
89+
@inject
90+
def test_provider_provider(service_provider: Annotated[Callable[..., Service], Provider[Container.service.provider]]):
91+
service = service_provider()
92+
return service
93+
94+
@inject
95+
def test_provided_instance(some_value: Annotated[int, Provide[Container.service.provided.foo["bar"].call()]]):
96+
return some_value
97+
98+
@inject
99+
def test_subcontainer_provider(some_value: Annotated[int, Provide[Container.sub.int_object]]):
100+
return some_value
101+
102+
@inject
103+
def test_config_invariant(some_value: Annotated[int, Provide[Container.config.option[Container.config.switch]]]):
104+
return some_value
105+
106+
@inject
107+
def test_provide_from_different_containers(
108+
service: Annotated[Service, Provide[Container.service]],
109+
some_value: Annotated[int, Provide[SubContainer.int_object]],
110+
):
111+
return service, some_value
112+
113+
class ClassDecorator:
114+
def __init__(self, fn):
115+
self._fn = fn
116+
117+
def __call__(self, *args, **kwargs):
118+
return self._fn(*args, **kwargs)
119+
120+
@ClassDecorator
121+
@inject
122+
def test_class_decorator(service: Annotated[Service, Provide[Container.service]]):
123+
return service
124+
125+
def test_container(container: Annotated[Container, Provide[Container]]):
126+
return container.service()

0 commit comments

Comments
 (0)