Skip to content

Commit 613db27

Browse files
authored
Merge pull request #60 from eadwinCode/injector_and_injectable
Resolvable Injectable Class
2 parents ced2f21 + bb5fca8 commit 613db27

File tree

6 files changed

+215
-114
lines changed

6 files changed

+215
-114
lines changed

ellar/di/injector/container.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import typing as t
22
from inspect import isabstract
33

4-
from injector import Binder as InjectorBinder, Binding, Module as InjectorModule
4+
from injector import (
5+
AssistedBuilder,
6+
Binder as InjectorBinder,
7+
Binding,
8+
Module as InjectorModule,
9+
Scope as InjectorScope,
10+
UnsatisfiedRequirement,
11+
_is_specialization,
12+
)
513

614
from ellar.constants import NOT_SET
715
from ellar.helper import get_name
@@ -15,7 +23,7 @@
1523
SingletonScope,
1624
TransientScope,
1725
)
18-
from ..service_config import get_scope
26+
from ..service_config import get_scope, is_decorated_with_injectable
1927

2028
if t.TYPE_CHECKING: # pragma: no cover
2129
from ellar.core.modules import ModuleBase
@@ -43,11 +51,36 @@ def create_binding(
4351
scope: t.Union[ScopeDecorator, t.Type[DIScope]] = None,
4452
) -> Binding:
4553
provider = self.provider_for(interface, to)
46-
scope = scope or getattr(to or interface, "__scope__", TransientScope)
54+
scope = scope or get_scope(to or interface) or TransientScope
4755
if isinstance(scope, ScopeDecorator):
4856
scope = scope.scope
4957
return Binding(interface, provider, scope)
5058

59+
def get_binding(self, interface: type) -> t.Tuple[Binding, InjectorBinder]:
60+
is_scope = isinstance(interface, type) and issubclass(interface, InjectorScope)
61+
is_assisted_builder = _is_specialization(interface, AssistedBuilder)
62+
try:
63+
return self._get_binding(
64+
interface, only_this_binder=is_scope or is_assisted_builder
65+
)
66+
except (KeyError, UnsatisfiedRequirement):
67+
if is_scope:
68+
scope = interface
69+
self.bind(scope, to=scope(self.injector))
70+
return self._get_binding(interface)
71+
# The special interface is added here so that requesting a special
72+
# interface with auto_bind disabled works
73+
if (
74+
self._auto_bind
75+
or self._is_special_interface(interface)
76+
or is_decorated_with_injectable(interface)
77+
):
78+
binding = self.create_binding(interface)
79+
self._bindings[interface] = binding
80+
return binding, self
81+
82+
raise UnsatisfiedRequirement(None, interface)
83+
5184
def register_binding(self, interface: t.Type, binding: Binding) -> None:
5285
self._bindings[interface] = binding
5386

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import pytest
2+
3+
from ellar.di import (
4+
EllarInjector,
5+
injectable,
6+
request_scope,
7+
singleton_scope,
8+
transient_scope,
9+
)
10+
from ellar.di.exceptions import UnsatisfiedRequirement
11+
12+
13+
@injectable(scope=transient_scope)
14+
class SampleInjectableA:
15+
pass
16+
17+
18+
@injectable(scope=request_scope)
19+
class SampleInjectableB:
20+
pass
21+
22+
23+
@injectable(scope=singleton_scope)
24+
class SampleInjectableC:
25+
pass
26+
27+
28+
class MustBeRegisteredToResolve:
29+
"""This class must be registered to resolved or EllarInjector auto_bind must be true"""
30+
31+
pass
32+
33+
34+
def test_injectable_class_can_be_resolved_at_runtime_without_if_they_are_not_registered():
35+
injector = EllarInjector(auto_bind=False)
36+
37+
assert isinstance(injector.get(SampleInjectableA), SampleInjectableA)
38+
assert isinstance(injector.get(SampleInjectableB), SampleInjectableB)
39+
assert isinstance(injector.get(SampleInjectableC), SampleInjectableC)
40+
41+
with pytest.raises(UnsatisfiedRequirement):
42+
injector.get(MustBeRegisteredToResolve)
43+
44+
injector.container.register_scoped(MustBeRegisteredToResolve)
45+
assert isinstance(
46+
injector.get(MustBeRegisteredToResolve), MustBeRegisteredToResolve
47+
)
48+
49+
injector = EllarInjector(auto_bind=True)
50+
assert isinstance(
51+
injector.get(MustBeRegisteredToResolve), MustBeRegisteredToResolve
52+
)
53+
54+
55+
@pytest.mark.asyncio
56+
async def test_injectable_class_uses_defined_scope_during_runtime():
57+
injector = EllarInjector(auto_bind=True)
58+
# transient scope
59+
assert injector.get(SampleInjectableA) != injector.get(SampleInjectableA)
60+
# request scope outside request
61+
assert injector.get(SampleInjectableB) != injector.get(SampleInjectableB)
62+
# singleton scope
63+
assert injector.get(SampleInjectableC) == injector.get(SampleInjectableC)
64+
# transient scope by default
65+
assert injector.get(MustBeRegisteredToResolve) != injector.get(
66+
MustBeRegisteredToResolve
67+
)
68+
69+
async with injector.create_asgi_args():
70+
# request scope outside request
71+
assert injector.get(SampleInjectableB) == injector.get(SampleInjectableB)

