Skip to content

Commit 84ee0ce

Browse files
authored
feat: ⚡️ Removal of the use of asyncio and queue
1 parent 9ff142d commit 84ee0ce

6 files changed

Lines changed: 121 additions & 155 deletions

File tree

injection/_core/common/asynchronous.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
1-
import asyncio
21
from abc import abstractmethod
3-
from collections.abc import Awaitable, Callable, Coroutine, Generator
2+
from collections.abc import Awaitable, Callable, Generator
43
from dataclasses import dataclass
5-
from typing import Any, Protocol, runtime_checkable
6-
7-
8-
def run_sync[T](coroutine: Coroutine[Any, Any, T]) -> T:
9-
loop = asyncio.get_event_loop()
10-
11-
try:
12-
return loop.run_until_complete(coroutine)
13-
finally:
14-
coroutine.close()
4+
from typing import Any, NoReturn, Protocol, runtime_checkable
155

166

177
@dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -37,13 +27,13 @@ def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
3727

3828
@dataclass(repr=False, eq=False, frozen=True, slots=True)
3929
class AsyncCaller[**P, T](Caller[P, T]):
40-
callable: Callable[P, Coroutine[Any, Any, T]]
30+
callable: Callable[P, Awaitable[T]]
4131

4232
async def acall(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
4333
return await self.callable(*args, **kwargs)
4434

45-
def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
46-
return run_sync(self.callable(*args, **kwargs))
35+
def call(self, /, *args: P.args, **kwargs: P.kwargs) -> NoReturn:
36+
raise RuntimeError("Can't call async callable synchronously.")
4737

4838

4939
@dataclass(repr=False, eq=False, frozen=True, slots=True)

injection/_core/common/lazy.py

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,55 @@
1-
from collections.abc import Callable, Iterator, Mapping
2-
from types import MappingProxyType
1+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
2+
from functools import partial
33

4-
from injection._core.common.invertible import Invertible
4+
from injection._core.common.asynchronous import SimpleAwaitable
5+
from injection._core.common.invertible import Invertible, SimpleInvertible
56

67

7-
class Lazy[T](Invertible[T]):
8-
__slots__ = ("__iterator", "__is_set")
8+
def lazy[T](factory: Callable[..., T]) -> Invertible[T]:
9+
def cache() -> Iterator[T]:
10+
nonlocal factory
11+
value = factory()
12+
del factory
913

10-
__iterator: Iterator[T]
11-
__is_set: bool
14+
while True:
15+
yield value
1216

13-
def __init__(self, factory: Callable[..., T]) -> None:
14-
self.__setup_cache(factory)
15-
16-
def __invert__(self) -> T:
17-
return next(self.__iterator)
18-
19-
@property
20-
def is_set(self) -> bool:
21-
return self.__is_set
17+
getter = partial(next, cache())
18+
return SimpleInvertible(getter)
2219

23-
def __setup_cache(self, factory: Callable[..., T]) -> None:
24-
def infinite_yield() -> Iterator[T]:
25-
nonlocal factory
26-
cached = factory()
27-
self.__is_set = True
28-
del factory
2920

30-
while True:
31-
yield cached
21+
def alazy[T](factory: Callable[..., Awaitable[T]]) -> Awaitable[T]:
22+
async def cache() -> AsyncIterator[T]:
23+
nonlocal factory
24+
value = await factory()
25+
del factory
3226

33-
self.__iterator = infinite_yield()
34-
self.__is_set = False
27+
while True:
28+
yield value
3529

30+
getter = partial(anext, cache())
31+
return SimpleAwaitable(getter)
3632

37-
class LazyMapping[K, V](Mapping[K, V]):
38-
__slots__ = ("__lazy",)
3933

40-
__lazy: Lazy[Mapping[K, V]]
34+
class Lazy[T](Invertible[T]):
35+
__slots__ = ("__invertible", "__is_set")
4136

42-
def __init__(self, iterator: Iterator[tuple[K, V]]) -> None:
43-
self.__lazy = Lazy(lambda: MappingProxyType(dict(iterator)))
37+
__invertible: Invertible[T]
38+
__is_set: bool
4439

45-
def __getitem__(self, key: K, /) -> V:
46-
return (~self.__lazy)[key]
40+
def __init__(self, factory: Callable[..., T]) -> None:
41+
@lazy
42+
def invertible() -> T:
43+
value = factory()
44+
self.__is_set = True
45+
return value
4746

48-
def __iter__(self) -> Iterator[K]:
49-
yield from ~self.__lazy
47+
self.__invertible = invertible
48+
self.__is_set = False
5049

51-
def __len__(self) -> int:
52-
return len(~self.__lazy)
50+
def __invert__(self) -> T:
51+
return ~self.__invertible
5352

5453
@property
5554
def is_set(self) -> bool:
56-
return self.__lazy.is_set
55+
return self.__is_set

injection/_core/injectables.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
runtime_checkable,
1313
)
1414

