Skip to content

Commit 4bfe645

Browse files
authored
Add resource type parameter to init and shutdown resources using specialized providers (#858)
1 parent b411807 commit 4bfe645

File tree

5 files changed

+212
-7
lines changed

5 files changed

+212
-7
lines changed

docs/providers/resource.rst

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,72 @@ first argument.
252252
253253
.. _resource-provider-wiring-closing:
254254

255+
Scoping Resources using specialized subclasses
256+
----------------------------------------------
257+
258+
You can use specialized subclasses of ``Resource`` provider to initialize and shutdown resources by type.
259+
Allowing for example to only initialize a subgroup of resources.
260+
261+
.. code-block:: python
262+
263+
class ScopedResource(resources.Resource):
264+
pass
265+
266+
def init_service(name) -> Service:
267+
print(f"Init {name}")
268+
yield Service()
269+
print(f"Shutdown {name}")
270+
271+
class Container(containers.DeclarativeContainer):
272+
273+
scoped = ScopedResource(
274+
init_service,
275+
"scoped",
276+
)
277+
278+
generic = providers.Resource(
279+
init_service,
280+
"generic",
281+
)
282+
283+
284+
To initialize resources by type you can use ``init_resources(resource_type)`` and ``shutdown_resources(resource_type)``
285+
methods adding the resource type as an argument:
286+
287+
.. code-block:: python
288+
289+
def main():
290+
container = Container()
291+
container.init_resources(ScopedResource)
292+
# Generates:
293+
# >>> Init scoped
294+
295+
container.shutdown_resources(ScopedResource)
296+
# Generates:
297+
# >>> Shutdown scoped
298+
299+
300+
And to initialize all resources you can use ``init_resources()`` and ``shutdown_resources()`` without arguments:
301+
302+
.. code-block:: python
303+
304+
def main():
305+
container = Container()
306+
container.init_resources()
307+
# Generates:
308+
# >>> Init scoped
309+
# >>> Init generic
310+
311+
container.shutdown_resources()
312+
# Generates:
313+
# >>> Shutdown scoped
314+
# >>> Shutdown generic
315+
316+
317+
It works using the :ref:`traverse` method to find all resources of the specified type, selecting all resources
318+
which are instances of the specified type.
319+
320+
255321
Resources, wiring, and per-function execution scope
256322
---------------------------------------------------
257323

src/dependency_injector/containers.pyi

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

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

2727
C_Base = TypeVar("C_Base", bound="Container")
2828
C = TypeVar("C", bound="DeclarativeContainer")
@@ -74,8 +74,8 @@ class Container:
7474
from_package: Optional[str] = None,
7575
) -> None: ...
7676
def unwire(self) -> None: ...
77-
def init_resources(self) -> Optional[Awaitable[None]]: ...
78-
def shutdown_resources(self) -> Optional[Awaitable[None]]: ...
77+
def init_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ...
78+
def shutdown_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ...
7979
def load_config(self) -> None: ...
8080
def apply_container_providers_overridings(self) -> None: ...
8181
def reset_singletons(self) -> SingletonResetContext[C_Base]: ...

src/dependency_injector/containers.pyx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,15 @@ class DynamicContainer(Container):
315315
self.wired_to_modules.clear()
316316
self.wired_to_packages.clear()
317317

318-
def init_resources(self):
318+
def init_resources(self, resource_type=providers.Resource):
319319
"""Initialize all container resources."""
320+
321+
if not issubclass(resource_type, providers.Resource):
322+
raise TypeError("resource_type must be a subclass of Resource provider")
323+
320324
futures = []
321325

322-
for provider in self.traverse(types=[providers.Resource]):
326+
for provider in self.traverse(types=[resource_type]):
323327
resource = provider.init()
324328

325329
if __is_future_or_coroutine(resource):
@@ -328,8 +332,12 @@ class DynamicContainer(Container):
328332
if futures:
329333
return asyncio.gather(*futures)
330334

331-
def shutdown_resources(self):
335+
def shutdown_resources(self, resource_type=providers.Resource):
332336
"""Shutdown all container resources."""
337+
338+
if not issubclass(resource_type, providers.Resource):
339+
raise TypeError("resource_type must be a subclass of Resource provider")
340+
333341
def _independent_resources(resources):
334342
for resource in resources:
335343
for other_resource in resources:
@@ -360,7 +368,7 @@ class DynamicContainer(Container):
360368
for resource in resources_to_shutdown:
361369
resource.shutdown()
362370

363-
resources = list(self.traverse(types=[providers.Resource]))
371+
resources = list(self.traverse(types=[resource_type]))
364372
if any(resource.is_async_mode_enabled() for resource in resources):
365373
return _async_ordered_shutdown(resources)
366374
else:

