Closing wiring marker (#315)

* Add closing marker

* Add example

* Fix flake8 errors

* Add test

* Update docs and README
This commit is contained in:
Roman Mogylatov 2020-10-29 22:55:09 -04:00 committed by GitHub
parent b18385a867
commit 707446a70f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 168 additions and 11 deletions

View File

@ -65,7 +65,7 @@ Key features of the ``Dependency Injector``:
- **Containers**. Provides declarative and dynamic containers. - **Containers**. Provides declarative and dynamic containers.
See `Containers <https://python-dependency-injector.ets-labs.org/containers/index.html>`_. See `Containers <https://python-dependency-injector.ets-labs.org/containers/index.html>`_.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread - **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 <https://python-dependency-injector.ets-labs.org/providers/resource.html>`_. See `Resource provider <https://python-dependency-injector.ets-labs.org/providers/resource.html>`_.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with - **Wiring**. Injects dependencies into functions and methods. Helps integrating with
other frameworks: Django, Flask, Aiohttp, etc. other frameworks: Django, Flask, Aiohttp, etc.

View File

@ -73,7 +73,8 @@ Key features of the ``Dependency Injector``:
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, environment variables - **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, environment variables
and dictionaries. See :ref:`configuration-provider`. and dictionaries. See :ref:`configuration-provider`.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread - **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`. - **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with - **Wiring**. Injects dependencies into functions and methods. Helps integrating with
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`. other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.

View File

@ -19,7 +19,8 @@ Key features of the ``Dependency Injector``:
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, environment variables - **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, environment variables
and dictionaries. See :ref:`configuration-provider`. and dictionaries. See :ref:`configuration-provider`.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread - **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`. - **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with - **Wiring**. Injects dependencies into functions and methods. Helps integrating with
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`. other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.

View File

@ -203,4 +203,36 @@ first argument.
# shutdown # 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:: .. disqus::

View File

@ -66,6 +66,10 @@ You can use configuration, provided instance and sub-container providers as you
def foo(bar: Bar = Provide[Container.subcontainer.bar]): 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 <resource-provider-wiring-closing>` for details.
Wiring with modules and packages Wiring with modules and packages
-------------------------------- --------------------------------

View File

@ -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()

View File

@ -5,7 +5,7 @@ import inspect
import pkgutil import pkgutil
import sys import sys
from types import ModuleType 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): if sys.version_info < (3, 7):
from typing import GenericMeta from typing import GenericMeta
@ -22,6 +22,7 @@ __all__ = (
'unwire', 'unwire',
'Provide', 'Provide',
'Provider', 'Provider',
'Closing',
) )
T = TypeVar('T') T = TypeVar('T')
@ -206,10 +207,10 @@ def _patch_fn(
fn: Callable[..., Any], fn: Callable[..., Any],
providers_map: ProvidersMap, providers_map: ProvidersMap,
) -> None: ) -> None:
injections = _resolve_injections(fn, providers_map) injections, closing = _resolve_injections(fn, providers_map)
if not injections: if not injections:
return return
setattr(module, name, _patch_with_injections(fn, injections)) setattr(module, name, _patch_with_injections(fn, injections, closing))
def _unpatch_fn( def _unpatch_fn(
@ -222,25 +223,37 @@ def _unpatch_fn(
setattr(module, name, _get_original_from_patched(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) signature = inspect.signature(fn)
injections = {} injections = {}
closing = []
for parameter_name, parameter in signature.parameters.items(): for parameter_name, parameter in signature.parameters.items():
if not isinstance(parameter.default, _Marker): if not isinstance(parameter.default, _Marker):
continue continue
marker = parameter.default marker = parameter.default
closing_modifier = False
if isinstance(marker, Closing):
closing_modifier = True
marker = marker.provider
provider = providers_map.resolve_provider(marker.provider) provider = providers_map.resolve_provider(marker.provider)
if provider is None: if provider is None:
continue continue
if closing_modifier:
closing.append(provider)
if isinstance(marker, Provide): if isinstance(marker, Provide):
injections[parameter_name] = provider injections[parameter_name] = provider
elif isinstance(marker, Provider): elif isinstance(marker, Provider):
injections[parameter_name] = provider.provider injections[parameter_name] = provider.provider
return injections return injections, closing
def _fetch_modules(package): def _fetch_modules(package):
@ -258,7 +271,7 @@ def _is_method(member):
return inspect.ismethod(member) or inspect.isfunction(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): if inspect.iscoroutinefunction(fn):
@functools.wraps(fn) @functools.wraps(fn)
async def _patched(*args, **kwargs): async def _patched(*args, **kwargs):
@ -268,7 +281,13 @@ def _patch_with_injections(fn, injections):
to_inject.update(kwargs) 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: else:
@functools.wraps(fn) @functools.wraps(fn)
def _patched(*args, **kwargs): def _patched(*args, **kwargs):
@ -278,11 +297,18 @@ def _patch_with_injections(fn, injections):
to_inject.update(kwargs) 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.__wired__ = True
_patched.__original__ = fn _patched.__original__ = fn
_patched.__injections__ = injections _patched.__injections__ = injections
_patched.__closing__ = []
return _patched return _patched
@ -322,3 +348,7 @@ class Provide(_Marker):
class Provider(_Marker): class Provider(_Marker):
... ...
class Closing(_Marker):
...

View File

@ -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

View File

@ -174,3 +174,22 @@ class WiringTest(unittest.TestCase):
self.assertIsInstance(service, Service) self.assertIsInstance(service, Service)
self.assertEqual(some_value, 1) 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)