Skip to content

Commit 342445a

Browse files
authored
feat: ⚡️ Scoped slot rework
1 parent 9baf265 commit 342445a

11 files changed

Lines changed: 196 additions & 129 deletions

File tree

documentation/integrations.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,19 @@ class InjectionScope(StrEnum):
5858

5959
@asynccontextmanager
6060
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
61-
async with adefine_scope(InjectionScope.LIFESPAN, shared=True):
61+
async with adefine_scope(InjectionScope.LIFESPAN, kind="shared"):
6262
yield
6363

6464
app = FastAPI(lifespan=lifespan)
6565

66-
request_slot = reserve_scoped_slot(Request, InjectionScope.REQUEST)
66+
request_slot_key = reserve_scoped_slot(Request, InjectionScope.REQUEST)
6767

6868
@app.middleware("http")
6969
async def define_request_scope_middleware(
7070
request: Request,
7171
handler: Callable[[Request], Awaitable[Response]],
7272
) -> Response:
73-
async with adefine_scope(InjectionScope.REQUEST):
74-
request_slot.set(request)
73+
async with adefine_scope(InjectionScope.REQUEST) as scope:
74+
scope.set_slot(request_slot_key, request)
7575
return await handler(request)
7676
```

documentation/scoped-dependencies.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ There are two kinds of scopes:
1919

2020
First of all, the scope must be defined:
2121

22-
_By default, the `shared` parameter is `False`._
22+
_By default, the `kind` parameter is `"contextual"`._
2323

2424
> Define an asynchronous scope:
2525
2626
```python
2727
from injection import adefine_scope
2828

2929
async def main() -> None:
30-
async with adefine_scope("<scope-name>", shared=True):
30+
async with adefine_scope("<scope-name>"):
3131
...
3232
```
3333

@@ -37,7 +37,7 @@ async def main() -> None:
3737
from injection import define_scope
3838

3939
def main() -> None:
40-
with define_scope("<scope-name>", shared=True):
40+
with define_scope("<scope-name>"):
4141
...
4242
```
4343

@@ -110,10 +110,13 @@ from injection import define_scope, reserve_scoped_slot
110110

111111
class Request: ...
112112

113-
request_slot = reserve_scoped_slot(Request, scope_name="request")
113+
request_slot_key = reserve_scoped_slot(Request, scope_name="request")
114114

