531 Provider import from string (#555)
* Implement string imports for Factory, Callable, Singletons, and Resource * Refactor the implementation * Add tests * Update tests to pass on Python 2 * Update typing and add typing tests * Update changelog * Update docs
This commit is contained in:
parent
38ca1cdeed
commit
86df7f91f6
|
@ -21,6 +21,10 @@ Development version
|
|||
``FactoryAggregate.factories`` attribute.
|
||||
- Add ``.set_providers()`` method to the ``FactoryAggregate`` provider. It is an alias for
|
||||
``FactoryAggregate.set_factories()`` method.
|
||||
- Add string imports for ``Factory``, ``Singleton``, ``Callable``, ``Resource``, and ``Coroutine``
|
||||
providers, e.g. ``Factory("module.Class")``.
|
||||
See issue `#531 <https://github.com/ets-labs/python-dependency-injector/issues/531>`_.
|
||||
Thanks to `@al-stefanitsky-mozdor <https://github.com/al-stefanitsky-mozdor>`_ for suggesting the feature.
|
||||
- Fix ``Dependency`` provider to don't raise "Dependency is not defined" error when the ``default``
|
||||
is a falsy value of proper type.
|
||||
See issue `#550 <https://github.com/ets-labs/python-dependency-injector/issues/550>`_. Thanks to
|
||||
|
|
|
@ -110,6 +110,45 @@ attribute of the provider that you're going to inject.
|
|||
|
||||
.. note:: Any provider has a ``.provider`` attribute.
|
||||
|
||||
.. _factory-string-imports:
|
||||
|
||||
String imports
|
||||
--------------
|
||||
|
||||
``Factory`` provider can handle string imports:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
service = providers.Factory("myapp.mypackage.mymodule.Service")
|
||||
|
||||
You can also make a relative import:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# in myapp/container.py
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
service = providers.Factory(".mypackage.mymodule.Service")
|
||||
|
||||
or import a member of the current module just specifying its name:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Service:
|
||||
...
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
service = providers.Factory("Service")
|
||||
|
||||
.. note::
|
||||
``Singleton``, ``Callable``, ``Resource``, and ``Coroutine`` providers handle string imports
|
||||
the same way as a ``Factory`` provider.
|
||||
|
||||
.. _factory-specialize-provided-type:
|
||||
|
||||
Specializing the provided type
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -158,10 +158,10 @@ class DependenciesContainer(Object):
|
|||
|
||||
|
||||
class Callable(Provider[T]):
|
||||
def __init__(self, provides: Optional[_Callable[..., T]] = None, *args: Injection, **kwargs: Injection) -> None: ...
|
||||
def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ...
|
||||
@property
|
||||
def provides(self) -> Optional[_Callable[..., T]]: ...
|
||||
def set_provides(self, provides: Optional[_Callable[..., T]]) -> Callable[T]: ...
|
||||
def set_provides(self, provides: Optional[Union[_Callable[..., T], str]]) -> Callable[T]: ...
|
||||
@property
|
||||
def args(self) -> Tuple[Injection]: ...
|
||||
def add_args(self, *args: Injection) -> Callable[T]: ...
|
||||
|
@ -283,12 +283,12 @@ class Configuration(Object[Any]):
|
|||
|
||||
class Factory(Provider[T]):
|
||||
provided_type: Optional[Type]
|
||||
def __init__(self, provides: Optional[_Callable[..., T]] = None, *args: Injection, **kwargs: Injection) -> None: ...
|
||||
def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ...
|
||||
@property
|
||||
def cls(self) -> Type[T]: ...
|
||||
@property
|
||||
def provides(self) -> Optional[_Callable[..., T]]: ...
|
||||
def set_provides(self, provides: Optional[_Callable[..., T]]) -> Factory[T]: ...
|
||||
def set_provides(self, provides: Optional[Union[_Callable[..., T], str]]) -> Factory[T]: ...
|
||||
@property
|
||||
def args(self) -> Tuple[Injection]: ...
|
||||
def add_args(self, *args: Injection) -> Factory[T]: ...
|
||||
|
@ -326,12 +326,12 @@ class FactoryAggregate(Aggregate[T]):
|
|||
|
||||
class BaseSingleton(Provider[T]):
|
||||
provided_type = Optional[Type]
|
||||
def __init__(self, provides: Optional[_Callable[..., T]] = None, *args: Injection, **kwargs: Injection) -> None: ...
|
||||
def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ...
|
||||
@property
|
||||
def cls(self) -> Type[T]: ...
|
||||
@property
|
||||
def provides(self) -> Optional[_Callable[..., T]]: ...
|
||||
def set_provides(self, provides: Optional[_Callable[..., T]]) -> BaseSingleton[T]: ...
|
||||
def set_provides(self, provides: Optional[Union[_Callable[..., T], str]]) -> BaseSingleton[T]: ...
|
||||
@property
|
||||
def args(self) -> Tuple[Injection]: ...
|
||||
def add_args(self, *args: Injection) -> BaseSingleton[T]: ...
|
||||
|
@ -377,7 +377,7 @@ class AbstractSingleton(BaseSingleton[T]):
|
|||
|
||||
|
||||
class SingletonDelegate(Delegate):
|
||||
def __init__(self, factory: BaseSingleton): ...
|
||||
def __init__(self, singleton: BaseSingleton): ...
|
||||
|
||||
|
||||
class List(Provider[_List]):
|
||||
|
@ -410,7 +410,7 @@ class Resource(Provider[T]):
|
|||
@overload
|
||||
def __init__(self, provides: Optional[_Callable[..., _Coroutine[Injection, Injection, T]]] = None, *args: Injection, **kwargs: Injection) -> None: ...
|
||||
@overload
|
||||
def __init__(self, provides: Optional[_Callable[..., T]] = None, *args: Injection, **kwargs: Injection) -> None: ...
|
||||
def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ...
|
||||
@property
|
||||
def provides(self) -> Optional[_Callable[..., Any]]: ...
|
||||
def set_provides(self, provides: Optional[Any]) -> Resource[T]: ...
|
||||
|
|
|
@ -6,6 +6,7 @@ import copy
|
|||
import errno
|
||||
import functools
|
||||
import inspect
|
||||
import importlib
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
@ -18,6 +19,11 @@ try:
|
|||
except ImportError:
|
||||
contextvars = None
|
||||
|
||||
try:
|
||||
import builtins
|
||||
except ImportError:
|
||||
# Python 2.7
|
||||
import __builtin__ as builtins
|
||||
|
||||
try:
|
||||
import asyncio
|
||||
|
@ -1221,6 +1227,7 @@ cdef class Callable(Provider):
|
|||
|
||||
def set_provides(self, provides):
|
||||
"""Set provider provides."""
|
||||
provides = _resolve_provides(provides)
|
||||
if provides and not callable(provides):
|
||||
raise Error(
|
||||
"Provider {0} expected to get callable, got {1} instead".format(
|
||||
|
@ -1426,9 +1433,10 @@ cdef class Coroutine(Callable):
|
|||
_is_coroutine = _is_coroutine_marker
|
||||
|
||||
def set_provides(self, provides):
|
||||
"""Set provider"s provides."""
|
||||
"""Set provider provides."""
|
||||
if not asyncio:
|
||||
raise Error("Package asyncio is not available")
|
||||
provides = _resolve_provides(provides)
|
||||
if provides and not asyncio.iscoroutinefunction(provides):
|
||||
raise Error(f"Provider {_class_qualname(self)} expected to get coroutine function, "
|
||||
f"got {provides} instead")
|
||||
|
@ -2452,6 +2460,7 @@ cdef class Factory(Provider):
|
|||
|
||||
def set_provides(self, provides):
|
||||
"""Set provider provides."""
|
||||
provides = _resolve_provides(provides)
|
||||
if (provides
|
||||
and self.__class__.provided_type and
|
||||
not issubclass(provides, self.__class__.provided_type)):
|
||||
|
@ -2742,6 +2751,7 @@ cdef class BaseSingleton(Provider):
|
|||
|
||||
def set_provides(self, provides):
|
||||
"""Set provider provides."""
|
||||
provides = _resolve_provides(provides)
|
||||
if (provides
|
||||
and self.__class__.provided_type and
|
||||
not issubclass(provides, self.__class__.provided_type)):
|
||||
|
@ -3580,6 +3590,7 @@ cdef class Resource(Provider):
|
|||
|
||||
def set_provides(self, provides):
|
||||
"""Set provider provides."""
|
||||
provides = _resolve_provides(provides)
|
||||
self.__provides = provides
|
||||
return self
|
||||
|
||||
|
@ -4882,6 +4893,44 @@ def isasyncgenfunction(obj):
|
|||
return False
|
||||
|
||||
|
||||
def _resolve_provides(provides):
|
||||
if provides is None:
|
||||
return provides
|
||||
|
||||
if not isinstance(provides, str):
|
||||
return provides
|
||||
|
||||
segments = provides.split(".")
|
||||
member_name = segments[-1]
|
||||
|
||||
if len(segments) == 1:
|
||||
if member_name in dir(builtins):
|
||||
module = builtins
|
||||
else:
|
||||
module = _resolve_calling_module()
|
||||
return getattr(module, member_name)
|
||||
|
||||
module_name = ".".join(segments[:-1])
|
||||
|
||||
package_name = _resolve_calling_package_name()
|
||||
if module_name.startswith(".") and package_name is None:
|
||||
raise ImportError("Attempted relative import with no known parent package")
|
||||
|
||||
module = importlib.import_module(module_name, package=package_name)
|
||||
return getattr(module, member_name)
|
||||
|
||||
|
||||
def _resolve_calling_module():
|
||||
stack = inspect.stack()
|
||||
pre_last_frame = stack[0]
|
||||
return inspect.getmodule(pre_last_frame[0])
|
||||
|
||||
|
||||
def _resolve_calling_package_name():
|
||||
module = _resolve_calling_module()
|
||||
return module.__package__
|
||||
|
||||
|
||||
cpdef _copy_parent(object from_, object to, dict memo):
|
||||
"""Copy and assign provider parent."""
|
||||
copied_parent = (
|
||||
|
|
|
@ -66,3 +66,7 @@ assert provides10 is Cat
|
|||
provider11 = providers.Callable[Animal](Cat)
|
||||
provides11: Optional[Callable[..., Animal]] = provider11.provides
|
||||
assert provides11 is Cat
|
||||
|
||||
# Test 12: to check string imports
|
||||
provider12: providers.Callable[dict] = providers.Callable("builtins.dict")
|
||||
provider12.set_provides("builtins.dict")
|
||||
|
|
|
@ -9,3 +9,7 @@ async def _coro() -> None:
|
|||
# Test 1: to check the return type
|
||||
provider1 = providers.Coroutine(_coro)
|
||||
var1: Coroutine = provider1()
|
||||
|
||||
# Test 2: to check string imports
|
||||
provider2: providers.Coroutine[None] = providers.Coroutine("_coro")
|
||||
provider2.set_provides("_coro")
|
||||
|
|
|
@ -99,3 +99,7 @@ provided_cls13: Type[Animal] = provider13.cls
|
|||
assert issubclass(provided_cls13, Animal)
|
||||
provided_provides13: Optional[Callable[..., Animal]] = provider13.provides
|
||||
assert provided_provides13 is not None and provided_provides13() == Cat()
|
||||
|
||||
# Test 14: to check string imports
|
||||
provider14: providers.Factory[dict] = providers.Factory("builtins.dict")
|
||||
provider14.set_provides("builtins.dict")
|
||||
|
|
|
@ -97,3 +97,8 @@ provider8 = providers.Resource(MyResource8)
|
|||
async def _provide8() -> None:
|
||||
var1: List[int] = await provider8() # type: ignore
|
||||
var2: List[int] = await provider8.async_()
|
||||
|
||||
|
||||
# Test 9: to check string imports
|
||||
provider9: providers.Resource[dict] = providers.Resource("builtins.dict")
|
||||
provider9.set_provides("builtins.dict")
|
||||
|
|
|
@ -89,3 +89,7 @@ provided_cls15: Type[Animal] = provider15.cls
|
|||
assert issubclass(provided_cls15, Animal)
|
||||
provided_provides15: Optional[Callable[..., Animal]] = provider15.provides
|
||||
assert provided_provides15 is not None and provided_provides15() == Cat()
|
||||
|
||||
# Test 16: to check string imports
|
||||
provider16: providers.Singleton[dict] = providers.Singleton("builtins.dict")
|
||||
provider16.set_provides("builtins.dict")
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
"""Callable provider tests."""
|
||||
|
||||
import decimal
|
||||
import sys
|
||||
|
||||
from dependency_injector import providers, errors
|
||||
from pytest import raises
|
||||
from pytest import raises, mark
|
||||
|
||||
from .common import example
|
||||
|
||||
|
@ -29,6 +30,20 @@ def test_set_provides_returns_():
|
|||
assert provider.set_provides(object) is provider
|
||||
|
||||
|
||||
@mark.parametrize(
|
||||
"str_name,cls",
|
||||
[
|
||||
("dependency_injector.providers.Factory", providers.Factory),
|
||||
("decimal.Decimal", decimal.Decimal),
|
||||
("list", list),
|
||||
(".common.example", example),
|
||||
("test_is_provider", test_is_provider),
|
||||
],
|
||||
)
|
||||
def test_set_provides_string_imports(str_name, cls):
|
||||
assert providers.Callable(str_name).provides is cls
|
||||
|
||||
|
||||
def test_provided_instance_provider():
|
||||
provider = providers.Callable(example)
|
||||
assert isinstance(provider.provided, providers.ProvidedInstance)
|
||||
|
|
|
@ -31,6 +31,17 @@ def test_set_provides_returns_self():
|
|||
assert provider.set_provides(example) is provider
|
||||
|
||||
|
||||
@mark.parametrize(
|
||||
"str_name,cls",
|
||||
[
|
||||
(".common.example", example),
|
||||
("example", example),
|
||||
],
|
||||
)
|
||||
def test_set_provides_string_imports(str_name, cls):
|
||||
assert providers.Coroutine(str_name).provides is cls
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_call_with_positional_args():
|
||||
provider = providers.Coroutine(example, 1, 2, 3, 4)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
"""Factory provider tests."""
|
||||
|
||||
import decimal
|
||||
import sys
|
||||
|
||||
from dependency_injector import providers, errors
|
||||
from pytest import raises
|
||||
from pytest import raises, mark
|
||||
|
||||
from .common import Example
|
||||
|
||||
|
@ -29,6 +30,20 @@ def test_set_provides_returns_():
|
|||
assert provider.set_provides(object) is provider
|
||||
|
||||
|
||||
@mark.parametrize(
|
||||
"str_name,cls",
|
||||
[
|
||||
("dependency_injector.providers.Factory", providers.Factory),
|
||||
("decimal.Decimal", decimal.Decimal),
|
||||
("list", list),
|
||||
(".common.Example", Example),
|
||||
("test_is_provider", test_is_provider),
|
||||
],
|
||||
)
|
||||
def test_set_provides_string_imports(str_name, cls):
|
||||
assert providers.Factory(str_name).provides is cls
|
||||
|
||||
|
||||
def test_init_with_valid_provided_type():
|
||||
class ExampleProvider(providers.Factory):
|
||||
provided_type = Example
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
"""Resource provider tests."""
|
||||
|
||||
import decimal
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from dependency_injector import containers, providers, resources, errors
|
||||
from pytest import raises
|
||||
from pytest import raises, mark
|
||||
|
||||
|
||||
def init_fn(*args, **kwargs):
|
||||
|
@ -27,6 +28,20 @@ def test_set_provides_returns_():
|
|||
assert provider.set_provides(init_fn) is provider
|
||||
|
||||
|
||||
@mark.parametrize(
|
||||
"str_name,cls",
|
||||
[
|
||||
("dependency_injector.providers.Factory", providers.Factory),
|
||||
("decimal.Decimal", decimal.Decimal),
|
||||
("list", list),
|
||||
(".test_resource_py35.test_is_provider", test_is_provider),
|
||||
("test_is_provider", test_is_provider),
|
||||
],
|
||||
)
|
||||
def test_set_provides_string_imports(str_name, cls):
|
||||
assert providers.Resource(str_name).provides is cls
|
||||
|
||||
|
||||
def test_provided_instance_provider():
|
||||
provider = providers.Resource(init_fn)
|
||||
assert isinstance(provider.provided, providers.ProvidedInstance)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"""Singleton provider tests."""
|
||||
|
||||
import decimal
|
||||
import sys
|
||||
|
||||
from dependency_injector import providers, errors
|
||||
from pytest import fixture, raises
|
||||
from pytest import fixture, raises, mark
|
||||
|
||||
from .common import Example
|
||||
|
||||
|
@ -49,6 +49,20 @@ def test_set_provides_returns_self(provider):
|
|||
assert provider.set_provides(object) is provider
|
||||
|
||||
|
||||
@mark.parametrize(
|
||||
"str_name,cls",
|
||||
[
|
||||
("dependency_injector.providers.Factory", providers.Factory),
|
||||
("decimal.Decimal", decimal.Decimal),
|
||||
("list", list),
|
||||
(".common.Example", Example),
|
||||
("test_is_provider", test_is_provider),
|
||||
],
|
||||
)
|
||||
def test_set_provides_string_imports(str_name, cls):
|
||||
assert providers.Singleton(str_name).provides is cls
|
||||
|
||||
|
||||
def test_init_with_valid_provided_type(singleton_cls):
|
||||
class ExampleProvider(singleton_cls):
|
||||
provided_type = Example
|
||||
|
|
Loading…
Reference in New Issue