Provided attributes (#282)

* Add sketch

* Cythonize MethodCaller

* Cythonize ItemGetter, AttributeGetter & ProvidedInstance providers

* Add docblock for .provided attribute

* Refactor repr methods

* Add .provided attribute to the Dependency provider

* Add tests for the .provided attribute to the majority of the providers

* Add docblock for the ProvidedInstance provider

* Add docblocks for the rest of the providers

* Add example of the provided instance usage

* Add tests for provided instance* providers

* Add complex provided instance example

* Update example provided_instance.py

* Add docs
This commit is contained in:
Roman Mogylatov 2020-08-20 21:52:12 -04:00 committed by GitHub
parent 50a9dda192
commit cf862fe8b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 13616 additions and 3901 deletions

View File

@ -29,4 +29,5 @@ Providers package API docs - :py:mod:`dependency_injector.providers`
selector selector
dependency dependency
overriding overriding
provided_instance
custom custom

View File

@ -0,0 +1,64 @@
Injecting attributes, items, or call methods of the provided instance
---------------------------------------------------------------------
.. currentmodule:: dependency_injector.providers
In this section you will know how to inject provided instance attribute or item into the other
provider.
It also describes how to call a method of the provided instance and use the result of
this call as an injection value.
.. literalinclude:: ../../examples/providers/provided_instance.py
:language: python
:emphasize-lines: 26-32
:lines: 3-
To use the feature you should use the ``.provided`` attribute of the injected provider. This
attribute helps to specify what happens with the provided instance. You can retrieve an injection
value from:
- an attribute of the provided instance
- an item of the provided instance
- a call of the provided instance method
When you use the call of the provided instance method you can specify the injections into this
method like you do with any other provider.
You can do nested constructions:
.. literalinclude:: ../../examples/providers/provided_instance_complex.py
:language: python
:emphasize-lines: 24-30
:lines: 3-
Attribute ``.provided`` is available for the providers that return instances. Providers that
have ``.provided`` attribute:
- :py:class:`Callable` and its subclasses
- :py:class:`Factory` and its subclasses
- :py:class:`Singleton` and its subclasses
- :py:class:`Object`
- :py:class:`List`
- :py:class:`Selector`
- :py:class:`Dependency`
Special providers like :py:class:`Configuration` or :py:class:`Delegate` do not have the
``.provided`` attribute.
Provider subclasses
-------------------
When you create a new provider subclass and want to implement the ``.provided`` attribute, you
should use the :py:class:`ProvidedInstance` provider.
.. code-block:: python
@property
def provided(self):
"""Return :py:class:`ProvidedInstance` provider."""
return ProvidedInstance(self)
In all other cases you should not use :py:class:`ProvidedInstance`, :py:class:`AttributeGetter`,
:py:class:`ItemGetter`, or :py:class:`MethodCaller` providers directly. Use the ``.provided``
attribute of the injected provider instead.

View File

@ -0,0 +1,38 @@
"""Example of the injecting of provided instance attributes and items."""
from dependency_injector import providers
class Service:
def __init__(self):
self.value = 'foo'
self.values = [self.value]
def get_value(self):
return self.value
def __getitem__(self, item):
return self.values[item]
class Client:
def __init__(self, value1, value2, value3, value4):
self.value1 = value1
self.value2 = value2
self.value3 = value3
self.value4 = value4
service = providers.Singleton(Service)
client_factory = providers.Factory(
Client,
value1=service.provided[0],
value2=service.provided.value,
value3=service.provided.values[0],
value4=service.provided.get_value.call(),
)
if __name__ == '__main__':
client = client_factory()
assert client.value1 == client.value2 == client.value3 == 'foo'

View File

@ -0,0 +1,41 @@
"""Complex example of the injecting of provided instance attributes and items."""
from dependency_injector import providers
class Service:
def __init__(self, value):
self.value = value
def get_value(self):
return self.value
service = providers.Singleton(Service, value=42)
dependency = providers.Object(
{
'foo': {
'bar': 10,
'baz': lambda arg: {'arg': arg}
},
},
)
demo_list = providers.List(
dependency.provided['foo']['bar'],
dependency.provided['foo']['baz'].call(22)['arg'],
dependency.provided['foo']['baz'].call(service)['arg'],
dependency.provided['foo']['baz'].call(service)['arg'].value,
dependency.provided['foo']['baz'].call(service)['arg'].get_value.call(),
)
if __name__ == '__main__':
assert demo_list() == [
10,
22,
service(),
42,
42,
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -197,6 +197,37 @@ cdef class Selector(Provider):
cpdef object _provide(self, tuple args, dict kwargs) cpdef object _provide(self, tuple args, dict kwargs)
# Provided instance
cdef class ProvidedInstance(Provider):
cdef Provider __provider
cpdef object _provide(self, tuple args, dict kwargs)
cdef class AttributeGetter(Provider):
cdef Provider __provider
cdef object __attribute
cpdef object _provide(self, tuple args, dict kwargs)
cdef class ItemGetter(Provider):
cdef Provider __provider
cdef object __item
cpdef object _provide(self, tuple args, dict kwargs)
cdef class MethodCaller(Provider):
cdef Provider __provider
cdef tuple __args
cdef int __args_len
cdef tuple __kwargs
cdef int __kwargs_len
cpdef object _provide(self, tuple args, dict kwargs)
# Injections # Injections
cdef class Injection(object): cdef class Injection(object):
@ -352,18 +383,42 @@ cdef inline object __inject_attributes(
__get_value(attr_injection)) __get_value(attr_injection))
cdef inline object __callable_call(Callable self, tuple args, dict kwargs): cdef inline object __call(
object call,
tuple context_args,
tuple injection_args,
int injection_args_len,
dict kwargs,
tuple injection_kwargs,
int injection_kwargs_len,
):
cdef tuple positional_args cdef tuple positional_args
cdef dict keyword_args cdef dict keyword_args
positional_args = __provide_positional_args(args, positional_args = __provide_positional_args(
self.__args, context_args,
self.__args_len) injection_args,
keyword_args = __provide_keyword_args(kwargs, injection_args_len,
self.__kwargs, )
self.__kwargs_len) keyword_args = __provide_keyword_args(
kwargs,
injection_kwargs,
injection_kwargs_len,
)
return self.__provides(*positional_args, **keyword_args) return call(*positional_args, **keyword_args)
cdef inline object __callable_call(Callable self, tuple args, dict kwargs):
return __call(
self.__provides,
args,
self.__args,
self.__args_len,
kwargs,
self.__kwargs,
self.__kwargs_len,
)
cdef inline object __factory_call(Factory self, tuple args, dict kwargs): cdef inline object __factory_call(Factory self, tuple args, dict kwargs):

View File

@ -324,6 +324,11 @@ cdef class Object(Provider):
""" """
return self.__str__() return self.__str__()
@property
def provided(self):
"""Return :py:class:`ProvidedInstance` provider."""
return ProvidedInstance(self)
cpdef object _provide(self, tuple args, dict kwargs): cpdef object _provide(self, tuple args, dict kwargs):
"""Return provided instance. """Return provided instance.
@ -479,6 +484,11 @@ cdef class Dependency(Provider):
""" """
return self.__str__() return self.__str__()
@property
def provided(self):
"""Return :py:class:`ProvidedInstance` provider."""
return ProvidedInstance(self)
@property @property
def instance_of(self): def instance_of(self):
"""Return class of required dependency.""" """Return class of required dependency."""
@ -769,6 +779,11 @@ cdef class Callable(Provider):
"""Return wrapped callable.""" """Return wrapped callable."""
return self.__provides return self.__provides
@property
def provided(self):
"""Return :py:class:`ProvidedInstance` provider."""
return ProvidedInstance(self)
@property @property
def args(self): def args(self):
"""Return positional argument injections.""" """Return positional argument injections."""
@ -1599,6 +1614,11 @@ cdef class Factory(Provider):
"""Return provided type.""" """Return provided type."""
return self.__instantiator.provides return self.__instantiator.provides
@property
def provided(self):
"""Return :py:class:`ProvidedInstance` provider."""
return ProvidedInstance(self)
@property @property
def args(self): def args(self):
"""Return positional argument injections.""" """Return positional argument injections."""
@ -1932,6 +1952,11 @@ cdef class BaseSingleton(Provider):
"""Return provided type.""" """Return provided type."""
return self.__instantiator.cls return self.__instantiator.cls
@property
def provided(self):
"""Return :py:class:`ProvidedInstance` provider."""
return ProvidedInstance(self)
@property @property
def args(self): def args(self):
"""Return positional argument injections.""" """Return positional argument injections."""
@ -2360,6 +2385,11 @@ cdef class List(Provider):
""" """
return represent_provider(provider=self, provides=list(self.args)) return represent_provider(provider=self, provides=list(self.args))
@property
def provided(self):
"""Return :py:class:`ProvidedInstance` provider."""
return ProvidedInstance(self)
@property @property
def args(self): def args(self):
"""Return positional argument injections.""" """Return positional argument injections."""
@ -2537,6 +2567,11 @@ cdef class Selector(Provider):
address=hex(id(self)), address=hex(id(self)),
) )
@property
def provided(self):
"""Return :py:class:`ProvidedInstance` provider."""
return ProvidedInstance(self)
@property @property
def providers(self): def providers(self):
"""Return providers.""" """Return providers."""
@ -2555,6 +2590,207 @@ cdef class Selector(Provider):
return self.__providers[selector_value](*args, **kwargs) return self.__providers[selector_value](*args, **kwargs)
cdef class ProvidedInstance(Provider):
"""Provider that helps to inject attributes and items of the injected instance.
You can use it like that:
.. code-block:: python
service = providers.Singleton(Service)
client_factory = providers.Factory(
Client,
value1=service.provided[0],
value2=service.provided.value,
value3=service.provided.values[0],
value4=service.provided.get_value.call(),
)
You should not create this provider directly. Get it from the ``.provided`` attribute of the
injected provider. This attribute returns the :py:class:`ProvidedInstance` for that provider.
Providers that have ``.provided`` attribute:
- :py:class:`Callable` and its subclasses
- :py:class:`Factory` and its subclasses
- :py:class:`Singleton` and its subclasses
- :py:class:`Object`
- :py:class:`List`
- :py:class:`Selector`
- :py:class:`Dependency`
"""
def __init__(self, provider):
self.__provider = provider
super().__init__()
def __repr__(self):
return f'{self.__class__.__name__}(\'{self.__provider}\')'
def __deepcopy__(self, memo=None):
cdef ProvidedInstance copied
copied = memo.get(id(self))
if copied is not None:
return copied
return self.__class__(
deepcopy(self.__provider, memo),
)
def __getattr__(self, item):
return AttributeGetter(self, item)
def __getitem__(self, item):
return ItemGetter(self, item)
def call(self, *args, **kwargs):
return MethodCaller(self, *args, **kwargs)
cpdef object _provide(self, tuple args, dict kwargs):
return self.__provider(*args, **kwargs)
cdef class AttributeGetter(Provider):
"""Provider that returns the attribute of the injected instance.
You should not create this provider directly. See :py:class:`ProvidedInstance` instead.
"""
def __init__(self, provider, attribute):
self.__provider = provider
self.__attribute = attribute
super().__init__()
def __repr__(self):
return f'{self.__class__.__name__}(\'{self.__attribute}\')'
def __deepcopy__(self, memo=None):
cdef AttributeGetter copied
copied = memo.get(id(self))
if copied is not None:
return copied
return self.__class__(
deepcopy(self.__provider, memo),
self.__attribute,
)
def __getattr__(self, item):
return AttributeGetter(self, item)
def __getitem__(self, item):
return ItemGetter(self, item)
def call(self, *args, **kwargs):
return MethodCaller(self, *args, **kwargs)
cpdef object _provide(self, tuple args, dict kwargs):
provided = self.__provider(*args, **kwargs)
return getattr(provided, self.__attribute)
cdef class ItemGetter(Provider):
"""Provider that returns the item of the injected instance.
You should not create this provider directly. See :py:class:`ProvidedInstance` instead.
"""
def __init__(self, Provider provider, object item):
self.__provider = provider
self.__item = item
super().__init__()
def __repr__(self):
return f'{self.__class__.__name__}(\'{self.__item}\')'
def __deepcopy__(self, memo=None):
cdef ItemGetter copied
copied = memo.get(id(self))
if copied is not None:
return copied
return self.__class__(
deepcopy(self.__provider, memo),
self.__item,
)
def __getattr__(self, item):
return AttributeGetter(self, item)
def __getitem__(self, item):
return ItemGetter(self, item)
def call(self, *args, **kwargs):
return MethodCaller(self, *args, **kwargs)
cpdef object _provide(self, tuple args, dict kwargs):
provided = self.__provider(*args, **kwargs)
return provided[self.__item]
cdef class MethodCaller(Provider):
"""Provider that calls the method of the injected instance.
You should not create this provider directly. See :py:class:`ProvidedInstance` instead.
"""
def __init__(self, provider, *args, **kwargs):
self.__provider = provider
self.__args = parse_positional_injections(args)
self.__args_len = len(self.__args)
self.__kwargs = parse_named_injections(kwargs)
self.__kwargs_len = len(self.__kwargs)
super().__init__()
def __repr__(self):
return f'{self.__class__.__name__}({self.__provider})'
def __deepcopy__(self, memo=None):
cdef MethodCaller copied
copied = memo.get(id(self))
if copied is not None:
return copied
copied = self.__class__(deepcopy(self.__provider, memo))
copied.__args = deepcopy(self.__args, memo)
copied.__args_len = self.__args_len
copied.__kwargs = deepcopy(self.__kwargs, memo)
copied.__kwargs_len = self.__kwargs_len
self._copy_overridings(copied, memo)
return copied
def __getattr__(self, item):
return AttributeGetter(self, item)
def __getitem__(self, item):
return ItemGetter(self, item)
def call(self, *args, **kwargs):
return MethodCaller(self, *args, **kwargs)
cpdef object _provide(self, tuple args, dict kwargs):
call = self.__provider()
return __call(
call,
args,
self.__args,
self.__args_len,
kwargs,
self.__kwargs,
self.__kwargs_len,
)
cdef class Injection(object): cdef class Injection(object):
"""Abstract injection class.""" """Abstract injection class."""

