From 4b3476cb4833034576a516e9939ee51d18cd07d9 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Wed, 28 May 2025 21:45:33 +0000 Subject: [PATCH 01/27] Use cache in _fetch_reference_injections() --- src/dependency_injector/containers.pyi | 2 ++ src/dependency_injector/containers.pyx | 11 ++++-- src/dependency_injector/wiring.py | 17 ++++++++++ tests/unit/wiring/test_cache.py | 46 ++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 tests/unit/wiring/test_cache.py diff --git a/src/dependency_injector/containers.pyi b/src/dependency_injector/containers.pyi index ec41ea8e..c637f19c 100644 --- a/src/dependency_injector/containers.pyi +++ b/src/dependency_injector/containers.pyi @@ -30,12 +30,14 @@ class WiringConfiguration: packages: List[Any] from_package: Optional[str] auto_wire: bool + keep_cache: bool def __init__( self, modules: Optional[Iterable[Any]] = None, packages: Optional[Iterable[Any]] = None, from_package: Optional[str] = None, auto_wire: bool = True, + keep_cache: bool = False, ) -> None: ... class Container: diff --git a/src/dependency_injector/containers.pyx b/src/dependency_injector/containers.pyx index 2f4c4af5..bd0a4821 100644 --- a/src/dependency_injector/containers.pyx +++ b/src/dependency_injector/containers.pyx @@ -20,14 +20,15 @@ from .wiring import wire, unwire class WiringConfiguration: """Container wiring configuration.""" - def __init__(self, modules=None, packages=None, from_package=None, auto_wire=True): + def __init__(self, modules=None, packages=None, from_package=None, auto_wire=True, keep_cache=False): self.modules = [*modules] if modules else [] self.packages = [*packages] if packages else [] self.from_package = from_package self.auto_wire = auto_wire + self.keep_cache = keep_cache def __deepcopy__(self, memo=None): - return self.__class__(self.modules, self.packages, self.from_package, self.auto_wire) + return self.__class__(self.modules, self.packages, self.from_package, self.auto_wire, self.keep_cache) class Container: @@ -258,7 +259,7 @@ class DynamicContainer(Container): """Check if auto wiring is needed.""" return self.wiring_config.auto_wire is True - def wire(self, modules=None, packages=None, from_package=None): + def wire(self, modules=None, packages=None, from_package=None, keep_cache=None): """Wire container providers with provided packages and modules. :rtype: None @@ -289,10 +290,14 @@ class DynamicContainer(Container): if not modules and not packages: return + if keep_cache is None: + keep_cache = self.wiring_config.keep_cache + wire( container=self, modules=modules, packages=packages, + keep_cache=keep_cache, ) if modules: diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index b8534ee5..9c976c2d 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -26,6 +26,13 @@ from typing_extensions import Self +try: + from functools import cache +except ImportError: + from functools import lru_cache + + cache = lru_cache(maxsize=None) + # Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/362 if sys.version_info >= (3, 9): from types import GenericAlias @@ -409,6 +416,7 @@ def wire( # noqa: C901 *, modules: Optional[Iterable[ModuleType]] = None, packages: Optional[Iterable[ModuleType]] = None, + keep_cache: bool = False, ) -> None: """Wire container providers with provided packages and modules.""" modules = [*modules] if modules else [] @@ -449,6 +457,9 @@ def wire( # noqa: C901 for patched in _patched_registry.get_callables_from_module(module): _bind_injections(patched, providers_map) + if not keep_cache: + clear_cache() + def unwire( # noqa: C901 *, @@ -604,6 +615,7 @@ def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]: return marker +@cache def _fetch_reference_injections( # noqa: C901 fn: Callable[..., Any], ) -> Tuple[Dict[str, Any], Dict[str, Any]]: @@ -1078,3 +1090,8 @@ def _get_members_and_annotated(obj: Any) -> Iterable[Tuple[str, Any]]: member = args[1] members.append((annotation_name, member)) return members + + +def clear_cache() -> None: + """Clear all caches used by :func:`wire`.""" + _fetch_reference_injections.cache_clear() diff --git a/tests/unit/wiring/test_cache.py b/tests/unit/wiring/test_cache.py new file mode 100644 index 00000000..d6c1f45f --- /dev/null +++ b/tests/unit/wiring/test_cache.py @@ -0,0 +1,46 @@ +"""Tests for string module and package names.""" + +from typing import Iterator, Optional + +from pytest import fixture, mark +from samples.wiring.container import Container + +from dependency_injector.wiring import _fetch_reference_injections + + +@fixture +def container() -> Iterator[Container]: + container = Container() + yield container + container.unwire() + + +@mark.parametrize( + ["arg_value", "wc_value", "empty_cache"], + [ + (None, False, True), + (False, True, True), + (True, False, False), + (None, True, False), + ], +) +def test_fetch_reference_injections_cache( + container: Container, + arg_value: Optional[bool], + wc_value: bool, + empty_cache: bool, +) -> None: + container.wiring_config.keep_cache = wc_value + container.wire( + modules=["samples.wiring.module"], + packages=["samples.wiring.package"], + keep_cache=arg_value, + ) + cache_info = _fetch_reference_injections.cache_info() + + if empty_cache: + assert cache_info == (0, 0, None, 0) + else: + assert cache_info.hits > 0 + assert cache_info.misses > 0 + assert cache_info.currsize > 0 From a322584308f337bc1ad4f9c95ef4a903900926af Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 1 Jun 2025 14:03:41 +0000 Subject: [PATCH 02/27] Add context manager support to Resource provider --- docs/providers/resource.rst | 59 +++- examples/providers/resource.py | 2 + src/dependency_injector/providers.pyx | 253 ++++++------------ src/dependency_injector/resources.py | 49 +++- .../resource/test_async_resource_py35.py | 45 +++- .../providers/resource/test_resource_py35.py | 43 ++- 6 files changed, 260 insertions(+), 191 deletions(-) diff --git a/docs/providers/resource.rst b/docs/providers/resource.rst index 918dfa66..cf935f3a 100644 --- a/docs/providers/resource.rst +++ b/docs/providers/resource.rst @@ -61,11 +61,12 @@ When you call ``.shutdown()`` method on a resource provider, it will remove the if any, and switch to uninitialized state. Some of resource initializer types support specifying custom resource shutdown. -Resource provider supports 3 types of initializers: +Resource provider supports 4 types of initializers: - Function -- Generator -- Subclass of ``resources.Resource`` +- Context Manager +- Generator (legacy) +- Subclass of ``resources.Resource`` (legacy) Function initializer -------------------- @@ -103,8 +104,44 @@ you configure global resource: Function initializer does not provide a way to specify custom resource shutdown. -Generator initializer ---------------------- +Context Manager initializer +--------------------------- + +This is an extension to the Function initializer. Resource provider automatically detects if the initializer returns a +context manager and uses it to manage the resource lifecycle. + +.. code-block:: python + + from dependency_injector import containers, providers + + class DatabaseConnection: + def __init__(self, host, port, user, password): + self.host = host + self.port = port + self.user = user + self.password = password + + def __enter__(self): + print(f"Connecting to {self.host}:{self.port} as {self.user}") + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + print("Closing connection") + + + class Container(containers.DeclarativeContainer): + + config = providers.Configuration() + db = providers.Resource( + DatabaseConnection, + host=config.db.host, + port=config.db.port, + user=config.db.user, + password=config.db.password, + ) + +Generator initializer (legacy) +------------------------------ Resource provider can use 2-step generators: @@ -154,8 +191,13 @@ object is not mandatory. You can leave ``yield`` statement empty: argument2=..., ) -Subclass initializer --------------------- +.. note:: + + Generator initializers are automatically wrapped with ``contextmanager`` or ``asynccontextmanager`` decorator when + provided to a ``Resource`` provider. + +Subclass initializer (legacy) +----------------------------- You can create resource initializer by implementing a subclass of the ``resources.Resource``: @@ -263,10 +305,11 @@ Asynchronous function initializer: argument2=..., ) -Asynchronous generator initializer: +Asynchronous Context Manager initializer: .. code-block:: python + @asynccontextmanager async def init_async_resource(argument1=..., argument2=...): connection = await connect() yield connection diff --git a/examples/providers/resource.py b/examples/providers/resource.py index 2079a929..c712468a 100644 --- a/examples/providers/resource.py +++ b/examples/providers/resource.py @@ -3,10 +3,12 @@ import sys import logging from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager from dependency_injector import containers, providers +@contextmanager def init_thread_pool(max_workers: int): thread_pool = ThreadPoolExecutor(max_workers=max_workers) yield thread_pool diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index d276903b..43e49d7e 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -15,8 +15,11 @@ import re import sys import threading import warnings +from asyncio import ensure_future from configparser import ConfigParser as IniConfigParser +from contextlib import asynccontextmanager, contextmanager from contextvars import ContextVar +from inspect import isasyncgenfunction, isgeneratorfunction try: from inspect import _is_coroutine_mark as _is_coroutine_marker @@ -3598,6 +3601,17 @@ cdef class Dict(Provider): return __provide_keyword_args(kwargs, self._kwargs, self._kwargs_len, self._async_mode) +@cython.no_gc +cdef class NullAwaitable: + def __next__(self): + raise StopIteration from None + + def __await__(self): + return self + + +cdef NullAwaitable NULL_AWAITABLE = NullAwaitable() + cdef class Resource(Provider): """Resource provider provides a component with initialization and shutdown.""" @@ -3653,6 +3667,12 @@ cdef class Resource(Provider): def set_provides(self, provides): """Set provider provides.""" provides = _resolve_string_import(provides) + + if isasyncgenfunction(provides): + provides = asynccontextmanager(provides) + elif isgeneratorfunction(provides): + provides = contextmanager(provides) + self._provides = provides return self @@ -3753,28 +3773,21 @@ cdef class Resource(Provider): """Shutdown resource.""" if not self._initialized: if self._async_mode == ASYNC_MODE_ENABLED: - result = asyncio.Future() - result.set_result(None) - return result + return NULL_AWAITABLE return if self._shutdowner: - try: - shutdown = self._shutdowner(self._resource) - except StopIteration: - pass - else: - if inspect.isawaitable(shutdown): - return self._create_shutdown_future(shutdown) + 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 if self._async_mode == ASYNC_MODE_ENABLED: - result = asyncio.Future() - result.set_result(None) - return result + return NULL_AWAITABLE @property def related(self): @@ -3784,165 +3797,75 @@ 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__ + 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 + + return self._resource + except: + self._initialized = False + raise + cpdef object _provide(self, tuple args, dict kwargs): if self._initialized: return self._resource - if self._is_resource_subclass(self._provides): - initializer = self._provides() - self._resource = __call( - initializer.init, - args, - self._args, - self._args_len, - kwargs, - self._kwargs, - self._kwargs_len, - self._async_mode, - ) - self._shutdowner = initializer.shutdown - elif self._is_async_resource_subclass(self._provides): - initializer = self._provides() - async_init = __call( - initializer.init, - args, - self._args, - self._args_len, - kwargs, - self._kwargs, - self._kwargs_len, - self._async_mode, - ) - self._initialized = True - return self._create_init_future(async_init, initializer.shutdown) - elif inspect.isgeneratorfunction(self._provides): - initializer = __call( - self._provides, - args, - self._args, - self._args_len, - kwargs, - self._kwargs, - self._kwargs_len, - self._async_mode, - ) - self._resource = next(initializer) - self._shutdowner = initializer.send - elif iscoroutinefunction(self._provides): - initializer = __call( - self._provides, - args, - self._args, - self._args_len, - kwargs, - self._kwargs, - self._kwargs_len, - self._async_mode, - ) + obj = __call( + self._provides, + args, + self._args, + self._args_len, + kwargs, + self._kwargs, + self._kwargs_len, + self._async_mode, + ) + + if __is_future_or_coroutine(obj): self._initialized = True - return self._create_init_future(initializer) - elif isasyncgenfunction(self._provides): - initializer = __call( - self._provides, - args, - self._args, - self._args_len, - kwargs, - self._kwargs, - self._kwargs_len, - self._async_mode, - ) + self._resource = resource = ensure_future(self._provide_async(obj)) + return 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 - return self._create_async_gen_init_future(initializer) - elif callable(self._provides): - self._resource = __call( - self._provides, - args, - self._args, - self._args_len, - kwargs, - self._kwargs, - self._kwargs_len, - self._async_mode, - ) + self._resource = resource = ensure_future(self._handle_async_cm(obj)) + return resource else: - raise Error("Unknown type of resource initializer") + self._resource = obj + self._shutdowner = None self._initialized = True return self._resource - def _create_init_future(self, future, shutdowner=None): - callback = self._async_init_callback - if shutdowner: - callback = functools.partial(callback, shutdowner=shutdowner) - - future = asyncio.ensure_future(future) - future.add_done_callback(callback) - self._resource = future - - return future - - def _create_async_gen_init_future(self, initializer): - if inspect.isasyncgen(initializer): - return self._create_init_future(initializer.__anext__(), initializer.asend) - - future = asyncio.Future() - - create_initializer = asyncio.ensure_future(initializer) - create_initializer.add_done_callback(functools.partial(self._async_create_gen_callback, future)) - self._resource = future - - return future - - def _async_init_callback(self, initializer, shutdowner=None): - try: - resource = initializer.result() - except Exception: - self._initialized = False - else: - self._resource = resource - self._shutdowner = shutdowner - - def _async_create_gen_callback(self, future, initializer_future): - initializer = initializer_future.result() - init_future = self._create_init_future(initializer.__anext__(), initializer.asend) - init_future.add_done_callback(functools.partial(self._async_trigger_result, future)) - - def _async_trigger_result(self, future, future_result): - future.set_result(future_result.result()) - - def _create_shutdown_future(self, shutdown_future): - future = asyncio.Future() - shutdown_future = asyncio.ensure_future(shutdown_future) - shutdown_future.add_done_callback(functools.partial(self._async_shutdown_callback, future)) - return future - - def _async_shutdown_callback(self, future_result, shutdowner): - try: - shutdowner.result() - except StopAsyncIteration: - pass - - self._resource = None - self._initialized = False - self._shutdowner = None - - future_result.set_result(None) - - @staticmethod - def _is_resource_subclass(instance): - if not isinstance(instance, type): - return - from . import resources - return issubclass(instance, resources.Resource) - - @staticmethod - def _is_async_resource_subclass(instance): - if not isinstance(instance, type): - return - from . import resources - return issubclass(instance, resources.AsyncResource) - cdef class Container(Provider): """Container provider provides an instance of declarative container. @@ -4993,14 +4916,6 @@ def iscoroutinefunction(obj): return False -def isasyncgenfunction(obj): - """Check if object is an asynchronous generator function.""" - try: - return inspect.isasyncgenfunction(obj) - except AttributeError: - return False - - def _resolve_string_import(provides): if provides is None: return provides diff --git a/src/dependency_injector/resources.py b/src/dependency_injector/resources.py index 7d71d4d8..8722af22 100644 --- a/src/dependency_injector/resources.py +++ b/src/dependency_injector/resources.py @@ -1,23 +1,54 @@ """Resources module.""" -import abc -from typing import TypeVar, Generic, Optional - +from abc import ABCMeta, abstractmethod +from typing import Any, ClassVar, Generic, Optional, Tuple, TypeVar T = TypeVar("T") -class Resource(Generic[T], metaclass=abc.ABCMeta): +class Resource(Generic[T], metaclass=ABCMeta): + __slots__: ClassVar[Tuple[str, ...]] = ("args", "kwargs", "obj") + + obj: Optional[T] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.args = args + self.kwargs = kwargs + self.obj = None - @abc.abstractmethod - def init(self, *args, **kwargs) -> Optional[T]: ... + @abstractmethod + def init(self, *args: Any, **kwargs: Any) -> Optional[T]: ... def shutdown(self, resource: Optional[T]) -> None: ... + def __enter__(self) -> Optional[T]: + self.obj = obj = self.init(*self.args, **self.kwargs) + return obj + + def __exit__(self, *exc_info: Any) -> None: + self.shutdown(self.obj) + self.obj = None + -class AsyncResource(Generic[T], metaclass=abc.ABCMeta): +class AsyncResource(Generic[T], metaclass=ABCMeta): + __slots__: ClassVar[Tuple[str, ...]] = ("args", "kwargs", "obj") - @abc.abstractmethod - async def init(self, *args, **kwargs) -> Optional[T]: ... + obj: Optional[T] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.args = args + self.kwargs = kwargs + self.obj = None + + @abstractmethod + async def init(self, *args: Any, **kwargs: Any) -> Optional[T]: ... async def shutdown(self, resource: Optional[T]) -> None: ... + + async def __aenter__(self) -> Optional[T]: + self.obj = obj = await self.init(*self.args, **self.kwargs) + return obj + + async def __aexit__(self, *exc_info: Any) -> None: + await self.shutdown(self.obj) + self.obj = None diff --git a/tests/unit/providers/resource/test_async_resource_py35.py b/tests/unit/providers/resource/test_async_resource_py35.py index 1ca950a8..6458584d 100644 --- a/tests/unit/providers/resource/test_async_resource_py35.py +++ b/tests/unit/providers/resource/test_async_resource_py35.py @@ -2,12 +2,13 @@ import asyncio import inspect -import sys +from contextlib import asynccontextmanager from typing import Any -from dependency_injector import containers, providers, resources from pytest import mark, raises +from dependency_injector import containers, providers, resources + @mark.asyncio async def test_init_async_function(): @@ -70,6 +71,46 @@ async def _init(): assert _init.shutdown_counter == 2 +@mark.asyncio +async def test_init_async_context_manager() -> None: + resource = object() + + init_counter = 0 + shutdown_counter = 0 + + @asynccontextmanager + async def _init(): + nonlocal init_counter, shutdown_counter + + await asyncio.sleep(0.001) + init_counter += 1 + + yield resource + + await asyncio.sleep(0.001) + shutdown_counter += 1 + + provider = providers.Resource(_init) + + result1 = await provider() + assert result1 is resource + assert init_counter == 1 + assert shutdown_counter == 0 + + await provider.shutdown() + assert init_counter == 1 + assert shutdown_counter == 1 + + result2 = await provider() + assert result2 is resource + assert init_counter == 2 + assert shutdown_counter == 1 + + await provider.shutdown() + assert init_counter == 2 + assert shutdown_counter == 2 + + @mark.asyncio async def test_init_async_class(): resource = object() diff --git a/tests/unit/providers/resource/test_resource_py35.py b/tests/unit/providers/resource/test_resource_py35.py index 9b906bd7..842d8ba6 100644 --- a/tests/unit/providers/resource/test_resource_py35.py +++ b/tests/unit/providers/resource/test_resource_py35.py @@ -2,10 +2,12 @@ import decimal import sys +from contextlib import contextmanager from typing import Any -from dependency_injector import containers, providers, resources, errors -from pytest import raises, mark +from pytest import mark, raises + +from dependency_injector import containers, errors, providers, resources def init_fn(*args, **kwargs): @@ -123,6 +125,41 @@ def _init(): assert _init.shutdown_counter == 2 +def test_init_context_manager() -> None: + init_counter, shutdown_counter = 0, 0 + + @contextmanager + def _init(): + nonlocal init_counter, shutdown_counter + + init_counter += 1 + yield + shutdown_counter += 1 + + init_counter = 0 + shutdown_counter = 0 + + provider = providers.Resource(_init) + + result1 = provider() + assert result1 is None + assert init_counter == 1 + assert shutdown_counter == 0 + + provider.shutdown() + assert init_counter == 1 + assert shutdown_counter == 1 + + result2 = provider() + assert result2 is None + assert init_counter == 2 + assert shutdown_counter == 1 + + provider.shutdown() + assert init_counter == 2 + assert shutdown_counter == 2 + + def test_init_class(): class TestResource(resources.Resource): init_counter = 0 @@ -190,7 +227,7 @@ def init(self): def test_init_not_callable(): provider = providers.Resource(1) - with raises(errors.Error): + with raises(TypeError, match=r"object is not callable"): provider.init() From 51c7db771d22eed1a6ff0a3ae1bdc5715f2ed516 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 1 Jun 2025 17:35:32 +0000 Subject: [PATCH 03/27] Fix csv newline handling in movie-lister example --- examples/miniapps/movie-lister/data/fixtures.py | 5 ++--- examples/miniapps/movie-lister/movies/finders.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/miniapps/movie-lister/data/fixtures.py b/examples/miniapps/movie-lister/data/fixtures.py index aa1691d5..0153e0cf 100644 --- a/examples/miniapps/movie-lister/data/fixtures.py +++ b/examples/miniapps/movie-lister/data/fixtures.py @@ -18,10 +18,9 @@ def create_csv(movies_data, path): - with open(path, "w") as opened_file: + with open(path, "w", newline="") as opened_file: writer = csv.writer(opened_file) - for row in movies_data: - writer.writerow(row) + writer.writerows(movies_data) def create_sqlite(movies_data, path): diff --git a/examples/miniapps/movie-lister/movies/finders.py b/examples/miniapps/movie-lister/movies/finders.py index 52b8ed55..5e6d2c9c 100644 --- a/examples/miniapps/movie-lister/movies/finders.py +++ b/examples/miniapps/movie-lister/movies/finders.py @@ -29,7 +29,7 @@ def __init__( super().__init__(movie_factory) def find_all(self) -> List[Movie]: - with open(self._csv_file_path) as csv_file: + with open(self._csv_file_path, newline="") as csv_file: csv_reader = csv.reader(csv_file, delimiter=self._delimiter) return [self._movie_factory(*row) for row in csv_reader] From cdd9ce504858146ee109e49a9a12f5dcb85383d9 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 1 Jun 2025 17:51:04 +0000 Subject: [PATCH 04/27] Add doc section on wire() caching --- docs/wiring.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/wiring.rst b/docs/wiring.rst index 02f64c60..456f1e89 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -631,6 +631,36 @@ or with a single container ``register_loader_containers(container)`` multiple ti To unregister a container use ``unregister_loader_containers(container)``. Wiring module will uninstall the import hook when unregister last container. +Few notes on performance +------------------------ + +``.wire()`` utilize caching to speed up the wiring process. At the end it clears the cache to avoid memory leaks. +But this may not always be desirable, when you want to keep the cache for the next wiring +(e.g. due to usage of multiple containers or during unit tests). + +To keep the cache after wiring, you can set flag ``keep_cache=True`` (works with ``WiringConfiguration`` too): + +.. code-block:: python + + container1.wire( + modules=["yourapp.module1", "yourapp.module2"], + keep_cache=True, + ) + container2.wire( + modules=["yourapp.module2", "yourapp.module3"], + keep_cache=True, + ) + ... + +and then clear it manually when you need it: + +.. code-block:: python + + from dependency_injector.wiring import clear_cache + + clear_cache() + + Integration with other frameworks --------------------------------- From a8914e54e0c8e1be9fbe409384d8774afcf4ebc9 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 1 Jun 2025 18:08:37 +0000 Subject: [PATCH 05/27] Fix Sphinx warnings --- docs/api/index.rst | 2 +- docs/conf.py | 2 +- docs/introduction/key_features.rst | 2 +- docs/tutorials/aiohttp.rst | 4 ++-- docs/tutorials/cli.rst | 26 +++++++++++++------------- docs/tutorials/flask.rst | 4 ++-- docs/wiring.rst | 1 + 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/docs/api/index.rst b/docs/api/index.rst index c6b4cfa8..86389688 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -2,7 +2,7 @@ API Documentation ================= .. toctree:: - :maxdepth: 2 + :maxdepth: 2 top-level providers diff --git a/docs/conf.py b/docs/conf.py index 380da2da..4de57da7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -72,7 +72,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/introduction/key_features.rst b/docs/introduction/key_features.rst index 0870ac11..1975e8fc 100644 --- a/docs/introduction/key_features.rst +++ b/docs/introduction/key_features.rst @@ -31,7 +31,7 @@ Key features of the ``Dependency Injector``: The framework stands on the `PEP20 (The Zen of Python) `_ principle: -.. code-block:: plain +.. code-block:: text Explicit is better than implicit diff --git a/docs/tutorials/aiohttp.rst b/docs/tutorials/aiohttp.rst index 57b1c959..10b33b49 100644 --- a/docs/tutorials/aiohttp.rst +++ b/docs/tutorials/aiohttp.rst @@ -257,7 +257,7 @@ Let's check that it works. Open another terminal session and use ``httpie``: You should see: -.. code-block:: json +.. code-block:: http HTTP/1.1 200 OK Content-Length: 844 @@ -596,7 +596,7 @@ and make a request to the API in the terminal: You should see: -.. code-block:: json +.. code-block:: http HTTP/1.1 200 OK Content-Length: 492 diff --git a/docs/tutorials/cli.rst b/docs/tutorials/cli.rst index 88014ff3..ea3c8467 100644 --- a/docs/tutorials/cli.rst +++ b/docs/tutorials/cli.rst @@ -84,7 +84,7 @@ Create next structure in the project root directory. All files are empty. That's Initial project layout: -.. code-block:: bash +.. code-block:: text ./ ├── movies/ @@ -109,7 +109,7 @@ Now it's time to install the project requirements. We will use next packages: Put next lines into the ``requirements.txt`` file: -.. code-block:: bash +.. code-block:: text dependency-injector pyyaml @@ -134,7 +134,7 @@ We will create a script that creates database files. First add the folder ``data/`` in the root of the project and then add the file ``fixtures.py`` inside of it: -.. code-block:: bash +.. code-block:: text :emphasize-lines: 2-3 ./ @@ -205,13 +205,13 @@ Now run in the terminal: You should see: -.. code-block:: bash +.. code-block:: text OK Check that files ``movies.csv`` and ``movies.db`` have appeared in the ``data/`` folder: -.. code-block:: bash +.. code-block:: text :emphasize-lines: 4-5 ./ @@ -289,7 +289,7 @@ After each step we will add the provider to the container. Create the ``entities.py`` in the ``movies`` package: -.. code-block:: bash +.. code-block:: text :emphasize-lines: 10 ./ @@ -356,7 +356,7 @@ Let's move on to the finders. Create the ``finders.py`` in the ``movies`` package: -.. code-block:: bash +.. code-block:: text :emphasize-lines: 11 ./ @@ -465,7 +465,7 @@ The configuration file is ready. Move on to the lister. Create the ``listers.py`` in the ``movies`` package: -.. code-block:: bash +.. code-block:: text :emphasize-lines: 12 ./ @@ -613,7 +613,7 @@ Run in the terminal: You should see: -.. code-block:: plain +.. code-block:: text Francis Lawrence movies: - Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence') @@ -752,7 +752,7 @@ Run in the terminal: You should see: -.. code-block:: plain +.. code-block:: text Francis Lawrence movies: - Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence') @@ -868,7 +868,7 @@ Run in the terminal line by line: The output should be similar for each command: -.. code-block:: plain +.. code-block:: text Francis Lawrence movies: - Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence') @@ -888,7 +888,7 @@ We will use `pytest `_ and Create ``tests.py`` in the ``movies`` package: -.. code-block:: bash +.. code-block:: text :emphasize-lines: 13 ./ @@ -977,7 +977,7 @@ Run in the terminal: You should see: -.. code-block:: +.. code-block:: text platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 plugins: cov-3.0.0 diff --git a/docs/tutorials/flask.rst b/docs/tutorials/flask.rst index 8c22aa5a..b8f81adf 100644 --- a/docs/tutorials/flask.rst +++ b/docs/tutorials/flask.rst @@ -280,7 +280,7 @@ Now let's fill in the layout. Put next into the ``base.html``: -.. code-block:: html +.. code-block:: jinja @@ -313,7 +313,7 @@ And put something to the index page. Put next into the ``index.html``: -.. code-block:: html +.. code-block:: jinja {% extends "base.html" %} diff --git a/docs/wiring.rst b/docs/wiring.rst index 456f1e89..912b320e 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -127,6 +127,7 @@ To inject the provider itself use ``Provide[foo.provider]``: def foo(bar_provider: Factory[Bar] = Provide[Container.bar.provider]): bar = bar_provider(argument="baz") ... + You can also use ``Provider[foo]`` for injecting the provider itself: .. code-block:: python From 6766ef3ebaddf305ec53b7601a0a54bccd8f1a20 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 1 Jun 2025 18:45:12 +0000 Subject: [PATCH 06/27] Remove `__init__.pyi` --- src/dependency_injector/__init__.pyi | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/dependency_injector/__init__.pyi diff --git a/src/dependency_injector/__init__.pyi b/src/dependency_injector/__init__.pyi deleted file mode 100644 index e69de29b..00000000 From ceed6a8222cc0d050a865ab0ef30ce6d8862a54f Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 1 Jun 2025 18:45:47 +0000 Subject: [PATCH 07/27] Add `combine_as_imports = true` isort option --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 7512cb94..88553178 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,7 @@ show_missing = true [tool.isort] profile = "black" +combine_as_imports = true [tool.pylint.main] ignore = ["tests"] From 67827a36d1257840bbb3464362457352ba864b38 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 1 Jun 2025 18:46:30 +0000 Subject: [PATCH 08/27] Fix mypy warnigns in containers.pyi --- src/dependency_injector/containers.pyi | 58 +++++++++++++------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/dependency_injector/containers.pyi b/src/dependency_injector/containers.pyi index c637f19c..8e82debd 100644 --- a/src/dependency_injector/containers.pyi +++ b/src/dependency_injector/containers.pyi @@ -1,23 +1,25 @@ from pathlib import Path from typing import ( - Generic, - Type, - Dict, - List, - Tuple, - Optional, Any, - Union, - ClassVar, + Awaitable, Callable as _Callable, + ClassVar, + Dict, + Generic, Iterable, Iterator, + List, + Optional, + Tuple, + Type, TypeVar, - Awaitable, + Union, overload, ) -from .providers import Provider, Self, ProviderParent +from typing_extensions import Self as _Self + +from .providers import Provider, ProviderParent, Self C_Base = TypeVar("C_Base", bound="Container") C = TypeVar("C", bound="DeclarativeContainer") @@ -41,23 +43,23 @@ class WiringConfiguration: ) -> None: ... class Container: - provider_type: Type[Provider] = Provider - providers: Dict[str, Provider] + provider_type: Type[Provider[Any]] = Provider + providers: Dict[str, Provider[Any]] dependencies: Dict[str, Provider[Any]] - overridden: Tuple[Provider] + overridden: Tuple[Provider[Any], ...] wiring_config: WiringConfiguration auto_load_config: bool = True __self__: Self def __init__(self) -> None: ... - def __deepcopy__(self, memo: Optional[Dict[str, Any]]) -> Provider: ... - def __setattr__(self, name: str, value: Union[Provider, Any]) -> None: ... - def __getattr__(self, name: str) -> Provider: ... + def __deepcopy__(self, memo: Optional[Dict[str, Any]]) -> _Self: ... + def __setattr__(self, name: str, value: Union[Provider[Any], Any]) -> None: ... + def __getattr__(self, name: str) -> Provider[Any]: ... def __delattr__(self, name: str) -> None: ... - def set_providers(self, **providers: Provider): ... - def set_provider(self, name: str, provider: Provider) -> None: ... + def set_providers(self, **providers: Provider[Any]) -> None: ... + def set_provider(self, name: str, provider: Provider[Any]) -> None: ... def override(self, overriding: Union[Container, Type[Container]]) -> None: ... def override_providers( - self, **overriding_providers: Union[Provider, Any] + self, **overriding_providers: Union[Provider[Any], Any] ) -> ProvidersOverridingContext[C_Base]: ... def reset_last_overriding(self) -> None: ... def reset_override(self) -> None: ... @@ -69,8 +71,8 @@ class Container: from_package: Optional[str] = None, ) -> None: ... def unwire(self) -> None: ... - def init_resources(self) -> Optional[Awaitable]: ... - def shutdown_resources(self) -> Optional[Awaitable]: ... + def init_resources(self) -> Optional[Awaitable[None]]: ... + def shutdown_resources(self) -> Optional[Awaitable[None]]: ... def load_config(self) -> None: ... def apply_container_providers_overridings(self) -> None: ... def reset_singletons(self) -> SingletonResetContext[C_Base]: ... @@ -81,10 +83,10 @@ class Container: ) -> None: ... def from_json_schema(self, filepath: Union[Path, str]) -> None: ... @overload - def resolve_provider_name(self, provider: Provider) -> str: ... + def resolve_provider_name(self, provider: Provider[Any]) -> str: ... @classmethod @overload - def resolve_provider_name(cls, provider: Provider) -> str: ... + def resolve_provider_name(cls, provider: Provider[Any]) -> str: ... @property def parent(self) -> Optional[ProviderParent]: ... @property @@ -99,14 +101,14 @@ class Container: class DynamicContainer(Container): ... class DeclarativeContainer(Container): - cls_providers: ClassVar[Dict[str, Provider]] - inherited_providers: ClassVar[Dict[str, Provider]] - def __init__(self, **overriding_providers: Union[Provider, Any]) -> None: ... + cls_providers: ClassVar[Dict[str, Provider[Any]]] + inherited_providers: ClassVar[Dict[str, Provider[Any]]] + def __init__(self, **overriding_providers: Union[Provider[Any], Any]) -> None: ... @classmethod def override(cls, overriding: Union[Container, Type[Container]]) -> None: ... @classmethod def override_providers( - cls, **overriding_providers: Union[Provider, Any] + cls, **overriding_providers: Union[Provider[Any], Any] ) -> ProvidersOverridingContext[C_Base]: ... @classmethod def reset_last_overriding(cls) -> None: ... @@ -115,7 +117,7 @@ class DeclarativeContainer(Container): class ProvidersOverridingContext(Generic[T]): def __init__( - self, container: T, overridden_providers: Iterable[Union[Provider, Any]] + self, container: T, overridden_providers: Iterable[Union[Provider[Any], Any]] ) -> None: ... def __enter__(self) -> T: ... def __exit__(self, *_: Any) -> None: ... From 0ada62acbfed05c995c8c0b27e00f27794b2c028 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 1 Jun 2025 18:48:28 +0000 Subject: [PATCH 09/27] Add .editorconfig --- .editorconfig | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7e6cba08 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{py,pyi,pxd,pyx}] +ij_visual_guides = 80,88 From c97a0cc515a514a3ed150a354ac8809b1b22ae2d Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 1 Jun 2025 18:57:47 +0000 Subject: [PATCH 10/27] Fix mypy warnings in dependency_injector.ext --- src/dependency_injector/ext/aiohttp.py | 1 - src/dependency_injector/ext/aiohttp.pyi | 20 +++++++++++--------- src/dependency_injector/ext/flask.py | 4 ++-- src/dependency_injector/ext/flask.pyi | 24 +++++++++++++----------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/dependency_injector/ext/aiohttp.py b/src/dependency_injector/ext/aiohttp.py index 976089c3..43990a7d 100644 --- a/src/dependency_injector/ext/aiohttp.py +++ b/src/dependency_injector/ext/aiohttp.py @@ -7,7 +7,6 @@ from dependency_injector import providers - warnings.warn( 'Module "dependency_injector.ext.aiohttp" is deprecated since ' 'version 4.0.0. Use "dependency_injector.wiring" module instead.', diff --git a/src/dependency_injector/ext/aiohttp.pyi b/src/dependency_injector/ext/aiohttp.pyi index 370cc9b0..c524712c 100644 --- a/src/dependency_injector/ext/aiohttp.pyi +++ b/src/dependency_injector/ext/aiohttp.pyi @@ -1,14 +1,16 @@ -from typing import Awaitable as _Awaitable +from typing import Any, Awaitable as _Awaitable, TypeVar from dependency_injector import providers -class Application(providers.Singleton): ... -class Extension(providers.Singleton): ... -class Middleware(providers.DelegatedCallable): ... -class MiddlewareFactory(providers.Factory): ... +T = TypeVar("T") -class View(providers.Callable): - def as_view(self) -> _Awaitable: ... +class Application(providers.Singleton[T]): ... +class Extension(providers.Singleton[T]): ... +class Middleware(providers.DelegatedCallable[T]): ... +class MiddlewareFactory(providers.Factory[T]): ... -class ClassBasedView(providers.Factory): - def as_view(self) -> _Awaitable: ... +class View(providers.Callable[T]): + def as_view(self) -> _Awaitable[T]: ... + +class ClassBasedView(providers.Factory[T]): + def as_view(self) -> _Awaitable[T]: ... diff --git a/src/dependency_injector/ext/flask.py b/src/dependency_injector/ext/flask.py index 498a9eee..15b9df0a 100644 --- a/src/dependency_injector/ext/flask.py +++ b/src/dependency_injector/ext/flask.py @@ -1,12 +1,12 @@ """Flask extension module.""" from __future__ import absolute_import + import warnings from flask import request as flask_request -from dependency_injector import providers, errors - +from dependency_injector import errors, providers warnings.warn( 'Module "dependency_injector.ext.flask" is deprecated since ' diff --git a/src/dependency_injector/ext/flask.pyi b/src/dependency_injector/ext/flask.pyi index 9b180c89..1c791b88 100644 --- a/src/dependency_injector/ext/flask.pyi +++ b/src/dependency_injector/ext/flask.pyi @@ -1,19 +1,21 @@ -from typing import Union, Optional, Callable as _Callable, Any +from typing import Any, Callable as _Callable, Optional, TypeVar, Union + +from flask.wrappers import Request -from flask import request as flask_request from dependency_injector import providers -request: providers.Object[flask_request] +request: providers.Object[Request] +T = TypeVar("T") -class Application(providers.Singleton): ... -class Extension(providers.Singleton): ... +class Application(providers.Singleton[T]): ... +class Extension(providers.Singleton[T]): ... -class View(providers.Callable): - def as_view(self) -> _Callable[..., Any]: ... +class View(providers.Callable[T]): + def as_view(self) -> _Callable[..., T]: ... -class ClassBasedView(providers.Factory): - def as_view(self, name: str) -> _Callable[..., Any]: ... +class ClassBasedView(providers.Factory[T]): + def as_view(self, name: str) -> _Callable[..., T]: ... def as_view( - provider: Union[View, ClassBasedView], name: Optional[str] = None -) -> _Callable[..., Any]: ... + provider: Union[View[T], ClassBasedView[T]], name: Optional[str] = None +) -> _Callable[..., T]: ... From c1f14a876a57858df249b9807c68344d0721c44b Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 2 Jun 2025 22:46:57 +0000 Subject: [PATCH 11/27] Expose null awaitables --- src/dependency_injector/providers.pxd | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/dependency_injector/providers.pxd b/src/dependency_injector/providers.pxd index b4eb471d..21ed7f22 100644 --- a/src/dependency_injector/providers.pxd +++ b/src/dependency_injector/providers.pxd @@ -697,3 +697,10 @@ cdef inline object __future_result(object instance): future_result = asyncio.Future() future_result.set_result(instance) return future_result + + +cdef class NullAwaitable: + pass + + +cdef NullAwaitable NULL_AWAITABLE From d8e49f7dd55ae28f580e6b88876bfcd1a33806d9 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Tue, 3 Jun 2025 21:45:43 +0300 Subject: [PATCH 12/27] Add support for async generator injections (#900) --- src/dependency_injector/_cwiring.pyi | 31 ++-- src/dependency_injector/_cwiring.pyx | 157 ++++++++++-------- src/dependency_injector/wiring.py | 44 +++-- tests/unit/samples/wiring/asyncinjections.py | 13 +- .../test_async_injections_py36.py | 17 ++ 5 files changed, 160 insertions(+), 102 deletions(-) diff --git a/src/dependency_injector/_cwiring.pyi b/src/dependency_injector/_cwiring.pyi index e7ff12f4..c779b8c4 100644 --- a/src/dependency_injector/_cwiring.pyi +++ b/src/dependency_injector/_cwiring.pyi @@ -1,23 +1,18 @@ -from typing import Any, Awaitable, Callable, Dict, Tuple, TypeVar +from typing import Any, Dict from .providers import Provider -T = TypeVar("T") +class DependencyResolver: + def __init__( + self, + kwargs: Dict[str, Any], + injections: Dict[str, Provider[Any]], + closings: Dict[str, Provider[Any]], + /, + ) -> None: ... + def __enter__(self) -> Dict[str, Any]: ... + def __exit__(self, *exc_info: Any) -> None: ... + async def __aenter__(self) -> Dict[str, Any]: ... + async def __aexit__(self, *exc_info: Any) -> None: ... -def _sync_inject( - fn: Callable[..., T], - args: Tuple[Any, ...], - kwargs: Dict[str, Any], - injections: Dict[str, Provider[Any]], - closings: Dict[str, Provider[Any]], - /, -) -> T: ... -async def _async_inject( - fn: Callable[..., Awaitable[T]], - args: Tuple[Any, ...], - kwargs: Dict[str, Any], - injections: Dict[str, Provider[Any]], - closings: Dict[str, Provider[Any]], - /, -) -> T: ... def _isawaitable(instance: Any) -> bool: ... diff --git a/src/dependency_injector/_cwiring.pyx b/src/dependency_injector/_cwiring.pyx index 84a5485f..3e2775c7 100644 --- a/src/dependency_injector/_cwiring.pyx +++ b/src/dependency_injector/_cwiring.pyx @@ -1,83 +1,110 @@ """Wiring optimizations module.""" -import asyncio -import collections.abc -import inspect -import types +from asyncio import gather +from collections.abc import Awaitable +from inspect import CO_ITERABLE_COROUTINE +from types import CoroutineType, GeneratorType +from .providers cimport Provider, Resource, NULL_AWAITABLE from .wiring import _Marker -from .providers cimport Provider, Resource +cimport cython -def _sync_inject(object fn, tuple args, dict kwargs, dict injections, dict closings, /): - cdef object result +@cython.internal +@cython.no_gc +cdef class KWPair: + cdef str name + cdef object value + + def __cinit__(self, str name, object value, /): + self.name = name + self.value = value + + +cdef inline bint _is_injectable(dict kwargs, str name): + return name not in kwargs or isinstance(kwargs[name], _Marker) + + +cdef class DependencyResolver: + cdef dict kwargs cdef dict to_inject - cdef object arg_key - cdef Provider provider + cdef dict injections + cdef dict closings - to_inject = kwargs.copy() - for arg_key, provider in injections.items(): - if arg_key not in kwargs or isinstance(kwargs[arg_key], _Marker): - to_inject[arg_key] = provider() + def __init__(self, dict kwargs, dict injections, dict closings, /): + self.kwargs = kwargs + self.to_inject = kwargs.copy() + self.injections = injections + self.closings = closings - result = fn(*args, **to_inject) + async def _await_injection(self, kw_pair: KWPair, /) -> None: + self.to_inject[kw_pair.name] = await kw_pair.value - if closings: - for arg_key, provider in closings.items(): - if arg_key in kwargs and not isinstance(kwargs[arg_key], _Marker): - continue - if not isinstance(provider, Resource): - continue - provider.shutdown() + cdef object _await_injections(self, to_await: list): + return gather(*map(self._await_injection, to_await)) - return result + cdef void _handle_injections_sync(self): + cdef Provider provider + for name, provider in self.injections.items(): + if _is_injectable(self.kwargs, name): + self.to_inject[name] = provider() -async def _async_inject(object fn, tuple args, dict kwargs, dict injections, dict closings, /): - cdef object result - cdef dict to_inject - cdef list to_inject_await = [] - cdef list to_close_await = [] - cdef object arg_key - cdef Provider provider - - to_inject = kwargs.copy() - for arg_key, provider in injections.items(): - if arg_key not in kwargs or isinstance(kwargs[arg_key], _Marker): - provide = provider() - if provider.is_async_mode_enabled(): - to_inject_await.append((arg_key, provide)) - elif _isawaitable(provide): - to_inject_await.append((arg_key, provide)) - else: - to_inject[arg_key] = provide - - if to_inject_await: - async_to_inject = await asyncio.gather(*(provide for _, provide in to_inject_await)) - for provide, (injection, _) in zip(async_to_inject, to_inject_await): - to_inject[injection] = provide - - result = await fn(*args, **to_inject) - - if closings: - for arg_key, provider in closings.items(): - if arg_key in kwargs and isinstance(kwargs[arg_key], _Marker): - continue - if not isinstance(provider, Resource): - continue - shutdown = provider.shutdown() - if _isawaitable(shutdown): - to_close_await.append(shutdown) - - await asyncio.gather(*to_close_await) - - return result + cdef list _handle_injections_async(self): + cdef list to_await = [] + cdef Provider provider + + for name, provider in self.injections.items(): + if _is_injectable(self.kwargs, name): + provide = provider() + + if provider.is_async_mode_enabled() or _isawaitable(provide): + to_await.append(KWPair(name, provide)) + else: + self.to_inject[name] = provide + + return to_await + + cdef void _handle_closings_sync(self): + cdef Provider provider + + for name, provider in self.closings.items(): + if _is_injectable(self.kwargs, name) and isinstance(provider, Resource): + provider.shutdown() + + cdef list _handle_closings_async(self): + cdef list to_await = [] + cdef Provider provider + + for name, provider in self.closings.items(): + if _is_injectable(self.kwargs, name) and isinstance(provider, Resource): + if _isawaitable(shutdown := provider.shutdown()): + to_await.append(shutdown) + + return to_await + + def __enter__(self): + self._handle_injections_sync() + return self.to_inject + + def __exit__(self, *_): + self._handle_closings_sync() + + async def __aenter__(self): + if to_await := self._handle_injections_async(): + await self._await_injections(to_await) + return self.to_inject + + def __aexit__(self, *_): + if to_await := self._handle_closings_async(): + return gather(*to_await) + return NULL_AWAITABLE cdef bint _isawaitable(object instance): """Return true if object can be passed to an ``await`` expression.""" - return (isinstance(instance, types.CoroutineType) or - isinstance(instance, types.GeneratorType) and - bool(instance.gi_code.co_flags & inspect.CO_ITERABLE_COROUTINE) or - isinstance(instance, collections.abc.Awaitable)) + return (isinstance(instance, CoroutineType) or + isinstance(instance, GeneratorType) and + bool(instance.gi_code.co_flags & CO_ITERABLE_COROUTINE) or + isinstance(instance, Awaitable)) diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 9c976c2d..aadf2cdc 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -10,6 +10,7 @@ from typing import ( TYPE_CHECKING, Any, + AsyncIterator, Callable, Dict, Iterable, @@ -720,6 +721,8 @@ def _get_patched( if inspect.iscoroutinefunction(fn): patched = _get_async_patched(fn, patched_object) + elif inspect.isasyncgenfunction(fn): + patched = _get_async_gen_patched(fn, patched_object) else: patched = _get_sync_patched(fn, patched_object) @@ -1035,36 +1038,41 @@ def is_loader_installed() -> bool: _loader = AutoLoader() # Optimizations -from ._cwiring import _async_inject # noqa -from ._cwiring import _sync_inject # noqa +from ._cwiring import DependencyResolver # noqa: E402 # Wiring uses the following Python wrapper because there is # no possibility to compile a first-type citizen coroutine in Cython. def _get_async_patched(fn: F, patched: PatchedCallable) -> F: @functools.wraps(fn) - async def _patched(*args, **kwargs): - return await _async_inject( - fn, - args, - kwargs, - patched.injections, - patched.closing, - ) + async def _patched(*args: Any, **raw_kwargs: Any) -> Any: + resolver = DependencyResolver(raw_kwargs, patched.injections, patched.closing) + + async with resolver as kwargs: + return await fn(*args, **kwargs) + + return cast(F, _patched) + + +def _get_async_gen_patched(fn: F, patched: PatchedCallable) -> F: + @functools.wraps(fn) + async def _patched(*args: Any, **raw_kwargs: Any) -> AsyncIterator[Any]: + resolver = DependencyResolver(raw_kwargs, patched.injections, patched.closing) + + async with resolver as kwargs: + async for obj in fn(*args, **kwargs): + yield obj return cast(F, _patched) def _get_sync_patched(fn: F, patched: PatchedCallable) -> F: @functools.wraps(fn) - def _patched(*args, **kwargs): - return _sync_inject( - fn, - args, - kwargs, - patched.injections, - patched.closing, - ) + def _patched(*args: Any, **raw_kwargs: Any) -> Any: + resolver = DependencyResolver(raw_kwargs, patched.injections, patched.closing) + + with resolver as kwargs: + return fn(*args, **kwargs) return cast(F, _patched) diff --git a/tests/unit/samples/wiring/asyncinjections.py b/tests/unit/samples/wiring/asyncinjections.py index 204300e3..e0861017 100644 --- a/tests/unit/samples/wiring/asyncinjections.py +++ b/tests/unit/samples/wiring/asyncinjections.py @@ -1,7 +1,9 @@ import asyncio +from typing_extensions import Annotated + from dependency_injector import containers, providers -from dependency_injector.wiring import inject, Provide, Closing +from dependency_injector.wiring import Closing, Provide, inject class TestResource: @@ -42,6 +44,15 @@ async def async_injection( return resource1, resource2 +@inject +async def async_generator_injection( + resource1: object = Provide[Container.resource1], + resource2: object = Closing[Provide[Container.resource2]], +): + yield resource1 + yield resource2 + + @inject async def async_injection_with_closing( resource1: object = Closing[Provide[Container.resource1]], diff --git a/tests/unit/wiring/provider_ids/test_async_injections_py36.py b/tests/unit/wiring/provider_ids/test_async_injections_py36.py index f17f19c7..70f9eb17 100644 --- a/tests/unit/wiring/provider_ids/test_async_injections_py36.py +++ b/tests/unit/wiring/provider_ids/test_async_injections_py36.py @@ -32,6 +32,23 @@ async def test_async_injections(): assert asyncinjections.resource2.shutdown_counter == 0 +@mark.asyncio +async def test_async_generator_injections() -> None: + resources = [] + + async for resource in asyncinjections.async_generator_injection(): + resources.append(resource) + + assert len(resources) == 2 + assert resources[0] is asyncinjections.resource1 + assert asyncinjections.resource1.init_counter == 1 + assert asyncinjections.resource1.shutdown_counter == 0 + + assert resources[1] is asyncinjections.resource2 + assert asyncinjections.resource2.init_counter == 1 + assert asyncinjections.resource2.shutdown_counter == 1 + + @mark.asyncio async def test_async_injections_with_closing(): resource1, resource2 = await asyncinjections.async_injection_with_closing() From 1b4b3d349f42d15d160586b73d1b7487a4c37cbd Mon Sep 17 00:00:00 2001 From: ZipFile Date: Tue, 3 Jun 2025 20:33:13 +0000 Subject: [PATCH 13/27] Fix some more Sphinx warnings --- docs/examples/django.rst | 2 +- docs/main/changelog.rst | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/examples/django.rst b/docs/examples/django.rst index 08e6e757..1a5b781f 100644 --- a/docs/examples/django.rst +++ b/docs/examples/django.rst @@ -78,7 +78,7 @@ Container is wired to the ``views`` module in the app config ``web/apps.py``: .. literalinclude:: ../../examples/miniapps/django/web/apps.py :language: python - :emphasize-lines: 13 + :emphasize-lines: 12 Tests ----- diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 06acc716..05bb9e9a 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -14,8 +14,8 @@ follows `Semantic versioning`_ with updated documentation and examples. See discussion: https://github.com/ets-labs/python-dependency-injector/pull/721#issuecomment-2025263718 -- Fix ``root`` property shadowing in ``ConfigurationOption`` (`#875 https://github.com/ets-labs/python-dependency-injector/pull/875`_) -- Fix incorrect monkeypatching during ``wire()`` that could violate MRO in some classes (`#886 https://github.com/ets-labs/python-dependency-injector/pull/886`_) +- Fix ``root`` property shadowing in ``ConfigurationOption`` (`#875 `_) +- Fix incorrect monkeypatching during ``wire()`` that could violate MRO in some classes (`#886 `_) - ABI3 wheels are now published for CPython. - Drop support of Python 3.7. @@ -371,8 +371,8 @@ Many thanks to `ZipFile `_ for both contributions. - Make refactoring of wiring module and tests. See PR # `#406 `_. Thanks to `@withshubh `_ for the contribution: - - Remove unused imports in tests. - - Use literal syntax to create data structure in tests. + - Remove unused imports in tests. + - Use literal syntax to create data structure in tests. - Add integration with a static analysis tool `DeepSource `_. 4.26.0 From 2293251986b46506f4b1c5b9d7d4a05b5a326e67 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Tue, 3 Jun 2025 20:43:06 +0000 Subject: [PATCH 14/27] Add docs for ASGI Lifespan support --- docs/api/index.rst | 1 + docs/providers/resource.rst | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/docs/api/index.rst b/docs/api/index.rst index 86389688..7258f7de 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -9,3 +9,4 @@ API Documentation containers wiring errors + asgi-lifespan diff --git a/docs/providers/resource.rst b/docs/providers/resource.rst index cf935f3a..fda5b3d7 100644 --- a/docs/providers/resource.rst +++ b/docs/providers/resource.rst @@ -401,5 +401,54 @@ See also: - Wiring :ref:`async-injections-wiring` - :ref:`fastapi-redis-example` +ASGI Lifespan Protocol Support +------------------------------ + +The :mod:`dependency_injector.ext.starlette` module provides a :class:`~dependency_injector.ext.starlette.Lifespan` +class that integrates resource providers with ASGI applications using the `Lifespan Protocol`_. This allows resources to +be automatically initialized at application startup and properly shut down when the application stops. + +.. code-block:: python + + from contextlib import asynccontextmanager + from dependency_injector import containers, providers + from dependency_injector.wiring import Provide, inject + from dependency_injector.ext.starlette import Lifespan + from fastapi import FastAPI, Request, Depends, APIRouter + + class Connection: ... + + @asynccontextmanager + async def init_database(): + print("opening database connection") + yield Connection() + print("closing database connection") + + router = APIRouter() + + @router.get("/") + @inject + async def index(request: Request, db: Connection = Depends(Provide["db"])): + # use the database connection here + return "OK!" + + class Container(containers.DeclarativeContainer): + __self__ = providers.Self() + db = providers.Resource(init_database) + lifespan = providers.Singleton(Lifespan, __self__) + app = providers.Singleton(FastAPI, lifespan=lifespan) + _include_router = providers.Resource( + app.provided.include_router.call(), + router, + ) + + if __name__ == "__main__": + import uvicorn + + container = Container() + app = container.app() + uvicorn.run(app, host="localhost", port=8000) + +.. _Lifespan Protocol: https://asgi.readthedocs.io/en/latest/specs/lifespan.html .. disqus:: From f2da51e0d4bee22966ab183d2db2819e04199f5d Mon Sep 17 00:00:00 2001 From: ZipFile Date: Thu, 5 Jun 2025 16:26:40 +0000 Subject: [PATCH 15/27] Use typing_extensions.Self as fallback (fixes #902) --- src/dependency_injector/containers.pyi | 5 ++++- src/dependency_injector/wiring.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/dependency_injector/containers.pyi b/src/dependency_injector/containers.pyi index 8e82debd..ca608f28 100644 --- a/src/dependency_injector/containers.pyi +++ b/src/dependency_injector/containers.pyi @@ -17,7 +17,10 @@ from typing import ( overload, ) -from typing_extensions import Self as _Self +try: + from typing import Self as _Self +except ImportError: + from typing_extensions import Self as _Self from .providers import Provider, ProviderParent, Self diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index aadf2cdc..0477eed4 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -25,7 +25,10 @@ cast, ) -from typing_extensions import Self +try: + from typing import Self +except ImportError: + from typing_extensions import Self try: from functools import cache From b41180757274c6f1ac86bdb3c1ae5a421b07635b Mon Sep 17 00:00:00 2001 From: AndrianEquestrian <88708533+AndrianEquestrian@users.noreply.github.com> Date: Mon, 16 Jun 2025 10:37:31 +0300 Subject: [PATCH 16/27] Add support for Fast Stream Depends (#898) --- docs/examples/fastdepends.rst | 48 +++++++++++++++++++ docs/examples/index.rst | 1 + docs/wiring.rst | 1 + requirements-dev.txt | 1 + src/dependency_injector/wiring.py | 47 ++++++++++++------ .../unit/samples/wiringfastdepends/sample.py | 39 +++++++++++++++ tests/unit/wiring/test_fastdepends.py | 9 ++++ tox.ini | 2 + 8 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 docs/examples/fastdepends.rst create mode 100644 tests/unit/samples/wiringfastdepends/sample.py create mode 100644 tests/unit/wiring/test_fastdepends.py diff --git a/docs/examples/fastdepends.rst b/docs/examples/fastdepends.rst new file mode 100644 index 00000000..5dc64fc4 --- /dev/null +++ b/docs/examples/fastdepends.rst @@ -0,0 +1,48 @@ +.. _fastdepends-example: + +FastDepends example +=================== + +.. meta:: + :keywords: Python,Dependency Injection,FastDepends,Example + :description: This example demonstrates a usage of the FastDepends and Dependency Injector. + + +This example demonstrates how to use ``Dependency Injector`` with `FastDepends `_, a lightweight dependency injection framework inspired by FastAPI's dependency system, but without the web framework components. + +Basic Usage +----------- + +The integration between FastDepends and Dependency Injector is straightforward. Simply use Dependency Injector's ``Provide`` marker within FastDepends' ``Depends`` function: + +.. code-block:: python + + import sys + + from dependency_injector import containers, providers + from dependency_injector.wiring import inject, Provide + from fast_depends import Depends + + + class CoefficientService: + @staticmethod + def get_coefficient() -> float: + return 1.2 + + + class Container(containers.DeclarativeContainer): + service = providers.Factory(CoefficientService) + + + @inject + def apply_coefficient( + a: int, + coefficient_provider: CoefficientService = Depends(Provide[Container.service]), + ) -> float: + return a * coefficient_provider.get_coefficient() + + + container = Container() + container.wire(modules=[sys.modules[__name__]]) + + apply_coefficient(100) == 120.0 diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 93595380..b166ceae 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -22,5 +22,6 @@ Explore the examples to see the ``Dependency Injector`` in action. fastapi fastapi-redis fastapi-sqlalchemy + fastdepends .. disqus:: diff --git a/docs/wiring.rst b/docs/wiring.rst index 912b320e..bb6ba156 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -693,5 +693,6 @@ Take a look at other application examples: - :ref:`fastapi-example` - :ref:`fastapi-redis-example` - :ref:`fastapi-sqlalchemy-example` +- :ref:`fastdepends-example` .. disqus:: diff --git a/requirements-dev.txt b/requirements-dev.txt index 47e3ca42..408b9bb6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,5 +20,6 @@ scipy boto3 mypy_boto3_s3 typing_extensions +fast-depends -r requirements-ext.txt diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 0477eed4..5856622b 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -59,10 +59,33 @@ def get_origin(tp): return None +MARKER_EXTRACTORS = [] + try: - import fastapi.params + from fastapi.params import Depends as FastAPIDepends except ImportError: - fastapi = None + pass +else: + + def extract_marker_from_fastapi(param: Any) -> Any: + if isinstance(param, FastAPIDepends): + return param.dependency + return None + + MARKER_EXTRACTORS.append(extract_marker_from_fastapi) + +try: + from fast_depends.dependencies import Depends as FastDepends +except ImportError: + pass +else: + + def extract_marker_from_fast_depends(param: Any) -> Any: + if isinstance(param, FastDepends): + return param.dependency + return None + + MARKER_EXTRACTORS.append(extract_marker_from_fast_depends) try: @@ -76,8 +99,7 @@ def get_origin(tp): except ImportError: werkzeug = None - -from . import providers +from . import providers # noqa: E402 __all__ = ( "wire", @@ -607,14 +629,13 @@ def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]: else: marker = parameter.default - if not isinstance(marker, _Marker) and not _is_fastapi_depends(marker): - return None - - if _is_fastapi_depends(marker): - marker = marker.dependency + for marker_extractor in MARKER_EXTRACTORS: + if _marker := marker_extractor(marker): + marker = _marker + break - if not isinstance(marker, _Marker): - return None + if not isinstance(marker, _Marker): + return None return marker @@ -735,10 +756,6 @@ def _get_patched( return patched -def _is_fastapi_depends(param: Any) -> bool: - return fastapi and isinstance(param, fastapi.params.Depends) - - def _is_patched(fn) -> bool: return _patched_registry.has_callable(fn) diff --git a/tests/unit/samples/wiringfastdepends/sample.py b/tests/unit/samples/wiringfastdepends/sample.py new file mode 100644 index 00000000..4d2b3d61 --- /dev/null +++ b/tests/unit/samples/wiringfastdepends/sample.py @@ -0,0 +1,39 @@ +import sys + +from fast_depends import Depends +from typing_extensions import Annotated + +from dependency_injector import containers, providers +from dependency_injector.wiring import Provide, inject + + +class CoefficientService: + @staticmethod + def get_coefficient() -> float: + return 1.2 + + +class Container(containers.DeclarativeContainer): + service = providers.Factory(CoefficientService) + + +@inject +def apply_coefficient( + a: int, + coefficient_provider: CoefficientService = Depends(Provide[Container.service]), +) -> float: + return a * coefficient_provider.get_coefficient() + + +@inject +def apply_coefficient_annotated( + a: int, + coefficient_provider: Annotated[ + CoefficientService, Depends(Provide[Container.service]) + ], +) -> float: + return a * coefficient_provider.get_coefficient() + + +container = Container() +container.wire(modules=[sys.modules[__name__]]) diff --git a/tests/unit/wiring/test_fastdepends.py b/tests/unit/wiring/test_fastdepends.py new file mode 100644 index 00000000..9c9e2ad6 --- /dev/null +++ b/tests/unit/wiring/test_fastdepends.py @@ -0,0 +1,9 @@ +from wiringfastdepends import sample + + +def test_apply_coefficient() -> None: + assert sample.apply_coefficient(100) == 120.0 + + +def test_apply_coefficient_annotated() -> None: + assert sample.apply_coefficient_annotated(100) == 120.0 diff --git a/tox.ini b/tox.ini index b2c5e79f..cadccd84 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,7 @@ deps= mypy_boto3_s3 pydantic-settings werkzeug + fast-depends extras= yaml commands = pytest @@ -44,6 +45,7 @@ deps = boto3 mypy_boto3_s3 werkzeug + fast-depends commands = pytest -m pydantic [testenv:coveralls] From 4bfe64563e52dd5c28b7472d8bd406998e55027b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aran=20Moncus=C3=AD=20Ram=C3=ADrez?= Date: Mon, 16 Jun 2025 10:34:02 +0200 Subject: [PATCH 17/27] Add resource type parameter to init and shutdown resources using specialized providers (#858) --- docs/providers/resource.rst | 66 ++++++++++ src/dependency_injector/containers.pyi | 6 +- src/dependency_injector/containers.pyx | 16 ++- .../instance/test_async_resources_py36.py | 118 ++++++++++++++++++ .../containers/instance/test_main_py2_py3.py | 13 ++ 5 files changed, 212 insertions(+), 7 deletions(-) diff --git a/docs/providers/resource.rst b/docs/providers/resource.rst index fda5b3d7..87a0a17e 100644 --- a/docs/providers/resource.rst +++ b/docs/providers/resource.rst @@ -252,6 +252,72 @@ first argument. .. _resource-provider-wiring-closing: +Scoping Resources using specialized subclasses +---------------------------------------------- + +You can use specialized subclasses of ``Resource`` provider to initialize and shutdown resources by type. +Allowing for example to only initialize a subgroup of resources. + +.. code-block:: python + + class ScopedResource(resources.Resource): + pass + + def init_service(name) -> Service: + print(f"Init {name}") + yield Service() + print(f"Shutdown {name}") + + class Container(containers.DeclarativeContainer): + + scoped = ScopedResource( + init_service, + "scoped", + ) + + generic = providers.Resource( + init_service, + "generic", + ) + + +To initialize resources by type you can use ``init_resources(resource_type)`` and ``shutdown_resources(resource_type)`` +methods adding the resource type as an argument: + +.. code-block:: python + + def main(): + container = Container() + container.init_resources(ScopedResource) + # Generates: + # >>> Init scoped + + container.shutdown_resources(ScopedResource) + # Generates: + # >>> Shutdown scoped + + +And to initialize all resources you can use ``init_resources()`` and ``shutdown_resources()`` without arguments: + +.. code-block:: python + + def main(): + container = Container() + container.init_resources() + # Generates: + # >>> Init scoped + # >>> Init generic + + container.shutdown_resources() + # Generates: + # >>> Shutdown scoped + # >>> Shutdown generic + + +It works using the :ref:`traverse` method to find all resources of the specified type, selecting all resources +which are instances of the specified type. + + Resources, wiring, and per-function execution scope --------------------------------------------------- diff --git a/src/dependency_injector/containers.pyi b/src/dependency_injector/containers.pyi index ca608f28..f21a8791 100644 --- a/src/dependency_injector/containers.pyi +++ b/src/dependency_injector/containers.pyi @@ -22,7 +22,7 @@ try: except ImportError: from typing_extensions import Self as _Self -from .providers import Provider, ProviderParent, Self +from .providers import Provider, Resource, Self, ProviderParent C_Base = TypeVar("C_Base", bound="Container") C = TypeVar("C", bound="DeclarativeContainer") @@ -74,8 +74,8 @@ class Container: from_package: Optional[str] = None, ) -> None: ... def unwire(self) -> None: ... - def init_resources(self) -> Optional[Awaitable[None]]: ... - def shutdown_resources(self) -> Optional[Awaitable[None]]: ... + def init_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ... + def shutdown_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ... def load_config(self) -> None: ... def apply_container_providers_overridings(self) -> None: ... def reset_singletons(self) -> SingletonResetContext[C_Base]: ... diff --git a/src/dependency_injector/containers.pyx b/src/dependency_injector/containers.pyx index bd0a4821..99762da2 100644 --- a/src/dependency_injector/containers.pyx +++ b/src/dependency_injector/containers.pyx @@ -315,11 +315,15 @@ class DynamicContainer(Container): self.wired_to_modules.clear() self.wired_to_packages.clear() - def init_resources(self): + def init_resources(self, resource_type=providers.Resource): """Initialize all container resources.""" + + if not issubclass(resource_type, providers.Resource): + raise TypeError("resource_type must be a subclass of Resource provider") + futures = [] - for provider in self.traverse(types=[providers.Resource]): + for provider in self.traverse(types=[resource_type]): resource = provider.init() if __is_future_or_coroutine(resource): @@ -328,8 +332,12 @@ class DynamicContainer(Container): if futures: return asyncio.gather(*futures) - def shutdown_resources(self): + def shutdown_resources(self, resource_type=providers.Resource): """Shutdown all container resources.""" + + if not issubclass(resource_type, providers.Resource): + raise TypeError("resource_type must be a subclass of Resource provider") + def _independent_resources(resources): for resource in resources: for other_resource in resources: @@ -360,7 +368,7 @@ class DynamicContainer(Container): for resource in resources_to_shutdown: resource.shutdown() - resources = list(self.traverse(types=[providers.Resource])) + resources = list(self.traverse(types=[resource_type])) if any(resource.is_async_mode_enabled() for resource in resources): return _async_ordered_shutdown(resources) else: diff --git a/tests/unit/containers/instance/test_async_resources_py36.py b/tests/unit/containers/instance/test_async_resources_py36.py index b365b60d..47fd03e7 100644 --- a/tests/unit/containers/instance/test_async_resources_py36.py +++ b/tests/unit/containers/instance/test_async_resources_py36.py @@ -145,3 +145,121 @@ class Container(containers.DeclarativeContainer): await container.shutdown_resources() assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"] assert shutdown_resources == ["r3", "r2", "r1", "r3", "r2", "r1"] + + +@mark.asyncio +async def test_init_and_shutdown_scoped_resources(): + initialized_resources = [] + shutdown_resources = [] + + def _sync_resource(name, **_): + initialized_resources.append(name) + yield name + shutdown_resources.append(name) + + async def _async_resource(name, **_): + initialized_resources.append(name) + yield name + shutdown_resources.append(name) + + + class ResourceA(providers.Resource): + pass + + + class ResourceB(providers.Resource): + pass + + + class Container(containers.DeclarativeContainer): + resource_a = ResourceA( + _sync_resource, + name="ra1", + ) + resource_b1 = ResourceB( + _sync_resource, + name="rb1", + r1=resource_a, + ) + resource_b2 = ResourceB( + _async_resource, + name="rb2", + r2=resource_b1, + ) + + container = Container() + + container.init_resources(resource_type=ResourceA) + assert initialized_resources == ["ra1"] + assert shutdown_resources == [] + + container.shutdown_resources(resource_type=ResourceA) + assert initialized_resources == ["ra1"] + assert shutdown_resources == ["ra1"] + + await container.init_resources(resource_type=ResourceB) + assert initialized_resources == ["ra1", "ra1", "rb1", "rb2"] + assert shutdown_resources == ["ra1"] + + await container.shutdown_resources(resource_type=ResourceB) + assert initialized_resources == ["ra1", "ra1", "rb1", "rb2"] + assert shutdown_resources == ["ra1", "rb2", "rb1"] + + +@mark.asyncio +async def test_init_and_shutdown_all_scoped_resources_using_default_value(): + initialized_resources = [] + shutdown_resources = [] + + def _sync_resource(name, **_): + initialized_resources.append(name) + yield name + shutdown_resources.append(name) + + async def _async_resource(name, **_): + initialized_resources.append(name) + yield name + shutdown_resources.append(name) + + + class ResourceA(providers.Resource): + pass + + + class ResourceB(providers.Resource): + pass + + + class Container(containers.DeclarativeContainer): + resource_a = ResourceA( + _sync_resource, + name="r1", + ) + resource_b1 = ResourceB( + _sync_resource, + name="r2", + r1=resource_a, + ) + resource_b2 = ResourceB( + _async_resource, + name="r3", + r2=resource_b1, + ) + + container = Container() + + await container.init_resources() + assert initialized_resources == ["r1", "r2", "r3"] + assert shutdown_resources == [] + + await container.shutdown_resources() + assert initialized_resources == ["r1", "r2", "r3"] + assert shutdown_resources == ["r3", "r2", "r1"] + + await container.init_resources() + assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"] + assert shutdown_resources == ["r3", "r2", "r1"] + + await container.shutdown_resources() + assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"] + assert shutdown_resources == ["r3", "r2", "r1", "r3", "r2", "r1"] diff --git a/tests/unit/containers/instance/test_main_py2_py3.py b/tests/unit/containers/instance/test_main_py2_py3.py index ddd61de9..0c19fa36 100644 --- a/tests/unit/containers/instance/test_main_py2_py3.py +++ b/tests/unit/containers/instance/test_main_py2_py3.py @@ -325,6 +325,19 @@ class Container(containers.DeclarativeContainer): assert _init2.shutdown_counter == 2 +def test_init_shutdown_resources_wrong_type() -> None: + class Container(containers.DeclarativeContainer): + pass + + c = Container() + + with raises(TypeError, match=r"resource_type must be a subclass of Resource provider"): + c.init_resources(int) # type: ignore[arg-type] + + with raises(TypeError, match=r"resource_type must be a subclass of Resource provider"): + c.shutdown_resources(int) # type: ignore[arg-type] + + def test_reset_singletons(): class SubSubContainer(containers.DeclarativeContainer): singleton = providers.Singleton(object) From b261251b34fa859b36dcc112199eb1426cae55c8 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 16 Jun 2025 08:48:16 +0000 Subject: [PATCH 18/27] Fix Sphinx warning --- docs/api/asgi-lifespan.rst | 9 +++++++++ docs/providers/resource.rst | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 docs/api/asgi-lifespan.rst diff --git a/docs/api/asgi-lifespan.rst b/docs/api/asgi-lifespan.rst new file mode 100644 index 00000000..bcf5431d --- /dev/null +++ b/docs/api/asgi-lifespan.rst @@ -0,0 +1,9 @@ +dependency_injector.ext.starlette +================================= + +.. automodule:: dependency_injector.ext.starlette + :members: + :inherited-members: + :show-inheritance: + +.. disqus:: diff --git a/docs/providers/resource.rst b/docs/providers/resource.rst index 87a0a17e..b07c2db0 100644 --- a/docs/providers/resource.rst +++ b/docs/providers/resource.rst @@ -314,7 +314,7 @@ And to initialize all resources you can use ``init_resources()`` and ``shutdown_ # >>> Shutdown generic -It works using the :ref:`traverse` method to find all resources of the specified type, selecting all resources +It works using the ``traverse()`` method to find all resources of the specified type, selecting all resources which are instances of the specified type. From 31a98f773140c02e1c0c34979011eff8c0380b9f Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 16 Jun 2025 08:51:10 +0000 Subject: [PATCH 19/27] Update changelog --- docs/main/changelog.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 05bb9e9a..18ca9672 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,8 +7,24 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +4.48.0 +------ + +- Improve performance of wiring (`#897 `_) +- Add Context Manager support to Resource provider (`#899 `_) +- Add support for async generator injections (`#900 `_) +- Fix unintended dependency on ``typing_extensions`` (`#902 `_) +- Add support for Fast Depends (`#898 `_) +- Add ``resource_type`` parameter to init and shutdown resources using specialized providers (`#858 `_) + +4.47.1 +------ + +- Fix typing for wiring marker (`#892 `_) +- Strip debug symbols in wheels + 4.47.0 -------- +------ - Add support for ``Annotated`` type for module and class attribute injection in wiring, with updated documentation and examples. From dd84a1b5d68ea04ce02452fd577c14954debc206 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 16 Jun 2025 08:53:36 +0000 Subject: [PATCH 20/27] Bump version --- src/dependency_injector/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index 90e6570b..ec63edf0 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = "4.47.1" +__version__ = "4.48.0" """Version number. :type: str From bf2ddbce329fb6bee881289c9a3a005cd2534ad8 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 16 Jun 2025 10:51:39 +0000 Subject: [PATCH 21/27] Upgrade cibuildwheel --- .github/workflows/publishing.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publishing.yml b/.github/workflows/publishing.yml index 0151704c..776256c7 100644 --- a/.github/workflows/publishing.yml +++ b/.github/workflows/publishing.yml @@ -70,10 +70,10 @@ jobs: steps: - uses: actions/checkout@v3 - name: Build wheels - uses: pypa/cibuildwheel@v2.23.3 + uses: pypa/cibuildwheel@v3.0.0 - uses: actions/upload-artifact@v4 with: - name: cibw-wheels-x86-${{ matrix.os }}-${{ strategy.job-index }} + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl test-publish: From e6cc12762f0355f2aff866b08eb1a3427b8b6403 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Wed, 18 Jun 2025 21:58:00 +0000 Subject: [PATCH 22/27] Add support for resource_type in Lifespans --- src/dependency_injector/ext/starlette.py | 17 +++++++++++++---- tests/unit/ext/test_starlette.py | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/dependency_injector/ext/starlette.py b/src/dependency_injector/ext/starlette.py index becadf0a..620d7d26 100644 --- a/src/dependency_injector/ext/starlette.py +++ b/src/dependency_injector/ext/starlette.py @@ -1,5 +1,5 @@ import sys -from typing import Any +from typing import Any, Type if sys.version_info >= (3, 11): # pragma: no cover from typing import Self @@ -7,6 +7,7 @@ from typing_extensions import Self from dependency_injector.containers import Container +from dependency_injector.providers import Resource class Lifespan: @@ -29,24 +30,32 @@ class Container(DeclarativeContainer): app = Factory(Starlette, lifespan=lifespan) :param container: container instance + :param resource_type: A :py:class:`~dependency_injector.resources.Resource` + subclass. Limits the resources to be initialized and shutdown. """ container: Container + resource_type: Type[Resource[Any]] - def __init__(self, container: Container) -> None: + def __init__( + self, + container: Container, + resource_type: Type[Resource[Any]] = Resource, + ) -> None: self.container = container + self.resource_type = resource_type def __call__(self, app: Any) -> Self: return self async def __aenter__(self) -> None: - result = self.container.init_resources() + result = self.container.init_resources(self.resource_type) if result is not None: await result async def __aexit__(self, *exc_info: Any) -> None: - result = self.container.shutdown_resources() + result = self.container.shutdown_resources(self.resource_type) if result is not None: await result diff --git a/tests/unit/ext/test_starlette.py b/tests/unit/ext/test_starlette.py index e569a382..f50d6f46 100644 --- a/tests/unit/ext/test_starlette.py +++ b/tests/unit/ext/test_starlette.py @@ -1,4 +1,4 @@ -from typing import AsyncIterator, Iterator +from typing import AsyncIterator, Iterator, TypeVar from unittest.mock import ANY from pytest import mark @@ -7,6 +7,12 @@ from dependency_injector.ext.starlette import Lifespan from dependency_injector.providers import Resource +T = TypeVar("T") + + +class XResource(Resource[T]): + """A test provider""" + class TestLifespan: @mark.parametrize("sync", [False, True]) @@ -28,11 +34,15 @@ async def async_resource() -> AsyncIterator[None]: yield shutdown = True + def nope(): + assert False, "should not be called" + class Container(DeclarativeContainer): - x = Resource(sync_resource if sync else async_resource) + x = XResource(sync_resource if sync else async_resource) + y = Resource(nope) container = Container() - lifespan = Lifespan(container) + lifespan = Lifespan(container, resource_type=XResource) async with lifespan(ANY) as scope: assert scope is None From 04b5907f21d40a6ddba57dbad46690e27cbaade4 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Fri, 20 Jun 2025 07:08:06 +0000 Subject: [PATCH 23/27] Add warning on extra `@inject` --- pyproject.toml | 1 + src/dependency_injector/__init__.py | 2 +- src/dependency_injector/wiring.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 88553178..f8a69cef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,6 +108,7 @@ markers = [ "pydantic: Tests with Pydantic as a dependency", ] filterwarnings = [ + "ignore::dependency_injector.wiring.DIWiringWarning", "ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\\.0\\.0:DeprecationWarning", "ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\\.0\\.0:DeprecationWarning", "ignore:Please use \\`.*?\\` from the \\`scipy.*?\\`(.*?)namespace is deprecated\\.:DeprecationWarning", diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index ec63edf0..af252a31 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = "4.48.0" +__version__ = "4.48.1" """Version number. :type: str diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 5856622b..f02070af 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -24,6 +24,7 @@ Union, cast, ) +from warnings import warn try: from typing import Self @@ -130,6 +131,10 @@ def extract_marker_from_fast_depends(param: Any) -> Any: Container = Any +class DIWiringWarning(RuntimeWarning): + """Base class for all warnings raised by the wiring module.""" + + class PatchedRegistry: def __init__(self) -> None: @@ -520,6 +525,11 @@ def unwire( # noqa: C901 def inject(fn: F) -> F: """Decorate callable with injecting decorator.""" reference_injections, reference_closing = _fetch_reference_injections(fn) + + if not reference_injections: + warn("@inject is not required here", DIWiringWarning, stacklevel=2) + return fn + patched = _get_patched(fn, reference_injections, reference_closing) return cast(F, patched) From be7d25518de833ef02f97567b8def9f5d1b80ab1 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Fri, 20 Jun 2025 08:07:59 +0000 Subject: [PATCH 24/27] Add typing-extensions as a dependency for older Python versions --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f8a69cef..ef0b946d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,11 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = ["version"] +dependencies = [ + # typing.Annotated since v3.9 + # typing.Self since v3.11 + "typing-extensions; python_version<'3.11'", +] [project.optional-dependencies] yaml = ["pyyaml"] From eb74b1e9d0b681008de531a697c7caeaafbd1e30 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Fri, 20 Jun 2025 08:30:44 +0000 Subject: [PATCH 25/27] Minor improvements for _cwiring.DependencyResolver code generation * Remove KWPair * Avoid type checks around _is_injectable --- src/dependency_injector/_cwiring.pyx | 33 +++++++--------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/src/dependency_injector/_cwiring.pyx b/src/dependency_injector/_cwiring.pyx index 3e2775c7..01871243 100644 --- a/src/dependency_injector/_cwiring.pyx +++ b/src/dependency_injector/_cwiring.pyx @@ -5,24 +5,11 @@ from collections.abc import Awaitable from inspect import CO_ITERABLE_COROUTINE from types import CoroutineType, GeneratorType -from .providers cimport Provider, Resource, NULL_AWAITABLE +from .providers cimport Provider, Resource from .wiring import _Marker -cimport cython - -@cython.internal -@cython.no_gc -cdef class KWPair: - cdef str name - cdef object value - - def __cinit__(self, str name, object value, /): - self.name = name - self.value = value - - -cdef inline bint _is_injectable(dict kwargs, str name): +cdef inline bint _is_injectable(dict kwargs, object name): return name not in kwargs or isinstance(kwargs[name], _Marker) @@ -38,11 +25,8 @@ cdef class DependencyResolver: self.injections = injections self.closings = closings - async def _await_injection(self, kw_pair: KWPair, /) -> None: - self.to_inject[kw_pair.name] = await kw_pair.value - - cdef object _await_injections(self, to_await: list): - return gather(*map(self._await_injection, to_await)) + async def _await_injection(self, name: str, value: object, /) -> None: + self.to_inject[name] = await value cdef void _handle_injections_sync(self): cdef Provider provider @@ -60,7 +44,7 @@ cdef class DependencyResolver: provide = provider() if provider.is_async_mode_enabled() or _isawaitable(provide): - to_await.append(KWPair(name, provide)) + to_await.append(self._await_injection(name, provide)) else: self.to_inject[name] = provide @@ -93,13 +77,12 @@ cdef class DependencyResolver: async def __aenter__(self): if to_await := self._handle_injections_async(): - await self._await_injections(to_await) + await gather(*to_await) return self.to_inject - def __aexit__(self, *_): + async def __aexit__(self, *_): if to_await := self._handle_closings_async(): - return gather(*to_await) - return NULL_AWAITABLE + await gather(*to_await) cdef bint _isawaitable(object instance): From 0c58064a36c5b46c6c897f40ea3a67ca56515bd4 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Fri, 20 Jun 2025 09:32:24 +0000 Subject: [PATCH 26/27] Make wiring inspect exclsuions extensible --- src/dependency_injector/wiring.py | 68 ++++++++++++------------------- 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index f02070af..6d5d1510 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -6,6 +6,8 @@ import inspect import pkgutil import sys +from contextlib import suppress +from inspect import isbuiltin, isclass from types import ModuleType from typing import ( TYPE_CHECKING, @@ -15,6 +17,7 @@ Dict, Iterable, Iterator, + List, Optional, Protocol, Set, @@ -60,13 +63,11 @@ def get_origin(tp): return None -MARKER_EXTRACTORS = [] +MARKER_EXTRACTORS: List[Callable[[Any], Any]] = [] +INSPECT_EXCLUSION_FILTERS: List[Callable[[Any], bool]] = [isbuiltin] -try: +with suppress(ImportError): from fastapi.params import Depends as FastAPIDepends -except ImportError: - pass -else: def extract_marker_from_fastapi(param: Any) -> Any: if isinstance(param, FastAPIDepends): @@ -75,11 +76,8 @@ def extract_marker_from_fastapi(param: Any) -> Any: MARKER_EXTRACTORS.append(extract_marker_from_fastapi) -try: +with suppress(ImportError): from fast_depends.dependencies import Depends as FastDepends -except ImportError: - pass -else: def extract_marker_from_fast_depends(param: Any) -> Any: if isinstance(param, FastDepends): @@ -89,16 +87,22 @@ def extract_marker_from_fast_depends(param: Any) -> Any: MARKER_EXTRACTORS.append(extract_marker_from_fast_depends) -try: - import starlette.requests -except ImportError: - starlette = None +with suppress(ImportError): + from starlette.requests import Request as StarletteRequest + def is_starlette_request_cls(obj: Any) -> bool: + return isclass(obj) and _safe_is_subclass(obj, StarletteRequest) -try: - import werkzeug.local -except ImportError: - werkzeug = None + INSPECT_EXCLUSION_FILTERS.append(is_starlette_request_cls) + + +with suppress(ImportError): + from werkzeug.local import LocalProxy as WerkzeugLocalProxy + + def is_werkzeug_local_proxy(obj: Any) -> bool: + return isinstance(obj, WerkzeugLocalProxy) + + INSPECT_EXCLUSION_FILTERS.append(is_werkzeug_local_proxy) from . import providers # noqa: E402 @@ -416,30 +420,11 @@ def _create_providers_map( return providers_map -class InspectFilter: - - def is_excluded(self, instance: object) -> bool: - if self._is_werkzeug_local_proxy(instance): +def is_excluded_from_inspect(obj: Any) -> bool: + for is_excluded in INSPECT_EXCLUSION_FILTERS: + if is_excluded(obj): return True - elif self._is_starlette_request_cls(instance): - return True - elif self._is_builtin(instance): - return True - else: - return False - - def _is_werkzeug_local_proxy(self, instance: object) -> bool: - return werkzeug and isinstance(instance, werkzeug.local.LocalProxy) - - def _is_starlette_request_cls(self, instance: object) -> bool: - return ( - starlette - and isinstance(instance, type) - and _safe_is_subclass(instance, starlette.requests.Request) - ) - - def _is_builtin(self, instance: object) -> bool: - return inspect.isbuiltin(instance) + return False def wire( # noqa: C901 @@ -460,7 +445,7 @@ def wire( # noqa: C901 for module in modules: for member_name, member in _get_members_and_annotated(module): - if _inspect_filter.is_excluded(member): + if is_excluded_from_inspect(member): continue if _is_marker(member): @@ -1064,7 +1049,6 @@ def is_loader_installed() -> bool: _patched_registry = PatchedRegistry() -_inspect_filter = InspectFilter() _loader = AutoLoader() # Optimizations From 84a14f2ca720d47a0dc6cdb898894a6cb832a7fc Mon Sep 17 00:00:00 2001 From: ZipFile Date: Fri, 20 Jun 2025 09:56:23 +0000 Subject: [PATCH 27/27] Update changelog --- docs/main/changelog.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 18ca9672..4ebbcbc3 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,14 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +4.48.1 +------ + +* Improve performance of ``dependency_injector._cwiring.DependencyResolver`` +* Add ``typing-extensions`` as a dependency for older Python versions (<3.11) +* Produce warning on ``@inject``s without ``Provide[...]`` marks +* Add support for `resource_type` in ``Lifespan``s + 4.48.0 ------