Skip to content

Commit 429f244

Browse files
author
remimd
committed
doc
1 parent ca4fea1 commit 429f244

File tree

8 files changed

+208
-117
lines changed

8 files changed

+208
-117
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ if __name__ == "__main__":
5656
## Resources
5757

5858
* [**Basic usage**](https://github.com/100nm/python-injection/tree/prod/documentation/basic-usage.md)
59+
* [**Scoped dependencies**](https://github.com/100nm/python-injection/tree/prod/documentation/scoped-dependencies.md)
5960
* [**Testing**](https://github.com/100nm/python-injection/tree/prod/documentation/testing.md)
6061
* [**Advanced usage**](https://github.com/100nm/python-injection/tree/prod/documentation/advanced-usage.md)
6162
* [**Utils**](https://github.com/100nm/python-injection/tree/prod/documentation/utils.md)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Scoped dependencies
2+
3+
The scoped dependencies were created for two reasons:
4+
* To have dependencies that have a defined lifespan.
5+
* To be able to open and close things in a dependency recipe.
6+
7+
> **Best practices:**
8+
> * Avoid making a singleton dependent on a scoped dependency.
9+
10+
## Scope
11+
12+
The scope is responsible for instance persistence and for cleaning up when it closes.
13+
There are two kinds of scopes:
14+
* **Contextual**: All threads have access to a different scope (based on [contextvars](https://docs.python.org/3.13/library/contextvars.html)).
15+
* **Shared**: All threads have access to the same scope.
16+
17+
First of all, the scope must be defined:
18+
19+
*By default, the `shared` parameter is `False`.*
20+
21+
> Define an asynchronous scope:
22+
23+
```python
24+
from injection import adefine_scope
25+
26+
async def main() -> None:
27+
async with adefine_scope("<scope-name>", shared=True):
28+
...
29+
```
30+
31+
> Define a synchronous scope:
32+
33+
```python
34+
from injection import define_scope
35+
36+
37+
def main() -> None:
38+
with define_scope("<scope-name>", shared=True):
39+
...
40+
```
41+
42+
## Register a scoped dependencies
43+
44+
`@scoped` works exactly like `@injectable`, it just has extra features.
45+
46+
### "contextmanager-like" recipes
47+
48+
*Anything after the `yield` keyword will be executed when the scope is closed.*
49+
50+
> Asynchronous (asynchronous scope required):
51+
52+
```python
53+
from collections.abc import AsyncIterator
54+
from injection import scoped
55+
56+
class Client:
57+
async def open_connection(self) -> None: ...
58+
59+
async def close_connection(self) -> None: ...
60+
61+
@scoped("<scope-name>")
62+
async def client_recipe() -> AsyncIterator[Client]:
63+
# On resolving dependency
64+
client = Client()
65+
await client.open_connection()
66+
67+
try:
68+
yield client
69+
finally:
70+
# On scope close
71+
await client.close_connection()
72+
```
73+
74+
> Synchronous:
75+
76+
```python
77+
from collections.abc import Iterator
78+
from injection import scoped
79+
80+
class Client:
81+
def open_connection(self) -> None: ...
82+
83+
def close_connection(self) -> None: ...
84+
85+
@scoped("<scope-name>")
86+
def client_recipe() -> Iterator[Client]:
87+
# On resolving dependency
88+
client = Client()
89+
client.open_connection()
90+
91+
try:
92+
yield client
93+
finally:
94+
# On scope close
95+
client.close_connection()
96+
```

injection/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
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 async_scope, sync_scope
4+
from ._core.scope import adefine_scope, define_scope
55

66
__all__ = (
77
"Injectable",
88
"LazyInstance",
99
"Mode",
1010
"Module",
1111
"Priority",
12+
"adefine_scope",
1213
"afind_instance",
1314
"aget_instance",
1415
"aget_lazy_instance",
15-
"async_scope",
1616
"constant",
17+
"define_scope",
1718
"find_instance",
1819
"get_instance",
1920
"get_lazy_instance",
@@ -24,7 +25,6 @@
2425
"set_constant",
2526
"should_be_injectable",
2627
"singleton",
27-
"sync_scope",
2828
)
2929

3030
afind_instance = mod().afind_instance

injection/__init__.pyi

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
from abc import abstractmethod
2-
from collections.abc import Awaitable, Callable
3-
from contextlib import ContextDecorator
2+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
3+
from contextlib import asynccontextmanager, contextmanager
44
from enum import Enum
55
from logging import Logger
6-
from typing import (
7-
Any,
8-
AsyncContextManager,
9-
ContextManager,
10-
Final,
11-
Protocol,
12-
Self,
13-
final,
14-
overload,
15-
runtime_checkable,
16-
)
6+
from typing import Any, Final, Protocol, Self, final, overload, runtime_checkable
177

188
from ._core.common.invertible import Invertible as _Invertible
199
from ._core.common.type import InputType as _InputType
@@ -37,8 +27,10 @@ set_constant = __MODULE.set_constant
3727
should_be_injectable = __MODULE.should_be_injectable
3828
singleton = __MODULE.singleton
3929

40-
def async_scope(name: str, *, shared: bool = ...) -> AsyncContextManager[None]: ...
41-
def sync_scope(name: str, *, shared: bool = ...) -> ContextManager[None]: ...
30+
@asynccontextmanager
31+
def adefine_scope(name: str, *, shared: bool = ...) -> AsyncIterator[None]: ...
32+
@contextmanager
33+
def define_scope(name: str, *, shared: bool = ...) -> Iterator[None]: ...
4234
def mod(name: str = ..., /) -> Module:
4335
"""
4436
Short syntax for `Module.from_name`.
@@ -268,12 +260,13 @@ class Module:
268260
Function to remove a module in use.
269261
"""
270262

263+
@contextmanager
271264
def use_temporarily(
272265
self,
273266
module: Module,
274267
*,
275268
priority: Priority | PriorityStr = ...,
276-
) -> ContextManager[None] | ContextDecorator:
269+
) -> Iterator[None]:
277270
"""
278271
Context manager or decorator for temporary use of a module.
279272
"""

injection/_core/scope.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def bind_contextual_scope(self, scope: Scope) -> Iterator[None]:
6262

6363
@contextmanager
6464
def bind_shared_scope(self, scope: Scope) -> Iterator[None]:
65-
if self.__references:
65+
if self.get_active_scopes():
6666
raise ScopeError(
6767
"A shared scope can't be defined when one or more contextual scopes "
6868
"are defined on the same name."
@@ -91,14 +91,14 @@ def get_active_scopes(self) -> tuple[Scope, ...]:
9191

9292

9393
@asynccontextmanager
94-
async def async_scope(name: str, *, shared: bool = False) -> AsyncIterator[None]:
94+
async def adefine_scope(name: str, *, shared: bool = False) -> AsyncIterator[None]:
9595
async with AsyncScope() as scope:
9696
scope.enter(_bind_scope(name, scope, shared))
9797
yield
9898

9999

100100
@contextmanager
101-
def sync_scope(name: str, *, shared: bool = False) -> Iterator[None]:
101+
def define_scope(name: str, *, shared: bool = False) -> Iterator[None]:
102102
with SyncScope() as scope:
103103
scope.enter(_bind_scope(name, scope, shared))
104104
yield
@@ -120,7 +120,7 @@ def get_scope(name: str) -> Scope:
120120

121121

122122
@contextmanager
123-
def _bind_scope(name: str, value: Scope, shared: bool) -> Iterator[None]:
123+
def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
124124
state = __SCOPES[name]
125125

126126
if state.get_scope():
@@ -129,14 +129,14 @@ def _bind_scope(name: str, value: Scope, shared: bool) -> Iterator[None]:
129129
)
130130

131131
strategy = (
132-
state.bind_shared_scope(value) if shared else state.bind_contextual_scope(value)
132+
state.bind_shared_scope(scope) if shared else state.bind_contextual_scope(scope)
133133
)
134134

135135
try:
136136
with strategy:
137137
yield
138138
finally:
139-
value.cache.clear()
139+
scope.cache.clear()
140140

141141

142142
@runtime_checkable
@@ -208,7 +208,9 @@ def __exit__(
208208
return self.delegate.__exit__(exc_type, exc_value, traceback)
209209

210210
async def aenter[T](self, context_manager: AsyncContextManager[T]) -> T:
211-
raise ScopeError("SyncScope doesn't support asynchronous context manager.")
211+
raise ScopeError(
212+
"Synchronous scope doesn't support asynchronous context manager."
213+
)
212214

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

tests/core/test_module.py

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

33
import pytest
44

5-
from injection import Module, sync_scope
5+
from injection import Module, define_scope
66
from injection.exceptions import (
77
ModuleError,
88
ModuleLockError,
@@ -346,7 +346,7 @@ class Dependency: ...
346346

347347
assert module.is_locked is False
348348

349-
with sync_scope("test"):
349+
with define_scope("test"):
350350
instance_1 = module.get_instance(Dependency)
351351
assert module.is_locked is True
352352

tests/test_scoped.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import pytest
44

55
from injection import (
6+
adefine_scope,
67
afind_instance,
7-
async_scope,
8+
define_scope,
89
find_instance,
910
injectable,
1011
scoped,
11-
sync_scope,
1212
)
1313
from injection.exceptions import ScopeError, ScopeUndefinedError
1414

@@ -18,7 +18,7 @@ def test_scoped_with_success(self):
1818
@scoped("test")
1919
class SomeInjectable: ...
2020

21-
with sync_scope("test"):
21+
with define_scope("test"):
2222
instance_1 = find_instance(SomeInjectable)
2323
instance_2 = find_instance(SomeInjectable)
2424

@@ -30,7 +30,7 @@ class A: ...
3030
@scoped("test", on=A)
3131
class B(A): ...
3232

33-
with sync_scope("test"):
33+
with define_scope("test"):
3434
a = find_instance(A)
3535
b = find_instance(B)
3636

@@ -44,7 +44,7 @@ class B(A): ...
4444
@scoped("test", on=(A, B))
4545
class C(B): ...
4646

47-
with sync_scope("test"):
47+
with define_scope("test"):
4848
a = find_instance(A)
4949
b = find_instance(B)
5050
c = find_instance(C)
@@ -76,7 +76,7 @@ class A: ...
7676
@scoped("test", on=A, mode="override")
7777
class B(A): ...
7878

79-
with sync_scope("test"):
79+
with define_scope("test"):
8080
a = find_instance(A)
8181

8282
assert isinstance(a, B)
@@ -91,7 +91,7 @@ class B(A): ...
9191
@scoped("test", on=A, mode="override")
9292
class C(B): ...
9393

94-
with sync_scope("test"):
94+
with define_scope("test"):
9595
a = find_instance(A)
9696

9797
assert isinstance(a, C)
@@ -103,7 +103,7 @@ class SomeInjectable: ...
103103
def some_injectable_recipe() -> SomeInjectable:
104104
return SomeInjectable()
105105

106-
with sync_scope("test"):
106+
with define_scope("test"):
107107
instance_1 = find_instance(SomeInjectable)
108108
instance_2 = find_instance(SomeInjectable)
109109

@@ -116,7 +116,7 @@ class SomeInjectable: ...
116116
async def some_injectable_recipe() -> SomeInjectable:
117117
return SomeInjectable()
118118

119-
with sync_scope("test"):
119+
with define_scope("test"):
120120
instance_1 = await afind_instance(SomeInjectable)
121121
instance_2 = await afind_instance(SomeInjectable)
122122

@@ -129,7 +129,7 @@ class SomeInjectable: ...
129129
def some_injectable_recipe() -> Iterator[SomeInjectable]:
130130
yield SomeInjectable()
131131

132-
with sync_scope("test"):
132+
with define_scope("test"):
133133
instance_1 = find_instance(SomeInjectable)
134134
instance_2 = find_instance(SomeInjectable)
135135

@@ -142,7 +142,7 @@ class SomeInjectable: ...
142142
def some_injectable_recipe() -> Iterator[SomeInjectable]:
143143
yield SomeInjectable()
144144

145-
async with async_scope("test"):
145+
async with adefine_scope("test"):
146146
instance_1 = find_instance(SomeInjectable)
147147
instance_2 = find_instance(SomeInjectable)
148148

@@ -157,7 +157,7 @@ class SomeInjectable: ...
157157
async def some_injectable_recipe() -> AsyncIterator[SomeInjectable]:
158158
yield SomeInjectable() # pragma: no cover
159159

160-
with sync_scope("test"):
160+
with define_scope("test"):
161161
with pytest.raises(ScopeError):
162162
await afind_instance(SomeInjectable)
163163

@@ -168,7 +168,7 @@ class SomeInjectable: ...
168168
async def some_injectable_recipe() -> AsyncIterator[SomeInjectable]:
169169
yield SomeInjectable()
170170

171-
async with async_scope("test"):
171+
async with adefine_scope("test"):
172172
instance_1 = await afind_instance(SomeInjectable)
173173
instance_2 = await afind_instance(SomeInjectable)
174174

0 commit comments

Comments
 (0)