tests/unit/containers/instance/test_async_resources_py36.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,121 @@ class Container(containers.DeclarativeContainer):
145145
await container.shutdown_resources()
146146
assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"]
147147
assert shutdown_resources == ["r3", "r2", "r1", "r3", "r2", "r1"]
148+
149+
150+
@mark.asyncio
151+
async def test_init_and_shutdown_scoped_resources():
152+
initialized_resources = []
153+
shutdown_resources = []
154+
155+
def _sync_resource(name, **_):
156+
initialized_resources.append(name)
157+
yield name
158+
shutdown_resources.append(name)
159+
160+
async def _async_resource(name, **_):
161+
initialized_resources.append(name)
162+
yield name
163+
shutdown_resources.append(name)
164+
165+
166+
class ResourceA(providers.Resource):
167+
pass
168+
169+
170+
class ResourceB(providers.Resource):
171+
pass
172+
173+
174+
class Container(containers.DeclarativeContainer):
175+
resource_a = ResourceA(
176+
_sync_resource,
177+
name="ra1",
178+
)
179+
resource_b1 = ResourceB(
180+
_sync_resource,
181+
name="rb1",
182+
r1=resource_a,
183+
)
184+
resource_b2 = ResourceB(
185+
_async_resource,
186+
name="rb2",
187+
r2=resource_b1,
188+
)
189+
190+
container = Container()
191+
192+
container.init_resources(resource_type=ResourceA)
193+
assert initialized_resources == ["ra1"]
194+
assert shutdown_resources == []
195+
196+
container.shutdown_resources(resource_type=ResourceA)
197+
assert initialized_resources == ["ra1"]
198+
assert shutdown_resources == ["ra1"]
199+
200+
await container.init_resources(resource_type=ResourceB)
201+
assert initialized_resources == ["ra1", "ra1", "rb1", "rb2"]
202+
assert shutdown_resources == ["ra1"]
203+
204+
await container.shutdown_resources(resource_type=ResourceB)
205+
assert initialized_resources == ["ra1", "ra1", "rb1", "rb2"]
206+
assert shutdown_resources == ["ra1", "rb2", "rb1"]
207+
208+
209+
@mark.asyncio
210+
async def test_init_and_shutdown_all_scoped_resources_using_default_value():
211+
initialized_resources = []
212+
shutdown_resources = []
213+
214+
def _sync_resource(name, **_):
215+
initialized_resources.append(name)
216+
yield name
217+
shutdown_resources.append(name)
218+
219+
async def _async_resource(name, **_):
220+
initialized_resources.append(name)
221+
yield name
222+
shutdown_resources.append(name)
223+
224+
225+
class ResourceA(providers.Resource):
226+
pass
227+
228+
229+
class ResourceB(providers.Resource):
230+
pass
231+
232+
233+
class Container(containers.DeclarativeContainer):
234+
resource_a = ResourceA(
235+
_sync_resource,
236+
name="r1",
237+
)
238+
resource_b1 = ResourceB(
239+
_sync_resource,
240+
name="r2",
241+
r1=resource_a,
242+
)
243+
resource_b2 = ResourceB(
244+
_async_resource,
245+
name="r3",
246+
r2=resource_b1,
247+
)
248+
249+
container = Container()
250+
251+
await container.init_resources()
252+
assert initialized_resources == ["r1", "r2", "r3"]
253+
assert shutdown_resources == []
254+
255+
await container.shutdown_resources()
256+
assert initialized_resources == ["r1", "r2", "r3"]
257+
assert shutdown_resources == ["r3", "r2", "r1"]
258+
259+
await container.init_resources()
260+
assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"]
261+
assert shutdown_resources == ["r3", "r2", "r1"]
262+
263+
await container.shutdown_resources()
264+
assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"]
265+
assert shutdown_resources == ["r3", "r2", "r1", "r3", "r2", "r1"]

tests/unit/containers/instance/test_main_py2_py3.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,19 @@ class Container(containers.DeclarativeContainer):
325325
assert _init2.shutdown_counter == 2
326326

327327

328+
def test_init_shutdown_resources_wrong_type() -> None:
329+
class Container(containers.DeclarativeContainer):
330+
pass
331+
332+
c = Container()
333+
334+
with raises(TypeError, match=r"resource_type must be a subclass of Resource provider"):
335+
c.init_resources(int) # type: ignore[arg-type]
336+
337+
with raises(TypeError, match=r"resource_type must be a subclass of Resource provider"):
338+
c.shutdown_resources(int) # type: ignore[arg-type]
339+
340+
328341
def test_reset_singletons():
329342
class SubSubContainer(containers.DeclarativeContainer):
330343
singleton = providers.Singleton(object)

0 commit comments

Comments
 (0)