Implement wiring autoloader (#381)

* Implement wiring autoloader

* Add docs

* Update changelog
This commit is contained in:
Roman Mogylatov 2021-01-28 19:49:24 -05:00 committed by GitHub
parent 9225f9dcd6
commit eb587933f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 175 additions and 1 deletions

View File

@ -7,6 +7,12 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_ follows `Semantic versioning`_
Development version
-------------------
- Add wiring import hook that auto-wires dynamically imported modules.
See issue: `#365 <https://github.com/ets-labs/python-dependency-injector/issues/365>`_.
Thanks to `@Balthus1989 <https://github.com/Balthus1989>`_ for providing a use case.
4.11.3 4.11.3
------ ------
- Replace weakrefs with normal refs in ``ConfigurationOption`` to support - Replace weakrefs with normal refs in ``ConfigurationOption`` to support

View File

@ -18,6 +18,10 @@ To use wiring you need:
:language: python :language: python
:lines: 3- :lines: 3-
.. contents::
:local:
:backlinks: none
Markers Markers
------- -------
@ -274,6 +278,35 @@ See also:
- Resource provider :ref:`resource-async-initializers` - Resource provider :ref:`resource-async-initializers`
- :ref:`fastapi-redis-example` - :ref:`fastapi-redis-example`
Wiring of dynamically imported modules
--------------------------------------
You can install an import hook that automatically wires containers to the imported modules.
This is useful when you import modules dynamically.
.. code-block:: python
import importlib
from dependency_injector.wiring import register_loader_containers
from .containers import Container
if __name__ == '__main__':
container = Container()
register_loader_containers(container) # <--- installs import hook
module = importlib.import_module('package.module')
module.foo()
You can register multiple containers in the import hook. For doing this call register function
with multiple containers ``register_loader_containers(container1, container2, ...)``
or with a single container ``register_loader_containers(container)`` multiple times.
To unregister a container use ``unregister_loader_containers(container)``.
Wiring module will uninstall the import hook when unregister last container.
Integration with other frameworks Integration with other frameworks
--------------------------------- ---------------------------------

View File

@ -4,6 +4,7 @@ import asyncio
import functools import functools
import inspect import inspect
import importlib import importlib
import importlib.machinery
import pkgutil import pkgutil
import sys import sys
from types import ModuleType from types import ModuleType
@ -52,6 +53,11 @@ __all__ = (
'Provide', 'Provide',
'Provider', 'Provider',
'Closing', 'Closing',
'register_loader_containers',
'unregister_loader_containers',
'install_loader',
'uninstall_loader',
'is_loader_installed',
) )
T = TypeVar('T') T = TypeVar('T')
@ -535,3 +541,98 @@ class Provider(_Marker):
class Closing(_Marker): class Closing(_Marker):
... ...
class AutoLoader:
"""Auto-wiring module loader.
Automatically wire containers when modules are imported.
"""
def __init__(self):
self.containers = []
self._path_hook = None
def register_containers(self, *containers):
self.containers.extend(containers)
if not self.installed:
self.install()
def unregister_containers(self, *containers):
for container in containers:
self.containers.remove(container)
if not self.containers:
self.uninstall()
def wire_module(self, module):
for container in self.containers:
container.wire(modules=[module])
@property
def installed(self):
return self._path_hook is not None
def install(self):
if self.installed:
return
loader = self
class SourcelessFileLoader(importlib.machinery.SourcelessFileLoader):
def exec_module(self, module):
super().exec_module(module)
loader.wire_module(module)
class SourceFileLoader(importlib.machinery.SourceFileLoader):
def exec_module(self, module):
super().exec_module(module)
loader.wire_module(module)
loader_details = [
(SourcelessFileLoader, importlib.machinery.BYTECODE_SUFFIXES),
(SourceFileLoader, importlib.machinery.SOURCE_SUFFIXES),
]
self._path_hook = importlib.machinery.FileFinder.path_hook(*loader_details)
sys.path_hooks.insert(0, self._path_hook)
sys.path_importer_cache.clear()
importlib.invalidate_caches()
def uninstall(self):
if not self.installed:
return
sys.path_hooks.remove(self._path_hook)
sys.path_importer_cache.clear()
importlib.invalidate_caches()
_loader = AutoLoader()
def register_loader_containers(*containers: Container) -> None:
"""Register containers in auto-wiring module loader."""
_loader.register_containers(*containers)
def unregister_loader_containers(*containers: Container) -> None:
"""Unregister containers from auto-wiring module loader."""
_loader.unregister_containers(*containers)
def install_loader() -> None:
"""Install auto-wiring module loader hook."""
_loader.install()
def uninstall_loader() -> None:
"""Uninstall auto-wiring module loader hook."""
_loader.uninstall()
def is_loader_installed() -> bool:
"""Check if auto-wiring module loader hook is installed."""
return _loader.installed

View File

@ -1,7 +1,15 @@
import contextlib
from decimal import Decimal from decimal import Decimal
import importlib
import unittest import unittest
from dependency_injector.wiring import wire, Provide, Closing from dependency_injector.wiring import (
wire,
Provide,
Closing,
register_loader_containers,
unregister_loader_containers,
)
from dependency_injector import errors from dependency_injector import errors
# Runtime import to avoid syntax errors in samples on Python < 3.5 # Runtime import to avoid syntax errors in samples on Python < 3.5
@ -367,3 +375,29 @@ class WiringAsyncInjectionsTest(AsyncTestCase):
self.assertIs(resource2, asyncinjections.resource2) self.assertIs(resource2, asyncinjections.resource2)
self.assertEqual(asyncinjections.resource2.init_counter, 2) self.assertEqual(asyncinjections.resource2.init_counter, 2)
self.assertEqual(asyncinjections.resource2.shutdown_counter, 2) self.assertEqual(asyncinjections.resource2.shutdown_counter, 2)
class AutoLoaderTest(unittest.TestCase):
container: Container
def setUp(self) -> None:
self.container = Container(config={'a': {'b': {'c': 10}}})
importlib.reload(module)
def tearDown(self) -> None:
with contextlib.suppress(ValueError):
unregister_loader_containers(self.container)
self.container.unwire()
@classmethod
def tearDownClass(cls) -> None:
importlib.reload(module)
def test_register_container(self):
register_loader_containers(self.container)
importlib.reload(module)
service = module.test_function()
self.assertIsInstance(service, Service)