mirror of
				https://github.com/ets-labs/python-dependency-injector.git
				synced 2025-10-31 16:07:51 +03:00 
			
		
		
		
	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  | From version 0.7.6 *Dependency Injector* framework strictly  | ||||||
| follows `Semantic versioning`_ | 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 | 4.9.1 | ||||||
| ----- | ----- | ||||||
| - Fix a bug in the ``Configuration`` provider to correctly handle undefined values. | - 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 | 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. | 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 | Injecting invariants | ||||||
| -------------------- | -------------------- | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								examples/providers/configuration/configuration_required.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								examples/providers/configuration/configuration_required.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" | ||||||
|  |         ... | ||||||
							
								
								
									
										30
									
								
								examples/providers/configuration/configuration_strict.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								examples/providers/configuration/configuration_strict.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 tuple __name | ||||||
|     cdef object __root_ref |     cdef object __root_ref | ||||||
|     cdef dict __children |     cdef dict __children | ||||||
|  |     cdef bint __required | ||||||
|     cdef object __cache |     cdef object __cache | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -107,6 +108,7 @@ cdef class TypedConfigurationOption(Callable): | ||||||
| 
 | 
 | ||||||
| cdef class Configuration(Object): | cdef class Configuration(Object): | ||||||
|     cdef str __name |     cdef str __name | ||||||
|  |     cdef bint __strict | ||||||
|     cdef dict __children |     cdef dict __children | ||||||
|     cdef object __weakref__ |     cdef object __weakref__ | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -146,6 +146,8 @@ class ConfigurationOption(Provider[Any]): | ||||||
|     def as_int(self) -> TypedConfigurationOption[int]: ... |     def as_int(self) -> TypedConfigurationOption[int]: ... | ||||||
|     def as_float(self) -> TypedConfigurationOption[float]: ... |     def as_float(self) -> TypedConfigurationOption[float]: ... | ||||||
|     def as_(self, callback: _Callable[..., T], *args: Injection, **kwargs: Injection) -> TypedConfigurationOption[T]: ... |     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 update(self, value: Any) -> None: ... | ||||||
|     def from_ini(self, filepath: Union[Path, str]) -> None: ... |     def from_ini(self, filepath: Union[Path, str]) -> None: ... | ||||||
|     def from_yaml(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]): | class Configuration(Object[Any]): | ||||||
|     DEFAULT_NAME: str = 'config' |     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 __getattr__(self, item: str) -> ConfigurationOption: ... | ||||||
|     def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ... |     def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ... | ||||||
|     def get_name(self) -> str: ... |     def get_name(self) -> str: ... | ||||||
|  |  | ||||||
|  | @ -1172,10 +1172,11 @@ cdef class ConfigurationOption(Provider): | ||||||
| 
 | 
 | ||||||
|     UNDEFINED = object() |     UNDEFINED = object() | ||||||
| 
 | 
 | ||||||
|     def __init__(self, name, root): |     def __init__(self, name, root, required=False): | ||||||
|         self.__name = name |         self.__name = name | ||||||
|         self.__root_ref = weakref.ref(root) |         self.__root_ref = weakref.ref(root) | ||||||
|         self.__children = {} |         self.__children = {} | ||||||
|  |         self.__required = required | ||||||
|         self.__cache = self.UNDEFINED |         self.__cache = self.UNDEFINED | ||||||
|         super().__init__() |         super().__init__() | ||||||
| 
 | 
 | ||||||
|  | @ -1193,7 +1194,7 @@ cdef class ConfigurationOption(Provider): | ||||||
|         if copied_root is None: |         if copied_root is None: | ||||||
|             copied_root = deepcopy(root, memo) |             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) |         copied.__children = deepcopy(self.__children, memo) | ||||||
| 
 | 
 | ||||||
|         return copied |         return copied | ||||||
|  | @ -1229,7 +1230,7 @@ cdef class ConfigurationOption(Provider): | ||||||
|             return self.__cache |             return self.__cache | ||||||
| 
 | 
 | ||||||
|         root = self.__root_ref() |         root = self.__root_ref() | ||||||
|         value = root.get(self._get_self_name()) |         value = root.get(self._get_self_name(), self.__required) | ||||||
|         self.__cache = value |         self.__cache = value | ||||||
|         return value |         return value | ||||||
| 
 | 
 | ||||||
|  | @ -1258,6 +1259,12 @@ cdef class ConfigurationOption(Provider): | ||||||
|     def as_(self, callback, *args, **kwargs): |     def as_(self, callback, *args, **kwargs): | ||||||
|         return TypedConfigurationOption(callback, self, *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): |     def override(self, value): | ||||||
|         if isinstance(value, Provider): |         if isinstance(value, Provider): | ||||||
|             raise Error('Configuration option can only be overridden by a value') |             raise Error('Configuration option can only be overridden by a value') | ||||||
|  | @ -1396,9 +1403,11 @@ cdef class Configuration(Object): | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     DEFAULT_NAME = 'config' |     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.__name = name | ||||||
|  |         self.__strict = strict | ||||||
| 
 | 
 | ||||||
