Skip to content

Commit 5259351

Browse files
Add context local resource (#931)
Co-authored-by: ZipFile <[email protected]>
1 parent 76d5932 commit 5259351

16 files changed

Lines changed: 811 additions & 97 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
.. _context-local-resource-provider:
2+
3+
Context Local Resource provider
4+
================================
5+
6+
.. meta::
7+
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Resource,Context Local,
8+
Context Variables,Singleton,Per-context
9+
:description: Context Local Resource provider provides a component with initialization and shutdown
10+
that is scoped to execution context using contextvars. This page demonstrates how to
11+
use context local resource provider.
12+
13+
.. currentmodule:: dependency_injector.providers
14+
15+
``ContextLocalResource`` inherits from :ref:`resource-provider` and uses the same initialization and shutdown logic
16+
as the standard ``Resource`` provider.
17+
It extends it with context-local storage using Python's ``contextvars`` module.
18+
This means that objects are context local singletons - the same context will
19+
receive the same instance, but different execution contexts will have their own separate instances.
20+
21+
This is particularly useful in asynchronous applications where you need per-request resource instances
22+
(such as database sessions) that are automatically cleaned up when the request context ends.
23+
Example:
24+
25+
.. literalinclude:: ../../examples/providers/context_local_resource.py
26+
:language: python
27+
:lines: 3-
28+
29+
30+
31+
.. disqus::
32+

docs/providers/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Providers module API docs - :py:mod:`dependency_injector.providers`
4646
dict
4747
configuration
4848
resource
49+
context_local_resource
4950
aggregate
5051
selector
5152
dependency

docs/providers/resource.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Resource provider
2121
Resource providers help to initialize and configure logging, event loop, thread or process pool, etc.
2222

2323
Resource provider is similar to ``Singleton``. Resource initialization happens only once.
24+
If you need a context local singleton (where each execution context has its own instance),
25+
see :ref:`context-local-resource-provider`.
26+
2427
You can make injections and use provided instance the same way like you do with any other provider.
2528

2629
.. code-block:: python
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from uuid import uuid4
2+
3+
from fastapi import Depends, FastAPI
4+
5+
from dependency_injector import containers, providers
6+
from dependency_injector.wiring import Closing, Provide, inject
7+
8+
global_list = []
9+
10+
11+
class AsyncSessionLocal:
12+
def __init__(self):
13+
self.id = uuid4()
14+
15+
async def __aenter__(self):
16+
print("Entering session !")
17+
return self
18+
19+
async def __aexit__(self, exc_type, exc_val, exc_tb):
20+
print("Closing session !")
21+
22+
async def execute(self, user_input):
23+
return f"Executing {user_input} in session {self.id}"
24+
25+
26+
app = FastAPI()
27+
28+
29+
class Container(containers.DeclarativeContainer):
30+
db_session = providers.ContextLocalResource(AsyncSessionLocal)
31+
32+
33+
@app.get("/")
34+
@inject
35+
async def index(db: AsyncSessionLocal = Depends(Closing[Provide["db_session"]])):
36+
if db.id in global_list:
37+
raise Exception("The db session was already used") # never reaches here
38+
global_list.append(db.id)
39+
res = await db.execute("SELECT 1")
40+
return str(res)
41+
42+
43+
if __name__ == "__main__":
44+
import uvicorn
45+
46+
container = Container()
47+
container.wire(modules=["__main__"])
48+
uvicorn.run(app, host="localhost", port=8000)
49+
container.unwire()

src/dependency_injector/_cwiring.pyx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ from collections.abc import Awaitable
55
from inspect import CO_ITERABLE_COROUTINE
66
from types import CoroutineType, GeneratorType
77

8-
from .providers cimport Provider, Resource
8+
from .providers cimport Provider, BaseResource
99
from .wiring import _Marker
1010

1111

@@ -54,15 +54,15 @@ cdef class DependencyResolver:
5454
cdef Provider provider
5555

5656
for name, provider in self.closings.items():
57-
if _is_injectable(self.kwargs, name) and isinstance(provider, Resource):
57+
if _is_injectable(self.kwargs, name) and isinstance(provider, BaseResource):
5858
provider.shutdown()
5959

6060
cdef list _handle_closings_async(self):
6161
cdef list to_await = []
6262
cdef Provider provider
6363

6464
for name, provider in self.closings.items():
65-
if _is_injectable(self.kwargs, name) and isinstance(provider, Resource):
65+
if _is_injectable(self.kwargs, name) and isinstance(provider, BaseResource):
6666
if _isawaitable(shutdown := provider.shutdown()):
6767
to_await.append(shutdown)
6868

src/dependency_injector/containers.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ try:
2222
except ImportError:
2323
from typing_extensions import Self as _Self
2424

25-
from .providers import Provider, Resource, Self, ProviderParent
25+
from .providers import Provider, BaseResource, Self, ProviderParent
2626

2727
C_Base = TypeVar("C_Base", bound="Container")
2828
C = TypeVar("C", bound="DeclarativeContainer")
@@ -77,8 +77,8 @@ class Container:
7777
warn_unresolved: bool = False,
7878
) -> None: ...
7979
def unwire(self) -> None: ...
80-
def init_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ...
81-
def shutdown_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ...
80+
def init_resources(self, resource_type: Type[BaseResource[Any]] = BaseResource) -> Optional[Awaitable[None]]: ...
81+
def shutdown_resources(self, resource_type: Type[BaseResource[Any]] = BaseResource) -> Optional[Awaitable[None]]: ...
8282
def load_config(self) -> None: ...
8383
def apply_container_providers_overridings(self) -> None: ...
8484
def reset_singletons(self) -> SingletonResetContext[C_Base]: ...

