Skip to content

Commit 457ba06

Browse files
authored
feat: ✨ get_lazy_instance is sync with module's injectables
1 parent 0ff2eb5 commit 457ba06

File tree

7 files changed

+118
-58
lines changed

7 files changed

+118
-58
lines changed

injection/_pkg.pyi

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ from typing import (
1313
runtime_checkable,
1414
)
1515

16-
from injection.common.lazy import Lazy
16+
from injection.common.invertible import Invertible
1717

1818
_T = TypeVar("_T")
1919

@@ -104,11 +104,16 @@ class Module:
104104
will be raised.
105105
"""
106106

107-
def get_lazy_instance(self, cls: type[_T]) -> Lazy[_T | None]:
107+
def get_lazy_instance(
108+
self,
109+
cls: type[_T],
110+
cache: bool = ...,
111+
) -> Invertible[_T | None]:
108112
"""
109113
Function used to retrieve an instance associated with the type passed in
110-
parameter or `None`. Return a `Lazy` object. To access the instance contained
111-
in a lazy object, simply use a wavy line (~).
114+
parameter or `None`. Return a `Invertible` object. To access the instance
115+
contained in an invertible object, simply use a wavy line (~).
116+
With `cache=True`, the instance retrieved will always be the same.
112117
113118
Example: instance = ~lazy_instance
114119
"""

injection/common/invertible.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from abc import abstractmethod
2+
from collections.abc import Callable
3+
from dataclasses import dataclass
4+
from typing import Protocol, TypeVar, runtime_checkable
5+
6+
__all__ = ("Invertible", "SimpleInvertible")
7+
8+
_T = TypeVar("_T")
9+
10+
11+
@runtime_checkable
12+
class Invertible(Protocol[_T]):
13+
@abstractmethod
14+
def __invert__(self) -> _T:
15+
raise NotImplementedError
16+
17+
18+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
19+
class SimpleInvertible(Invertible[_T]):
20+
callable: Callable[[], _T]
21+
22+
def __invert__(self) -> _T:
23+
return self.callable()

injection/common/lazy.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from collections.abc import Callable, Iterator, Mapping
22
from types import MappingProxyType
3-
from typing import Generic, TypeVar
3+
from typing import TypeVar
4+
5+
from injection.common.invertible import Invertible
46

57
__all__ = ("Lazy", "LazyMapping")
68

@@ -9,7 +11,7 @@
911
_V = TypeVar("_V")
1012

1113

12-
class Lazy(Generic[_T]):
14+
class Lazy(Invertible[_T]):
1315
__slots__ = ("__cache", "__is_set")
1416

1517
def __init__(self, factory: Callable[[], _T]):
@@ -23,7 +25,7 @@ def is_set(self) -> bool:
2325
return self.__is_set
2426

2527
def __setup_cache(self, factory: Callable[[], _T]):
26-
def new_cache() -> Iterator[_T]:
28+
def cache_generator() -> Iterator[_T]:
2729
nonlocal factory
2830
cached = factory()
2931
self.__is_set = True
@@ -32,7 +34,7 @@ def new_cache() -> Iterator[_T]:
3234
while True:
3335
yield cached
3436

35-
self.__cache = new_cache()
37+
self.__cache = cache_generator()
3638
self.__is_set = False
3739

3840

injection/common/queue.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def add(self, item: _T):
3232
return self
3333

3434

35-
class NoQueue(Queue[_T]):
35+
class DeadQueue(Queue[_T]):
3636
__slots__ = ()
3737

3838
def __bool__(self) -> bool:
@@ -42,23 +42,22 @@ def __next__(self) -> NoReturn:
4242
raise StopIteration
4343

4444
def add(self, item: _T) -> NoReturn:
45-
raise TypeError("Queue doesn't exist.")
45+
raise TypeError("Queue is dead.")
4646

4747

4848
@dataclass(repr=False, slots=True)
4949
class LimitedQueue(Queue[_T]):
50-
__queue: Queue[_T] = field(default_factory=SimpleQueue)
50+
__state: Queue[_T] = field(default_factory=SimpleQueue)
5151

5252
def __next__(self) -> _T:
53-
if not self.__queue:
54-
raise StopIteration
55-
5653
try:
57-
return next(self.__queue)
54+
return next(self.__state)
5855
except StopIteration as exc:
59-
self.__queue = NoQueue()
56+
if self.__state:
57+
self.__state = DeadQueue()
58+
6059
raise exc
6160

6261
def add(self, item: _T):
63-
self.__queue.add(item)
62+
self.__state.add(item)
6463
return self

injection/common/tools/threading.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
__all__ = ("synchronized",)
66

7-
__thread_lock = RLock()
7+
__lock = RLock()
88

99

1010
@contextmanager
1111
def synchronized() -> ContextManager | ContextDecorator:
12-
with __thread_lock:
12+
with __lock:
1313
yield

injection/core/module.py

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
)
3838

3939
from injection.common.event import Event, EventChannel, EventListener
40+
from injection.common.invertible import Invertible, SimpleInvertible
4041
from injection.common.lazy import Lazy, LazyMapping
4142
from injection.common.queue import LimitedQueue
4243
from injection.common.tools.threading import synchronized
@@ -157,6 +158,13 @@ def get_instance(self) -> _T:
157158
raise NotImplementedError
158159

159160

161+
class FallbackInjectable(Injectable[_T], ABC):
162+
__slots__ = ()
163+
164+
def __bool__(self) -> bool:
165+
return False
166+
167+
160168
@dataclass(repr=False, frozen=True, slots=True)
161169
class BaseInjectable(Injectable[_T], ABC):
162170
factory: Callable[[], _T]
@@ -197,12 +205,9 @@ def get_instance(self) -> _T:
197205

198206

199207
@dataclass(repr=False, frozen=True, slots=True)
200-
class ShouldBeInjectable(Injectable[_T]):
208+
class ShouldBeInjectable(FallbackInjectable[_T]):
201209
cls: type[_T]
202210

203-
def __bool__(self) -> bool:
204-
return False
205-
206211
def get_instance(self) -> NoReturn:
207212
raise InjectionError(f"`{format_type(self.cls)}` should be an injectable.")
208213

@@ -260,28 +265,28 @@ def is_locked(self) -> bool:
260265

261266
@property
262267
def __classes(self) -> frozenset[type]:
263-
return frozenset(self.__data.keys())
268+
return frozenset(self.__data)
264269

265270
@property
266271
def __injectables(self) -> frozenset[Injectable]:
267272
return frozenset(self.__data.values())
268273

274+
@synchronized()
269275
def update(self, classes: Iterable[type], injectable: Injectable, override: bool):
270276
classes = frozenset(get_origins(*classes))
271277

272-
with synchronized():
273-
if not injectable:
274-
classes -= self.__classes
275-
override = True
278+
if not injectable:
279+
classes -= self.__classes
280+
override = True
276281

277-
if classes:
278-
event = ContainerDependenciesUpdated(self, classes, override)
282+
if classes:
283+
event = ContainerDependenciesUpdated(self, classes, override)
279284

280-
with self.notify(event):
281-
if not override:
282-
self.__check_if_exists(classes)
285+
with self.notify(event):
286+
if not override:
287+
self.__check_if_exists(classes)
283288

284-
self.__data.update((cls, injectable) for cls in classes)
289+
self.__data.update((cls, injectable) for cls in classes)
285290

286291
return self
287292

@@ -290,6 +295,8 @@ def unlock(self):
290295
for injectable in self.__injectables:
291296
injectable.unlock()
292297

298+
return self
299+
293300
def add_listener(self, listener: EventListener):
294301
self.__channel.add_listener(listener)
295302
return self
@@ -438,8 +445,17 @@ def get_instance(self, cls: type[_T], none: bool = True) -> _T | None:
438445
instance = injectable.get_instance()
439446
return cast(cls, instance)
440447

441-
def get_lazy_instance(self, cls: type[_T]) -> Lazy[_T | None]:
442-
return Lazy(lambda: self.get_instance(cls))
448+
def get_lazy_instance(
449+
self,
450+
cls: type[_T],
451+
cache: bool = False,
452+
) -> Invertible[_T | None]:
453+
if cache:
454+
return Lazy(lambda: self.get_instance(cls))
455+
456+
function = self.inject(lambda instance=None: instance)
457+
function.set_owner(cls)
458+
return SimpleInvertible(function)
443459

444460
def update(
445461
self,
@@ -503,6 +519,8 @@ def unlock(self):
503519
for broker in self.__brokers:
504520
broker.unlock()
505521

522+
return self
523+
506524
def add_listener(self, listener: EventListener):
507525
self.__channel.add_listener(listener)
508526
return self
@@ -661,15 +679,7 @@ def __get__(self, instance: object = None, owner: type = None):
661679
return self.__wrapper.__get__(instance, owner)
662680

663681
def __set_name__(self, owner: type, name: str):
664-
if self.__dependencies.are_resolved:
665-
raise TypeError(
666-
"Function owner must be assigned before dependencies are resolved."
667-
)
668-
669-
if self.__owner:
670-
raise TypeError("Function owner is already defined.")
671-
672-
self.__owner = owner
682+
self.set_owner(owner)
673683

674684
@property
675685
def signature(self) -> Signature:
@@ -692,14 +702,21 @@ def bind(
692702
)
693703
return Arguments(bound.args, bound.kwargs)
694704

695-
def update(self, module: Module):
696-
with synchronized():
697-
self.__dependencies = Dependencies.resolve(
698-
self.signature,
699-
module,
700-
self.__owner,
705+
def set_owner(self, owner: type):
706+
if self.__dependencies.are_resolved:
707+
raise TypeError(
708+
"Function owner must be assigned before dependencies are resolved."
701709
)
702710

711+
if self.__owner:
712+
raise TypeError("Function owner is already defined.")
713+
714+
self.__owner = owner
715+
return self
716+
717+
@synchronized()
718+
def update(self, module: Module):
719+
self.__dependencies = Dependencies.resolve(self.signature, module, self.__owner)
703720
return self
704721

705722
def on_setup(self, wrapped: Callable[[], Any] = None, /):

tests/core/test_module.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,32 @@ def test_get_instance_with_empty_annotated_return_none(self, module):
109109
"""
110110