View File

@ -150,6 +150,10 @@ class ObjectProviderTests(unittest.TestCase):
def test_is_provider(self): def test_is_provider(self):
self.assertTrue(providers.is_provider(providers.Object(object()))) self.assertTrue(providers.is_provider(providers.Object(object())))
def test_provided_instance_provider(self):
provider = providers.Object(object())
self.assertIsInstance(provider.provided, providers.ProvidedInstance)
def test_call_object_provider(self): def test_call_object_provider(self):
obj = object() obj = object()
self.assertIs(providers.Object(obj)(), obj) self.assertIs(providers.Object(obj)(), obj)
@ -264,6 +268,9 @@ class DependencyTests(unittest.TestCase):
def test_is_provider(self): def test_is_provider(self):
self.assertTrue(providers.is_provider(self.provider)) self.assertTrue(providers.is_provider(self.provider))
def test_provided_instance_provider(self):
self.assertIsInstance(self.provider.provided, providers.ProvidedInstance)
def test_call_overridden(self): def test_call_overridden(self):
self.provider.provided_by(providers.Factory(list)) self.provider.provided_by(providers.Factory(list))
self.assertIsInstance(self.provider(), list) self.assertIsInstance(self.provider(), list)

View File

@ -22,6 +22,10 @@ class CallableTests(unittest.TestCase):
def test_init_with_not_callable(self): def test_init_with_not_callable(self):
self.assertRaises(errors.Error, providers.Callable, 123) self.assertRaises(errors.Error, providers.Callable, 123)
def test_provided_instance_provider(self):
provider = providers.Callable(_example)
self.assertIsInstance(provider.provided, providers.ProvidedInstance)
def test_call(self): def test_call(self):
provider = providers.Callable(lambda: True) provider = providers.Callable(lambda: True)
self.assertTrue(provider()) self.assertTrue(provider())

