Merge branch 'release/4.33.0' into master

This commit is contained in:
Roman Mogylatov 2021-06-13 22:06:36 -04:00
commit 9abf34cb88
8 changed files with 9292 additions and 8408 deletions

View File

@ -7,6 +7,17 @@ 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`_
4.33.0
------
- Add support of default value for environment variable in INI and YAML
configuration files with ``${ENV_NAME:default}`` format.
See issue `#459 <https://github.com/ets-labs/python-dependency-injector/issues/459>`_.
Thanks to `Maksym Shemet @hbmshemet <https://github.com/hbmshemet>`_ for suggesting the feature.
- Add method ``Configuration.from_value()``.
See issue `#462 <https://github.com/ets-labs/python-dependency-injector/issues/462>`_.
Thanks to Mr. `Slack Clone <https://disqus.com/by/slackclone/>`_ for bringing it up
in the comments for configuration provider docs.
4.32.3 4.32.3
------ ------
- This fix a typo in ``di_in_python.rst`` doc. - This fix a typo in ``di_in_python.rst`` doc.

View File

@ -5,7 +5,7 @@ Configuration provider
.. meta:: .. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Configuration,Injection, :keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Configuration,Injection,
Option,Ini,Json,Yaml,Pydantic,Dict,Environment Variable,Load,Read,Get Option,Ini,Json,Yaml,Pydantic,Dict,Environment Variable,Default,Load,Read,Get
:description: Configuration provides configuration options to the other providers. This page :description: Configuration provides configuration options to the other providers. This page
demonstrates how to use Configuration provider to inject the dependencies, load demonstrates how to use Configuration provider to inject the dependencies, load
a configuration from an ini or yaml file, a dictionary, an environment variable, a configuration from an ini or yaml file, a dictionary, an environment variable,
@ -43,8 +43,18 @@ where ``examples/providers/configuration/config.ini`` is:
:language: ini :language: ini
:py:meth:`Configuration.from_ini` method supports environment variables interpolation. Use :py:meth:`Configuration.from_ini` method supports environment variables interpolation. Use
``${ENV_NAME}`` format in the configuration file to substitute value of the environment ``${ENV_NAME}`` format in the configuration file to substitute value from ``ENV_NAME`` environment
variable ``ENV_NAME``. variable.
You can also specify a default value using ``${ENV_NAME:default}`` format. If environment
variable ``ENV_NAME`` is undefined, configuration provider will substitute value ``default``.
.. code-block:: ini
[section]
option1 = {$ENV_VAR}
option2 = {$ENV_VAR}/path
option3 = {$ENV_VAR:default}
Loading from a YAML file Loading from a YAML file
------------------------ ------------------------
@ -62,12 +72,22 @@ where ``examples/providers/configuration/config.yml`` is:
.. literalinclude:: ../../examples/providers/configuration/config.yml .. literalinclude:: ../../examples/providers/configuration/config.yml
:language: ini :language: ini
:py:meth:`Configuration.from_yaml` method supports environment variables interpolation. Use
``${ENV_NAME}`` format in the configuration file to substitute value from ``ENV_NAME`` environment
variable.
You can also specify a default value using ``${ENV_NAME:default}`` format. If environment
variable ``ENV_NAME`` is undefined, configuration provider will substitute value ``default``.
.. code-block:: ini
section:
option1: {$ENV_VAR}
option2: {$ENV_VAR}/path
option3: {$ENV_VAR:default}
:py:meth:`Configuration.from_yaml` method uses custom version of ``yaml.SafeLoader``. :py:meth:`Configuration.from_yaml` method uses custom version of ``yaml.SafeLoader``.
To use another loader use ``loader`` argument:
The loader supports environment variables interpolation. Use ``${ENV_NAME}`` format
in the configuration file to substitute value of the environment variable ``ENV_NAME``.
You can also specify a YAML loader as an argument:
.. code-block:: python .. code-block:: python
@ -144,6 +164,17 @@ Loading from an environment variable
:lines: 3- :lines: 3-
:emphasize-lines: 18-20 :emphasize-lines: 18-20
Loading a value
---------------
``Configuration`` provider can load configuration value using the
:py:meth:`Configuration.from_value` method:
.. literalinclude:: ../../examples/providers/configuration/configuration_value.py
:language: python
:lines: 3-
:emphasize-lines: 14-15
Loading from the multiple sources Loading from the multiple sources
--------------------------------- ---------------------------------

