From 824ed4f3e99f0c18819bc211196b908ddd5cd5e2 Mon Sep 17 00:00:00 2001 From: Roman Mogilatov Date: Wed, 14 Oct 2015 14:30:01 +0300 Subject: [PATCH] Add support of positional args for Factory & Singleton providers --- dependency_injector/__init__.py | 4 +++ dependency_injector/injections.py | 28 +++++++++++---- dependency_injector/providers.py | 28 +++++++++------ dependency_injector/utils.py | 24 +++++++++---- tests/test_injections.py | 16 ++++++--- tests/test_providers.py | 59 ++++++++++++++++++++++++++----- tests/test_utils.py | 29 +++++++++++++-- 7 files changed, 150 insertions(+), 38 deletions(-) diff --git a/dependency_injector/__init__.py b/dependency_injector/__init__.py index 5e38585a..9da694d8 100644 --- a/dependency_injector/__init__.py +++ b/dependency_injector/__init__.py @@ -18,6 +18,7 @@ from .providers import Callable from .providers import Config from .injections import Injection +from .injections import Arg from .injections import KwArg from .injections import Attribute from .injections import Method @@ -27,6 +28,7 @@ from .utils import is_provider from .utils import ensure_is_provider from .utils import is_injection from .utils import ensure_is_injection +from .utils import is_arg_injection from .utils import is_kwarg_injection from .utils import is_attribute_injection from .utils import is_method_injection @@ -59,6 +61,7 @@ __all__ = ( # Injections 'Injection', + 'Arg', 'KwArg', 'Attribute', 'Method', @@ -69,6 +72,7 @@ __all__ = ( 'ensure_is_provider', 'is_injection', 'ensure_is_injection', + 'is_arg_injection', 'is_kwarg_injection', 'is_attribute_injection', 'is_method_injection', diff --git a/dependency_injector/injections.py b/dependency_injector/injections.py index a7a29084..2806b7c0 100644 --- a/dependency_injector/injections.py +++ b/dependency_injector/injections.py @@ -21,11 +21,10 @@ class Injection(object): """Base injection class.""" __IS_INJECTION__ = True - __slots__ = ('name', 'injectable', 'is_provider') + __slots__ = ('injectable', 'is_provider') - def __init__(self, name, injectable): + def __init__(self, injectable): """Initializer.""" - self.name = name self.injectable = injectable self.is_provider = is_provider(injectable) @@ -37,19 +36,36 @@ class Injection(object): return self.injectable -class KwArg(Injection): +class NamedInjection(Injection): + """Base class of named injections.""" + + __slots__ = ('name',) + + def __init__(self, name, injectable): + """Initializer.""" + self.name = name + super(NamedInjection, self).__init__(injectable) + + +class Arg(Injection): + """Positional argument injection.""" + + __IS_ARG_INJECTION__ = True + + +class KwArg(NamedInjection): """Keyword argument injection.""" __IS_KWARG_INJECTION__ = True -class Attribute(Injection): +class Attribute(NamedInjection): """Attribute injection.""" __IS_ATTRIBUTE_INJECTION__ = True -class Method(Injection): +class Method(NamedInjection): """Method injection.""" __IS_METHOD_INJECTION__ = True diff --git a/dependency_injector/providers.py b/dependency_injector/providers.py index b510bd6b..71643749 100644 --- a/dependency_injector/providers.py +++ b/dependency_injector/providers.py @@ -2,12 +2,16 @@ import six +from .injections import Arg from .injections import KwArg from .utils import ensure_is_provider +from .utils import is_injection +from .utils import is_arg_injection from .utils import is_kwarg_injection from .utils import is_attribute_injection from .utils import is_method_injection +from .utils import get_injectable_args from .utils import get_injectable_kwargs from .utils import GLOBAL_LOCK @@ -104,33 +108,35 @@ class Factory(Provider): Factory provider creates new instance of specified class on every call. """ - __slots__ = ('provides', 'kwargs', 'attributes', 'methods') + __slots__ = ('provides', 'args', 'kwargs', 'attributes', 'methods') - def __init__(self, provides, *injections, **kwargs): + def __init__(self, provides, *args, **kwargs): """Initializer.""" if not callable(provides): raise Error('Factory provider expects to get callable, ' + 'got {0} instead'.format(str(provides))) self.provides = provides + self.args = tuple(Arg(arg) if not is_injection(arg) else arg + for arg in args + if not is_injection(arg) or is_arg_injection(arg)) self.kwargs = tuple(injection - for injection in injections + for injection in args if is_kwarg_injection(injection)) if kwargs: self.kwargs += tuple(KwArg(name, value) for name, value in six.iteritems(kwargs)) self.attributes = tuple(injection - for injection in injections + for injection in args if is_attribute_injection(injection)) self.methods = tuple(injection - for injection in injections + for injection in args if is_method_injection(injection)) super(Factory, self).__init__() def _provide(self, *args, **kwargs): """Return provided instance.""" - instance = self.provides(*args, - **get_injectable_kwargs(kwargs, - self.kwargs)) + instance = self.provides(*get_injectable_args(args, self.args), + **get_injectable_kwargs(kwargs, self.kwargs)) for attribute in self.attributes: setattr(instance, attribute.name, attribute.value) for method in self.methods: @@ -141,7 +147,7 @@ class Factory(Provider): @property def injections(self): """Return tuple of all injections.""" - return self.kwargs + self.attributes + self.methods + return self.args + self.kwargs + self.attributes + self.methods class Singleton(Provider): @@ -152,10 +158,10 @@ class Singleton(Provider): __slots__ = ('instance', 'factory') - def __init__(self, provides, *injections, **kwargs): + def __init__(self, provides, *args, **kwargs): """Initializer.""" self.instance = None - self.factory = Factory(provides, *injections, **kwargs) + self.factory = Factory(provides, *args, **kwargs) super(Singleton, self).__init__() def _provide(self, *args, **kwargs): diff --git a/dependency_injector/utils.py b/dependency_injector/utils.py index 29547b9c..007b70b0 100644 --- a/dependency_injector/utils.py +++ b/dependency_injector/utils.py @@ -1,6 +1,7 @@ """Utils module.""" import threading +import itertools import six @@ -41,6 +42,12 @@ def ensure_is_injection(instance): return instance +def is_arg_injection(instance): + """Check if instance is positional argument injection instance.""" + return (not isinstance(instance, six.class_types) and + getattr(instance, '__IS_ARG_INJECTION__', False) is True) + + def is_kwarg_injection(instance): """Check if instance is keyword argument injection instance.""" return (not isinstance(instance, six.class_types) and @@ -82,9 +89,14 @@ def ensure_is_catalog_bundle(instance): return instance -def get_injectable_kwargs(kwargs, injections): - """Return dictionary of kwargs, patched with injections.""" - init_kwargs = dict(((injection.name, injection.value) - for injection in injections)) - init_kwargs.update(kwargs) - return init_kwargs +def get_injectable_args(context_args, arg_injections): + """Return tuple of positional args, patched with injections.""" + return itertools.chain((arg.value for arg in arg_injections), context_args) + + +def get_injectable_kwargs(context_kwargs, kwarg_injections): + """Return dictionary of keyword args, patched with injections.""" + kwargs = dict((kwarg.name, kwarg.value) + for kwarg in kwarg_injections) + kwargs.update(context_kwargs) + return kwargs diff --git a/tests/test_injections.py b/tests/test_injections.py index c96567b7..2226a2fc 100644 --- a/tests/test_injections.py +++ b/tests/test_injections.py @@ -9,18 +9,17 @@ class InjectionTests(unittest.TestCase): def test_init(self): """Test Injection creation and initialization.""" - injection = di.Injection('some_arg_name', 'some_value') - self.assertEqual(injection.name, 'some_arg_name') + injection = di.Injection('some_value') self.assertEqual(injection.injectable, 'some_value') def test_value_with_scalar_injectable(self): """Test Injection value property with scalar value.""" - injection = di.Injection('some_arg_name', 'some_value') + injection = di.Injection('some_value') self.assertEqual(injection.value, 'some_value') def test_value_with_provider_injectable(self): """Test Injection value property with provider.""" - injection = di.Injection('some_arg_name', di.Factory(object)) + injection = di.Injection(di.Factory(object)) self.assertIsInstance(injection.value, object) def test_value_with_catalog_bundle_injectable(self): @@ -35,6 +34,15 @@ class InjectionTests(unittest.TestCase): self.assertIsInstance(injection.value, TestCatalog.Bundle) +class ArgTests(unittest.TestCase): + """Positional arg injection test cases.""" + + def test_init(self): + """Test Arg creation and initialization.""" + injection = di.Arg('some_value') + self.assertEqual(injection.injectable, 'some_value') + + class KwArgTests(unittest.TestCase): """Keyword arg injection test cases.""" diff --git a/tests/test_providers.py b/tests/test_providers.py index 35ced2f9..9591fc43 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -181,14 +181,12 @@ class FactoryTests(unittest.TestCase): self.assertIsInstance(instance1, self.Example) self.assertIsInstance(instance2, self.Example) - def test_call_with_init_args_simplified_syntax(self): - """Test creation of new instances with init args injections. + def test_call_with_init_positional_args(self): + """Test creation of new instances with init positional args. New simplified syntax. """ - provider = di.Factory(self.Example, - init_arg1='i1', - init_arg2='i2') + provider = di.Factory(self.Example, 'i1', 'i2') instance1 = provider() instance2 = provider() @@ -203,10 +201,53 @@ class FactoryTests(unittest.TestCase): self.assertIsInstance(instance1, self.Example) self.assertIsInstance(instance2, self.Example) - def test_call_with_init_args_old_syntax(self): - """Test creation of new instances with init args injections.""" + def test_call_with_init_keyword_args(self): + """Test creation of new instances with init keyword args. + + New simplified syntax. + """ + provider = di.Factory(self.Example, init_arg1='i1', init_arg2='i2') + + instance1 = provider() + instance2 = provider() + + self.assertEqual(instance1.init_arg1, 'i1') + self.assertEqual(instance1.init_arg2, 'i2') + + self.assertEqual(instance2.init_arg1, 'i1') + self.assertEqual(instance2.init_arg2, 'i2') + + self.assertIsNot(instance1, instance2) + self.assertIsInstance(instance1, self.Example) + self.assertIsInstance(instance2, self.Example) + + def test_call_with_init_positional_and_keyword_args(self): + """Test creation of new instances with init positional and keyword args. + + Simplified syntax of positional and keyword arg injections. + """ + provider = di.Factory(self.Example,'i1', init_arg2='i2') + + instance1 = provider() + instance2 = provider() + + self.assertEqual(instance1.init_arg1, 'i1') + self.assertEqual(instance1.init_arg2, 'i2') + + self.assertEqual(instance2.init_arg1, 'i1') + self.assertEqual(instance2.init_arg2, 'i2') + + self.assertIsNot(instance1, instance2) + self.assertIsInstance(instance1, self.Example) + self.assertIsInstance(instance2, self.Example) + + def test_call_with_init_positional_and_keyword_args_extended_syntax(self): + """Test creation of new instances with init positional and keyword args. + + Extended syntax of positional and keyword arg injections. + """ provider = di.Factory(self.Example, - di.KwArg('init_arg1', 'i1'), + di.Arg('i1'), di.KwArg('init_arg2', 'i2')) instance1 = provider() @@ -300,7 +341,7 @@ class FactoryTests(unittest.TestCase): def test_injections(self): """Test getting a full list of injections using Factory.injections.""" provider = di.Factory(self.Example, - di.KwArg('init_arg1', 1), + di.Arg(1), di.KwArg('init_arg2', 2), di.Attribute('attribute1', 3), di.Attribute('attribute2', 4), diff --git a/tests/test_utils.py b/tests/test_utils.py index 61705ff1..275989f0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -68,10 +68,11 @@ class IsInjectionTests(unittest.TestCase): def test_with_instance(self): """Test with instance.""" - self.assertTrue(di.is_injection(di.Injection('name', 'value'))) + self.assertTrue(di.is_injection(di.Injection('value'))) def test_with_subclass_instances(self): """Test with subclass instances.""" + self.assertTrue(di.is_injection(di.Arg('value'))) self.assertTrue(di.is_injection(di.KwArg('name', 'value'))) self.assertTrue(di.is_injection(di.Attribute('name', 'value'))) self.assertTrue(di.is_injection(di.Method('name', 'value'))) @@ -94,7 +95,7 @@ class EnsureIsInjectionTests(unittest.TestCase): def test_with_instance(self): """Test with instance.""" - injection = di.Injection('name', 'value') + injection = di.Injection('value') self.assertIs(di.ensure_is_injection(injection), injection) def test_with_class(self): @@ -110,6 +111,30 @@ class EnsureIsInjectionTests(unittest.TestCase): self.assertRaises(di.Error, di.ensure_is_injection, object()) +class IsArgInjectionTests(unittest.TestCase): + """`is_arg_injection()` test cases.""" + + def test_with_instance(self): + """Test with instance.""" + self.assertTrue(di.is_arg_injection(di.Arg('value'))) + + def test_with_class(self): + """Test with class.""" + self.assertFalse(di.is_arg_injection(di.Arg)) + + def test_with_parent_class(self): + """Test with parent class.""" + self.assertFalse(di.is_arg_injection(di.Injection)) + + def test_with_string(self): + """Test with string.""" + self.assertFalse(di.is_arg_injection('some_string')) + + def test_with_object(self): + """Test with object.""" + self.assertFalse(di.is_arg_injection(object())) + + class IsKwArgInjectionTests(unittest.TestCase): """`is_kwarg_injection()` test cases."""