Required config options and strict mode (#360)
* Add strict mode + tests * Add .required() for configuration option * Add wiring tests for required() modifier * Add wiring support * Add tests for defined None values in required/strict mode * Add docs * Update changelog * Update example doc block
This commit is contained in:
parent
3b69ed91c6
commit
d74e8248a1
|
@ -7,6 +7,12 @@ that were made in every particular version.
|
|||
From version 0.7.6 *Dependency Injector* framework strictly
|
||||
follows `Semantic versioning`_
|
||||
|
||||
Development version
|
||||
-------------------
|
||||
- Add ``strict`` mode and ``required`` modifier for ``Configuration`` provider.
|
||||
See issue `#341 <https://github.com/ets-labs/python-dependency-injector/issues/341>`_.
|
||||
Thanks `ms-lolo <https://github.com/ms-lolo>`_ for the feature request.
|
||||
|
||||
4.9.1
|
||||
-----
|
||||
- Fix a bug in the ``Configuration`` provider to correctly handle undefined values.
|
||||
|
|
|
@ -143,6 +143,28 @@ With the ``.as_(callback, *args, **kwargs)`` you can specify a function that wil
|
|||
before the injection. The value from the config will be passed as a first argument. The returned
|
||||
value will be injected. Parameters ``*args`` and ``**kwargs`` are handled as any other injections.
|
||||
|
||||
Strict mode and required options
|
||||
--------------------------------
|
||||
|
||||
You can use configuration provider in strict mode. In strict mode configuration provider raises an error
|
||||
on access to any undefined option.
|
||||
|
||||
.. literalinclude:: ../../examples/providers/configuration/configuration_strict.py
|
||||
:language: python
|
||||
:lines: 3-
|
||||
:emphasize-lines: 12
|
||||
|
||||
You can also use ``.required()`` option modifier when making an injection.
|
||||
|
||||
.. literalinclude:: ../../examples/providers/configuration/configuration_required.py
|
||||
:language: python
|
||||
:lines: 11-20
|
||||
:emphasize-lines: 8-9
|
||||
|
||||
.. note::
|
||||
|
||||
Modifier ``.required()`` should be specified before type modifier ``.as_*()``.
|
||||
|
||||
Injecting invariants
|
||||
--------------------
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
"""`Configuration` provider required modifier example."""
|
||||
|
||||
from dependency_injector import containers, providers, errors
|
||||
|
||||
|
||||
class ApiClient:
|
||||
def __init__(self, api_key: str, timeout: int):
|
||||
self.api_key = api_key
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
api_client_factory = providers.Factory(
|
||||
ApiClient,
|
||||
api_key=config.api.key.required(),
|
||||
timeout=config.api.timeout.required().as_int(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
|
||||
try:
|
||||
api_client = container.api_client_factory()
|
||||
except errors.Error:
|
||||
# raises error: Undefined configuration option "config.api.key"
|
||||
...
|
|
@ -0,0 +1,30 @@
|
|||
"""`Configuration` provider strict mode example."""
|
||||
|
||||
from dependency_injector import containers, providers, errors
|
||||
|
||||
|
||||
class ApiClient:
|
||||
def __init__(self, api_key: str, timeout: int):
|
||||
self.api_key = api_key
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration(strict=True)
|
||||
|
||||
api_client_factory = providers.Factory(
|
||||
ApiClient,
|
||||
api_key=config.api.key,
|
||||
timeout=config.api.timeout.as_int(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
|
||||
try:
|
||||
api_client = container.api_client_factory()
|
||||
except errors.Error:
|
||||
# raises error: Undefined configuration option "config.api.key"
|
||||
...
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -98,6 +98,7 @@ cdef class ConfigurationOption(Provider):
|
|||
cdef tuple __name
|
||||
cdef object __root_ref
|
||||
cdef dict __children
|
||||
cdef bint __required
|
||||
cdef object __cache
|
||||
|
||||
|
||||
|
@ -107,6 +108,7 @@ cdef class TypedConfigurationOption(Callable):
|
|||
|
||||
cdef class Configuration(Object):
|
||||
cdef str __name
|
||||
cdef bint __strict
|
||||
cdef dict __children
|
||||
cdef object __weakref__
|
||||
|
||||
|
|
|
@ -146,6 +146,8 @@ class ConfigurationOption(Provider[Any]):
|
|||
def as_int(self) -> TypedConfigurationOption[int]: ...
|
||||
def as_float(self) -> TypedConfigurationOption[float]: ...
|
||||
def as_(self, callback: _Callable[..., T], *args: Injection, **kwargs: Injection) -> TypedConfigurationOption[T]: ...
|
||||
def required(self) -> ConfigurationOption: ...
|
||||
def is_required(self) -> bool: ...
|
||||
def update(self, value: Any) -> None: ...
|
||||
def from_ini(self, filepath: Union[Path, str]) -> None: ...
|
||||
def from_yaml(self, filepath: Union[Path, str]) -> None: ...
|
||||
|
@ -160,7 +162,7 @@ class TypedConfigurationOption(Callable[T]):
|
|||
|
||||
class Configuration(Object[Any]):
|
||||
DEFAULT_NAME: str = 'config'
|
||||
def __init__(self, name: str = DEFAULT_NAME, default: Optional[Any] = None) -> None: ...
|
||||
def __init__(self, name: str = DEFAULT_NAME, default: Optional[Any] = None, *, strict: bool = False) -> None: ...
|
||||
def __getattr__(self, item: str) -> ConfigurationOption: ...
|
||||
def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ...
|
||||
def get_name(self) -> str: ...
|
||||
|
|
|
@ -1172,10 +1172,11 @@ cdef class ConfigurationOption(Provider):
|
|||
|
||||
UNDEFINED = object()
|
||||
|
||||
def __init__(self, name, root):
|
||||
def __init__(self, name, root, required=False):
|
||||
self.__name = name
|
||||
self.__root_ref = weakref.ref(root)
|
||||
self.__children = {}
|
||||
self.__required = required
|
||||
self.__cache = self.UNDEFINED
|
||||
super().__init__()
|
||||
|
||||
|
@ -1193,7 +1194,7 @@ cdef class ConfigurationOption(Provider):
|
|||
if copied_root is None:
|
||||
copied_root = deepcopy(root, memo)
|
||||
|
||||
copied = self.__class__(copied_name, copied_root)
|
||||
copied = self.__class__(copied_name, copied_root, self.__required)
|
||||
copied.__children = deepcopy(self.__children, memo)
|
||||
|
||||
return copied
|
||||
|
@ -1229,7 +1230,7 @@ cdef class ConfigurationOption(Provider):
|
|||
return self.__cache
|
||||
|
||||
root = self.__root_ref()
|
||||
value = root.get(self._get_self_name())
|
||||
value = root.get(self._get_self_name(), self.__required)
|
||||
self.__cache = value
|
||||
return value
|
||||
|
||||
|
@ -1258,6 +1259,12 @@ cdef class ConfigurationOption(Provider):
|
|||
def as_(self, callback, *args, **kwargs):
|
||||
return TypedConfigurationOption(callback, self, *args, **kwargs)
|
||||
|
||||
def required(self):
|
||||
return self.__class__(self.__name, self.__root_ref(), required=True)
|
||||
|
||||
def is_required(self):
|
||||
return self.__required
|
||||
|
||||
def override(self, value):
|
||||
if isinstance(value, Provider):
|
||||
raise Error('Configuration option can only be overridden by a value')
|
||||
|
@ -1396,9 +1403,11 @@ cdef class Configuration(Object):
|
|||
"""
|
||||
|
||||
DEFAULT_NAME = 'config'
|
||||
UNDEFINED = object()
|
||||
|
||||
def __init__(self, name=DEFAULT_NAME, default=None):
|
||||
def __init__(self, name=DEFAULT_NAME, default=None, strict=False):
|
||||
self.__name = name
|
||||
self.__strict = strict
|
||||
|
||||
value = {}
|
||||
if default is not None:
|
||||
|
@ -1416,7 +1425,7 @@ cdef class Configuration(Object):
|
|||
if copied is not None:
|
||||
return copied
|
||||
|
||||
copied = self.__class__(self.__name, self.__provides)
|
||||
copied = self.__class__(self.__name, self.__provides, self.__strict)
|
||||
memo[id(self)] = copied
|
||||
|
||||
copied.__children = deepcopy(self.__children, memo)
|
||||
|
@ -1450,12 +1459,15 @@ cdef class Configuration(Object):
|
|||
def get_name(self):
|
||||
return self.__name
|
||||
|
||||
def get(self, selector):
|
||||
def get(self, selector, required=False):
|
||||
"""Return configuration option.
|
||||
|
||||
:param selector: Selector string, e.g. "option1.option2"
|
||||
:type selector: str
|
||||
|
||||
:param required: Required flag, raise error if required option is missing
|
||||
:type required: bool
|
||||
|
||||
:return: Option value.
|
||||
:rtype: Any
|
||||
"""
|
||||
|
@ -1467,9 +1479,12 @@ cdef class Configuration(Object):
|
|||
|
||||
while len(keys) > 0:
|
||||
key = keys.pop(0)
|
||||
value = value.get(key)
|
||||
if value is None:
|
||||
break
|
||||
value = value.get(key, self.UNDEFINED)
|
||||
|
||||
if value is self.UNDEFINED:
|
||||
if self.__strict or required:
|
||||
raise Error('Undefined configuration option "{0}.{1}"'.format(self.__name, selector))
|
||||
return None
|
||||
|
||||
return value
|
||||
|
||||
|
|
|
@ -157,6 +157,9 @@ class ProvidersMap:
|
|||
else:
|
||||
new = getattr(new, segment)
|
||||
|
||||
if original.is_required():
|
||||
new = new.required()
|
||||
|
||||
if as_:
|
||||
new = new.as_(as_)
|
||||
|
||||
|
|
|
@ -21,3 +21,7 @@ config3 = providers.Configuration()
|
|||
int3: providers.Callable[int] = config3.option.as_int()
|
||||
float3: providers.Callable[float] = config3.option.as_float()
|
||||
int3_custom: providers.Callable[int] = config3.option.as_(int)
|
||||
|
||||
# Test 4: to check required() method
|
||||
config4 = providers.Configuration()
|
||||
option4: providers.ConfigurationOption = config4.option.required()
|
||||
|
|
|
@ -97,6 +97,39 @@ class ConfigTests(unittest.TestCase):
|
|||
|
||||
self.assertEqual(value, decimal.Decimal('123.123'))
|
||||
|
||||
def test_required(self):
|
||||
provider = providers.Callable(
|
||||
lambda value: value,
|
||||
self.config.a.required(),
|
||||
)
|
||||
with self.assertRaisesRegex(errors.Error, 'Undefined configuration option "config.a"'):
|
||||
provider()
|
||||
|
||||
def test_required_defined_none(self):
|
||||
provider = providers.Callable(
|
||||
lambda value: value,
|
||||
self.config.a.required(),
|
||||
)
|
||||
self.config.from_dict({'a': None})
|
||||
self.assertIsNone(provider())
|
||||
|
||||
def test_required_no_side_effect(self):
|
||||
_ = providers.Callable(
|
||||
lambda value: value,
|
||||
self.config.a.required(),
|
||||
)
|
||||
self.assertIsNone(self.config.a())
|
||||
|
||||
def test_required_as_(self):
|
||||
provider = providers.List(
|
||||
self.config.int_test.required().as_int(),
|
||||
self.config.float_test.required().as_float(),
|
||||
self.config._as_test.required().as_(decimal.Decimal),
|
||||
)
|
||||
self.config.from_dict({'int_test': '1', 'float_test': '2.0', '_as_test': '3.0'})
|
||||
|
||||
self.assertEqual(provider(), [1, 2.0, decimal.Decimal('3.0')])
|
||||
|
||||
def test_providers_value_override(self):
|
||||
a = self.config.a
|
||||
ab = self.config.a.b
|
||||
|
@ -176,6 +209,16 @@ class ConfigTests(unittest.TestCase):
|
|||
def test_value_of_undefined_option(self):
|
||||
self.assertIsNone(self.config.a())
|
||||
|
||||
def test_value_of_undefined_option_in_strict_mode(self):
|
||||
self.config = providers.Configuration(strict=True)
|
||||
with self.assertRaisesRegex(errors.Error, 'Undefined configuration option "config.a"'):
|
||||
self.config.a()
|
||||
|
||||
def test_value_of_defined_none_option_in_strict_mode(self):
|
||||
self.config = providers.Configuration(strict=True)
|
||||
self.config.from_dict({'a': None})
|
||||
self.assertIsNone(self.config.a())
|
||||
|
||||
def test_getting_of_special_attributes(self):
|
||||
with self.assertRaises(AttributeError):
|
||||
self.config.__name__
|
||||
|
|
|
@ -43,11 +43,30 @@ def test_function_provider(service_provider: Callable[..., Service] = Provider[C
|
|||
|
||||
@inject
|
||||
def test_config_value(
|
||||
some_value_int: int = Provide[Container.config.a.b.c.as_int()],
|
||||
some_value_str: str = Provide[Container.config.a.b.c.as_(str)],
|
||||
some_value_decimal: Decimal = Provide[Container.config.a.b.c.as_(Decimal)],
|
||||
value_int: int = Provide[Container.config.a.b.c.as_int()],
|
||||
value_str: str = Provide[Container.config.a.b.c.as_(str)],
|
||||
value_decimal: Decimal = Provide[Container.config.a.b.c.as_(Decimal)],
|
||||
value_required: str = Provide[Container.config.a.b.c.required()],
|
||||
value_required_int: int = Provide[Container.config.a.b.c.required().as_int()],
|
||||
value_required_str: str = Provide[Container.config.a.b.c.required().as_(str)],
|
||||
value_required_decimal: str = Provide[Container.config.a.b.c.required().as_(Decimal)],
|
||||
):
|
||||
return some_value_int, some_value_str, some_value_decimal
|
||||
return (
|
||||
value_int,
|
||||
value_str,
|
||||
value_decimal,
|
||||
value_required,
|
||||
value_required_int,
|
||||
value_required_str,
|
||||
value_required_decimal,
|
||||
)
|
||||
|
||||
|
||||
@inject
|
||||
def test_config_value_required_undefined(
|
||||
value_required: int = Provide[Container.config.a.b.c.required()],
|
||||
):
|
||||
return value_required
|
||||
|
||||
|
||||
@inject
|
||||
|
|
|
@ -2,6 +2,7 @@ from decimal import Decimal
|
|||
import unittest
|
||||
|
||||
from dependency_injector.wiring import wire, Provide, Closing
|
||||
from dependency_injector import errors
|
||||
|
||||
# Runtime import to avoid syntax errors in samples on Python < 3.5
|
||||
import os
|
||||
|
@ -109,10 +110,28 @@ class WiringTest(unittest.TestCase):
|
|||
self.assertIs(service, test_service)
|
||||
|
||||
def test_configuration_option(self):
|
||||
int_value, str_value, decimal_value = module.test_config_value()
|
||||
self.assertEqual(int_value, 10)
|
||||
self.assertEqual(str_value, '10')
|
||||
self.assertEqual(decimal_value, Decimal(10))
|
||||
(
|
||||
value_int,
|
||||
value_str,
|
||||
value_decimal,
|
||||
value_required,
|
||||
value_required_int,
|
||||
value_required_str,
|
||||
value_required_decimal,
|
||||
) = module.test_config_value()
|
||||
|
||||
self.assertEqual(value_int, 10)
|
||||
self.assertEqual(value_str, '10')
|
||||
self.assertEqual(value_decimal, Decimal(10))
|
||||
self.assertEqual(value_required, 10)
|
||||
self.assertEqual(value_required_int, 10)
|
||||
self.assertEqual(value_required_str, '10')
|
||||
self.assertEqual(value_required_decimal, Decimal(10))
|
||||
|
||||
def test_configuration_option_required_undefined(self):
|
||||
self.container.config.reset_override()
|
||||
with self.assertRaisesRegex(errors.Error, 'Undefined configuration option "config.a.b.c"'):
|
||||
module.test_config_value_required_undefined()
|
||||
|
||||
def test_provide_provider(self):
|
||||
service = module.test_provide_provider()
|
||||
|
|
Loading…
Reference in New Issue