src/dependency_injector/containers.pyx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -339,10 +339,10 @@ class DynamicContainer(Container):
339339
self.wired_to_modules.clear()
340340
self.wired_to_packages.clear()
341341

342-
def init_resources(self, resource_type=providers.Resource):
342+
def init_resources(self, resource_type=providers.BaseResource):
343343
"""Initialize all container resources."""
344344

345-
if not issubclass(resource_type, providers.Resource):
345+
if not issubclass(resource_type, providers.BaseResource):
346346
raise TypeError("resource_type must be a subclass of Resource provider")
347347

348348
futures = []
@@ -356,10 +356,10 @@ class DynamicContainer(Container):
356356
if futures:
357357
return asyncio.gather(*futures)
358358

359-
def shutdown_resources(self, resource_type=providers.Resource):
359+
def shutdown_resources(self, resource_type=providers.BaseResource):
360360
"""Shutdown all container resources."""
361361

362-
if not issubclass(resource_type, providers.Resource):
362+
if not issubclass(resource_type, providers.BaseResource):
363363
raise TypeError("resource_type must be a subclass of Resource provider")
364364

365365
def _independent_resources(resources):

src/dependency_injector/providers.pxd

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,19 @@ cdef class Dict(Provider):
224224
cpdef object _provide(self, tuple args, dict kwargs)
225225

226226

227-
cdef class Resource(Provider):
227+
cdef class ResourceState:
228+
cdef object resource
229+
cdef object shutdowner
230+
cdef bint is_async
231+
cdef bint async_done
232+
233+
cdef void from_coro(self, coro, error_callback)
234+
cdef void from_async_context_manager(self, acm, error_callback)
235+
cdef object from_context_manager(self, cm, error_callback)
236+
237+
238+
cdef class BaseResource(Provider):
228239
cdef object _provides
229-
cdef bint _initialized
230-
cdef object _shutdowner
231-
cdef object _resource
232240

233241
cdef tuple _args
234242
cdef int _args_len
@@ -237,6 +245,16 @@ cdef class Resource(Provider):
237245
cdef int _kwargs_len
238246

239247
cpdef object _provide(self, tuple args, dict kwargs)
248+
cdef void set_state(self, ResourceState state)
249+
cdef ResourceState get_state(self)
250+
251+
252+
cdef class Resource(BaseResource):
253+
cdef ResourceState _state
254+
255+
256+
cdef class ContextLocalResource(BaseResource):
257+
cdef object _cvar
240258

241259

242260
cdef class Container(Provider):

src/dependency_injector/providers.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ class Dict(Provider[_Dict]):
449449
) -> _Self: ...
450450
def clear_kwargs(self) -> _Self: ...
451451

452-
class Resource(Provider[T]):
452+
class BaseResource(Provider[T]):
453453
@overload
454454
def __init__(
455455
self,
@@ -524,6 +524,9 @@ class Resource(Provider[T]):
524524
def init(self) -> Optional[Awaitable[T]]: ...
525525
def shutdown(self) -> Optional[Awaitable]: ...
526526

527+
class Resource(BaseResource[T]): ...
528+
class ContextLocalResource(BaseResource[T]):...
529+
527530
class Container(Provider[T]):
528531
def __init__(
529532
self,

0 commit comments

Comments
 (0)