From 23fbb71fde4e82d12e1cc652f9e30ef669bae46b Mon Sep 17 00:00:00 2001 From: Aran Moncusi Ramirez Date: Fri, 21 Feb 2025 20:51:21 +0100 Subject: [PATCH 1/5] feat: Add resource_type argument on `init_resources` and `shutdown_resources` methods to provide a more granular way to initialize the desired resources and add the possibility to scope that ones. --- src/dependency_injector/containers.pyi | 6 +++--- src/dependency_injector/containers.pyx | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dependency_injector/containers.pyi b/src/dependency_injector/containers.pyi index ca608f28..69fcb9f5 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]=None) -> Optional[Awaitable]: ... + def shutdown_resources(self, resource_type: Type[Resource]=None) -> Optional[Awaitable]: ... 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..818fd61a 100644 --- a/src/dependency_injector/containers.pyx +++ b/src/dependency_injector/containers.pyx @@ -315,11 +315,11 @@ 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.""" 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,7 +328,7 @@ 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.""" def _independent_resources(resources): for resource in resources: @@ -360,7 +360,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: From cf52316f3a0def52c69e99068c69289b42a356ca Mon Sep 17 00:00:00 2001 From: Aran Moncusi Ramirez Date: Fri, 21 Feb 2025 21:17:01 +0100 Subject: [PATCH 2/5] feat: Add testing specifying the resource type creating scoping resources. --- .../instance/test_async_resources_py36.py | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) 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"] From c6c1797075d2dc96b5799ffcc3b1412968fa9891 Mon Sep 17 00:00:00 2001 From: Aran Moncusi Ramirez Date: Sat, 22 Feb 2025 19:49:47 +0100 Subject: [PATCH 3/5] feat(docs): Added documentation about how to use the init_resources and shutdown_resources using the resource_type argument and how can scope the resources --- docs/providers/resource.rst | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) 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 --------------------------------------------------- From d11b5c10b7cd8f11d08b4741e5b34587f14053be Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 16 Jun 2025 08:12:06 +0000 Subject: [PATCH 4/5] Add issubclass check for init_resources --- src/dependency_injector/containers.pyx | 8 ++++++++ tests/unit/containers/instance/test_main_py2_py3.py | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/dependency_injector/containers.pyx b/src/dependency_injector/containers.pyx index 818fd61a..99762da2 100644 --- a/src/dependency_injector/containers.pyx +++ b/src/dependency_injector/containers.pyx @@ -317,6 +317,10 @@ class DynamicContainer(Container): 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=[resource_type]): @@ -330,6 +334,10 @@ class DynamicContainer(Container): 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: 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 4604341f3bebcf0d28e8ac32da83c81d4c30dbaa Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 16 Jun 2025 08:12:40 +0000 Subject: [PATCH 5/5] Fix typing for init_resources/shutdown_resources --- src/dependency_injector/containers.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dependency_injector/containers.pyi b/src/dependency_injector/containers.pyi index 69fcb9f5..f21a8791 100644 --- a/src/dependency_injector/containers.pyi +++ b/src/dependency_injector/containers.pyi @@ -74,8 +74,8 @@ class Container: from_package: Optional[str] = None, ) -> None: ... def unwire(self) -> None: ... - def init_resources(self, resource_type: Type[Resource]=None) -> Optional[Awaitable]: ... - def shutdown_resources(self, resource_type: Type[Resource]=None) -> Optional[Awaitable]: ... + 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]: ...