15-
from injection._core.common.asynchronous import Caller, run_sync
15+
from injection._core.common.asynchronous import Caller
1616
from injection._core.scope import Scope, get_active_scopes, get_scope
1717
from injection.exceptions import InjectionError
1818

@@ -138,8 +138,8 @@ async def abuild(self, scope: Scope) -> T:
138138
cm = await self.factory.acall()
139139
return await scope.aenter(cm)
140140

141-
def build(self, scope: Scope) -> T:
142-
return run_sync(self.abuild(scope))
141+
def build(self, scope: Scope) -> NoReturn:
142+
raise RuntimeError("Can't use async context manager synchronously.")
143143

144144

145145
class CMScopedInjectable[T](ScopedInjectable[ContextManager[T], T]):

injection/_core/module.py

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from __future__ import annotations
22

3-
import asyncio
43
from abc import ABC, abstractmethod
5-
from collections import OrderedDict
4+
from collections import OrderedDict, deque
65
from collections.abc import (
76
AsyncIterator,
87
Awaitable,
@@ -26,7 +25,6 @@
2625
)
2726
from inspect import signature as inspect_signature
2827
from logging import Logger, getLogger
29-
from queue import Empty, Queue
3028
from types import MethodType
3129
from typing import (
3230
Any,
@@ -50,7 +48,7 @@
5048
from injection._core.common.event import Event, EventChannel, EventListener
5149
from injection._core.common.invertible import Invertible, SimpleInvertible
5250
from injection._core.common.key import new_short_key
53-
from injection._core.common.lazy import Lazy, LazyMapping
51+
from injection._core.common.lazy import Lazy, alazy, lazy
5452
from injection._core.common.type import (
5553
InputType,
5654
TypeInfo,
@@ -296,6 +294,9 @@ def unlock(self) -> Self:
296294

297295
async def all_ready(self) -> None:
298296
for injectable in self.__injectables:
297+
if injectable.is_locked:
298+
continue
299+
299300
await injectable.aget_instance()
300301

301302
def add_listener(self, listener: EventListener) -> Self:
@@ -511,7 +512,7 @@ def constant[T](
511512
mode: Mode | ModeStr = Mode.get_default(),
512513
) -> Any:
513514
def decorator(wp: type[T]) -> type[T]:
514-
lazy_instance = Lazy(wp)
515+
lazy_instance = lazy(wp)
515516
self.injectable(
516517
lambda: ~lazy_instance,
517518
ignore_type_hint=True,
@@ -568,7 +569,7 @@ def make_injected_function[**P, T](
568569
def make_injected_function(self, wrapped, /): # type: ignore[no-untyped-def]
569570
metadata = InjectMetadata(wrapped)
570571

571-
@metadata.on_setup
572+
@metadata.task
572573
def listen() -> None:
573574
metadata.update(self)
574575
self.add_listener(metadata)
@@ -646,12 +647,10 @@ def aget_lazy_instance[T](
646647

647648
def aget_lazy_instance(self, cls, default=None, *, cache=False): # type: ignore[no-untyped-def]
648649
if cache:
649-
coroutine = self.aget_instance(cls, default)
650-
return asyncio.ensure_future(coroutine)
650+
return alazy(lambda: self.aget_instance(cls, default))
651651

652652
function = self.make_injected_function(lambda instance=default: instance)
653-
metadata = function.__inject_metadata__
654-
metadata.set_owner(cls)
653+
metadata = function.__inject_metadata__.set_owner(cls)
655654
return SimpleAwaitable(metadata.acall)
656655

657656
@overload
@@ -674,11 +673,10 @@ def get_lazy_instance[T](
674673

675674
def get_lazy_instance(self, cls, default=None, *, cache=False): # type: ignore[no-untyped-def]
676675
if cache:
677-
return Lazy(lambda: self.get_instance(cls, default))
676+
return lazy(lambda: self.get_instance(cls, default))
678677

679678
function = self.make_injected_function(lambda instance=default: instance)
680-
metadata = function.__inject_metadata__
681-
metadata.set_owner(cls)
679+
metadata = function.__inject_metadata__.set_owner(cls)
682680
return SimpleInvertible(metadata.call)
683681

684682
def update[T](self, updater: Updater[T]) -> Self:
@@ -832,10 +830,7 @@ def mod(name: str | None = None, /) -> Module:
832830

833831
@dataclass(repr=False, frozen=True, slots=True)
834832
class Dependencies:
835-
mapping: Mapping[str, Injectable[Any]]
836-
837-
def __bool__(self) -> bool:
838-
return bool(self.mapping)
833+
lazy_mapping: Lazy[Mapping[str, Injectable[Any]]]
839834

840835
def __iter__(self) -> Iterator[tuple[str, Any]]:
841836
for name, injectable in self.mapping.items():
@@ -849,10 +844,11 @@ async def __aiter__(self) -> AsyncIterator[tuple[str, Any]]:
849844

850845
@property
851846
def are_resolved(self) -> bool:
852-
if isinstance(self.mapping, LazyMapping) and not self.mapping.is_set:
853-
return False
847+
return self.lazy_mapping.is_set
854848

855-
return bool(self)
849+
@property
850+
def mapping(self) -> Mapping[str, Injectable[Any]]:
851+
return ~self.lazy_mapping
856852

857853
async def aget_arguments(self) -> dict[str, Any]:
858854
return {key: value async for key, value in self}
@@ -861,12 +857,13 @@ def get_arguments(self) -> dict[str, Any]:
861857
return dict(self)
862858

863859
@classmethod
864-
def from_mapping(cls, mapping: Mapping[str, Injectable[Any]]) -> Self:
865-
return cls(mapping)
860+
def from_iterable(cls, iterable: Iterable[tuple[str, Injectable[Any]]]) -> Self:
861+
lazy_mapping = Lazy(lambda: dict(iterable))
862+
return cls(lazy_mapping)
866863

867864
@classmethod
868865
def empty(cls) -> Self:
869-
return cls.from_mapping({})
866+
return cls.from_iterable(())
870867

871868
@classmethod
872869
def resolve(
@@ -875,8 +872,8 @@ def resolve(
875872
module: Module,
876873
owner: type | None = None,
877874
) -> Self:
878-
dependencies = LazyMapping(cls.__resolver(signature, module, owner))
879-
return cls.from_mapping(dependencies)
875+
iterable = cls.__resolver(signature, module, owner)
876+
return cls.from_iterable(iterable)
880877

881878
@classmethod
882879
def __resolver(
@@ -917,21 +914,21 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
917914
__slots__ = (
918915
"__dependencies",
919916
"__owner",
920-
"__setup_queue",
921917
"__signature",
918+
"__tasks",
922919
"__wrapped",
923920
)
924921

925922
__dependencies: Dependencies
926923
__owner: type | None
927-
__setup_queue: Queue[Callable[..., Any]] | None
928924
__signature: Signature
925+
__tasks: deque[Callable[..., Any]]
929926
__wrapped: Callable[P, T]
930927

931928
def __init__(self, wrapped: Callable[P, T], /) -> None:
932929
self.__dependencies = Dependencies.empty()
933930
self.__owner = None
934-
self.__setup_queue = Queue()
931+
self.__tasks = deque()
935932
self.__wrapped = wrapped
936933

937934
@property
@@ -964,12 +961,12 @@ def bind(
964961
return self.__bind(args, kwargs, additional_arguments)
965962

966963
async def acall(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
967-
self.__setup()
964+
self.__run_tasks()
968965
arguments = await self.abind(args, kwargs)
969966
return self.wrapped(*arguments.args, **arguments.kwargs)
970967

971968
def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
972-
self.__setup()
969+
self.__run_tasks()
973970
arguments = self.bind(args, kwargs)
974971
return self.wrapped(*arguments.args, **arguments.kwargs)
975972

@@ -989,14 +986,9 @@ def update(self, module: Module) -> Self:
989986
self.__dependencies = Dependencies.resolve(self.signature, module, self.__owner)
990987
return self
991988

992-
def on_setup[**_P, _T](self, wrapped: Callable[_P, _T] | None = None, /) -> Any:
989+
def task[**_P, _T](self, wrapped: Callable[_P, _T] | None = None, /) -> Any:
993990
def decorator(wp: Callable[_P, _T]) -> Callable[_P, _T]:
994-
queue = self.__setup_queue
995-
996-
if queue is None:
997-
raise RuntimeError(f"`{self}` is already up.")
998-
999-
queue.put_nowait(wp)
991+
self.__tasks.append(wp)
1000992
return wp
1001993

1002994
return decorator(wrapped) if wrapped else decorator
@@ -1027,24 +1019,10 @@ def __bind(
10271019
bound.arguments = bound.arguments | additional_arguments | bound.arguments
10281020
return Arguments(bound.args, bound.kwargs)
10291021

1030-
def __close_setup_queue(self) -> None:
1031-
self.__setup_queue = None
1032-
1033-
def __setup(self) -> None:
1034-
if (queue := self.__setup_queue) is None:
1035-
return
1036-
1037-
while True:
1038-
try:
1039-
task = queue.get_nowait()
1040-
except Empty:
1041-
break
1042-
1022+
def __run_tasks(self) -> None:
1023+
while tasks := self.__tasks:
1024+
task = tasks.popleft()
10431025
task()
1044-
queue.task_done()
1045-
1046-
queue.join()
1047-
self.__close_setup_queue()
10481026

10491027

10501028
class InjectedFunction[**P, T](ABC):

injection/_core/scope.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,7 @@ def __exit__(
201201
return self.delegate.__exit__(exc_type, exc_value, traceback)
202202

203203
async def aenter[T](self, context_manager: AsyncContextManager[T]) -> NoReturn:
204-
raise ScopeError(
205-
"Synchronous scope doesn't support asynchronous context manager."
206-
)
204+
raise ScopeError("Synchronous scope doesn't support async context manager.")
207205

208206
def enter[T](self, context_manager: ContextManager[T]) -> T:
209207
return self.delegate.enter_context(context_manager)

0 commit comments

Comments
 (0)