mirror of
				https://github.com/ets-labs/python-dependency-injector.git
				synced 2025-10-31 07:57:43 +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  | ||||
| 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 | ||||
| -------------------- | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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 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
	
	Block a user