Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions opyoid/providers/provider_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
FromCacheProviderFactory,
FromEnvVarProviderFactory,
JitProviderFactory,
ListFromItemsProviderFactory,
ListProviderFactory,
ProviderFactory,
ProviderProviderFactory,
Expand All @@ -32,6 +33,7 @@ def __init__(self) -> None:
FromEnvVarProviderFactory(),
FromBindingProviderFactory(),
ListProviderFactory(),
ListFromItemsProviderFactory(),
SetProviderFactory(),
TupleProviderFactory(),
UnionProviderFactory(),
Expand Down
1 change: 1 addition & 0 deletions opyoid/providers/providers_factories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
33 changes: 11 additions & 22 deletions opyoid/providers/providers_factories/list_provider_factory.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 1 addition & 4 deletions opyoid/type_checker/__init__.py
Original file line number Diff line number Diff line change
@@ -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]
45 changes: 0 additions & 45 deletions opyoid/type_checker/pep560_type_checker.py

This file was deleted.

51 changes: 33 additions & 18 deletions opyoid/type_checker/pep585_type_checker.py
Original file line number Diff line number Diff line change
@@ -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[<Any>] or list[<Any>]"""
return Pep560TypeChecker.is_list(target_type) or (
isinstance(target_type, GenericAlias) and target_type.__origin__ == list
)
"""Returns True if target_type is List[<Any>] 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[<Any>]"""
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[<Any>] or set[<Any>]"""
return Pep560TypeChecker.is_set(target_type) or (
isinstance(target_type, GenericAlias) and target_type.__origin__ == set
)
"""Returns True if target_type is Set[<Any>]"""
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[<Any>] or tuple[<Any>]"""
return Pep560TypeChecker.is_tuple(target_type) or (
isinstance(target_type, GenericAlias) and target_type.__origin__ == tuple
)
"""Returns True if target_type is Tuple[<Any>]"""
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[<Any>]"""
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[<Any>]"""
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[<Any>, <Any>...] or Optional[<Any>]"""
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[<Any>]"""
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)
4 changes: 1 addition & 3 deletions tests/test_type_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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]))
Expand Down
38 changes: 38 additions & 0 deletions tests_e2e/test_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Loading