111111
def test_get_lazy_instance_with_success_return_lazy_instance(self, module):
112-
module[SomeClass] = self.get_test_injectable(SomeClass())
112+
@module.injectable
113+
class A:
114+
pass
113115

114-
lazy_instance = module.get_lazy_instance(SomeClass)
115-
assert not lazy_instance.is_set
116-
assert isinstance(~lazy_instance, SomeClass)
117-
assert lazy_instance.is_set
116+
lazy_instance = module.get_lazy_instance(A)
117+
instance1 = ~lazy_instance
118+
instance2 = ~lazy_instance
119+
assert isinstance(instance1, A)
120+
assert isinstance(instance2, A)
121+
assert instance1 is not instance2
122+
123+
def test_get_lazy_instance_with_cache_return_lazy_instance(self, module):
124+
@module.injectable
125+
class A:
126+
pass
127+
128+
lazy_instance = module.get_lazy_instance(A, cache=True)
129+
instance1 = ~lazy_instance
130+
instance2 = ~lazy_instance
131+
assert isinstance(instance1, A)
132+
assert isinstance(instance2, A)
133+
assert instance1 is instance2
118134

119135
def test_get_lazy_instance_with_no_injectable_return_lazy_none(self, module):
120136
lazy_instance = module.get_lazy_instance(SomeClass)
121-
assert not lazy_instance.is_set
122137
assert ~lazy_instance is None
123-
assert lazy_instance.is_set
124138

125139
"""
126140
set_constant

0 commit comments

Comments
 (0)