Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/providers/context_local_resource.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.. _context-local-resource-provider:

Context Local Resource provider
================================

.. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Resource,Context Local,
Context Variables,Singleton,Per-context
:description: Context Local Resource provider provides a component with initialization and shutdown
that is scoped to execution context using contextvars. This page demonstrates how to
use context local resource provider.

.. currentmodule:: dependency_injector.providers

``ContextLocalResource`` inherits from :ref:`resource-provider` and uses the same initialization and shutdown logic
as the standard ``Resource`` provider.
It extends it with context-local storage using Python's ``contextvars`` module.
This means that objects are context local singletons - the same context will
receive the same instance, but different execution contexts will have their own separate instances.

This is particularly useful in asynchronous applications where you need per-request resource instances
(such as database sessions) that are automatically cleaned up when the request context ends.
Example:

.. literalinclude:: ../../examples/providers/context_local_resource.py
:language: python
:lines: 3-



.. disqus::

1 change: 1 addition & 0 deletions docs/providers/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Providers module API docs - :py:mod:`dependency_injector.providers`
dict
configuration
resource
context_local_resource
aggregate
selector
dependency
Expand Down
3 changes: 3 additions & 0 deletions docs/providers/resource.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Resource provider
Resource providers help to initialize and configure logging, event loop, thread or process pool, etc.

Resource provider is similar to ``Singleton``. Resource initialization happens only once.
If you need a context local singleton (where each execution context has its own instance),
see :ref:`context-local-resource-provider`.

You can make injections and use provided instance the same way like you do with any other provider.

.. code-block:: python
Expand Down
50 changes: 50 additions & 0 deletions examples/providers/context_local_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from uuid import uuid4

from fastapi import Depends, FastAPI

from dependency_injector import containers, providers
from dependency_injector.wiring import Closing, Provide, inject

global_list = []


class AsyncSessionLocal:
def __init__(self):
self.id = uuid4()

async def __aenter__(self):
print("Entering session !")
return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
print("Closing session !")

async def execute(self, user_input):
return f"Executing {user_input} in session {self.id}"


app = FastAPI()


class Container(containers.DeclarativeContainer):
db_session = providers.ContextLocalResource(AsyncSessionLocal)


@app.get("/")
@inject
async def index(db: AsyncSessionLocal = Depends(Closing[Provide["db_session"]])):
global global_list
if db.id in global_list:
raise Exception("The db session is already used") # never reaches here
global_list.append(db.id)
res = await db.execute("SELECT 1")
return str(res)


if __name__ == "__main__":
import uvicorn

container = Container()
container.wire(modules=["__main__"])
uvicorn.run(app, host="localhost", port=8000)
container.unwire()
12 changes: 9 additions & 3 deletions src/dependency_injector/providers.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,9 @@ cdef class Dict(Provider):

cdef class Resource(Provider):
cdef object _provides
cdef bint _initialized
cdef object _shutdowner
cdef object _resource
cdef bint __initialized
cdef object __shutdowner
cdef object __resource

cdef tuple _args
cdef int _args_len
Expand All @@ -239,6 +239,12 @@ cdef class Resource(Provider):
cpdef object _provide(self, tuple args, dict kwargs)


cdef class ContextLocalResource(Resource):
cdef object _resource_context_var
cdef object _initialized_context_var
cdef object _shutdowner_context_var


cdef class Container(Provider):
cdef object _container_cls
cdef dict _overriding_providers
Expand Down
2 changes: 2 additions & 0 deletions src/dependency_injector/providers.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,8 @@ class Resource(Provider[T]):
def init(self) -> Optional[Awaitable[T]]: ...
def shutdown(self) -> Optional[Awaitable]: ...

class ContextLocalResource(Resource[T]):...