|         value = {} |         value = {} | ||||||
|         if default is not None: |         if default is not None: | ||||||
|  | @ -1416,7 +1425,7 @@ cdef class Configuration(Object): | ||||||
|         if copied is not None: |         if copied is not None: | ||||||
|             return copied |             return copied | ||||||
| 
 | 
 | ||||||
|         copied = self.__class__(self.__name, self.__provides) |         copied = self.__class__(self.__name, self.__provides, self.__strict) | ||||||
|         memo[id(self)] = copied |         memo[id(self)] = copied | ||||||
| 
 | 
 | ||||||
|         copied.__children = deepcopy(self.__children, memo) |         copied.__children = deepcopy(self.__children, memo) | ||||||
|  | @ -1450,12 +1459,15 @@ cdef class Configuration(Object): | ||||||
|     def get_name(self): |     def get_name(self): | ||||||
|         return self.__name |         return self.__name | ||||||
| 
 | 
 | ||||||
|     def get(self, selector): |     def get(self, selector, required=False): | ||||||
|         """Return configuration option. |         """Return configuration option. | ||||||
| 
 | 
 | ||||||
|         :param selector: Selector string, e.g. "option1.option2" |         :param selector: Selector string, e.g. "option1.option2" | ||||||
|         :type selector: str |         :type selector: str | ||||||
| 
 | 
 | ||||||
|  |         :param required: Required flag, raise error if required option is missing | ||||||
|  |         :type required: bool | ||||||
|  | 
 | ||||||
|         :return: Option value. |         :return: Option value. | ||||||
|         :rtype: Any |         :rtype: Any | ||||||
|         """ |         """ | ||||||
|  | @ -1467,9 +1479,12 @@ cdef class Configuration(Object): | ||||||
| 
 | 
 | ||||||
|         while len(keys) > 0: |         while len(keys) > 0: | ||||||
|             key = keys.pop(0) |             key = keys.pop(0) | ||||||
|             value = value.get(key) |             value = value.get(key, self.UNDEFINED) | ||||||
|             if value is None: | 
 | ||||||
|                 break |             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 |         return value | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -157,6 +157,9 @@ class ProvidersMap: | ||||||
|             else: |             else: | ||||||
|                 new = getattr(new, segment) |                 new = getattr(new, segment) | ||||||
| 
 | 
 | ||||||
|  |         if original.is_required(): | ||||||
|  |             new = new.required() | ||||||
|  | 
 | ||||||
|         if as_: |         if as_: | ||||||
|             new = new.as_(as_) |             new = new.as_(as_) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -21,3 +21,7 @@ config3 = providers.Configuration() | ||||||
| int3: providers.Callable[int] = config3.option.as_int() | int3: providers.Callable[int] = config3.option.as_int() | ||||||
| float3: providers.Callable[float] = config3.option.as_float() | float3: providers.Callable[float] = config3.option.as_float() | ||||||
| int3_custom: providers.Callable[int] = config3.option.as_(int) | 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')) |         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): |     def test_providers_value_override(self): | ||||||
|         a = self.config.a |         a = self.config.a | ||||||
|         ab = self.config.a.b |         ab = self.config.a.b | ||||||
|  | @ -176,6 +209,16 @@ class ConfigTests(unittest.TestCase): | ||||||
|     def test_value_of_undefined_option(self): |     def test_value_of_undefined_option(self): | ||||||
|         self.assertIsNone(self.config.a()) |         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): |     def test_getting_of_special_attributes(self): | ||||||
|         with self.assertRaises(AttributeError): |         with self.assertRaises(AttributeError): | ||||||
|             self.config.__name__ |             self.config.__name__ | ||||||
|  |  | ||||||
|  | @ -43,11 +43,30 @@ def test_function_provider(service_provider: Callable[..., Service] = Provider[C | ||||||
| 
 | 
 | ||||||
| @inject | @inject | ||||||
| def test_config_value( | def test_config_value( | ||||||
|         some_value_int: int = Provide[Container.config.a.b.c.as_int()], |         value_int: int = Provide[Container.config.a.b.c.as_int()], | ||||||
|         some_value_str: str = Provide[Container.config.a.b.c.as_(str)], |         value_str: str = Provide[Container.config.a.b.c.as_(str)], | ||||||
|         some_value_decimal: Decimal = Provide[Container.config.a.b.c.as_(Decimal)], |         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 | @inject | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ from decimal import Decimal | ||||||
| import unittest | import unittest | ||||||
| 
 | 
 | ||||||
| from dependency_injector.wiring import wire, Provide, Closing | 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 | # Runtime import to avoid syntax errors in samples on Python < 3.5 | ||||||
| import os | import os | ||||||
|  | @ -109,10 +110,28 @@ class WiringTest(unittest.TestCase): | ||||||
|         self.assertIs(service, test_service) |         self.assertIs(service, test_service) | ||||||
| 
 | 
 | ||||||
|     def test_configuration_option(self): |     def test_configuration_option(self): | ||||||
|         int_value, str_value, decimal_value = module.test_config_value() |         ( | ||||||
|         self.assertEqual(int_value, 10) |             value_int, | ||||||
|         self.assertEqual(str_value, '10') |             value_str, | ||||||
|         self.assertEqual(decimal_value, Decimal(10)) |             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): |     def test_provide_provider(self): | ||||||
|         service = module.test_provide_provider() |         service = module.test_provide_provider() | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user