Skip to content

Commit f33b740

Browse files
authored
feat: ✨ Scoped slots
1 parent 4f601af commit f33b740

16 files changed

Lines changed: 195 additions & 25 deletions

documentation/advanced-usage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ there may be a problem with this instance. It may contain an obsolete dependency
135135

136136
_First of all, make sure that all scripts containing injectables have been imported before executing the main function._
137137

138-
> **Tips**
138+
> [!TIP]
139139
> * Avoid local imports
140140
> * Avoid singletons if not necessary
141141

documentation/basic-usage.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## Register an injectable
44

5-
> **Note**: If the class needs dependencies, these will be resolved when the instance is retrieved.
5+
> [!NOTE]
6+
> If the class needs dependencies, these will be resolved when the instance is retrieved.
67
78
If you wish to inject a singleton, use `singleton` decorator.
89

@@ -65,7 +66,8 @@ def some_function(service_a: ServiceA):
6566
If `inject` decorates a class, it will be applied to the `__init__` method.
6667
_Especially useful for dataclasses:_
6768

68-
> **Note**: Doesn't work with Pydantic `BaseModel` because the signature of the `__init__` method doesn't contain the
69+
> [!NOTE]
70+
> Doesn't work with Pydantic `BaseModel` because the signature of the `__init__` method doesn't contain the
6971
> dependencies.
7072
7173
```python
@@ -132,8 +134,9 @@ service_a = await lazy_service_a
132134
In the case of inheritance, you can use the decorator parameter `on` to link the injection to one or several other
133135
classes.
134136

135-
**Warning: if the child class is in another file, make sure that file is imported before injection.**
136-
[_See `load_packages` function._](utils.md#load_packages)
137+
> [!WARNING]
138+
> If the child class is in another file, make sure that file is imported before injection.
139+
> [_See `load_packages` function._](utils.md#load_packages)
137140
138141
_Example with one class:_
139142

documentation/integrations.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ from contextlib import asynccontextmanager
5050
from enum import StrEnum, auto
5151

5252
from fastapi import FastAPI, Request, Response
53-
from injection import adefine_scope
53+
from injection import adefine_scope, reserve_scoped_slot
5454

5555
class InjectionScope(StrEnum):
5656
LIFESPAN = auto()
@@ -63,11 +63,14 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
6363

6464
app = FastAPI(lifespan=lifespan)
6565

66+
request_slot = reserve_scoped_slot(Request, InjectionScope.REQUEST)
67+
6668
@app.middleware("http")
6769
async def define_request_scope_middleware(
6870
request: Request,
6971
handler: Callable[[Request], Awaitable[Response]],
7072
) -> Response:
7173
async with adefine_scope(InjectionScope.REQUEST):
74+
request_slot.set(request)
7275
return await handler(request)
7376
```

