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
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
------
- This fix a typo in ``di_in_python.rst`` doc.

View File

@ -5,7 +5,7 @@ Configuration provider
.. meta::
: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
demonstrates how to use Configuration provider to inject the dependencies, load
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
:py:meth:`Configuration.from_ini` method supports environment variables interpolation. Use
``${ENV_NAME}`` format in the configuration file to substitute value of the environment
variable ``ENV_NAME``.
``${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}
Loading from a YAML file
------------------------
@ -62,12 +72,22 @@ where ``examples/providers/configuration/config.yml`` is:
.. literalinclude:: ../../examples/providers/configuration/config.yml
: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``.
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:
To use another loader use ``loader`` argument:
.. code-block:: python
@ -144,6 +164,17 @@ Loading from an environment variable
:lines: 3-
: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
---------------------------------

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."""
__version__ = '4.32.3'
__version__ = '4.33.0'
"""Version number.
: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_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_value(self, value: Any) -> None: ...
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_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_value(self, value: Any) -> None: ...
class Factory(Provider[T]):

View File

@ -64,6 +64,25 @@ else: # pragma: no cover
copy.deepcopy(obj.im_self, memo),
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:
class EnvInterpolation(iniconfigparser.BasicInterpolation):
@ -71,7 +90,7 @@ if sys.version_info[0] == 3:
def before_get(self, 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):
parser = iniconfigparser.ConfigParser(interpolation=EnvInterpolation())
@ -84,19 +103,18 @@ else:
def _parse_ini_file(filepath):
parser = iniconfigparser.ConfigParser()
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))
return parser
if yaml:
# TODO: use SafeLoader without env interpolation by default in version 5.*
yaml_env_marker_pattern = re.compile(r'\$\{([^}^{]+)\}')
def yaml_env_marker_constructor(_, node):
""""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)
class YamlLoader(yaml.SafeLoader):
@ -105,7 +123,7 @@ if yaml:
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)
else:
class YamlLoader:
@ -1680,6 +1698,16 @@ cdef class ConfigurationOption(Provider):
self.override(value)
def from_value(self, value):
"""Load configuration value.
:param value: Configuration value
:type value: object
:rtype: None
"""
self.override(value)
@property
def related(self):
"""Return related providers generator."""
@ -2086,6 +2114,16 @@ cdef class Configuration(Object):
self.override(value)
def from_value(self, value):
"""Load configuration value.
:param value: Configuration value
:type value: object
:rtype: None
"""
self.override(value)
@property
def related(self):
"""Return related providers generator."""

View File

@ -591,17 +591,20 @@ class ConfigFromIniWithEnvInterpolationTests(unittest.TestCase):
self.config = providers.Configuration(name='config')
os.environ['CONFIG_TEST_ENV'] = 'test-value'
os.environ['CONFIG_TEST_PATH'] = 'test-path'
_, self.config_file = tempfile.mkstemp()
with open(self.config_file, 'w') as config_file:
config_file.write(
'[section1]\n'
'value1=${CONFIG_TEST_ENV}\n'
'value2=${CONFIG_TEST_PATH}/path\n'
)
def tearDown(self):
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)
def test_env_variable_interpolation(self):
@ -612,11 +615,67 @@ class ConfigFromIniWithEnvInterpolationTests(unittest.TestCase):
{
'section1': {
'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.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):
@ -781,17 +840,20 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
self.config = providers.Configuration(name='config')
os.environ['CONFIG_TEST_ENV'] = 'test-value'
os.environ['CONFIG_TEST_PATH'] = 'test-path'
_, self.config_file = tempfile.mkstemp()
with open(self.config_file, 'w') as config_file:
config_file.write(
'section1:\n'
' value1: ${CONFIG_TEST_ENV}\n'
' value2: ${CONFIG_TEST_PATH}/path\n'
)
def tearDown(self):
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)
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
@ -803,11 +865,69 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
{
'section1': {
'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.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')
def test_option_env_variable_interpolation(self):
@ -818,41 +938,47 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
{
'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',
'value2': 'test-path/path',
},
)
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')
def test_env_variable_interpolation_custom_loader(self):
self.config.from_yaml(self.config_file, loader=yaml.UnsafeLoader)
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.value2(), 'test-path/path')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_option_env_variable_interpolation_custom_loader(self):
self.config.option.from_yaml(self.config_file, loader=yaml.UnsafeLoader)
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.value2(), 'test-path/path')
class ConfigFromPydanticTests(unittest.TestCase):
@ -1250,3 +1376,25 @@ class ConfigFromEnvTests(unittest.TestCase):
self.config = providers.Configuration(strict=True)
self.config.option.from_env('UNDEFINED_ENV', default='default-value', required=False)
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)