View File

@ -0,0 +1,24 @@
"""`Configuration` provider values loading example."""
from datetime import date
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
if __name__ == '__main__':
container = Container()
container.config.option1.from_value(date(2021, 6, 13))
container.config.option2.from_value(date(2021, 6, 14))
assert container.config() == {
'option1': date(2021, 6, 13),
'option2': date(2021, 6, 14),
}
assert container.config.option1() == date(2021, 6, 13)
assert container.config.option2() == date(2021, 6, 14)

View File

@ -1,6 +1,6 @@
"""Top-level package.""" """Top-level package."""
__version__ = '4.32.3' __version__ = '4.33.0'
"""Version number. """Version number.
:type: str :type: str

File diff suppressed because it is too large Load Diff

View File

@ -205,6 +205,7 @@ class ConfigurationOption(Provider[Any]):
def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ... def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ...
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ... def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
def from_value(self, value: Any) -> None: ...
class TypedConfigurationOption(Callable[T]): class TypedConfigurationOption(Callable[T]):
@ -241,6 +242,7 @@ class Configuration(Object[Any]):
def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ... def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ...
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ... def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
def from_value(self, value: Any) -> None: ...
class Factory(Provider[T]): class Factory(Provider[T]):

View File

@ -64,6 +64,25 @@ else: # pragma: no cover
copy.deepcopy(obj.im_self, memo), copy.deepcopy(obj.im_self, memo),
obj.im_class) obj.im_class)
config_env_marker_pattern = re.compile(
r'\${(?P<name>[^}^{:]+)(?P<separator>:?)(?P<default>.*?)}',
)
def _resolve_config_env_markers(config_value):
""""Replace environment variable markers with their values."""
for match in reversed(list(config_env_marker_pattern.finditer(config_value))):
has_default = match.group('separator') == ':'
value = os.getenv(match.group('name'))
if value is None:
if not has_default:
continue
value = match.group('default')
span_min, span_max = match.span()
config_value = f'{config_value[:span_min]}{value}{config_value[span_max:]}'
return config_value
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
class EnvInterpolation(iniconfigparser.BasicInterpolation): class EnvInterpolation(iniconfigparser.BasicInterpolation):
@ -71,7 +90,7 @@ if sys.version_info[0] == 3:
def before_get(self, parser, section, option, value, defaults): def before_get(self, parser, section, option, value, defaults):
value = super().before_get(parser, section, option, value, defaults) value = super().before_get(parser, section, option, value, defaults)
return os.path.expandvars(value) return _resolve_config_env_markers(value)
def _parse_ini_file(filepath): def _parse_ini_file(filepath):
parser = iniconfigparser.ConfigParser(interpolation=EnvInterpolation()) parser = iniconfigparser.ConfigParser(interpolation=EnvInterpolation())
@ -84,19 +103,18 @@ else:
def _parse_ini_file(filepath): def _parse_ini_file(filepath):
parser = iniconfigparser.ConfigParser() parser = iniconfigparser.ConfigParser()
with open(filepath) as config_file: with open(filepath) as config_file:
config_string = os.path.expandvars(config_file.read()) config_string = _resolve_config_env_markers(config_file.read())
parser.readfp(StringIO.StringIO(config_string)) parser.readfp(StringIO.StringIO(config_string))
return parser return parser
if yaml: if yaml:
# TODO: use SafeLoader without env interpolation by default in version 5.* # TODO: use SafeLoader without env interpolation by default in version 5.*
yaml_env_marker_pattern = re.compile(r'\$\{([^}^{]+)\}')
def yaml_env_marker_constructor(_, node): def yaml_env_marker_constructor(_, node):
""""Replace environment variable marker with its value.""" """"Replace environment variable marker with its value."""
return os.path.expandvars(node.value) return _resolve_config_env_markers(node.value)
yaml.add_implicit_resolver('!path', yaml_env_marker_pattern) yaml.add_implicit_resolver('!path', config_env_marker_pattern)
yaml.add_constructor('!path', yaml_env_marker_constructor) yaml.add_constructor('!path', yaml_env_marker_constructor)
class YamlLoader(yaml.SafeLoader): class YamlLoader(yaml.SafeLoader):
@ -105,7 +123,7 @@ if yaml:
Inherits ``yaml.SafeLoader`` and add environment variables interpolation. Inherits ``yaml.SafeLoader`` and add environment variables interpolation.
""" """
YamlLoader.add_implicit_resolver('!path', yaml_env_marker_pattern, None) YamlLoader.add_implicit_resolver('!path', config_env_marker_pattern, None)
YamlLoader.add_constructor('!path', yaml_env_marker_constructor) YamlLoader.add_constructor('!path', yaml_env_marker_constructor)
else: else:
class YamlLoader: class YamlLoader:
@ -1680,6 +1698,16 @@ cdef class ConfigurationOption(Provider):
self.override(value) self.override(value)
def from_value(self, value):
"""Load configuration value.
:param value: Configuration value
:type value: object
:rtype: None
"""
self.override(value)
@property @property
def related(self): def related(self):
"""Return related providers generator.""" """Return related providers generator."""
@ -2086,6 +2114,16 @@ cdef class Configuration(Object):
self.override(value) self.override(value)
def from_value(self, value):
"""Load configuration value.
:param value: Configuration value
:type value: object
:rtype: None
"""
self.override(value)
@property @property
def related(self): def related(self):
"""Return related providers generator.""" """Return related providers generator."""

