mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2025-07-06 05:13:13 +03:00
Add wiring module
This commit is contained in:
parent
53b7ad0275
commit
506e01aeee
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,5 @@
|
|||
from typing import Type, Dict, Tuple, Optional, Any, Union, ClassVar, Callable as _Callable
|
||||
from types import ModuleType
|
||||
from typing import Type, Dict, Tuple, Optional, Any, Union, ClassVar, Callable as _Callable, Iterable
|
||||
|
||||
from .providers import Provider
|
||||
|
||||
|
@ -16,6 +17,7 @@ class Container:
|
|||
def override_providers(self, **overriding_providers: Provider) -> None: ...
|
||||
def reset_last_overriding(self) -> None: ...
|
||||
def reset_override(self) -> None: ...
|
||||
def wire(self, modules: Optional[Iterable[ModuleType]] = None, packages: Optional[Iterable[ModuleType]] = None) -> None: ...
|
||||
|
||||
|
||||
class DynamicContainer(Container): ...
|
||||
|
|
|
@ -8,6 +8,7 @@ from .providers cimport (
|
|||
Provider,
|
||||
deepcopy,
|
||||
)
|
||||
from .wiring import wire
|
||||
|
||||
|
||||
class DynamicContainer(object):
|
||||
|
@ -171,6 +172,17 @@ class DynamicContainer(object):
|
|||
for provider in six.itervalues(self.providers):
|
||||
provider.reset_override()
|
||||
|
||||
def wire(self, modules=None, packages=None):
|
||||
"""Wire container providers with provided packages and modules by name.
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
wire(
|
||||
container=self,
|
||||
modules=modules,
|
||||
packages=packages,
|
||||
)
|
||||
|
||||
|
||||
class DeclarativeContainerMetaClass(type):
|
||||
"""Declarative inversion of control container meta class."""
|
||||
|
@ -363,6 +375,18 @@ class DeclarativeContainer(object):
|
|||
for provider in six.itervalues(cls.providers):
|
||||
provider.reset_override()
|
||||
|
||||
@classmethod
|
||||
def wire(cls, modules=None, packages=None):
|
||||
"""Wire container providers with provided packages and modules by name.
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
wire(
|
||||
container=cls,
|
||||
modules=modules,
|
||||
packages=packages,
|
||||
)
|
||||
|
||||
|
||||
def override(object container):
|
||||
""":py:class:`DeclarativeContainer` overriding decorator.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -122,11 +122,12 @@ class CoroutineDelegate(Delegate):
|
|||
|
||||
class ConfigurationOption(Provider):
|
||||
UNDEFINED: object
|
||||
def __init__(self, name: str, root: Configuration) -> None: ...
|
||||
def __init__(self, name: Tuple[str], root: Configuration) -> None: ...
|
||||
def __call__(self, *args: Injection, **kwargs: Injection) -> Any: ...
|
||||
def __getattr__(self, item: str) -> ConfigurationOption: ...
|
||||
def __getitem__(self, item: str) -> ConfigurationOption: ...
|
||||
def get_name(self) -> str: ...
|
||||
def get_option_provider(self, selector: str) -> ConfigurationOption: ...
|
||||
def as_int(self) -> Callable[int]: ...
|
||||
def as_float(self) -> Callable[float]: ...
|
||||
def as_(self, callback: _Callable[..., T], *args: Injection, **kwargs: Injection) -> Callable[T]: ...
|
||||
|
@ -144,6 +145,7 @@ class Configuration(Object):
|
|||
def __getitem__(self, item: str) -> ConfigurationOption: ...
|
||||
def get_name(self) -> str: ...
|
||||
def get(self, selector: str) -> Any: ...
|
||||
def get_option_provider(self, selector: str) -> ConfigurationOption: ...
|
||||
def set(self, selector: str, value: Any) -> OverridingContext: ...
|
||||
def reset_cache(self) -> None: ...
|
||||
def update(self, value: Any) -> None: ...
|
||||
|
|
|
@ -1143,6 +1143,23 @@ cdef class ConfigurationOption(Provider):
|
|||
root = self.__root_ref()
|
||||
return '.'.join((root.get_name(), self._get_self_name()))
|
||||
|
||||
def get_option_provider(self, selector):
|
||||
"""Return configuration option provider.
|
||||
|
||||
:param selector: Selector string, e.g. "option1.option2"
|
||||
:type selector: str
|
||||
|
||||
:return: Option provider.
|
||||
:rtype: :py:class:`ConfigurationOption`
|
||||
"""
|
||||
key, *other_keys = selector.split('.')
|
||||
child = getattr(self, key)
|
||||
|
||||
if other_keys:
|
||||
child = child.get_option_provider('.'.join(other_keys))
|
||||
|
||||
return child
|
||||
|
||||
def as_int(self):
|
||||
return Callable(int, self)
|
||||
|
||||
|
@ -1357,6 +1374,23 @@ cdef class Configuration(Object):
|
|||
|
||||
return value
|
||||
|
||||
def get_option_provider(self, selector):
|
||||
"""Return configuration option provider.
|
||||
|
||||
:param selector: Selector string, e.g. "option1.option2"
|
||||
:type selector: str
|
||||
|
||||
:return: Option provider.
|
||||
:rtype: :py:class:`ConfigurationOption`
|
||||
"""
|
||||
key, *other_keys = selector.split('.')
|
||||
child = getattr(self, key)
|
||||
|
||||
if other_keys:
|
||||
child = child.get_option_provider('.'.join(other_keys))
|
||||
|
||||
return child
|
||||
|
||||
def set(self, selector, value):
|
||||
"""Override configuration option.
|
||||
|
||||
|
|
130
src/dependency_injector/wiring.py
Normal file
130
src/dependency_injector/wiring.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
"""Wiring module."""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import pkgutil
|
||||
from types import ModuleType
|
||||
from typing import Optional, Iterable, Callable, Any, Type, Dict
|
||||
|
||||
from . import providers
|
||||
|
||||
AnyContainer = Any
|
||||
|
||||
|
||||
def wire(
|
||||
container: AnyContainer,
|
||||
*,
|
||||
modules: Optional[Iterable[ModuleType]] = None,
|
||||
packages: Optional[Iterable[ModuleType]] = None,
|
||||
) -> None:
|
||||
"""Wire container providers with provided packages and modules by name."""
|
||||
if not modules:
|
||||
modules = []
|
||||
|
||||
if packages:
|
||||
for package in packages:
|
||||
modules.extend(_fetch_modules(package))
|
||||
|
||||
for module in modules:
|
||||
for name, member in inspect.getmembers(module):
|
||||
if inspect.isfunction(member):
|
||||
_patch_fn(module, name, member, container)
|
||||
elif inspect.isclass(member):
|
||||
_patch_cls(member, container)
|
||||
|
||||
|
||||
def _patch_cls(
|
||||
cls: Type[Any],
|
||||
container: AnyContainer,
|
||||
) -> None:
|
||||
if not hasattr(cls, '__init__'):
|
||||
return
|
||||
init_method = getattr(cls, '__init__')
|
||||
|
||||
injections = _resolve_injections(init_method, container)
|
||||
if not injections:
|
||||
return
|
||||
|
||||
setattr(cls, '__init__', _patch_with_injections(init_method, injections))
|
||||
|
||||
|
||||
def _patch_fn(
|
||||
module: ModuleType,
|
||||
name: str,
|
||||
fn: Callable[..., Any],
|
||||
container: AnyContainer,
|
||||
) -> None:
|
||||
injections = _resolve_injections(fn, container)
|
||||
if not injections:
|
||||
return
|
||||
|
||||
setattr(module, name, _patch_with_injections(fn, injections))
|
||||
|
||||
|
||||
def _resolve_injections(fn: Callable[..., Any], container: AnyContainer) -> Dict[str, Any]:
|
||||
signature = inspect.signature(fn)
|
||||
|
||||
config = _resolve_container_config(container)
|
||||
|
||||
injections = {}
|
||||
for parameter_name, parameter in signature.parameters.items():
|
||||
if parameter_name in container.providers:
|
||||
injections[parameter_name] = container.providers[parameter_name]
|
||||
|
||||
if parameter_name.endswith('_provider'):
|
||||
provider_name = parameter_name[:-9]
|
||||
if provider_name in container.providers:
|
||||
injections[parameter_name] = container.providers[provider_name].provider
|
||||
|
||||
if config and isinstance(parameter.default, ConfigurationOption):
|
||||
option_provider = config.get_option_provider(parameter.default.selector)
|
||||
if parameter.annotation:
|
||||
injections[parameter_name] = option_provider.as_(parameter.annotation)
|
||||
else:
|
||||
injections[parameter_name] = option_provider
|
||||
|
||||
return injections
|
||||
|
||||
|
||||
def _resolve_container_config(container: AnyContainer) -> Optional[providers.Configuration]:
|
||||
for provider in container.providers.values():
|
||||
if isinstance(provider, providers.Configuration):
|
||||
return provider
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_modules(package):
|
||||
modules = []
|
||||
for loader, module_name, is_pkg in pkgutil.walk_packages(
|
||||
path=package.__path__,
|
||||
prefix=package.__name__ + '.',
|
||||
):
|
||||
module = loader.find_module(module_name).load_module(module_name)
|
||||
modules.append(module)
|
||||
return modules
|
||||
|
||||
|
||||
def _patch_with_injections(fn, injections):
|
||||
@functools.wraps(fn)
|
||||
def _patched(*args, **kwargs):
|
||||
to_inject = {}
|
||||
for injection, provider in injections.items():
|
||||
to_inject[injection] = provider()
|
||||
|
||||
to_inject.update(kwargs)
|
||||
|
||||
return fn(*args, **to_inject)
|
||||
return _patched
|
||||
|
||||
|
||||
class ConfigurationOption:
|
||||
"""Configuration option marker."""
|
||||
|
||||
def __init__(self, selector: str):
|
||||
self.selector = selector
|
||||
|
||||
def __class_getitem__(cls, item):
|
||||
return cls(item)
|
||||
|
||||
|
1
tests/unit/wiring/__init__.py
Normal file
1
tests/unit/wiring/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Wiring tests."""
|
24
tests/unit/wiring/module.py
Normal file
24
tests/unit/wiring/module.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
"""Test module for wiring."""
|
||||
|
||||
from dependency_injector.wiring import ConfigurationOption
|
||||
|
||||
|
||||
class TestClass:
|
||||
|
||||
def __init__(self, service):
|
||||
self.service = service
|
||||
|
||||
|
||||
def test_function(service):
|
||||
return service
|
||||
|
||||
|
||||
def test_function_provider(service_provider):
|
||||
return service_provider()
|
||||
|
||||
|
||||
def test_config_value(
|
||||
some_value_int: int = ConfigurationOption['a.b.c'],
|
||||
some_value_str: str = ConfigurationOption['a.b.c'],
|
||||
):
|
||||
return some_value_int, some_value_str
|
0
tests/unit/wiring/package/__init__.py
Normal file
0
tests/unit/wiring/package/__init__.py
Normal file
0
tests/unit/wiring/package/subpackage/__init__.py
Normal file
0
tests/unit/wiring/package/subpackage/__init__.py
Normal file
2
tests/unit/wiring/package/subpackage/submodule.py
Normal file
2
tests/unit/wiring/package/subpackage/submodule.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
def test_function(service):
|
||||
return service
|
69
tests/unit/wiring/test_wiring_py36.py
Normal file
69
tests/unit/wiring/test_wiring_py36.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
import unittest
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
|
||||
from . import module, package
|
||||
|
||||
|
||||
class Service:
|
||||
...
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
service = providers.Factory(Service)
|
||||
|
||||
|
||||
class WiringTest(unittest.TestCase):
|
||||
|
||||
container: Container
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.container = Container(config={'a': {'b': {'c': 10}}})
|
||||
cls.container.wire(
|
||||
modules=[module],
|
||||
packages=[package],
|
||||
)
|
||||
|
||||
def test_package_lookup(self):
|
||||
from .package.subpackage.submodule import test_function
|
||||
service = module.test_function()
|
||||
self.assertIsInstance(service, Service)
|
||||
|
||||
def test_class_wiring(self):
|
||||
test_class_object = module.TestClass()
|
||||
self.assertIsInstance(test_class_object.service, Service)
|
||||
|
||||
def test_class_wiring_context_arg(self):
|
||||
test_service = self.container.service()
|
||||
|
||||
test_class_object = module.TestClass(service=test_service)
|
||||
self.assertIs(test_class_object.service, test_service)
|
||||
|
||||
def test_function_wiring(self):
|
||||
service = module.test_function()
|
||||
self.assertIsInstance(service, Service)
|
||||
|
||||
def test_function_wiring_context_arg(self):
|
||||
test_service = self.container.service()
|
||||
|
||||
service = module.test_function(service=test_service)
|
||||
self.assertIs(service, test_service)
|
||||
|
||||
def test_function_wiring_provider(self):
|
||||
service = module.test_function_provider()
|
||||
self.assertIsInstance(service, Service)
|
||||
|
||||
def test_function_wiring_provider_context_arg(self):
|
||||
test_service = self.container.service()
|
||||
|
||||
service = module.test_function_provider(service_provider=lambda: test_service)
|
||||
self.assertIs(service, test_service)
|
||||
|
||||
def test_configuration_option(self):
|
||||
int_value, str_value = module.test_config_value()
|
||||
self.assertEqual(int_value, 10)
|
||||
self.assertEqual(str_value, '10')
|
Loading…
Reference in New Issue
Block a user