Skip to content

Commit 90a2106

Browse files
authored
Merge pull request #161 from illuin-tech/fix-pep585-list-injection
Fix NoBindingFound error being raised when using multi bindings and i…
2 parents 8ab79d9 + 62504e3 commit 90a2106

File tree

10 files changed

+126
-92
lines changed

10 files changed

+126
-92
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
Opyoid follows [semver guidelines](https://semver.org) for versioning.
44

55
## Unreleased
6+
## 3.0.1
7+
### Fixes
8+
- Fix NoBindingFound error being raised when using multi bindings and injecting pep585 style list arguments
9+
610
## 3.0.0
711
### Breaking changes
812
- Remove support for Python 3.8

opyoid/providers/provider_creator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
FromCacheProviderFactory,
1212
FromEnvVarProviderFactory,
1313
JitProviderFactory,
14+
ListFromItemsProviderFactory,
1415
ListProviderFactory,
1516
ProviderFactory,
1617
ProviderProviderFactory,
@@ -32,6 +33,7 @@ def __init__(self) -> None:
3233
FromEnvVarProviderFactory(),
3334
FromBindingProviderFactory(),
3435
ListProviderFactory(),
36+
ListFromItemsProviderFactory(),
3537
SetProviderFactory(),
3638
TupleProviderFactory(),
3739
UnionProviderFactory(),

opyoid/providers/providers_factories/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .from_cache_provider_factory import FromCacheProviderFactory
33
from .from_env_var_provider_factory import FromEnvVarProviderFactory
44
from .jit_provider_factory import JitProviderFactory
5+
from .list_from_items_provider_factory import ListFromItemsProviderFactory
56
from .list_provider_factory import ListProviderFactory
67
from .provider_factory import ProviderFactory
78
from .provider_provider_factory import ProviderProviderFactory
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import Any, cast, List
2+
3+
from opyoid.bindings import ListProvider
4+
from opyoid.injection_context import InjectionContext
5+
from opyoid.provider import Provider
6+
from opyoid.target import Target
7+
from opyoid.type_checker import TypeChecker
8+
from opyoid.utils import InjectedT
9+
from .provider_factory import ProviderFactory
10+
from ...exceptions import IncompatibleProviderFactory, NoBindingFound
11+
12+
13+
class ListFromItemsProviderFactory(ProviderFactory):
14+
"""Creates a Provider that groups the target list items providers."""
15+
16+
def create(self, context: InjectionContext[InjectedT]) -> Provider[InjectedT]:
17+
if not TypeChecker.is_list(context.target.type):
18+
raise IncompatibleProviderFactory
19+
item_providers: List[Provider[Any]] = []
20+
first_item_type = context.target.type.__args__[0] # type: ignore[union-attr]
21+
if TypeChecker.is_union(first_item_type):
22+
item_types = first_item_type.__args__
23+
else:
24+
item_types = [first_item_type]
25+
26+
for item_target_type in item_types:
27+
new_target: Target[Any] = Target(item_target_type, context.target.named)
28+
new_context = context.get_child_context(new_target)
29+
try:
30+
item_providers.append(new_context.get_provider())
31+
except NoBindingFound:
32+
pass
33+
if not item_providers:
34+
raise NoBindingFound(f"No binding found for list items of type {context.target}")
35+
return cast(Provider[InjectedT], ListProvider(item_providers))
Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,24 @@
1-
from typing import Any, cast, List
1+
from typing import Callable, cast, List
22

3-
from opyoid.bindings import ListProvider
3+
from opyoid.bindings import FromCallableProvider
44
from opyoid.injection_context import InjectionContext
55
from opyoid.provider import Provider
66
from opyoid.target import Target
77
from opyoid.type_checker import TypeChecker
88
from opyoid.utils import InjectedT
99
from .provider_factory import ProviderFactory
10-
from ...exceptions import IncompatibleProviderFactory, NoBindingFound
10+
from ...exceptions import IncompatibleProviderFactory
1111

1212

1313
class ListProviderFactory(ProviderFactory):
14-
"""Creates a Provider that groups the target set items providers."""
14+
"""Creates a Provider that groups the target list items providers."""
1515

1616
def create(self, context: InjectionContext[InjectedT]) -> Provider[InjectedT]:
17-
if not TypeChecker.is_list(context.target.type):
18-
raise IncompatibleProviderFactory
19-
item_providers: List[Provider[Any]] = []
20-
first_item_type = context.target.type.__args__[0] # type: ignore[union-attr]
21-
if TypeChecker.is_union(first_item_type):
22-
item_types = first_item_type.__args__
23-
else:
24-
item_types = [first_item_type]
25-
26-
for item_target_type in item_types:
27-
new_target: Target[Any] = Target(item_target_type, context.target.named)
17+
if TypeChecker.is_pep585_list(context.target.type):
18+
new_target: Target[List[InjectedT]] = Target(
19+
List[context.target.type.__args__[0]], # type: ignore[name-defined]
20+
context.target.named,
21+
)
2822
new_context = context.get_child_context(new_target)
29-
try:
30-
item_providers.append(new_context.get_provider())
31-
except NoBindingFound:
32-
pass
33-
if not item_providers:
34-
raise NoBindingFound(f"No binding found for list items of type {context.target}")
35-
return cast(Provider[InjectedT], ListProvider(item_providers))
23+
return FromCallableProvider(cast(Callable[..., InjectedT], list), [new_context.get_provider()], None, {})
24+
raise IncompatibleProviderFactory

opyoid/type_checker/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import sys
22

33
PEP_604 = sys.version_info[:3] >= (3, 10, 0)
4-
PEP_585 = sys.version_info[:3] >= (3, 9, 0)
54

65
if PEP_604: # pragma: nocover
76
from .pep604_type_checker import Pep604TypeChecker as TypeChecker
8-
elif PEP_585: # pragma: nocover
9-
from .pep585_type_checker import Pep585TypeChecker as TypeChecker # type: ignore[assignment]
107
else: # pragma: nocover
11-
from .pep560_type_checker import Pep560TypeChecker as TypeChecker # type: ignore[assignment]
8+
from .pep585_type_checker import Pep585TypeChecker as TypeChecker # type: ignore[assignment]

opyoid/type_checker/pep560_type_checker.py

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,51 @@
11
from types import GenericAlias
2-
from typing import Any
32

4-
from opyoid.type_checker.pep560_type_checker import Pep560TypeChecker
3+
# noinspection PyProtectedMember
4+
from typing import _GenericAlias, Any, Union # type: ignore[attr-defined]
55

6+
from opyoid.named import Named
7+
from opyoid.provider import Provider
68

7-
class Pep585TypeChecker(Pep560TypeChecker):
9+
10+
class Pep585TypeChecker:
811
"""Various helpers to check type hints."""
912

1013
@staticmethod
1114
def is_list(target_type: Any) -> bool:
12-
"""Returns True if target_type is List[<Any>] or list[<Any>]"""
13-
return Pep560TypeChecker.is_list(target_type) or (
14-
isinstance(target_type, GenericAlias) and target_type.__origin__ == list
15-
)
15+
"""Returns True if target_type is List[<Any>] or list[Any]"""
16+
return isinstance(target_type, (_GenericAlias, GenericAlias)) and bool(target_type.__origin__ == list)
17+
18+
@staticmethod
19+
def is_pep585_list(target_type: Any) -> bool:
20+
"""Returns True if target_type is list[<Any>]"""
21+
return isinstance(target_type, GenericAlias) and bool(target_type.__origin__ == list)
1622

1723
@staticmethod
1824
def is_set(target_type: Any) -> bool:
19-
"""Returns True if target_type is Set[<Any>] or set[<Any>]"""
20-
return Pep560TypeChecker.is_set(target_type) or (
21-
isinstance(target_type, GenericAlias) and target_type.__origin__ == set
22-
)
25+
"""Returns True if target_type is Set[<Any>]"""
26+
return isinstance(target_type, (_GenericAlias, GenericAlias)) and bool(target_type.__origin__ == set)
2327

2428
@staticmethod
2529
def is_tuple(target_type: Any) -> bool:
26-
"""Returns True if target_type is Tuple[<Any>] or tuple[<Any>]"""
27-
return Pep560TypeChecker.is_tuple(target_type) or (
28-
isinstance(target_type, GenericAlias) and target_type.__origin__ == tuple
29-
)
30+
"""Returns True if target_type is Tuple[<Any>]"""
31+
return isinstance(target_type, (_GenericAlias, GenericAlias)) and bool(target_type.__origin__ == tuple)
32+
33+
@staticmethod
34+
def is_provider(target_type: Any) -> bool:
35+
"""Returns True if target_type is Provider[<Any>]"""
36+
return isinstance(target_type, _GenericAlias) and target_type.__origin__ == Provider
37+
38+
@staticmethod
39+
def is_named(target_type: Any) -> bool:
40+
"""Returns True if target_type is Named[<Any>]"""
41+
return isinstance(target_type, type) and issubclass(target_type, Named)
42+
43+
@staticmethod
44+
def is_union(target_type: Any) -> bool:
45+
"""Returns True if target_type is Union[<Any>, <Any>...] or Optional[<Any>]"""
46+
return isinstance(target_type, _GenericAlias) and target_type.__origin__ == Union
3047

3148
@staticmethod
3249
def is_type(target_type: Any) -> bool:
3350
"""Returns True if target_type is Type[<Any>]"""
34-
return Pep560TypeChecker.is_type(target_type) or (
35-
isinstance(target_type, GenericAlias) and target_type.__origin__ == type
36-
)
51+
return isinstance(target_type, (_GenericAlias, GenericAlias)) and bool(target_type.__origin__ == type)

tests/test_type_checker.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from opyoid import Provider
55
from opyoid.named import Named
6-
from opyoid.type_checker import PEP_585, PEP_604, TypeChecker
6+
from opyoid.type_checker import PEP_604, TypeChecker
77

88

99
class TestClass:
@@ -126,8 +126,6 @@ class MyNamedType(Named[str]):
126126
self.assertFalse(self.type_checker.is_named(Tuple[TestClass]))
127127
self.assertTrue(self.type_checker.is_named(MyNamedType))
128128

129-
# pylint: disable=unsubscriptable-object
130-
@unittest.skipIf(not PEP_585, "Python 3.9 required")
131129
def test_pep585_style(self):
132130
self.assertTrue(self.type_checker.is_list(list[str]))
133131
self.assertFalse(self.type_checker.is_set(list[str]))

tests_e2e/test_injection.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,3 +1413,41 @@ def configure(self) -> None:
14131413
self.assertIsNot(result[0], result[1])
14141414
self.assertIs(result[0], result[2])
14151415
self.assertIs(result[2], instance)
1416+
1417+
def test_multi_bind_with_pep_589(self):
1418+
class SubClass1(MyClass):
1419+
pass
1420+
1421+
class SubClass2(MyClass):
1422+
pass
1423+
1424+
class MultiModule(Module):
1425+
def configure(self) -> None:
1426+
self.bind(MyClass, to_class=SubClass1)
1427+
self.multi_bind(
1428+
MyClass,
1429+
[
1430+
self.bind_item(to_class=SubClass1),
1431+
self.bind_item(to_class=SubClass2),
1432+
self.bind_item(to_class=SubClass1),
1433+
],
1434+
)
1435+
1436+
injector = Injector([MultiModule()])
1437+
result_1 = injector.inject(list[MyClass])
1438+
result_2 = injector.inject(List[MyClass])
1439+
instance = injector.inject(MyClass)
1440+
self.assertEqual(3, len(result_1))
1441+
self.assertEqual(3, len(result_2))
1442+
self.assertIsNot(result_1[0], result_1[1])
1443+
self.assertIsNot(result_2[0], result_2[1])
1444+
self.assertIs(result_1[1], result_2[1])
1445+
self.assertIs(result_1[0], result_1[2])
1446+
self.assertIs(result_2[0], result_2[2])
1447+
self.assertIs(result_1[0], result_2[2])
1448+
self.assertIs(result_1[2], instance)
1449+
1450+
def test_multi_bind_with_no_binding(self):
1451+
injector = Injector([])
1452+
with self.assertRaises(NoBindingFound):
1453+
injector.inject(list[MyClass])

0 commit comments

Comments
 (0)