From 19600d9d080a8fa7555b09148b472eeb25c0d706 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 12:00:48 -0400 Subject: [PATCH 01/15] Add example for Annotated attribute injection for module/class attributes --- .../wiring/example_attribute_annotated.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 examples/wiring/example_attribute_annotated.py diff --git a/examples/wiring/example_attribute_annotated.py b/examples/wiring/example_attribute_annotated.py new file mode 100644 index 00000000..69d2769e --- /dev/null +++ b/examples/wiring/example_attribute_annotated.py @@ -0,0 +1,29 @@ +"""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) \ No newline at end of file From 783b91bf19809228aa615fb653222bb897d9784e Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 12:21:10 -0400 Subject: [PATCH 02/15] Fix attribute injection with Annotated types --- src/dependency_injector/wiring.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 1effd16f..6a4c5c7a 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -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 @@ -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 @@ -1025,3 +1025,18 @@ def _patched(*args, **kwargs): patched.closing, ) return cast(F, _patched) + + +def _get_members_and_annotated(obj: Any) -> list[tuple[str, Any]]: + members = inspect.getmembers(obj) + try: + annotations = inspect.get_annotations(obj) + except Exception: + annotations = {} + 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 From 770694bdd01f53247918a4077b6821ebe9c9c4d0 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 12:34:28 -0400 Subject: [PATCH 03/15] Add unit tests for Annotated attribute and argument injection in wiring --- tests/unit/samples/wiring/module_annotated.py | 120 ++++++++++++ .../provider_ids/test_main_annotated_py36.py | 176 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 tests/unit/samples/wiring/module_annotated.py create mode 100644 tests/unit/wiring/provider_ids/test_main_annotated_py36.py diff --git a/tests/unit/samples/wiring/module_annotated.py b/tests/unit/samples/wiring/module_annotated.py new file mode 100644 index 00000000..624ac41d --- /dev/null +++ b/tests/unit/samples/wiring/module_annotated.py @@ -0,0 +1,120 @@ +"""Test module for wiring with Annotated.""" + +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() \ No newline at end of file diff --git a/tests/unit/wiring/provider_ids/test_main_annotated_py36.py b/tests/unit/wiring/provider_ids/test_main_annotated_py36.py new file mode 100644 index 00000000..517f3283 --- /dev/null +++ b/tests/unit/wiring/provider_ids/test_main_annotated_py36.py @@ -0,0 +1,176 @@ +"""Main wiring tests for Annotated attribute and argument injection.""" + +from decimal import Decimal +import typing + +from dependency_injector import errors +from dependency_injector.wiring import Closing, Provide, Provider, wire +from pytest import fixture, mark, raises + +from samples.wiring import module_annotated as module, package, resourceclosing +from samples.wiring.service import Service +from samples.wiring.container import Container, SubContainer + +@fixture(autouse=True) +def container(): + container = Container(config={"a": {"b": {"c": 10}}}) + container.wire( + modules=[module], + packages=[package], + ) + yield container + container.unwire() + +@fixture +def subcontainer(): + container = SubContainer() + container.wire( + modules=[module], + packages=[package], + ) + yield container + container.unwire() + +@fixture +def resourceclosing_container(): + container = resourceclosing.Container() + container.wire(modules=[resourceclosing]) + yield container + container.unwire() + +def test_module_attributes_wiring(): + assert isinstance(module.service, Service) + assert isinstance(module.service_provider(), Service) + assert isinstance(module.__annotations__['undefined'], typing._AnnotatedAlias) + +def test_class_wiring(): + test_class_object = module.TestClass() + assert isinstance(test_class_object.service, Service) + +def test_class_wiring_context_arg(container: Container): + test_service = container.service() + test_class_object = module.TestClass(service=test_service) + assert test_class_object.service is test_service + +def test_class_method_wiring(): + test_class_object = module.TestClass() + service = test_class_object.method() + assert isinstance(service, Service) + +def test_class_classmethod_wiring(): + service = module.TestClass.class_method() + assert isinstance(service, Service) + +def test_instance_classmethod_wiring(): + instance = module.TestClass() + service = instance.class_method() + assert isinstance(service, Service) + +def test_class_staticmethod_wiring(): + service = module.TestClass.static_method() + assert isinstance(service, Service) + +def test_instance_staticmethod_wiring(): + instance = module.TestClass() + service = instance.static_method() + assert isinstance(service, Service) + +def test_class_attribute_wiring(): + assert isinstance(module.TestClass.service, Service) + assert isinstance(module.TestClass.service_provider(), Service) + assert isinstance(module.TestClass.__annotations__['undefined'], typing._AnnotatedAlias) + +def test_function_wiring(): + service = module.test_function() + assert isinstance(service, Service) + +def test_function_wiring_context_arg(container: Container): + test_service = container.service() + service = module.test_function(service=test_service) + assert service is test_service + +def test_function_wiring_provider(): + service = module.test_function_provider() + assert isinstance(service, Service) + +def test_function_wiring_provider_context_arg(container: Container): + test_service = container.service() + service = module.test_function_provider(service_provider=lambda: test_service) + assert service is test_service + +def test_configuration_option(): + ( + value_int, + value_float, + value_str, + value_decimal, + value_required, + value_required_int, + value_required_float, + value_required_str, + value_required_decimal, + ) = module.test_config_value() + + assert value_int == 10 + assert value_float == 10.0 + assert value_str == "10" + assert value_decimal == Decimal(10) + assert value_required == 10 + assert value_required_int == 10 + assert value_required_float == 10.0 + assert value_required_str == "10" + assert value_required_decimal == Decimal(10) + +def test_configuration_option_required_undefined(container: Container): + container.config.reset_override() + with raises(errors.Error, match="Undefined configuration option \"config.a.b.c\""): + module.test_config_value_required_undefined() + +def test_provide_provider(): + service = module.test_provide_provider() + assert isinstance(service, Service) + +def test_provider_provider(): + service = module.test_provider_provider() + assert isinstance(service, Service) + +def test_provided_instance(container: Container): + class TestService: + foo = {"bar": lambda: 10} + + with container.service.override(TestService()): + some_value = module.test_provided_instance() + assert some_value == 10 + +def test_subcontainer(): + some_value = module.test_subcontainer_provider() + assert some_value == 1 + +def test_config_invariant(container: Container): + config = { + "option": { + "a": 1, + "b": 2, + }, + "switch": "a", + } + container.config.from_dict(config) + + value_default = module.test_config_invariant() + assert value_default == 1 + + with container.config.switch.override("a"): + value_a = module.test_config_invariant() + assert value_a == 1 + + with container.config.switch.override("b"): + value_b = module.test_config_invariant() + assert value_b == 2 + +def test_class_decorator(): + service = module.test_class_decorator() + assert isinstance(service, Service) + +def test_container(): + service = module.test_container() + assert isinstance(service, Service) \ No newline at end of file From 7048c35138c592b1498ca961f435dc36d5976ce4 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 12:35:39 -0400 Subject: [PATCH 04/15] Add .cursor to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a9f80bee..eef3cb91 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,6 @@ src/**/*.html .workspace/ .vscode/ + +# Cursor project files +.cursor From 4e774084bc770cec51fbcc88705a23383c512f34 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 12:51:01 -0400 Subject: [PATCH 05/15] Style: add blank lines between class definitions and attributes in annotated attribute example --- examples/wiring/example_attribute_annotated.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/wiring/example_attribute_annotated.py b/examples/wiring/example_attribute_annotated.py index 69d2769e..e302e218 100644 --- a/examples/wiring/example_attribute_annotated.py +++ b/examples/wiring/example_attribute_annotated.py @@ -11,6 +11,7 @@ class Service: class Container(containers.DeclarativeContainer): + service = providers.Factory(Service) @@ -18,6 +19,7 @@ class Container(containers.DeclarativeContainer): class Main: + service: Annotated[Service, Provide[Container.service]] From 457b6de00cfa0d648151e8ca0ea099fa10cf5df6 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 12:51:37 -0400 Subject: [PATCH 06/15] Docs: clarify and format module/class attribute injection for classic and Annotated forms --- docs/wiring.rst | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/wiring.rst b/docs/wiring.rst index 74026879..02f64c60 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -254,13 +254,43 @@ To inject a container use special identifier ````: 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 From 686fc921687e5617e16486c7daabf1f8de74d95a Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 13:02:32 -0400 Subject: [PATCH 07/15] Changelog: add note and discussion link for Annotated attribute injection support --- docs/main/changelog.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 4d724da1..165d003f 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -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 ------ From 3e5dffe9bb1ce9b582f145be15076368ef924bea Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 13:05:39 -0400 Subject: [PATCH 08/15] Fix nls --- examples/wiring/example_attribute_annotated.py | 2 +- tests/unit/samples/wiring/module_annotated.py | 2 +- tests/unit/wiring/provider_ids/test_main_annotated_py36.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/wiring/example_attribute_annotated.py b/examples/wiring/example_attribute_annotated.py index e302e218..ae43caa0 100644 --- a/examples/wiring/example_attribute_annotated.py +++ b/examples/wiring/example_attribute_annotated.py @@ -28,4 +28,4 @@ class Main: container.wire(modules=[__name__]) assert isinstance(service, Service) - assert isinstance(Main.service, Service) \ No newline at end of file + assert isinstance(Main.service, Service) diff --git a/tests/unit/samples/wiring/module_annotated.py b/tests/unit/samples/wiring/module_annotated.py index 624ac41d..6b4bf35b 100644 --- a/tests/unit/samples/wiring/module_annotated.py +++ b/tests/unit/samples/wiring/module_annotated.py @@ -117,4 +117,4 @@ def test_class_decorator(service: Annotated[Service, Provide[Container.service]] return service def test_container(container: Annotated[Container, Provide[Container]]): - return container.service() \ No newline at end of file + return container.service() diff --git a/tests/unit/wiring/provider_ids/test_main_annotated_py36.py b/tests/unit/wiring/provider_ids/test_main_annotated_py36.py index 517f3283..34d1d747 100644 --- a/tests/unit/wiring/provider_ids/test_main_annotated_py36.py +++ b/tests/unit/wiring/provider_ids/test_main_annotated_py36.py @@ -173,4 +173,4 @@ def test_class_decorator(): def test_container(): service = module.test_container() - assert isinstance(service, Service) \ No newline at end of file + assert isinstance(service, Service) From 9622442e3e5592cbf56b1af2de259dfe344a70c1 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 13:11:20 -0400 Subject: [PATCH 09/15] Fix CI checks and Python 3.8 tests --- examples/.pydocstylerc | 2 +- src/dependency_injector/wiring.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/.pydocstylerc b/examples/.pydocstylerc index 4d4d1367..05a2fa6c 100644 --- a/examples/.pydocstylerc +++ b/examples/.pydocstylerc @@ -1,2 +1,2 @@ [pydocstyle] -ignore = D100,D101,D102,D103,D105,D107,D203,D213 +ignore = D100,D101,D102,D103,D105,D107,D203,D213,F821 diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 6a4c5c7a..fbc99c7a 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -1027,7 +1027,7 @@ def _patched(*args, **kwargs): return cast(F, _patched) -def _get_members_and_annotated(obj: Any) -> list[tuple[str, Any]]: +def _get_members_and_annotated(obj: Any) -> Iterable[tuple[str, Any]]: members = inspect.getmembers(obj) try: annotations = inspect.get_annotations(obj) From c6d82b015e6e4a83184deae36553c381cedb178b Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 13:18:07 -0400 Subject: [PATCH 10/15] Fix PR issues --- .flake8 | 4 ++++ examples/.pydocstylerc | 2 +- src/dependency_injector/wiring.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..94af96a2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +exclude = .git,__pycache__,.venv,venv +per-file-ignores = + examples/*: F821 \ No newline at end of file diff --git a/examples/.pydocstylerc b/examples/.pydocstylerc index 05a2fa6c..4d4d1367 100644 --- a/examples/.pydocstylerc +++ b/examples/.pydocstylerc @@ -1,2 +1,2 @@ [pydocstyle] -ignore = D100,D101,D102,D103,D105,D107,D203,D213,F821 +ignore = D100,D101,D102,D103,D105,D107,D203,D213 diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index fbc99c7a..45ffd2ba 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -1027,7 +1027,7 @@ def _patched(*args, **kwargs): return cast(F, _patched) -def _get_members_and_annotated(obj: Any) -> Iterable[tuple[str, Any]]: +def _get_members_and_annotated(obj: Any) -> Iterable[Tuple[str, Any]]: members = inspect.getmembers(obj) try: annotations = inspect.get_annotations(obj) From 08e233079fb5ef239da608953dbdd4877ab5a2bf Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 13:41:53 -0400 Subject: [PATCH 11/15] Fix Python 3.8 tests --- tests/unit/samples/wiring/module_annotated.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/samples/wiring/module_annotated.py b/tests/unit/samples/wiring/module_annotated.py index 6b4bf35b..f954d0cb 100644 --- a/tests/unit/samples/wiring/module_annotated.py +++ b/tests/unit/samples/wiring/module_annotated.py @@ -1,5 +1,11 @@ """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 c2619cc43ff5726c917fb13d1ad43aa870e1f2d1 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 13:44:47 -0400 Subject: [PATCH 12/15] Fix flake8 issues --- .flake8 | 4 ---- setup.cfg | 2 +- tox.ini | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 94af96a2..00000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -exclude = .git,__pycache__,.venv,venv -per-file-ignores = - examples/*: F821 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 9bb1e56b..5da18a25 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tox.ini b/tox.ini index 29bc5a4f..b524b88d 100644 --- a/tox.ini +++ b/tox.ini @@ -89,8 +89,8 @@ commands= deps= flake8 commands= - flake8 --max-complexity=10 src/dependency_injector/ - flake8 --max-complexity=10 examples/ + flake8 src/dependency_injector/ + flake8 examples/ [testenv:pydocstyle] deps= From 164a45cd8268fc77a7a01106ba107aae050da6c6 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 13:58:35 -0400 Subject: [PATCH 13/15] Fix: robust Annotated detection for wiring across Python versions --- src/dependency_injector/wiring.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 45ffd2ba..b8bd968c 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -578,8 +578,13 @@ 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] + is_annotated = ( + isinstance(annotation, type(Annotated)) + and get_origin(annotation) is not None + and get_origin(annotation) is get_origin(Annotated) + ) + if is_annotated: + marker = get_args(annotation)[1] else: marker = parameter.default @@ -1034,7 +1039,12 @@ def _get_members_and_annotated(obj: Any) -> Iterable[Tuple[str, Any]]: except Exception: annotations = {} for annotation_name, annotation in annotations.items(): - if get_origin(annotation) is Annotated: + is_annotated = ( + isinstance(annotation, type(Annotated)) + and get_origin(annotation) is not None + and get_origin(annotation) is get_origin(Annotated) + ) + if is_annotated: args = get_args(annotation) if len(args) > 1: member = args[1] From 2ab78f34ebc41b0e5eaa3edcc3853052842c8050 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 14:42:43 -0400 Subject: [PATCH 14/15] Refactor: extract annotation retrieval and improve typing for Python 3.9 compatibility --- src/dependency_injector/wiring.py | 32 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index b8bd968c..898ac377 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -578,13 +578,12 @@ def _unpatch_attribute(patched: PatchedAttribute) -> None: def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]: - is_annotated = ( - isinstance(annotation, type(Annotated)) - and get_origin(annotation) is not None - and get_origin(annotation) is get_origin(Annotated) - ) - if is_annotated: - marker = get_args(annotation)[1] + if get_origin(parameter.annotation) is Annotated: + args = get_args(parameter.annotation) + if len(args) > 1: + marker = args[1] + else: + marker = None else: marker = parameter.default @@ -1032,19 +1031,18 @@ def _patched(*args, **kwargs): 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) - try: - annotations = inspect.get_annotations(obj) - except Exception: - annotations = {} + annotations = _get_annotations(obj) for annotation_name, annotation in annotations.items(): - is_annotated = ( - isinstance(annotation, type(Annotated)) - and get_origin(annotation) is not None - and get_origin(annotation) is get_origin(Annotated) - ) - if is_annotated: + if get_origin(annotation) is Annotated: args = get_args(annotation) if len(args) > 1: member = args[1] From 50303873082c9ea4518cf4f39c3eb9cf8e5db1d8 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 16:02:22 -0400 Subject: [PATCH 15/15] Update src/dependency_injector/wiring.py Co-authored-by: ZipFile --- src/dependency_injector/wiring.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 898ac377..6829d53b 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -1031,10 +1031,11 @@ def _patched(*args, **kwargs): return cast(F, _patched) -def _get_annotations(obj: Any) -> Dict[str, Any]: - if sys.version_info >= (3, 10): +if sys.version_info >= (3, 10): + def _get_annotations(obj: Any) -> Dict[str, Any]: return inspect.get_annotations(obj) - else: +else: + def _get_annotations(obj: Any) -> Dict[str, Any]: return getattr(obj, "__annotations__", {})