class Container(Provider[T]):
def __init__(
self,
Expand Down
155 changes: 115 additions & 40 deletions src/dependency_injector/providers.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -3186,7 +3186,7 @@ cdef class ThreadLocalSingleton(BaseSingleton):
return future_result

self._storage.instance = instance

return instance

def _async_init_instance(self, future_result, result):
Expand Down Expand Up @@ -3620,9 +3620,9 @@ cdef class Resource(Provider):
self._provides = None
self.set_provides(provides)

self._initialized = False
self._resource = None
self._shutdowner = None
self.__initialized = False
self.__resource = None
self.__shutdowner = None

self._args = tuple()
self._args_len = 0
Expand Down Expand Up @@ -3760,6 +3760,36 @@ cdef class Resource(Provider):
self._kwargs_len = len(self._kwargs)
return self

@property
def _initialized(self):
"""Get initialized state."""
return self.__initialized

@_initialized.setter
def _initialized(self, value):
"""Set initialized state."""
self.__initialized = value

@property
def _resource(self):
"""Get resource."""
return self.__resource

@_resource.setter
def _resource(self, value):
"""Set resource."""
self.__resource = value

@property
def _shutdowner(self):
"""Get shutdowner."""
return self.__shutdowner

@_shutdowner.setter
def _shutdowner(self, value):
"""Set shutdowner."""
self.__shutdowner = value

@property
def initialized(self):
"""Check if resource is initialized."""
Expand All @@ -3771,24 +3801,27 @@ cdef class Resource(Provider):

def shutdown(self):
"""Shutdown resource."""
if not self._initialized:
if not self._initialized :
self._reset_all_contex_vars()
if self._async_mode == ASYNC_MODE_ENABLED:
return NULL_AWAITABLE
return

if self._shutdowner:
future = self._shutdowner(None, None, None)

if __is_future_or_coroutine(future):
return ensure_future(self._shutdown_async(future))

self._resource = None
self._initialized = False
self._shutdowner = None
self._reset_all_contex_vars()
return ensure_future(future)

self._reset_all_contex_vars()
if self._async_mode == ASYNC_MODE_ENABLED:
return NULL_AWAITABLE

def _reset_all_contex_vars(self):
self._initialized = False
self._resource = None
self._shutdowner = None

@property
def related(self):
"""Return related providers generator."""
Expand All @@ -3797,41 +3830,28 @@ cdef class Resource(Provider):
yield from filter(is_provider, self.kwargs.values())
yield from super().related

async def _shutdown_async(self, future) -> None:
try:
await future
finally:
self._resource = None
self._initialized = False
self._shutdowner = None

async def _handle_async_cm(self, obj) -> None:
try:
self._resource = resource = await obj.__aenter__()
self._shutdowner = obj.__aexit__
resource = await obj.__aenter__()
return resource
except:
self._initialized = False
raise

async def _provide_async(self, future) -> None:
try:
obj = await future

if hasattr(obj, '__aenter__') and hasattr(obj, '__aexit__'):
self._resource = await obj.__aenter__()
self._shutdowner = obj.__aexit__
elif hasattr(obj, '__enter__') and hasattr(obj, '__exit__'):
self._resource = obj.__enter__()
self._shutdowner = obj.__exit__
else:
self._resource = obj
self._shutdowner = None
async def _provide_async(self, future):
obj = await future

return self._resource
except:
self._initialized = False
raise
if hasattr(obj, '__aenter__') and hasattr(obj, '__aexit__'):
resource = await obj.__aenter__()
shutdowner = obj.__aexit__
elif hasattr(obj, '__enter__') and hasattr(obj, '__exit__'):
resource = obj.__enter__()
shutdowner = obj.__exit__
else:
resource = obj
shutdowner = None

return resource, shutdowner

cpdef object _provide(self, tuple args, dict kwargs):
if self._initialized:
Expand All @@ -3850,14 +3870,18 @@ cdef class Resource(Provider):

if __is_future_or_coroutine(obj):
self._initialized = True
self._resource = resource = ensure_future(self._provide_async(obj))
return resource
future_result = asyncio.Future()
future = ensure_future(self._provide_async(obj))
future.add_done_callback(functools.partial(self._async_init_instance, future_result))
self._resource = future_result
return self._resource
elif hasattr(obj, '__enter__') and hasattr(obj, '__exit__'):
self._resource = obj.__enter__()
self._shutdowner = obj.__exit__
elif hasattr(obj, '__aenter__') and hasattr(obj, '__aexit__'):
self._initialized = True
self._resource = resource = ensure_future(self._handle_async_cm(obj))
self._shutdowner = obj.__aexit__
return resource
else:
self._resource = obj
Expand All @@ -3866,6 +3890,57 @@ cdef class Resource(Provider):
self._initialized = True
return self._resource

def _async_init_instance(self, future_result, result):
try:
resource, shutdowner = result.result()
except Exception as exception:
self._resource = None
self._shutdowner = None
self._initialized = False
future_result.set_exception(exception)
else:
self._resource = resource
self._shutdowner = shutdowner
future_result.set_result(resource)


cdef class ContextLocalResource(Resource):
def __init__(self, provides=None, *args, **kwargs):
self._initialized_context_var = ContextVar("_initialized_context_var", default=False)
self._resource_context_var = ContextVar("_resource_context_var", default=None)
self._shutdowner_context_var = ContextVar("_shutdowner_context_var", default=None)
super().__init__(provides, *args, **kwargs)

@property
def _initialized(self):
"""Get initialized state."""
return self._initialized_context_var.get()

@_initialized.setter
def _initialized(self, value):
"""Set initialized state."""
self._initialized_context_var.set(value)

@property
def _resource(self):
"""Get resource."""
return self._resource_context_var.get()

@_resource.setter
def _resource(self, value):
"""Set resource."""
self._resource_context_var.set(value)

@property
def _shutdowner(self):
"""Get shutdowner."""
return self._shutdowner_context_var.get()

@_shutdowner.setter
def _shutdowner(self, value):
"""Set shutdowner."""
self._shutdowner_context_var.set(value)


cdef class Container(Provider):
"""Container provider provides an instance of declarative container.
Expand Down
Loading