Skip to content

Commit 060d901

Browse files
author
remimd
committed
feat: ⚡️ Scoped slot rework
1 parent 9baf265 commit 060d901

11 files changed

Lines changed: 165 additions & 118 deletions

File tree

documentation/integrations.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ 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)
@@ -70,7 +70,7 @@ 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, request)
7575
return await handler(request)
7676
```

documentation/scoped-dependencies.md

Lines changed: 5 additions & 5 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

@@ -113,7 +113,7 @@ class Request: ...
113113
request_slot = 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, request)
118118
# ...
119119
```

injection/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
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
4+
from ._core.scope import ScopeFacade as Scope
5+
from ._core.scope import ScopeKind, adefine_scope, define_scope
56
from ._core.slots import Slot
67

78
__all__ = (
@@ -10,6 +11,8 @@
1011
"Mode",
1112
"Module",
1213
"Priority",
14+
"Scope",
15+
"ScopeKind",
1316
"Slot",
1417
"adefine_scope",
1518
"afind_instance",

injection/__init__.pyi

Lines changed: 21 additions & 8 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,15 @@ 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+
kind: ScopeKind | ScopeKindStr = ...,
37+
) -> AsyncIterator[Scope]: ...
3438
@contextmanager
35-
def define_scope(name: str, *, shared: bool = ...) -> Iterator[None]: ...
39+
def define_scope(
40+
name: str,
41+
kind: ScopeKind | ScopeKindStr = ...,
42+
) -> Iterator[Scope]: ...
3643
def mod(name: str = ..., /) -> Module:
3744
"""
3845
Short syntax for `Module.from_name`.
@@ -47,10 +54,16 @@ class Injectable[T](Protocol):
4754
@abstractmethod
4855
def get_instance(self) -> T: ...
4956

50-
@runtime_checkable
51-
class Slot[T](Protocol):
52-
@abstractmethod
53-
def set(self, instance: T, /) -> None: ...
57+
@final
58+
class ScopeKind(Enum):
59+
CONTEXTUAL = ...
60+
SHARED = ...
61+
62+
class Scope:
63+
def set_slot[T](self, slot: Slot[T], value: T) -> Self: ...
64+
def slot_map(self, values: Mapping[Slot[Any], Any]) -> Self: ...
65+
66+
class Slot[T]: ...
5467

5568
class LazyInstance[T]:
5669
def __init__(
@@ -179,7 +192,7 @@ class Module:
179192

180193
def reserve_scoped_slot[T](
181194
self,
182-
on: _TypeInfo[T],
195+
cls: type[T],
183196
/,
184197
scope_name: str,
185198
*,

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 Slot
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+
slot: Slot[T] = field(default_factory=Slot)
201+
202+
@property
203+
def is_locked(self) -> bool:
204+
return in_scope_cache(self.slot, 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.slot]
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.slot, self.scope_name)
193222

194223

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

injection/_core/module.py

Lines changed: 6 additions & 11 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 Slot
7374
from injection.exceptions import (
74-
EmptySlotError,
7575
ModuleError,
7676
ModuleLockError,
7777
ModuleNotUsedError,
@@ -543,21 +543,16 @@ def set_constant[T](
543543

544544
def reserve_scoped_slot[T](
545545
self,
546-
on: TypeInfo[T],
546+
cls: type[T],
547547
/,
548548
scope_name: str,
549549
*,
550550
mode: Mode | ModeStr = Mode.get_default(),
551551
) -> 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)
552+
injectable = ScopedSlotInjectable(cls, scope_name)
553+
updater = Updater.with_basics(cls, injectable, mode)
559554
self.update(updater)
560-
return ScopedSlot(injectable)
555+
return injectable.slot
561556

562557
def inject[**P, T](
563558
self,

0 commit comments

Comments
 (0)