115115
def process_request(request: Request) -> None:
116-
with define_scope("request"):
117-
request_slot.set(request)
116+
with define_scope("request") as scope:
117+
scope.set_slot(request_slot_key, request)
118118
# ...
119119
```
120+
121+
> [!NOTE]
122+
> You can set several slots at once with the `slot_map` method.

injection/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
from ._core.descriptors import LazyInstance
22
from ._core.injectables import Injectable
33
from ._core.module import Mode, Module, Priority, mod
4-
from ._core.scope import adefine_scope, define_scope
5-
from ._core.slots import Slot
4+
from ._core.scope import ScopeFacade as Scope
5+
from ._core.scope import ScopeKind, adefine_scope, define_scope
6+
from ._core.slots import SlotKey
67

78
__all__ = (
89
"Injectable",
910
"LazyInstance",
1011
"Mode",
1112
"Module",
1213
"Priority",
13-
"Slot",
14+
"Scope",
15+
"ScopeKind",
16+
"SlotKey",
1417
"adefine_scope",
1518
"afind_instance",
1619
"aget_instance",

injection/__init__.pyi

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import abstractmethod
2-
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
2+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
33
from contextlib import asynccontextmanager, contextmanager
44
from enum import Enum
55
from logging import Logger
@@ -11,6 +11,7 @@ from ._core.common.type import TypeInfo as _TypeInfo
1111
from ._core.module import InjectableFactory as _InjectableFactory
1212
from ._core.module import ModeStr, PriorityStr
1313
from ._core.module import Recipe as _Recipe
14+
from ._core.scope import ScopeKindStr
1415

1516
__MODULE: Final[Module] = ...
1617

@@ -30,9 +31,17 @@ should_be_injectable = __MODULE.should_be_injectable
3031
singleton = __MODULE.singleton
3132

3233
@asynccontextmanager
33-
def adefine_scope(name: str, *, shared: bool = ...) -> AsyncIterator[None]: ...
34+
def adefine_scope(
35+
name: str,
36+
/,
37+
kind: ScopeKind | ScopeKindStr = ...,
38+
) -> AsyncIterator[Scope]: ...
3439
@contextmanager
35-
def define_scope(name: str, *, shared: bool = ...) -> Iterator[None]: ...
40+
def define_scope(
41+
name: str,
42+
/,
43+
kind: ScopeKind | ScopeKindStr = ...,
44+
) -> Iterator[Scope]: ...
3645
def mod(name: str = ..., /) -> Module:
3746
"""
3847
Short syntax for `Module.from_name`.
@@ -47,10 +56,19 @@ class Injectable[T](Protocol):
4756
@abstractmethod
4857
def get_instance(self) -> T: ...
4958

59+
@final
60+
class ScopeKind(Enum):
61+
CONTEXTUAL = ...
62+
SHARED = ...
63+
5064
@runtime_checkable
51-
class Slot[T](Protocol):
65+
class Scope(Protocol):
5266
@abstractmethod
53-
def set(self, instance: T, /) -> None: ...
67+
def set_slot[T](self, key: SlotKey[T], value: T) -> Self: ...
68+
@abstractmethod
69+
def slot_map(self, mapping: Mapping[SlotKey[Any], Any], /) -> Self: ...
70+
71+
class SlotKey[T]: ...
5472

5573
class LazyInstance[T]:
5674
def __init__(
@@ -179,12 +197,12 @@ class Module:
179197

180198
def reserve_scoped_slot[T](
181199
self,
182-
on: _TypeInfo[T],
200+
cls: type[T],
183201
/,
184202
scope_name: str,
185203
*,
186204
mode: Mode | ModeStr = ...,
187-
) -> Slot[T]: ...
205+
) -> SlotKey[T]: ...
188206
def make_injected_function[**P, T](
189207
self,
190208
wrapped: Callable[P, T],

injection/_core/injectables.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@
1717
from injection._core.common.asynchronous import (
1818
create_semaphore as _create_async_semaphore,
1919
)
20-
from injection._core.scope import Scope, get_active_scopes, get_scope
21-
from injection.exceptions import InjectionError
20+
from injection._core.scope import (
21+
Scope,
22+
get_scope,
23+
in_scope_cache,
24+
remove_scoped_values,
25+
)
26+
from injection._core.slots import SlotKey
27+
from injection.exceptions import EmptySlotError, InjectionError
2228

2329

2430
@runtime_checkable
@@ -123,7 +129,7 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
123129

124130
@property
125131
def is_locked(self) -> bool:
126-
return any(self in scope.cache for scope in get_active_scopes(self.scope_name))
132+
return in_scope_cache(self, self.scope_name)
127133

128134
@abstractmethod
129135
async def abuild(self, scope: Scope) -> T:
@@ -143,10 +149,6 @@ def get_instance(self) -> T:
143149
factory = partial(self.build, scope)
144150
return self.logic.get_or_create(scope.cache, self, factory)
145151

146-
def setdefault(self, instance: T) -> T:
147-
scope = self.__get_scope()
148-
return self.logic.get_or_create(scope.cache, self, lambda: instance)
149-
150152
def unlock(self) -> None:
151153
if self.is_locked:
152154
raise RuntimeError(f"To unlock, close the `{self.scope_name}` scope.")
@@ -188,8 +190,35 @@ def build(self, scope: Scope) -> T:
188190
return self.factory.call()
189191

190192
def unlock(self) -> None:
191-
for scope in get_active_scopes(self.scope_name):
192-
scope.cache.pop(self, None)
193+
remove_scoped_values(self, self.scope_name)
194+
195+
196+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
197+
class ScopedSlotInjectable[T](Injectable[T]):
198+
cls: type[T]
199+
scope_name: str
200+
key: SlotKey[T] = field(default_factory=SlotKey)
201+
202+
@property
203+
def is_locked(self) -> bool:
204+
return in_scope_cache(self.key, self.scope_name)
205+
206+
async def aget_instance(self) -> T:
207+
return self.get_instance()
208+
209+
def get_instance(self) -> T:
210+
scope_name = self.scope_name
211+
scope = get_scope(scope_name)
212+
213+
try:
214+
return scope.cache[self.key]
215+
except KeyError as exc:
216+
raise EmptySlotError(
217+
f"The slot for `{self.cls}` isn't set in the current `{scope_name}` scope."
218+
) from exc
219+
220+
def unlock(self) -> None:
221+
remove_scoped_values(self.key, self.scope_name)
193222

194223

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

injection/_core/module.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@
6464
CMScopedInjectable,
6565
Injectable,
6666
ScopedInjectable,
67+
ScopedSlotInjectable,
6768
ShouldBeInjectable,
6869
SimpleInjectable,
6970
SimpleScopedInjectable,
7071
SingletonInjectable,
7172
)
72-
from injection._core.slots import ScopedSlot, Slot
73+
from injection._core.slots import SlotKey
7374
from injection.exceptions import (
74-
EmptySlotError,
7575
ModuleError,
7676
ModuleLockError,
7777
ModuleNotUsedError,
@@ -492,11 +492,8 @@ def decorator(
492492

493493
def should_be_injectable[T](self, wrapped: type[T] | None = None, /) -> Any:
494494
def decorator(wp: type[T]) -> type[T]:
495-
updater = Updater(
496-
classes=(wp,),
497-
injectable=ShouldBeInjectable(wp),
498-
mode=Mode.FALLBACK,
499-
)
495+
injectable = ShouldBeInjectable(wp)
496+
updater = Updater.with_basics(wp, injectable, Mode.FALLBACK)
500497
self.update(updater)
501498
return wp
502499

@@ -543,21 +540,16 @@ def set_constant[T](
543540

544541
def reserve_scoped_slot[T](
545542
self,
546-
on: TypeInfo[T],
543+
cls: type[T],
547544
/,
548545
scope_name: str,
549546
*,
550547
mode: Mode | ModeStr = Mode.get_default(),
551-
) -> Slot[T]:
552-
def when_empty() -> T:
553-
raise EmptySlotError(
554-
f"The slot for `{on}` isn't set in the current `{scope_name}` scope."
555-
)
556-
557-
injectable = SimpleScopedInjectable(SyncCaller(when_empty), scope_name)
558-
updater = Updater.with_basics(on, injectable, mode)
548+
) -> SlotKey[T]:
549+
injectable = ScopedSlotInjectable(cls, scope_name)
550+
updater = Updater.with_basics(cls, injectable, mode)
559551
self.update(updater)
560-
return ScopedSlot(injectable)
552+
return injectable.key
561553

562554
def inject[**P, T](
563555
self,

0 commit comments

Comments
 (0)