Add DelegatedCallable, DelegatedFactory & DelegatedSingleton providers

This commit is contained in:
Roman Mogilatov 2015-12-28 17:25:25 +02:00
parent 0d00e2b0f5
commit 319128260a
13 changed files with 476 additions and 133 deletions

View File

@ -8,15 +8,18 @@ from .catalogs import override
from .providers import Provider
from .providers import Delegate
from .providers import Callable
from .providers import DelegatedCallable
from .providers import Factory
from .providers import DelegatedFactory
from .providers import Singleton
from .providers import DelegatedSingleton
from .providers import ExternalDependency
from .providers import StaticProvider
from .providers import Class
from .providers import Object
from .providers import Function
from .providers import Value
from .providers import Callable
from .providers import Config
from .injections import Injection
@ -28,6 +31,7 @@ from .injections import inject
from .utils import is_provider
from .utils import ensure_is_provider
from .utils import is_delegated_provider
from .utils import is_injection
from .utils import ensure_is_injection
from .utils import is_arg_injection
@ -65,15 +69,18 @@ __all__ = (
# Providers
'Provider',
'Delegate',
'Callable',
'DelegatedCallable',
'Factory',
'DelegatedFactory',
'Singleton',
'DelegatedSingleton',
'ExternalDependency',
'StaticProvider',
'Class',
'Object',
'Function',
'Value',
'Callable',
'Config',
# Injections
@ -87,6 +94,7 @@ __all__ = (
# Utils
'is_provider',
'ensure_is_provider',
'is_delegated_provider',
'is_injection',
'ensure_is_injection',
'is_arg_injection',

View File

@ -6,6 +6,7 @@ import itertools
import six
from .utils import is_provider
from .utils import is_delegated_provider
from .utils import is_injection
from .utils import is_arg_injection
from .utils import is_kwarg_injection
@ -32,15 +33,17 @@ class Injection(object):
:type: object | :py:class:`dependency_injector.providers.Provider`
.. py:attribute:: injectable_is_provider
.. py:attribute:: call_injectable
Flag that is set to ``True`` if injectable value is provider.
Flag that is set to ``True`` if it is needed to call injectable.
Injectable needs to be called if it is not delegated provider.
:type: bool
"""
__IS_INJECTION__ = True
__slots__ = ('injectable', 'injectable_is_provider')
__slots__ = ('injectable', 'call_injectable')
def __init__(self, injectable):
"""Initializer.
@ -51,20 +54,21 @@ class Injection(object):
:py:class:`dependency_injector.providers.Provider`
"""
self.injectable = injectable
self.injectable_is_provider = is_provider(injectable)
self.call_injectable = (is_provider(injectable) and
not is_delegated_provider(injectable))
super(Injection, self).__init__()
@property
def value(self):
"""Read-only property that represents injectable value.
Injectable values are provided "as is", except of providers
(subclasses of :py:class:`dependency_injector.providers.Provider`).
Providers will be called every time, when injection needs to be done.
Injectable values and delegated providers are provided "as is".
Other providers will be called every time, when injection needs to
be done.
:rtype: object
"""
if self.injectable_is_provider:
if self.call_injectable:
return self.injectable()
return self.injectable

View File

@ -319,6 +319,34 @@ class Callable(Provider):
__repr__ = __str__
class DelegatedCallable(Callable):
""":py:class:`DelegatedCallable` is a delegated :py:class:`Callable`.
:py:class:`DelegatedCallable` is a :py:class:`Callable`, that is injected
"as is".
.. py:attribute:: provides
Provided callable.
:type: callable
.. py:attribute:: args
Tuple of positional argument injections.
:type: tuple[:py:class:`dependency_injector.injections.Arg`]
.. py:attribute:: kwargs
Tuple of keyword argument injections.
:type: tuple[:py:class:`dependency_injector.injections.KwArg`]
"""
__IS_DELEGATED__ = True
class Factory(Callable):
""":py:class:`Factory` provider creates new instance on every call.
@ -448,6 +476,54 @@ class Factory(Callable):
return instance
class DelegatedFactory(Factory):
""":py:class:`DelegatedFactory` is a delegated :py:class:`Factory`.
:py:class:`DelegatedFactory` is a :py:class:`Factory`, that is injected
"as is".
.. py:attribute:: provided_type
If provided type is defined, :py:class:`Factory` checks that
:py:attr:`Factory.provides` is subclass of
:py:attr:`Factory.provided_type`.
:type: type | None
.. py:attribute:: provides
Class or other callable that provides object.
:type: type | callable
.. py:attribute:: args
Tuple of positional argument injections.
:type: tuple[:py:class:`dependency_injector.injections.Arg`]
.. py:attribute:: kwargs
Tuple of keyword argument injections.
:type: tuple[:py:class:`dependency_injector.injections.KwArg`]
.. py:attribute:: attributes
Tuple of attribute injections.
:type: tuple[:py:class:`dependency_injector.injections.Attribute`]
.. py:attribute:: methods
Tuple of method injections.
:type: tuple[:py:class:`dependency_injector.injections.Method`]
"""
__IS_DELEGATED__ = True
class Singleton(Factory):
""":py:class:`Singleton` provider returns same instance on every call.
@ -556,6 +632,60 @@ class Singleton(Factory):
return self.instance
class DelegatedSingleton(Singleton):
""":py:class:`DelegatedSingleton` is a delegated :py:class:`Singleton`.
:py:class:`DelegatedSingleton` is a :py:class:`Singleton`, that is injected
"as is".
.. py:attribute:: provided_type
If provided type is defined, :py:class:`Factory` checks that
:py:attr:`Factory.provides` is subclass of
:py:attr:`Factory.provided_type`.
:type: type | None
.. py:attribute:: instance
Read-only reference to singleton's instance.
:type: object
.. py:attribute:: provides
Class or other callable that provides object.
:type: type | callable
.. py:attribute:: args
Tuple of positional argument injections.
:type: tuple[:py:class:`dependency_injector.injections.Arg`]
.. py:attribute:: kwargs
Tuple of keyword argument injections.
:type: tuple[:py:class:`dependency_injector.injections.KwArg`]
.. py:attribute:: attributes
Tuple of attribute injections.
:type: tuple[:py:class:`dependency_injector.injections.Attribute`]
.. py:attribute:: methods
Tuple of method injections.
:type: tuple[:py:class:`dependency_injector.injections.Method`]
"""
__IS_DELEGATED__ = True
@six.python_2_unicode_compatible
class ExternalDependency(Provider):
""":py:class:`ExternalDependency` provider describes dependency interface.

View File

@ -44,6 +44,19 @@ def ensure_is_provider(instance):
return instance
def is_delegated_provider(instance):
"""Check if instance is delegated provider instance.
:param instance: Instance to be checked.
:type instance: object
:rtype: bool
"""
return (is_provider(instance) and
hasattr(instance, '__IS_DELEGATED__') and
getattr(instance, '__IS_DELEGATED__') is True)
def is_injection(instance):
"""Check if instance is injection instance.

View File

@ -9,7 +9,9 @@ follows `Semantic versioning`_
Development version
-------------------
- No features.
- Add ``DelegatedCallable`` provider.
- Add ``DelegatedFactory`` provider.
- Add ``DelegatedSingleton`` provider.
1.12.0
------

View File

@ -16,12 +16,17 @@ arguments that are used as decorated callable injections. Every time, when
would be passed as an callable arguments.
Such behaviour is very similar to the standard Python ``functools.partial``
object, except of one thing: all injectable values are provided
*"as is"*, except of providers (subclasses of :py:class:`Provider`). Providers
will be called every time, when injection needs to be done. For example,
if injectable value of injection is a :py:class:`Factory`, it will provide
new one instance (as a result of its call) every time, when injection needs
to be done.
object with several more things:
+ All providers (instances of :py:class:`Provider`) are called every time
when injection needs to be done.
+ Providers could be injected "as is" (delegated), if it is defined obviously.
Check out `Callable providers delegation`_.
+ All other injectable values are provided *"as is"*
For example, if injectable value of injection is a :py:class:`Factory`, it
will provide new one instance (as a result of its call) every time, when
injection needs to be done.
:py:class:`Callable` behaviour with context positional and keyword arguments
is very like a standard Python ``functools.partial``:
@ -47,6 +52,8 @@ injections:
.. literalinclude:: ../../examples/providers/callable_kwargs.py
:language: python
.. _callable_delegation:
Callable providers delegation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -62,3 +69,12 @@ Example:
.. literalinclude:: ../../examples/providers/callable_delegation.py
:language: python
Alternative way of doing :py:class:`Callable` delegation is an usage of
:py:class:`DelegatedCallable`. :py:class:`DelegatedCallable` is a
:py:class:`Callable` that is always injected "as is".
Example:
.. literalinclude:: ../../examples/providers/delegated_callable.py
:language: python

View File

@ -24,12 +24,17 @@ that are used as ``__init__()`` injections. Every time, when
argument injections would be passed as an instance's arguments.
Such behaviour is very similar to the standard Python ``functools.partial``
object, except of one thing: all injectable values are provided
*"as is"*, except of providers (subclasses of :py:class:`Provider`). Providers
will be called every time, when injection needs to be done. For example,
if injectable value of injection is a :py:class:`Factory`, it will provide
new one instance (as a result of its call) every time, when injection needs
to be done.
object with several more things:
+ All providers (instances of :py:class:`Provider`) are called every time
when injection needs to be done.
+ Providers could be injected "as is" (delegated), if it is defined obviously.
Check out `Factory providers delegation`_.
+ All other injectable values are provided *"as is"*
For example, if injectable value of injection is a :py:class:`Factory`, it
will provide new one instance (as a result of its call) every time, when
injection needs to be done.
Example below is a little bit more complicated. It shows how to create
:py:class:`Factory` of particular class with ``__init__()`` argument
@ -175,6 +180,15 @@ Example:
.. literalinclude:: ../../examples/providers/factory_delegation.py
:language: python
Alternative way of doing :py:class:`Factory` delegation is an usage of
:py:class:`DelegatedFactory`. :py:class:`DelegatedFactory` is a
:py:class:`Factory` that is always injected "as is".
Example:
.. literalinclude:: ../../examples/providers/delegated_factory.py
:language: python
Factory providers specialization
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -24,8 +24,8 @@ Providers module API docs - :py:mod:`dependency_injector.providers`
factory
singleton
static
callable
external_dependency
static
overriding
custom

View File

@ -36,9 +36,10 @@ provider.
during the first call). Be aware that such behaviour was made with opened
eyes and is not a bug.
By the way, in such case, :py:class:`Delegate` provider can be useful. It
makes possible to inject providers *as is*. Please check out full example
in *Providers delegation* section.
By the way, in such case, :py:class:`Delegate` or
:py:class:`DelegatedSingleton` provider can be useful
. It makes possible to inject providers *as is*. Please check out
`Singleton providers delegation`_ section.
Singleton providers resetting
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -70,6 +71,15 @@ Example:
.. literalinclude:: ../../examples/providers/singleton_delegation.py
:language: python
Alternative way of doing :py:class:`Singleton` delegation is an usage of
:py:class:`DelegatedSingleton`. :py:class:`DelegatedSingleton` is a
:py:class:`Singleton` that is always injected "as is".
Example:
.. literalinclude:: ../../examples/providers/delegated_singleton.py
:language: python
Singleton providers specialization
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -0,0 +1,22 @@
"""`DelegatedCallable` providers example."""
from dependency_injector import providers
def command1(config):
"""Some example command."""
return config['some_value'] * 5
def command2(command1):
"""Some example command."""
return command1() / 2
# Creating callable providers for commands:
command1_provider = providers.DelegatedCallable(command1,
config={'some_value': 4})
command2_provider = providers.DelegatedCallable(command2,
command1=command1_provider)
# Making some asserts:
assert command2_provider() == 10

View File

@ -0,0 +1,46 @@
"""`DelegatedFactory` providers example."""
from dependency_injector import providers
class User(object):
"""Example class User."""
def __init__(self, photos_factory):
"""Initializer.
:param photos_factory: providers.Factory -> Photo
"""
self.photos_factory = photos_factory
self._main_photo = None
super(User, self).__init__()
@property
def main_photo(self):
"""Return user's main photo."""
if not self._main_photo:
self._main_photo = self.photos_factory()
return self._main_photo
class Photo(object):
"""Example class Photo."""
# User and Photo factories:
photos_factory = providers.DelegatedFactory(Photo)
users_factory = providers.DelegatedFactory(User,
photos_factory=photos_factory)
# Creating several User objects:
user1 = users_factory()
user2 = users_factory()
# Making some asserts:
assert isinstance(user1, User)
assert isinstance(user1.main_photo, Photo)
assert isinstance(user2, User)
assert isinstance(user2.main_photo, Photo)
assert user1 is not user2
assert user1.main_photo is not user2.main_photo

View File

@ -0,0 +1,18 @@
"""`DelegatedSingleton` providers example."""
from dependency_injector import providers
# Some delegated singleton provider:
singleton_provider = providers.DelegatedSingleton(object)
registry = providers.DelegatedSingleton(dict,
object1=singleton_provider,
object2=singleton_provider)
# Getting several references to singleton object:
registry = registry()
singleton_object1 = registry['object1']()
singleton_object2 = registry['object2']()
# Making some asserts:
assert singleton_object1 is singleton_object2

View File

@ -165,6 +165,136 @@ class DelegateTests(unittest.TestCase):
hex(id(self.delegate))))
class CallableTests(unittest.TestCase):
"""Callable test cases."""
def example(self, arg1, arg2, arg3, arg4):
"""Example callback."""
return arg1, arg2, arg3, arg4
def test_init_with_callable(self):
"""Test creation of provider with a callable."""
self.assertTrue(providers.Callable(self.example))
def test_init_with_not_callable(self):
"""Test creation of provider with not a callable."""
self.assertRaises(errors.Error, providers.Callable, 123)
def test_call(self):
"""Test call."""
provider = providers.Callable(lambda: True)
self.assertTrue(provider())
def test_call_with_positional_args(self):
"""Test call with positional args.
New simplified syntax.
"""
provider = providers.Callable(self.example, 1, 2, 3, 4)
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_call_with_keyword_args(self):
"""Test call with keyword args.
New simplified syntax.
"""
provider = providers.Callable(self.example,
arg1=1,
arg2=2,
arg3=3,
arg4=4)
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_call_with_positional_and_keyword_args(self):
"""Test call with positional and keyword args.
Simplified syntax of positional and keyword arg injections.
"""
provider = providers.Callable(self.example, 1, 2, arg3=3, arg4=4)
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_call_with_positional_and_keyword_args_extended_syntax(self):
"""Test call with positional and keyword args.
Extended syntax of positional and keyword arg injections.
"""
provider = providers.Callable(self.example,
injections.Arg(1),
injections.Arg(2),
injections.KwArg('arg3', 3),
injections.KwArg('arg4', 4))
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_call_with_context_args(self):
"""Test call with context args."""
provider = providers.Callable(self.example, 1, 2)
self.assertTupleEqual(provider(3, 4), (1, 2, 3, 4))
def test_call_with_context_kwargs(self):
"""Test call with context kwargs."""
provider = providers.Callable(self.example,
injections.KwArg('arg1', 1))
self.assertTupleEqual(provider(arg2=2, arg3=3, arg4=4), (1, 2, 3, 4))
def test_call_with_context_args_and_kwargs(self):
"""Test call with context args and kwargs."""
provider = providers.Callable(self.example, 1)
self.assertTupleEqual(provider(2, arg3=3, arg4=4), (1, 2, 3, 4))
def test_call_overridden(self):
"""Test creation of new instances on overridden provider."""
provider = providers.Callable(self.example)
provider.override(providers.Value((4, 3, 2, 1)))
provider.override(providers.Value((1, 2, 3, 4)))
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_injections(self):
"""Test getting a full list of injections using injections property."""
provider = providers.Callable(self.example, 1, 2, arg3=3, arg4=4)
self.assertEquals(len(provider.injections), 4)
def test_repr(self):
"""Test representation of provider."""
provider = providers.Callable(self.example,
injections.KwArg(
'arg1',
providers.Factory(dict)),
injections.KwArg(
'arg2',
providers.Factory(list)),
injections.KwArg(
'arg3',
providers.Factory(set)),
injections.KwArg(
'arg4',
providers.Factory(tuple)))
self.assertEqual(repr(provider),
'<dependency_injector.providers.'
'Callable({0}) at {1}>'.format(
repr(self.example),
hex(id(provider))))
class DelegatedCallableTests(unittest.TestCase):
"""DelegatedCallable test cases."""
def test_inheritance(self):
"""Test inheritance."""
self.assertIsInstance(providers.DelegatedCallable(len),
providers.Callable)
def test_is_provider(self):
"""Test is_provider."""
self.assertTrue(utils.is_provider(providers.DelegatedCallable(len)))
def test_is_delegated_provider(self):
"""Test is_delegated_provider."""
self.assertTrue(utils.is_delegated_provider(
providers.DelegatedCallable(len)))
self.assertFalse(utils.is_delegated_provider(providers.Callable(len)))
class FactoryTests(unittest.TestCase):
"""Factory test cases."""
@ -419,6 +549,26 @@ class FactoryTests(unittest.TestCase):
hex(id(provider))))
class DelegatedFactoryTests(unittest.TestCase):
"""DelegatedFactory test cases."""
def test_inheritance(self):
"""Test inheritance."""
self.assertIsInstance(providers.DelegatedFactory(object),
providers.Factory)
def test_is_provider(self):
"""Test is_provider."""
self.assertTrue(utils.is_provider(providers.DelegatedFactory(object)))
def test_is_delegated_provider(self):
"""Test is_delegated_provider."""
self.assertTrue(utils.is_delegated_provider(
providers.DelegatedFactory(object)))
self.assertFalse(utils.is_delegated_provider(
providers.Factory(object)))
class SingletonTests(unittest.TestCase):
"""Singleton test cases."""
@ -719,115 +869,25 @@ class SingletonTests(unittest.TestCase):
hex(id(provider))))
class CallableTests(unittest.TestCase):
"""Callable test cases."""
class DelegatedSingletonTests(unittest.TestCase):
"""DelegatedSingleton test cases."""
def example(self, arg1, arg2, arg3, arg4):
"""Example callback."""
return arg1, arg2, arg3, arg4
def test_inheritance(self):
"""Test inheritance."""
self.assertIsInstance(providers.DelegatedSingleton(object),
providers.Singleton)
def test_init_with_callable(self):
"""Test creation of provider with a callable."""
self.assertTrue(providers.Callable(self.example))
def test_is_provider(self):
"""Test is_provider."""
self.assertTrue(utils.is_provider(
providers.DelegatedSingleton(object)))
def test_init_with_not_callable(self):
"""Test creation of provider with not a callable."""
self.assertRaises(errors.Error, providers.Callable, 123)
def test_call(self):
"""Test call."""
provider = providers.Callable(lambda: True)
self.assertTrue(provider())
def test_call_with_positional_args(self):
"""Test call with positional args.
New simplified syntax.
"""
provider = providers.Callable(self.example, 1, 2, 3, 4)
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_call_with_keyword_args(self):
"""Test call with keyword args.
New simplified syntax.
"""
provider = providers.Callable(self.example,
arg1=1,
arg2=2,
arg3=3,
arg4=4)
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_call_with_positional_and_keyword_args(self):
"""Test call with positional and keyword args.
Simplified syntax of positional and keyword arg injections.
"""
provider = providers.Callable(self.example, 1, 2, arg3=3, arg4=4)
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_call_with_positional_and_keyword_args_extended_syntax(self):
"""Test call with positional and keyword args.
Extended syntax of positional and keyword arg injections.
"""
provider = providers.Callable(self.example,
injections.Arg(1),
injections.Arg(2),
injections.KwArg('arg3', 3),
injections.KwArg('arg4', 4))
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_call_with_context_args(self):
"""Test call with context args."""
provider = providers.Callable(self.example, 1, 2)
self.assertTupleEqual(provider(3, 4), (1, 2, 3, 4))
def test_call_with_context_kwargs(self):
"""Test call with context kwargs."""
provider = providers.Callable(self.example,
injections.KwArg('arg1', 1))
self.assertTupleEqual(provider(arg2=2, arg3=3, arg4=4), (1, 2, 3, 4))
def test_call_with_context_args_and_kwargs(self):
"""Test call with context args and kwargs."""
provider = providers.Callable(self.example, 1)
self.assertTupleEqual(provider(2, arg3=3, arg4=4), (1, 2, 3, 4))
def test_call_overridden(self):
"""Test creation of new instances on overridden provider."""
provider = providers.Callable(self.example)
provider.override(providers.Value((4, 3, 2, 1)))
provider.override(providers.Value((1, 2, 3, 4)))
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_injections(self):
"""Test getting a full list of injections using injections property."""
provider = providers.Callable(self.example, 1, 2, arg3=3, arg4=4)
self.assertEquals(len(provider.injections), 4)
def test_repr(self):
"""Test representation of provider."""
provider = providers.Callable(self.example,
injections.KwArg(
'arg1',
providers.Factory(dict)),
injections.KwArg(
'arg2',
providers.Factory(list)),
injections.KwArg(
'arg3',
providers.Factory(set)),
injections.KwArg(
'arg4',
providers.Factory(tuple)))
self.assertEqual(repr(provider),
'<dependency_injector.providers.'
'Callable({0}) at {1}>'.format(
repr(self.example),
hex(id(provider))))
def test_is_delegated_provider(self):
"""Test is_delegated_provider."""
self.assertTrue(utils.is_delegated_provider(
providers.DelegatedSingleton(object)))
self.assertFalse(utils.is_delegated_provider(
providers.Singleton(object)))
class ExternalDependencyTests(unittest.TestCase):