View File

@ -60,6 +60,10 @@ class FactoryTests(unittest.TestCase):
with self.assertRaises(errors.Error): with self.assertRaises(errors.Error):
ExampleProvider(list) ExampleProvider(list)
def test_provided_instance_provider(self):
provider = providers.Factory(Example)
self.assertIsInstance(provider.provided, providers.ProvidedInstance)
def test_call(self): def test_call(self):
provider = providers.Factory(Example) provider = providers.Factory(Example)

View File

@ -12,6 +12,10 @@ class ListTests(unittest.TestCase):
def test_is_provider(self): def test_is_provider(self):
self.assertTrue(providers.is_provider(providers.List())) self.assertTrue(providers.is_provider(providers.List()))
def test_provided_instance_provider(self):
provider = providers.List()
self.assertIsInstance(provider.provided, providers.ProvidedInstance)
def test_call_with_init_positional_args(self): def test_call_with_init_positional_args(self):
provider = providers.List('i1', 'i2') provider = providers.List('i1', 'i2')

View File

@ -0,0 +1,139 @@
"""Dependency injector provided instance provider unit tests."""
import unittest2 as unittest
from dependency_injector import containers, providers
class Service:
def __init__(self, value):
self.value = value
self.values = [self.value]
def get_value(self):
return self.value
def __getitem__(self, item):
return self.values[item]
class Client:
def __init__(self, value):
self.value = value
class Container(containers.DeclarativeContainer):
service = providers.Singleton(Service, value='foo')
client_attribute = providers.Factory(
Client,
value=service.provided.value,
)
client_item = providers.Factory(
Client,
value=service.provided[0],
)
client_attribute_item = providers.Factory(
Client,
value=service.provided.values[0],
)
client_method_call = providers.Factory(
Client,
value=service.provided.get_value.call(),
)
class ProvidedInstanceTests(unittest.TestCase):
def setUp(self):
self.container = Container()
def test_is_provider(self):
self.assertTrue(providers.is_provider(self.container.service.provided))
def test_attribute(self):
client = self.container.client_attribute()
self.assertEqual(client.value, 'foo')
def test_item(self):
client = self.container.client_item()
self.assertEqual(client.value, 'foo')
def test_attribute_item(self):
client = self.container.client_attribute_item()
self.assertEqual(client.value, 'foo')
def test_method_call(self):
client = self.container.client_method_call()
self.assertEqual(client.value, 'foo')
def test_call_overridden(self):
value = 'bar'
with self.container.service.override(Service(value)):
self.assertEqual(self.container.client_attribute().value, value)
self.assertEqual(self.container.client_item().value, value)
self.assertEqual(self.container.client_attribute_item().value, value)
self.assertEqual(self.container.client_method_call().value, value)
def test_repr_provided_instance(self):
provider = self.container.service.provided
self.assertEqual(
'ProvidedInstance(\'{0}\')'.format(repr(self.container.service)),
repr(provider),
)
def test_repr_attribute_getter(self):
provider = self.container.service.provided.value
self.assertEqual(
'AttributeGetter(\'value\')',
repr(provider),
)
def test_repr_item_getter(self):
provider = self.container.service.provided['test-test']
self.assertEqual(
'ItemGetter(\'test-test\')',
repr(provider),
)
class ProvidedInstancePuzzleTests(unittest.TestCase):
def test_puzzled(self):
service = providers.Singleton(Service, value='foo-bar')
dependency = providers.Object(
{
'a': {
'b': {
'c1': 10,
'c2': lambda arg: {'arg': arg}
},
},
},
)
test_list = providers.List(
dependency.provided['a']['b']['c1'],
dependency.provided['a']['b']['c2'].call(22)['arg'],
dependency.provided['a']['b']['c2'].call(service)['arg'],
dependency.provided['a']['b']['c2'].call(service)['arg'].value,
dependency.provided['a']['b']['c2'].call(service)['arg'].get_value.call(),
)
result = test_list()
self.assertEqual(
result,
[
10,
22,
service(),
'foo-bar',
'foo-bar',
],
)

View File

@ -16,6 +16,10 @@ class SelectorTests(unittest.TestCase):
def test_is_provider(self): def test_is_provider(self):
self.assertTrue(providers.is_provider(providers.Selector(self.selector))) self.assertTrue(providers.is_provider(providers.Selector(self.selector)))
def test_provided_instance_provider(self):
provider = providers.Selector(self.selector)
self.assertIsInstance(provider.provided, providers.ProvidedInstance)
def test_call(self): def test_call(self):
provider = providers.Selector( provider = providers.Selector(
self.selector, self.selector,

View File

@ -62,6 +62,10 @@ class _BaseSingletonTestCase(object):
with self.assertRaises(errors.Error): with self.assertRaises(errors.Error):
ExampleProvider(list) ExampleProvider(list)
def test_provided_instance_provider(self):
provider = providers.Singleton(Example)
self.assertIsInstance(provider.provided, providers.ProvidedInstance)
def test_call(self): def test_call(self):
provider = self.singleton_cls(Example) provider = self.singleton_cls(Example)