View File

@ -591,17 +591,20 @@ class ConfigFromIniWithEnvInterpolationTests(unittest.TestCase):
self.config = providers.Configuration(name='config') self.config = providers.Configuration(name='config')
os.environ['CONFIG_TEST_ENV'] = 'test-value' os.environ['CONFIG_TEST_ENV'] = 'test-value'
os.environ['CONFIG_TEST_PATH'] = 'test-path'
_, self.config_file = tempfile.mkstemp() _, self.config_file = tempfile.mkstemp()
with open(self.config_file, 'w') as config_file: with open(self.config_file, 'w') as config_file:
config_file.write( config_file.write(
'[section1]\n' '[section1]\n'
'value1=${CONFIG_TEST_ENV}\n' 'value1=${CONFIG_TEST_ENV}\n'
'value2=${CONFIG_TEST_PATH}/path\n'
) )
def tearDown(self): def tearDown(self):
del self.config del self.config
del os.environ['CONFIG_TEST_ENV'] os.environ.pop('CONFIG_TEST_ENV', None)
os.environ.pop('CONFIG_TEST_PATH', None)
os.unlink(self.config_file) os.unlink(self.config_file)
def test_env_variable_interpolation(self): def test_env_variable_interpolation(self):
@ -612,11 +615,67 @@ class ConfigFromIniWithEnvInterpolationTests(unittest.TestCase):
{ {
'section1': { 'section1': {
'value1': 'test-value', 'value1': 'test-value',
'value2': 'test-path/path',
}, },
}, },
) )
self.assertEqual(self.config.section1(), {'value1': 'test-value'}) self.assertEqual(
self.config.section1(),
{
'value1': 'test-value',
'value2': 'test-path/path',
},
)
self.assertEqual(self.config.section1.value1(), 'test-value') self.assertEqual(self.config.section1.value1(), 'test-value')
self.assertEqual(self.config.section1.value2(), 'test-path/path')
def test_missing_envs(self):
del os.environ['CONFIG_TEST_ENV']
del os.environ['CONFIG_TEST_PATH']
self.config.from_ini(self.config_file)
self.assertEqual(
self.config(),
{
'section1': {
'value1': '${CONFIG_TEST_ENV}',
'value2': '${CONFIG_TEST_PATH}/path',
},
},
)
self.assertEqual(
self.config.section1(),
{
'value1': '${CONFIG_TEST_ENV}',
'value2': '${CONFIG_TEST_PATH}/path',
},
)
self.assertEqual(self.config.section1.value1(), '${CONFIG_TEST_ENV}')
self.assertEqual(self.config.section1.value2(), '${CONFIG_TEST_PATH}/path')
def test_default_values(self):
os.environ['DEFINED'] = 'defined'
self.addCleanup(os.environ.pop, 'DEFINED')
with open(self.config_file, 'w') as config_file:
config_file.write(
'[section]\n'
'defined_with_default=${DEFINED:default}\n'
'undefined_with_default=${UNDEFINED:default}\n'
'complex=${DEFINED}/path/${DEFINED:default}/${UNDEFINED}/${UNDEFINED:default}\n'
)
self.config.from_ini(self.config_file)
self.assertEqual(
self.config.section(),
{
'defined_with_default': 'defined',
'undefined_with_default': 'default',
'complex': 'defined/path/defined/${UNDEFINED}/default',
},
)
class ConfigFromYamlTests(unittest.TestCase): class ConfigFromYamlTests(unittest.TestCase):
@ -781,17 +840,20 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
self.config = providers.Configuration(name='config') self.config = providers.Configuration(name='config')
os.environ['CONFIG_TEST_ENV'] = 'test-value' os.environ['CONFIG_TEST_ENV'] = 'test-value'
os.environ['CONFIG_TEST_PATH'] = 'test-path'
_, self.config_file = tempfile.mkstemp() _, self.config_file = tempfile.mkstemp()
with open(self.config_file, 'w') as config_file: with open(self.config_file, 'w') as config_file:
config_file.write( config_file.write(
'section1:\n' 'section1:\n'
' value1: ${CONFIG_TEST_ENV}\n' ' value1: ${CONFIG_TEST_ENV}\n'
' value2: ${CONFIG_TEST_PATH}/path\n'
) )
def tearDown(self): def tearDown(self):
del self.config del self.config
del os.environ['CONFIG_TEST_ENV'] os.environ.pop('CONFIG_TEST_ENV', None)
os.environ.pop('CONFIG_TEST_PATH', None)
os.unlink(self.config_file) os.unlink(self.config_file)
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4') @unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
@ -803,11 +865,69 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
{ {
'section1': { 'section1': {
'value1': 'test-value', 'value1': 'test-value',
'value2': 'test-path/path',
}, },
}, },
) )
self.assertEqual(self.config.section1(), {'value1': 'test-value'}) self.assertEqual(
self.config.section1(),
{
'value1': 'test-value',
'value2': 'test-path/path',
},
)
self.assertEqual(self.config.section1.value1(), 'test-value') self.assertEqual(self.config.section1.value1(), 'test-value')
self.assertEqual(self.config.section1.value2(), 'test-path/path')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_missing_envs(self):
del os.environ['CONFIG_TEST_ENV']
del os.environ['CONFIG_TEST_PATH']
self.config.from_yaml(self.config_file)
self.assertEqual(
self.config(),
{
'section1': {
'value1': '${CONFIG_TEST_ENV}',
'value2': '${CONFIG_TEST_PATH}/path',
},
},
)
self.assertEqual(
self.config.section1(),
{
'value1': '${CONFIG_TEST_ENV}',
'value2': '${CONFIG_TEST_PATH}/path',
},
)
self.assertEqual(self.config.section1.value1(), '${CONFIG_TEST_ENV}')
self.assertEqual(self.config.section1.value2(), '${CONFIG_TEST_PATH}/path')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_default_values(self):
os.environ['DEFINED'] = 'defined'
self.addCleanup(os.environ.pop, 'DEFINED')
with open(self.config_file, 'w') as config_file:
config_file.write(
'section:\n'
' defined_with_default: ${DEFINED:default}\n'
' undefined_with_default: ${UNDEFINED:default}\n'
' complex: ${DEFINED}/path/${DEFINED:default}/${UNDEFINED}/${UNDEFINED:default}\n'
)
self.config.from_yaml(self.config_file)
self.assertEqual(
self.config.section(),
{
'defined_with_default': 'defined',
'undefined_with_default': 'default',
'complex': 'defined/path/defined/${UNDEFINED}/default',
},
)
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4') @unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_option_env_variable_interpolation(self): def test_option_env_variable_interpolation(self):
@ -818,41 +938,47 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
{ {
'section1': { 'section1': {
'value1': 'test-value', 'value1': 'test-value',
'value2': 'test-path/path',
}, },
}, },
) )
self.assertEqual(self.config.option.section1(), {'value1': 'test-value'}) self.assertEqual(
self.config.option.section1(),
{
'value1': 'test-value',
'value2': 'test-path/path',
},
)
self.assertEqual(self.config.option.section1.value1(), 'test-value') self.assertEqual(self.config.option.section1.value1(), 'test-value')
self.assertEqual(self.config.option.section1.value2(), 'test-path/path')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4') @unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_env_variable_interpolation_custom_loader(self): def test_env_variable_interpolation_custom_loader(self):
self.config.from_yaml(self.config_file, loader=yaml.UnsafeLoader) self.config.from_yaml(self.config_file, loader=yaml.UnsafeLoader)
self.assertEqual( self.assertEqual(
self.config(), self.config.section1(),
{ {
'section1': { 'value1': 'test-value',
'value1': 'test-value', 'value2': 'test-path/path',
},
}, },
) )
self.assertEqual(self.config.section1(), {'value1': 'test-value'})
self.assertEqual(self.config.section1.value1(), 'test-value') self.assertEqual(self.config.section1.value1(), 'test-value')
self.assertEqual(self.config.section1.value2(), 'test-path/path')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4') @unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_option_env_variable_interpolation_custom_loader(self): def test_option_env_variable_interpolation_custom_loader(self):
self.config.option.from_yaml(self.config_file, loader=yaml.UnsafeLoader) self.config.option.from_yaml(self.config_file, loader=yaml.UnsafeLoader)
self.assertEqual( self.assertEqual(
self.config.option(), self.config.option.section1(),
{ {
'section1': { 'value1': 'test-value',
'value1': 'test-value', 'value2': 'test-path/path',
},
}, },
) )
self.assertEqual(self.config.option.section1(), {'value1': 'test-value'})
self.assertEqual(self.config.option.section1.value1(), 'test-value') self.assertEqual(self.config.option.section1.value1(), 'test-value')
self.assertEqual(self.config.option.section1.value2(), 'test-path/path')
class ConfigFromPydanticTests(unittest.TestCase): class ConfigFromPydanticTests(unittest.TestCase):
@ -1250,3 +1376,25 @@ class ConfigFromEnvTests(unittest.TestCase):
self.config = providers.Configuration(strict=True) self.config = providers.Configuration(strict=True)
self.config.option.from_env('UNDEFINED_ENV', default='default-value', required=False) self.config.option.from_env('UNDEFINED_ENV', default='default-value', required=False)
self.assertEqual(self.config.option(), 'default-value') self.assertEqual(self.config.option(), 'default-value')
class ConfigFromValueTests(unittest.TestCase):
def setUp(self):
self.config = providers.Configuration(name='config')
def test_from_value(self):
test_value = 123321
self.config.from_value(test_value)
self.assertEqual(self.config(), test_value)
def test_option_from_value(self):
test_value_1 = 123
test_value_2 = 321
self.config.option1.from_value(test_value_1)
self.config.option2.from_value(test_value_2)
self.assertEqual(self.config(), {'option1': test_value_1, 'option2': test_value_2})
self.assertEqual(self.config.option1(), test_value_1)
self.assertEqual(self.config.option2(), test_value_2)