From 707446a70f8c25d58843952da5c6e8d49313e2eb Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Thu, 29 Oct 2020 22:55:09 -0400 Subject: [PATCH] Closing wiring marker (#315) * Add closing marker * Add example * Fix flake8 errors * Add test * Update docs and README --- README.rst | 2 +- docs/index.rst | 3 +- docs/introduction/key_features.rst | 3 +- docs/providers/resource.rst | 32 +++++++++++++ docs/wiring.rst | 4 ++ examples/wiring/flask_resource_closing.py | 39 ++++++++++++++++ src/dependency_injector/wiring.py | 46 +++++++++++++++---- .../samples/wiringsamples/resourceclosing.py | 31 +++++++++++++ tests/unit/wiring/test_wiring_py36.py | 19 ++++++++ 9 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 examples/wiring/flask_resource_closing.py create mode 100644 tests/unit/samples/wiringsamples/resourceclosing.py diff --git a/README.rst b/README.rst index 80f532af..8a221eb8 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,7 @@ Key features of the ``Dependency Injector``: - **Containers**. Provides declarative and dynamic containers. See `Containers `_. - **Resources**. Helps with initialization and configuring of logging, event loop, thread - or process pool, etc. + or process pool, etc. Can be used for per-function execution scope in tandem with wiring. See `Resource provider `_. - **Wiring**. Injects dependencies into functions and methods. Helps integrating with other frameworks: Django, Flask, Aiohttp, etc. diff --git a/docs/index.rst b/docs/index.rst index 762bca32..4527a6af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,7 +73,8 @@ Key features of the ``Dependency Injector``: - **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, environment variables and dictionaries. See :ref:`configuration-provider`. - **Resources**. Helps with initialization and configuring of logging, event loop, thread - or process pool, etc. See :ref:`resource-provider`. + or process pool, etc. Can be used for per-function execution scope in tandem with wiring. + See :ref:`resource-provider`. - **Containers**. Provides declarative and dynamic containers. See :ref:`containers`. - **Wiring**. Injects dependencies into functions and methods. Helps integrating with other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`. diff --git a/docs/introduction/key_features.rst b/docs/introduction/key_features.rst index 26dfa6b9..e586514a 100644 --- a/docs/introduction/key_features.rst +++ b/docs/introduction/key_features.rst @@ -19,7 +19,8 @@ Key features of the ``Dependency Injector``: - **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, environment variables and dictionaries. See :ref:`configuration-provider`. - **Resources**. Helps with initialization and configuring of logging, event loop, thread - or process pool, etc. See :ref:`resource-provider`. + or process pool, etc. Can be used for per-function execution scope in tandem with wiring. + See :ref:`resource-provider`. - **Containers**. Provides declarative and dynamic containers. See :ref:`containers`. - **Wiring**. Injects dependencies into functions and methods. Helps integrating with other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`. diff --git a/docs/providers/resource.rst b/docs/providers/resource.rst index 31bf3d8c..95ec164e 100644 --- a/docs/providers/resource.rst +++ b/docs/providers/resource.rst @@ -203,4 +203,36 @@ first argument. # shutdown ... + +.. _resource-provider-wiring-closing: + +Resources, wiring and per-function execution scope +-------------------------------------------------- + +You can compound ``Resource`` provider with :ref:`wiring` to implement per-function +execution scope. For doing this you need to use additional ``Closing`` marker from +``wiring`` module. + +.. literalinclude:: ../../examples/wiring/flask_resource_closing.py + :language: python + :lines: 3- + :emphasize-lines: 23 + +Framework initializes and injects the resource into the function. With the ``Closing`` marker +framework calls resource ``shutdown()`` method when function execution is over. + +The example above produces next output: + +.. code-block:: bash + + Init service + Shutdown service + 127.0.0.1 - - [29/Oct/2020 22:39:40] "GET / HTTP/1.1" 200 - + Init service + Shutdown service + 127.0.0.1 - - [29/Oct/2020 22:39:41] "GET / HTTP/1.1" 200 - + Init service + Shutdown service + 127.0.0.1 - - [29/Oct/2020 22:39:41] "GET / HTTP/1.1" 200 - + .. disqus:: diff --git a/docs/wiring.rst b/docs/wiring.rst index eeaed305..412ba770 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -66,6 +66,10 @@ You can use configuration, provided instance and sub-container providers as you def foo(bar: Bar = Provide[Container.subcontainer.bar]): ... + +You can compound wiring and ``Resource`` provider to implement per-function execution scope. +See :ref:`Resources, wiring and per-function execution scope ` for details. + Wiring with modules and packages -------------------------------- diff --git a/examples/wiring/flask_resource_closing.py b/examples/wiring/flask_resource_closing.py new file mode 100644 index 00000000..dec40140 --- /dev/null +++ b/examples/wiring/flask_resource_closing.py @@ -0,0 +1,39 @@ +"""`Resource` - Flask request scope example.""" + +import sys + +from dependency_injector import containers, providers +from dependency_injector.wiring import Provide, Closing +from flask import Flask, current_app + + +class Service: + ... + + +def init_service() -> Service: + print('Init service') + yield Service() + print('Shutdown service') + + +class Container(containers.DeclarativeContainer): + + service = providers.Resource(init_service) + + +def index_view(service: Service = Closing[Provide[Container.service]]): + assert service is current_app.container.service() + return 'Hello World!' + + +container = Container() +container.wire(modules=[sys.modules[__name__]]) + +app = Flask(__name__) +app.container = container +app.add_url_rule('/', 'index', view_func=index_view) + + +if __name__ == '__main__': + app.run() diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index e58a97cd..1b7c8f2d 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -5,7 +5,7 @@ import inspect import pkgutil import sys from types import ModuleType -from typing import Optional, Iterable, Callable, Any, Dict, Generic, TypeVar, cast +from typing import Optional, Iterable, Callable, Any, Tuple, List, Dict, Generic, TypeVar, cast if sys.version_info < (3, 7): from typing import GenericMeta @@ -22,6 +22,7 @@ __all__ = ( 'unwire', 'Provide', 'Provider', + 'Closing', ) T = TypeVar('T') @@ -206,10 +207,10 @@ def _patch_fn( fn: Callable[..., Any], providers_map: ProvidersMap, ) -> None: - injections = _resolve_injections(fn, providers_map) + injections, closing = _resolve_injections(fn, providers_map) if not injections: return - setattr(module, name, _patch_with_injections(fn, injections)) + setattr(module, name, _patch_with_injections(fn, injections, closing)) def _unpatch_fn( @@ -222,25 +223,37 @@ def _unpatch_fn( setattr(module, name, _get_original_from_patched(fn)) -def _resolve_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> Dict[str, Any]: +def _resolve_injections( + fn: Callable[..., Any], + providers_map: ProvidersMap, +) -> Tuple[Dict[str, Any], List[Any]]: signature = inspect.signature(fn) injections = {} + closing = [] for parameter_name, parameter in signature.parameters.items(): if not isinstance(parameter.default, _Marker): continue marker = parameter.default + closing_modifier = False + if isinstance(marker, Closing): + closing_modifier = True + marker = marker.provider + provider = providers_map.resolve_provider(marker.provider) if provider is None: continue + if closing_modifier: + closing.append(provider) + if isinstance(marker, Provide): injections[parameter_name] = provider elif isinstance(marker, Provider): injections[parameter_name] = provider.provider - return injections + return injections, closing def _fetch_modules(package): @@ -258,7 +271,7 @@ def _is_method(member): return inspect.ismethod(member) or inspect.isfunction(member) -def _patch_with_injections(fn, injections): +def _patch_with_injections(fn, injections, closing): if inspect.iscoroutinefunction(fn): @functools.wraps(fn) async def _patched(*args, **kwargs): @@ -268,7 +281,13 @@ def _patch_with_injections(fn, injections): to_inject.update(kwargs) - return await fn(*args, **to_inject) + result = await fn(*args, **to_inject) + + for provider in closing: + if isinstance(provider, providers.Resource): + provider.shutdown() + + return result else: @functools.wraps(fn) def _patched(*args, **kwargs): @@ -278,11 +297,18 @@ def _patch_with_injections(fn, injections): to_inject.update(kwargs) - return fn(*args, **to_inject) + result = fn(*args, **to_inject) + + for provider in closing: + if isinstance(provider, providers.Resource): + provider.shutdown() + + return result _patched.__wired__ = True _patched.__original__ = fn _patched.__injections__ = injections + _patched.__closing__ = [] return _patched @@ -322,3 +348,7 @@ class Provide(_Marker): class Provider(_Marker): ... + + +class Closing(_Marker): + ... diff --git a/tests/unit/samples/wiringsamples/resourceclosing.py b/tests/unit/samples/wiringsamples/resourceclosing.py new file mode 100644 index 00000000..33a160ca --- /dev/null +++ b/tests/unit/samples/wiringsamples/resourceclosing.py @@ -0,0 +1,31 @@ +from dependency_injector import containers, providers +from dependency_injector.wiring import Provide, Closing + + +class Service: + init_counter: int = 0 + shutdown_counter: int = 0 + + @classmethod + def init(cls): + cls.init_counter += 1 + + @classmethod + def shutdown(cls): + cls.shutdown_counter += 1 + + +def init_service(): + service = Service() + service.init() + yield service + service.shutdown() + + +class Container(containers.DeclarativeContainer): + + service = providers.Resource(init_service) + + +def test_function(service: Service = Closing[Provide[Container.service]]): + return service diff --git a/tests/unit/wiring/test_wiring_py36.py b/tests/unit/wiring/test_wiring_py36.py index 14bdad84..f5b1a509 100644 --- a/tests/unit/wiring/test_wiring_py36.py +++ b/tests/unit/wiring/test_wiring_py36.py @@ -174,3 +174,22 @@ class WiringTest(unittest.TestCase): self.assertIsInstance(service, Service) self.assertEqual(some_value, 1) + + def test_closing_resource(self): + from wiringsamples import resourceclosing + + container = resourceclosing.Container() + container.wire(modules=[resourceclosing]) + self.addCleanup(container.unwire) + + result_1 = resourceclosing.test_function() + self.assertIsInstance(result_1, resourceclosing.Service) + self.assertEqual(result_1.init_counter, 1) + self.assertEqual(result_1.shutdown_counter, 1) + + result_2 = resourceclosing.test_function() + self.assertIsInstance(result_2, resourceclosing.Service) + self.assertEqual(result_2.init_counter, 2) + self.assertEqual(result_2.shutdown_counter, 2) + + self.assertIsNot(result_1, result_2)