documentation/scoped-dependencies.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,24 @@ def client_recipe() -> Iterator[Client]:
9696
# On scope close
9797
client.close_connection()
9898
```
99+
100+
### Scoped slots
101+
102+
Scoped slots allow you to reserve a place for an instance within a predefined scope. This ensures that injected
103+
functions can resolve dependencies efficiently without unnecessary recomputation. This is why the syntax can seem a
104+
little verbose.
105+
106+
Example:
107+
108+
```python
109+
from injection import define_scope, reserve_scoped_slot
110+
111+
class Request: ...
112+
113+
request_slot = reserve_scoped_slot(Request, scope_name="request")
114+
115+
def process_request(request: Request) -> None:
116+
with define_scope("request"):
117+
request_slot.set(request)
118+
# ...
119+
```

documentation/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def autouse_test_injectables():
2020

2121
## Register a test injectable
2222

23-
> **Notes**
23+
> [!NOTE]
2424
> * Test injectables replace conventional injectables if they are registered on the same type.
2525
> * A test injectable can't depend on a conventional injectable.
2626

documentation/utils.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ load_packages(package)
3030
`load_profile` is an injection module initialization function based on profile name.
3131
This is very useful when you want to use a set of dependencies based on the execution profile.
3232

33-
> **Note:** A profile name is equivalent to an injection module name.
33+
> [!NOTE]
34+
> A profile name is equivalent to an injection module name.
3435
3536
For example, when I'm doing my development tests, I don't really feel like sending SMS messages.
3637

injection/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
from ._core.injectables import Injectable
33
from ._core.module import Mode, Module, Priority, mod
44
from ._core.scope import adefine_scope, define_scope
5+
from ._core.slots import Slot
56

67
__all__ = (
78
"Injectable",
89
"LazyInstance",
910
"Mode",
1011
"Module",
1112
"Priority",
13+
"Slot",
1214
"adefine_scope",
1315
"afind_instance",
1416
"aget_instance",
@@ -21,6 +23,7 @@
2123
"inject",
2224
"injectable",
2325
"mod",
26+
"reserve_scoped_slot",
2427
"scoped",
2528
"set_constant",
2629
"should_be_injectable",
@@ -36,6 +39,7 @@
3639
get_lazy_instance = mod().get_lazy_instance
3740
inject = mod().inject
3841
injectable = mod().injectable
42+
reserve_scoped_slot = mod().reserve_scoped_slot
3943
scoped = mod().scoped
4044
set_constant = mod().set_constant
4145
should_be_injectable = mod().should_be_injectable

injection/__init__.pyi

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ get_instance = __MODULE.get_instance
2323
get_lazy_instance = __MODULE.get_lazy_instance
2424
inject = __MODULE.inject
2525
injectable = __MODULE.injectable
26+
reserve_scoped_slot = __MODULE.reserve_scoped_slot
2627
scoped = __MODULE.scoped
2728
set_constant = __MODULE.set_constant
2829
should_be_injectable = __MODULE.should_be_injectable
@@ -46,6 +47,11 @@ class Injectable[T](Protocol):
4647
@abstractmethod
4748
def get_instance(self) -> T: ...
4849

50+
@runtime_checkable
51+
class Slot[T](Protocol):
52+
@abstractmethod
53+
def set(self, instance: T, /) -> Self: ...
54+
4955
class LazyInstance[T]:
5056
def __init__(
5157
self,
@@ -171,6 +177,14 @@ class Module:
171177
that no dependencies are resolved, so the module doesn't need to be locked.
172178
"""
173179

180+
def reserve_scoped_slot[T](
181+
self,
182+
on: _TypeInfo[T],
183+
/,
184+
scope_name: str,
185+
*,
186+
mode: Mode | ModeStr = ...,
187+
) -> Slot[T]: ...
174188
def make_injected_function[**P, T](
175189
self,
176190
wrapped: Callable[P, T],

injection/_core/common/type.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
AsyncGenerator,
33
AsyncIterable,
44
AsyncIterator,
5+
Awaitable,
56
Callable,
67
Generator,
78
Iterable,
@@ -21,7 +22,12 @@
2122

2223
type TypeDef[T] = type[T] | TypeAliasType | GenericAlias
2324
type InputType[T] = TypeDef[T] | UnionType
24-
type TypeInfo[T] = InputType[T] | Callable[..., T] | Iterable[TypeInfo[T]]
25+
type TypeInfo[T] = (
26+
InputType[T]
27+
| Callable[..., T]
28+
| Callable[..., Awaitable[T]]
29+
| Iterable[TypeInfo[T]]
30+
)
2531

2632

2733
def get_return_types(*args: TypeInfo[Any]) -> Iterator[InputType[Any]]:

injection/_core/injectables.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,25 +107,31 @@ def build(self, scope: Scope) -> T:
107107
raise NotImplementedError
108108

109109
async def aget_instance(self) -> T:
110-
scope = get_scope(self.scope_name)
110+
scope = self.get_scope()
111111

112112
with suppress(KeyError):
113113
return scope.cache[self]
114114

115115
instance = await self.abuild(scope)
116-
scope.cache[self] = instance
116+
self.set_instance(instance, scope)
117117
return instance
118118

119119
def get_instance(self) -> T:
120-
scope = get_scope(self.scope_name)
120+
scope = self.get_scope()
121121

122122
with suppress(KeyError):
123123
return scope.cache[self]
124124

125125
instance = self.build(scope)
126-
scope.cache[self] = instance
126+
self.set_instance(instance, scope)
127127
return instance
128128

129+
def get_scope(self) -> Scope:
130+
return get_scope(self.scope_name)
131+
132+
def set_instance(self, instance: T, scope: Scope) -> None:
133+
scope.cache[self] = instance
134+
129135
def unlock(self) -> None:
130136
if self.is_locked:
131137
raise RuntimeError(f"To unlock, close the `{self.scope_name}` scope.")

0 commit comments

Comments
 (0)