diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5d807..ba5c05a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Opyoid follows [semver guidelines](https://semver.org) for versioning. ## Unreleased +## 3.0.1 +### Fixes +- Fix NoBindingFound error being raised when using multi bindings and injecting pep585 style list arguments + ## 3.0.0 ### Breaking changes - Remove support for Python 3.8 diff --git a/opyoid/providers/provider_creator.py b/opyoid/providers/provider_creator.py index de6eada..4c40cc2 100644 --- a/opyoid/providers/provider_creator.py +++ b/opyoid/providers/provider_creator.py @@ -11,6 +11,7 @@ FromCacheProviderFactory, FromEnvVarProviderFactory, JitProviderFactory, + ListFromItemsProviderFactory, ListProviderFactory, ProviderFactory, ProviderProviderFactory, @@ -32,6 +33,7 @@ def __init__(self) -> None: FromEnvVarProviderFactory(), FromBindingProviderFactory(), ListProviderFactory(), + ListFromItemsProviderFactory(), SetProviderFactory(), TupleProviderFactory(), UnionProviderFactory(), diff --git a/opyoid/providers/providers_factories/__init__.py b/opyoid/providers/providers_factories/__init__.py index 338d304..87a0c7f 100644 --- a/opyoid/providers/providers_factories/__init__.py +++ b/opyoid/providers/providers_factories/__init__.py @@ -2,6 +2,7 @@ from .from_cache_provider_factory import FromCacheProviderFactory from .from_env_var_provider_factory import FromEnvVarProviderFactory from .jit_provider_factory import JitProviderFactory +from .list_from_items_provider_factory import ListFromItemsProviderFactory from .list_provider_factory import ListProviderFactory from .provider_factory import ProviderFactory from .provider_provider_factory import ProviderProviderFactory diff --git a/opyoid/providers/providers_factories/list_from_items_provider_factory.py b/opyoid/providers/providers_factories/list_from_items_provider_factory.py new file mode 100644 index 0000000..e6c235d --- /dev/null +++ b/opyoid/providers/providers_factories/list_from_items_provider_factory.py @@ -0,0 +1,35 @@ +from typing import Any, cast, List + +from opyoid.bindings import ListProvider +from opyoid.injection_context import InjectionContext +from opyoid.provider import Provider +from opyoid.target import Target +from opyoid.type_checker import TypeChecker +from opyoid.utils import InjectedT +from .provider_factory import ProviderFactory +from ...exceptions import IncompatibleProviderFactory, NoBindingFound + + +class ListFromItemsProviderFactory(ProviderFactory): + """Creates a Provider that groups the target list items providers.""" + + def create(self, context: InjectionContext[InjectedT]) -> Provider[InjectedT]: + if not TypeChecker.is_list(context.target.type): + raise IncompatibleProviderFactory + item_providers: List[Provider[Any]] = [] + first_item_type = context.target.type.__args__[0] # type: ignore[union-attr] + if TypeChecker.is_union(first_item_type): + item_types = first_item_type.__args__ + else: + item_types = [first_item_type] + + for item_target_type in item_types: + new_target: Target[Any] = Target(item_target_type, context.target.named) + new_context = context.get_child_context(new_target) + try: + item_providers.append(new_context.get_provider()) + except NoBindingFound: + pass + if not item_providers: + raise NoBindingFound(f"No binding found for list items of type {context.target}") + return cast(Provider[InjectedT], ListProvider(item_providers)) diff --git a/opyoid/providers/providers_factories/list_provider_factory.py b/opyoid/providers/providers_factories/list_provider_factory.py index 7d4ca7e..b77277f 100644 --- a/opyoid/providers/providers_factories/list_provider_factory.py +++ b/opyoid/providers/providers_factories/list_provider_factory.py @@ -1,35 +1,24 @@ -from typing import Any, cast, List +from typing import Callable, cast, List -from opyoid.bindings import ListProvider +from opyoid.bindings import FromCallableProvider from opyoid.injection_context import InjectionContext from opyoid.provider import Provider from opyoid.target import Target from opyoid.type_checker import TypeChecker from opyoid.utils import InjectedT from .provider_factory import ProviderFactory -from ...exceptions import IncompatibleProviderFactory, NoBindingFound +from ...exceptions import IncompatibleProviderFactory class ListProviderFactory(ProviderFactory): - """Creates a Provider that groups the target set items providers.""" + """Creates a Provider that groups the target list items providers.""" def create(self, context: InjectionContext[InjectedT]) -> Provider[InjectedT]: - if not TypeChecker.is_list(context.target.type): - raise IncompatibleProviderFactory - item_providers: List[Provider[Any]] = [] - first_item_type = context.target.type.__args__[0] # type: ignore[union-attr] - if TypeChecker.is_union(first_item_type): - item_types = first_item_type.__args__ - else: - item_types = [first_item_type] - - for item_target_type in item_types: - new_target: Target[Any] = Target(item_target_type, context.target.named) + if TypeChecker.is_pep585_list(context.target.type): + new_target: Target[List[InjectedT]] = Target( + List[context.target.type.__args__[0]], # type: ignore[name-defined] + context.target.named, + ) new_context = context.get_child_context(new_target) - try: - item_providers.append(new_context.get_provider()) - except NoBindingFound: - pass - if not item_providers: - raise NoBindingFound(f"No binding found for list items of type {context.target}") - return cast(Provider[InjectedT], ListProvider(item_providers)) + return FromCallableProvider(cast(Callable[..., InjectedT], list), [new_context.get_provider()], None, {}) + raise IncompatibleProviderFactory diff --git a/opyoid/type_checker/__init__.py b/opyoid/type_checker/__init__.py index 8ffd9b8..ea9ae13 100644 --- a/opyoid/type_checker/__init__.py +++ b/opyoid/type_checker/__init__.py @@ -1,11 +1,8 @@ import sys PEP_604 = sys.version_info[:3] >= (3, 10, 0) -PEP_585 = sys.version_info[:3] >= (3, 9, 0) if PEP_604: # pragma: nocover from .pep604_type_checker import Pep604TypeChecker as TypeChecker -elif PEP_585: # pragma: nocover - from .pep585_type_checker import Pep585TypeChecker as TypeChecker # type: ignore[assignment] else: # pragma: nocover - from .pep560_type_checker import Pep560TypeChecker as TypeChecker # type: ignore[assignment] + from .pep585_type_checker import Pep585TypeChecker as TypeChecker # type: ignore[assignment] diff --git a/opyoid/type_checker/pep560_type_checker.py b/opyoid/type_checker/pep560_type_checker.py deleted file mode 100644 index cbff2d2..0000000 --- a/opyoid/type_checker/pep560_type_checker.py +++ /dev/null @@ -1,45 +0,0 @@ -# noinspection PyUnresolvedReferences,PyProtectedMember -from typing import _GenericAlias, Any, Union # type: ignore[attr-defined] - -from opyoid.named import Named -from opyoid.provider import Provider - - -# noinspection PyUnresolvedReferences -class Pep560TypeChecker: - """Various helpers to check type hints.""" - - @staticmethod - def is_list(target_type: Any) -> bool: - """Returns True if target_type is List[]""" - return isinstance(target_type, _GenericAlias) and target_type.__origin__ == list - - @staticmethod - def is_set(target_type: Any) -> bool: - """Returns True if target_type is Set[]""" - return isinstance(target_type, _GenericAlias) and target_type.__origin__ == set - - @staticmethod - def is_tuple(target_type: Any) -> bool: - """Returns True if target_type is Tuple[]""" - return isinstance(target_type, _GenericAlias) and target_type.__origin__ == tuple - - @staticmethod - def is_provider(target_type: Any) -> bool: - """Returns True if target_type is Provider[]""" - return isinstance(target_type, _GenericAlias) and target_type.__origin__ == Provider - - @staticmethod - def is_named(target_type: Any) -> bool: - """Returns True if target_type is Named[]""" - return isinstance(target_type, type) and issubclass(target_type, Named) - - @staticmethod - def is_union(target_type: Any) -> bool: - """Returns True if target_type is Union[, ...] or Optional[]""" - return isinstance(target_type, _GenericAlias) and target_type.__origin__ == Union - - @staticmethod - def is_type(target_type: Any) -> bool: - """Returns True if target_type is Type[]""" - return isinstance(target_type, _GenericAlias) and target_type.__origin__ == type diff --git a/opyoid/type_checker/pep585_type_checker.py b/opyoid/type_checker/pep585_type_checker.py index 9373e91..b7d6369 100644 --- a/opyoid/type_checker/pep585_type_checker.py +++ b/opyoid/type_checker/pep585_type_checker.py @@ -1,36 +1,51 @@ from types import GenericAlias -from typing import Any -from opyoid.type_checker.pep560_type_checker import Pep560TypeChecker +# noinspection PyProtectedMember +from typing import _GenericAlias, Any, Union # type: ignore[attr-defined] +from opyoid.named import Named +from opyoid.provider import Provider -class Pep585TypeChecker(Pep560TypeChecker): + +class Pep585TypeChecker: """Various helpers to check type hints.""" @staticmethod def is_list(target_type: Any) -> bool: - """Returns True if target_type is List[] or list[]""" - return Pep560TypeChecker.is_list(target_type) or ( - isinstance(target_type, GenericAlias) and target_type.__origin__ == list - ) + """Returns True if target_type is List[] or list[Any]""" + return isinstance(target_type, (_GenericAlias, GenericAlias)) and bool(target_type.__origin__ == list) + + @staticmethod + def is_pep585_list(target_type: Any) -> bool: + """Returns True if target_type is list[]""" + return isinstance(target_type, GenericAlias) and bool(target_type.__origin__ == list) @staticmethod def is_set(target_type: Any) -> bool: - """Returns True if target_type is Set[] or set[]""" - return Pep560TypeChecker.is_set(target_type) or ( - isinstance(target_type, GenericAlias) and target_type.__origin__ == set - ) + """Returns True if target_type is Set[]""" + return isinstance(target_type, (_GenericAlias, GenericAlias)) and bool(target_type.__origin__ == set) @staticmethod def is_tuple(target_type: Any) -> bool: - """Returns True if target_type is Tuple[] or tuple[]""" - return Pep560TypeChecker.is_tuple(target_type) or ( - isinstance(target_type, GenericAlias) and target_type.__origin__ == tuple - ) + """Returns True if target_type is Tuple[]""" + return isinstance(target_type, (_GenericAlias, GenericAlias)) and bool(target_type.__origin__ == tuple) + + @staticmethod + def is_provider(target_type: Any) -> bool: + """Returns True if target_type is Provider[]""" + return isinstance(target_type, _GenericAlias) and target_type.__origin__ == Provider + + @staticmethod + def is_named(target_type: Any) -> bool: + """Returns True if target_type is Named[]""" + return isinstance(target_type, type) and issubclass(target_type, Named) + + @staticmethod + def is_union(target_type: Any) -> bool: + """Returns True if target_type is Union[, ...] or Optional[]""" + return isinstance(target_type, _GenericAlias) and target_type.__origin__ == Union @staticmethod def is_type(target_type: Any) -> bool: """Returns True if target_type is Type[]""" - return Pep560TypeChecker.is_type(target_type) or ( - isinstance(target_type, GenericAlias) and target_type.__origin__ == type - ) + return isinstance(target_type, (_GenericAlias, GenericAlias)) and bool(target_type.__origin__ == type) diff --git a/tests/test_type_checker.py b/tests/test_type_checker.py index 2e62f6e..34848be 100644 --- a/tests/test_type_checker.py +++ b/tests/test_type_checker.py @@ -3,7 +3,7 @@ from opyoid import Provider from opyoid.named import Named -from opyoid.type_checker import PEP_585, PEP_604, TypeChecker +from opyoid.type_checker import PEP_604, TypeChecker class TestClass: @@ -126,8 +126,6 @@ class MyNamedType(Named[str]): self.assertFalse(self.type_checker.is_named(Tuple[TestClass])) self.assertTrue(self.type_checker.is_named(MyNamedType)) - # pylint: disable=unsubscriptable-object - @unittest.skipIf(not PEP_585, "Python 3.9 required") def test_pep585_style(self): self.assertTrue(self.type_checker.is_list(list[str])) self.assertFalse(self.type_checker.is_set(list[str])) diff --git a/tests_e2e/test_injection.py b/tests_e2e/test_injection.py index ad69d79..15011f3 100644 --- a/tests_e2e/test_injection.py +++ b/tests_e2e/test_injection.py @@ -1413,3 +1413,41 @@ def configure(self) -> None: self.assertIsNot(result[0], result[1]) self.assertIs(result[0], result[2]) self.assertIs(result[2], instance) + + def test_multi_bind_with_pep_589(self): + class SubClass1(MyClass): + pass + + class SubClass2(MyClass): + pass + + class MultiModule(Module): + def configure(self) -> None: + self.bind(MyClass, to_class=SubClass1) + self.multi_bind( + MyClass, + [ + self.bind_item(to_class=SubClass1), + self.bind_item(to_class=SubClass2), + self.bind_item(to_class=SubClass1), + ], + ) + + injector = Injector([MultiModule()]) + result_1 = injector.inject(list[MyClass]) + result_2 = injector.inject(List[MyClass]) + instance = injector.inject(MyClass) + self.assertEqual(3, len(result_1)) + self.assertEqual(3, len(result_2)) + self.assertIsNot(result_1[0], result_1[1]) + self.assertIsNot(result_2[0], result_2[1]) + self.assertIs(result_1[1], result_2[1]) + self.assertIs(result_1[0], result_1[2]) + self.assertIs(result_2[0], result_2[2]) + self.assertIs(result_1[0], result_2[2]) + self.assertIs(result_1[2], instance) + + def test_multi_bind_with_no_binding(self): + injector = Injector([]) + with self.assertRaises(NoBindingFound): + injector.inject(list[MyClass])