From eb587933f4feb723845a4306e53aa4d9964aae30 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Thu, 28 Jan 2021 19:49:24 -0500 Subject: [PATCH 1/2] Implement wiring autoloader (#381) * Implement wiring autoloader * Add docs * Update changelog --- docs/main/changelog.rst | 6 ++ docs/wiring.rst | 33 +++++++++ src/dependency_injector/wiring.py | 101 ++++++++++++++++++++++++++ tests/unit/wiring/test_wiring_py36.py | 36 ++++++++- 4 files changed, 175 insertions(+), 1 deletion(-) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 699b4567..a3ec3c6a 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,12 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +Development version +------------------- +- Add wiring import hook that auto-wires dynamically imported modules. + See issue: `#365 `_. + Thanks to `@Balthus1989 `_ for providing a use case. + 4.11.3 ------ - Replace weakrefs with normal refs in ``ConfigurationOption`` to support diff --git a/docs/wiring.rst b/docs/wiring.rst index d4f62587..2aed6396 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -18,6 +18,10 @@ To use wiring you need: :language: python :lines: 3- +.. contents:: + :local: + :backlinks: none + Markers ------- @@ -274,6 +278,35 @@ See also: - Resource provider :ref:`resource-async-initializers` - :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 --------------------------------- diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index c738bf02..81fa3985 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -4,6 +4,7 @@ import asyncio import functools import inspect import importlib +import importlib.machinery import pkgutil import sys from types import ModuleType @@ -52,6 +53,11 @@ __all__ = ( 'Provide', 'Provider', 'Closing', + 'register_loader_containers', + 'unregister_loader_containers', + 'install_loader', + 'uninstall_loader', + 'is_loader_installed', ) T = TypeVar('T') @@ -535,3 +541,98 @@ class Provider(_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 diff --git a/tests/unit/wiring/test_wiring_py36.py b/tests/unit/wiring/test_wiring_py36.py index 84938a6a..25f4b9f2 100644 --- a/tests/unit/wiring/test_wiring_py36.py +++ b/tests/unit/wiring/test_wiring_py36.py @@ -1,7 +1,15 @@ +import contextlib from decimal import Decimal +import importlib 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 # 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.assertEqual(asyncinjections.resource2.init_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) From ebeb258e96692c8ee3120746bfcd7c654997d24d Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Thu, 28 Jan 2021 19:50:35 -0500 Subject: [PATCH 2/2] Bump version to 4.12.0 --- docs/main/changelog.rst | 4 ++-- src/dependency_injector/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index a3ec3c6a..9aa84d4e 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,8 +7,8 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ -Development version -------------------- +4.12.0 +------ - Add wiring import hook that auto-wires dynamically imported modules. See issue: `#365 `_. Thanks to `@Balthus1989 `_ for providing a use case. diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index e72ae5d2..d4a89f60 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = '4.11.3' +__version__ = '4.12.0' """Version number. :type: str