tests/test_di/test_provider_scopes.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
2+
from injector import inject
23

3-
from ellar.di import EllarInjector, ProviderConfig
4+
from ellar.di import EllarInjector, ProviderConfig, has_binding
5+
from ellar.di.exceptions import DIImproperConfiguration
46
from ellar.di.scopes import RequestScope, SingletonScope, TransientScope
57

68
from .examples import AnyContext, Foo, IContext
@@ -44,3 +46,20 @@ async def test_request_scope_instance():
4446
async with injector.create_asgi_args() as request_provider:
4547
# resolving RequestScope during request will behave like singleton
4648
assert request_provider.get(IContext) == request_provider.get(IContext)
49+
50+
51+
def test_invalid_use_of_provider_config():
52+
with pytest.raises(DIImproperConfiguration):
53+
ProviderConfig(IContext, use_class=AnyContext, use_value=AnyContext())
54+
55+
56+
def test_has_binding_works():
57+
@inject
58+
def inject_function(a: IContext):
59+
pass
60+
61+
assert has_binding(IContext) is False
62+
assert has_binding(AnyContext) is True
63+
64+
assert has_binding(lambda n: print("Hello")) is False
65+
assert has_binding(inject_function) is True

tests/test_di/test_providers.py

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import pytest
2-
from injector import (
3-
CircularDependency,
4-
UnsatisfiedRequirement,
5-
is_decorated_with_inject,
6-
)
2+
from injector import CircularDependency, is_decorated_with_inject
73

84
from ellar.di import (
95
EllarInjector,
@@ -99,25 +95,12 @@ def test_provider_advance_use_case():
9995
assert isinstance(db_context, AnyDBContext)
10096
assert repository.context == db_context # service registered as singleton
10197

102-
with pytest.raises(UnsatisfiedRequirement):
103-
injector.get(AnyDBContext)
104-
105-
with pytest.raises(UnsatisfiedRequirement):
106-
injector.get(FooDBCatsRepository)
107-
108-
providers_advance.append(ProviderConfig(AnyDBContext))
109-
providers_advance.append(ProviderConfig(FooDBCatsRepository))
110-
111-
injector = EllarInjector(auto_bind=False)
112-
113-
for provider in providers_advance:
114-
provider.register(injector.container)
115-
116-
assert injector.get(AnyDBContext)
117-
assert injector.get(FooDBCatsRepository)
118-
119-
assert isinstance(injector.get(IRepository), FooDBCatsRepository)
120-
assert isinstance(injector.get(IDBContext), AnyDBContext)
98+
assert isinstance(
99+
injector.get(FooDBCatsRepository), FooDBCatsRepository
100+
) # only possible because they are decorated with injectable
101+
assert isinstance(
102+
injector.get(AnyDBContext), AnyDBContext
103+
) # only possible because they are decorated with injectable
121104

122105

123106
def test_module_provider_works():

0 commit comments

Comments
 (0)