From 00a2dd7a3dc65a88b0a02f112a5e1149390da55b Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 23 Nov 2023 19:12:33 -0300 Subject: [PATCH 1/3] fix: resolve protocols without needing to define init --- rodi/__init__.py | 7 +++- tests/test_services.py | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/rodi/__init__.py b/rodi/__init__.py index ffead96..08515f7 100644 --- a/rodi/__init__.py +++ b/rodi/__init__.py @@ -16,6 +16,7 @@ Type, TypeVar, Union, + _no_init_or_replace_init, cast, get_type_hints, ) @@ -605,7 +606,11 @@ def __call__(self, context: ResolutionContext): chain = context.dynamic_chain chain.append(concrete_type) - if getattr(concrete_type, "__init__") is object.__init__: + if getattr(concrete_type, "__init__") in [ + object.__init__, + # for protocols that doesn't defile its own init: + _no_init_or_replace_init, + ]: annotations = get_type_hints( concrete_type, vars(sys.modules[concrete_type.__module__]), diff --git a/tests/test_services.py b/tests/test_services.py index 9430813..1015049 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -2,6 +2,7 @@ from abc import ABC from dataclasses import dataclass from typing import ( + Any, ClassVar, Dict, Generic, @@ -2474,6 +2475,62 @@ class B: assert isinstance(value, DynamicResolver) +def test_provide_protocol_with_attribute_dependency() -> None: + class P(Protocol): + def foo(self) -> Any: + ... + + class Dependency: + pass + + class Impl(P): + # attribute dependency + dependency: Dependency + + def foo(self) -> Any: + pass + + container = Container() + container.register(Dependency) + container.register(Impl) + + try: + resolved = container.resolve(Impl) + except CannotResolveParameterException as e: + pytest.fail(str(e)) + + assert isinstance(resolved, Impl) + assert isinstance(resolved.dependency, Dependency) + + +def test_provide_protocol_with_init_dependency() -> None: + class P(Protocol): + def foo(self) -> Any: + ... + + class Dependency: + pass + + class Impl(P): + def __init__(self, dependency: Dependency) -> None: + self.dependency = dependency + + def foo(self) -> Any: + pass + + container = Container() + container.register(Dependency) + container.register(Impl) + + try: + resolved = container.resolve(Impl) + except CannotResolveParameterException as e: + pytest.fail(str(e)) + + assert isinstance(resolved, Impl) + assert isinstance(resolved.dependency, Dependency) + + def test_provide_protocol_generic() -> None: T = TypeVar("T") @@ -2500,6 +2557,39 @@ def foo(self, t: A) -> A: assert isinstance(resolved, Impl) +def test_provide_protocol_generic_with_inner_dependency() -> None: + T = TypeVar("T") + + class P(Protocol[T]): + def foo(self, t: T) -> T: + ... + + class A: + ... + + class Dependency: + pass + + class Impl(P[A]): + dependency: Dependency + + def foo(self, t: A) -> A: + return t + + container = Container() + + container.register(Impl) + container.register(Dependency) + + try: + resolved = container.resolve(Impl) + except CannotResolveParameterException as e: + pytest.fail(str(e)) + + assert isinstance(resolved, Impl) + assert isinstance(resolved.dependency, Dependency) + + def test_ignore_class_var(): """ ClassVar attributes must be ignored, because they are not instance attributes. From 1b9b749c01153ac2ac20ee1788dc6e2ae912c31a Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 23 Nov 2023 20:07:21 -0300 Subject: [PATCH 2/3] fix: support python 3.8 --- rodi/__init__.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/rodi/__init__.py b/rodi/__init__.py index 08515f7..5852acf 100644 --- a/rodi/__init__.py +++ b/rodi/__init__.py @@ -16,11 +16,14 @@ Type, TypeVar, Union, - _no_init_or_replace_init, cast, get_type_hints, ) +if sys.version_info >= (3, 9): # pragma: no cover + # Python 3.9 + from typing import _no_init_or_replace_init + try: from typing import Protocol except ImportError: # pragma: no cover @@ -580,6 +583,16 @@ def _ignore_class_attribute(self, key: str, value) -> bool: return is_classvar or is_initialized + def _has_default_init(self): + if self.concrete_type.__init__ is object.__init__: + return True + + if sys.version_info >= (3, 9): # pragma: no cover + # Python 3.9 + if self.concrete_type.__init__ is _no_init_or_replace_init: + return True + return False + def _resolve_by_annotations( self, context: ResolutionContext, annotations: Dict[str, Type] ): @@ -606,11 +619,7 @@ def __call__(self, context: ResolutionContext): chain = context.dynamic_chain chain.append(concrete_type) - if getattr(concrete_type, "__init__") in [ - object.__init__, - # for protocols that doesn't defile its own init: - _no_init_or_replace_init, - ]: + if self._has_default_init(): annotations = get_type_hints( concrete_type, vars(sys.modules[concrete_type.__module__]), From 754407c587acb2b2a1afd9fa06480336bb3755b3 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Fri, 24 Nov 2023 01:22:08 -0300 Subject: [PATCH 3/3] fix: support _no_init on python 3.8 --- rodi/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rodi/__init__.py b/rodi/__init__.py index 5852acf..d367dde 100644 --- a/rodi/__init__.py +++ b/rodi/__init__.py @@ -21,8 +21,9 @@ ) if sys.version_info >= (3, 9): # pragma: no cover - # Python 3.9 - from typing import _no_init_or_replace_init + from typing import _no_init_or_replace_init as _no_init +elif sys.version_info >= (3, 8): # pragma: no cover + from typing import _no_init try: from typing import Protocol @@ -584,12 +585,13 @@ def _ignore_class_attribute(self, key: str, value) -> bool: return is_classvar or is_initialized def _has_default_init(self): - if self.concrete_type.__init__ is object.__init__: + init = getattr(self.concrete_type, "__init__", None) + + if init is object.__init__: return True - if sys.version_info >= (3, 9): # pragma: no cover - # Python 3.9 - if self.concrete_type.__init__ is _no_init_or_replace_init: + if sys.version_info >= (3, 8): # pragma: no cover + if init is _no_init: return True return False