mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2024-11-22 09:36:48 +03:00
463 Config environment variables interpolation required and nones (#467)
* Make prototype with enterpolation before parsing * Add test for option.from_yaml() with missing env not required * Make some cosmetic changes to _resolve_config_env_markers() * Add test for option.from_ini() missing envs not required * Skip schema test cause it requires internet connection * Add tests for .from_yaml() for config and config option * Add tests for .from_ini() for config and config option * Add example for os.environ.setdefault() and envs interpolation * Add/update docs on environment variables interpolation * Update changelog
This commit is contained in:
parent
9abf34cb88
commit
ef049daae5
|
@ -7,6 +7,19 @@ 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 option ``envs_required`` for configuration provider ``.from_yaml()`` and ``.from_ini()``
|
||||||
|
methods. With ``envs_required=True`` methods ``.from_yaml()`` and ``.from_ini()`` raise
|
||||||
|
an exception when encounter an undefined environment variable in the configuration file.
|
||||||
|
By default this option is set to false for preserving previous behavior ``envs_required=False``.
|
||||||
|
- Add raising of an exception in configuration provider strict mode when provider encounters
|
||||||
|
an undefined environment variable in the configuration file.
|
||||||
|
- Update configuration provider environment variables interpolation to replace
|
||||||
|
undefined environment variables with an empty value.
|
||||||
|
- Update configuration provider to perform environment variables interpolation before passing
|
||||||
|
configuration file content to the parser.
|
||||||
|
|
||||||
4.33.0
|
4.33.0
|
||||||
------
|
------
|
||||||
- Add support of default value for environment variable in INI and YAML
|
- Add support of default value for environment variable in INI and YAML
|
||||||
|
|
|
@ -5,11 +5,14 @@ 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,Default,Load,Read,Get
|
Option,Ini,Json,Yaml,Pydantic,Dict,Environment Variable Interpolation,
|
||||||
|
Environment Variable Substitution,Environment Variable in Config,
|
||||||
|
Environment Variable in YAML file,Environment Variable in INI file,Default,Load,Read
|
||||||
: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,
|
||||||
or a pydantic settings object.
|
or a pydantic settings object. This page also describes how to substitute (interpolate)
|
||||||
|
environment variables in YAML and INI configuration files.
|
||||||
|
|
||||||
.. currentmodule:: dependency_injector.providers
|
.. currentmodule:: dependency_injector.providers
|
||||||
|
|
||||||
|
@ -42,12 +45,7 @@ where ``examples/providers/configuration/config.ini`` is:
|
||||||
.. literalinclude:: ../../examples/providers/configuration/config.ini
|
.. literalinclude:: ../../examples/providers/configuration/config.ini
|
||||||
: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.
|
||||||
``${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
|
.. code-block:: ini
|
||||||
|
|
||||||
|
@ -56,6 +54,8 @@ variable ``ENV_NAME`` is undefined, configuration provider will substitute value
|
||||||
option2 = {$ENV_VAR}/path
|
option2 = {$ENV_VAR}/path
|
||||||
option3 = {$ENV_VAR:default}
|
option3 = {$ENV_VAR:default}
|
||||||
|
|
||||||
|
See also: :ref:`configuration-envs-interpolation`.
|
||||||
|
|
||||||
Loading from a YAML file
|
Loading from a YAML file
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
@ -72,12 +72,7 @@ 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
|
:py:meth:`Configuration.from_yaml` method supports environment variables interpolation.
|
||||||
``${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
|
.. code-block:: ini
|
||||||
|
|
||||||
|
@ -86,6 +81,8 @@ variable ``ENV_NAME`` is undefined, configuration provider will substitute value
|
||||||
option2: {$ENV_VAR}/path
|
option2: {$ENV_VAR}/path
|
||||||
option3: {$ENV_VAR:default}
|
option3: {$ENV_VAR:default}
|
||||||
|
|
||||||
|
See also: :ref:`configuration-envs-interpolation`.
|
||||||
|
|
||||||
: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:
|
To use another loader use ``loader`` argument:
|
||||||
|
|
||||||
|
@ -191,6 +188,73 @@ where ``examples/providers/configuration/config.local.yml`` is:
|
||||||
.. literalinclude:: ../../examples/providers/configuration/config.local.yml
|
.. literalinclude:: ../../examples/providers/configuration/config.local.yml
|
||||||
:language: ini
|
:language: ini
|
||||||
|
|
||||||
|
.. _configuration-envs-interpolation:
|
||||||
|
|
||||||
|
Using environment variables in configuration files
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
``Configuration`` provider supports environment variables interpolation in configuration files.
|
||||||
|
Use ``${ENV_NAME}`` in the configuration file to substitute value from environment
|
||||||
|
variable ``ENV_NAME``.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
section:
|
||||||
|
option: ${ENV_NAME}
|
||||||
|
|
||||||
|
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]
|
||||||
|
option = {$ENV_NAME:default}
|
||||||
|
|
||||||
|
If you'd like to specify a default value for environment variable inside of the application you can use
|
||||||
|
``os.environ.setdefault()``.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/providers/configuration/configuration_env_interpolation_os_default.py
|
||||||
|
:language: python
|
||||||
|
:lines: 3-
|
||||||
|
:emphasize-lines: 12
|
||||||
|
|
||||||
|
If environment variable is undefined and doesn't have a default, ``Configuration`` provider
|
||||||
|
will replace it with an empty value. This is a default behavior. To raise an error on
|
||||||
|
undefined environment variable that doesn't have a default value, pass argument
|
||||||
|
``envs_required=True`` to a configuration reading method:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
container.config.from_yaml('config.yml', envs_required=True)
|
||||||
|
|
||||||
|
See also: :ref:`configuration-strict-mode`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
``Configuration`` provider makes environment variables interpolation before parsing. This preserves
|
||||||
|
original parser behavior. For instance, undefined environment variable in YAML configuration file
|
||||||
|
will be replaced with an empty value and then YAML parser will load the file.
|
||||||
|
|
||||||
|
Original configuration file:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
section:
|
||||||
|
option: ${ENV_NAME}
|
||||||
|
|
||||||
|
Configuration file after interpolation where ``ENV_NAME`` is undefined:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
section:
|
||||||
|
option:
|
||||||
|
|
||||||
|
Configuration provider after parsing interpolated YAML file contains ``None`` in
|
||||||
|
option ``section.option``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
assert container.config.section.option() is None
|
||||||
|
|
||||||
Mandatory and optional sources
|
Mandatory and optional sources
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
|
@ -310,6 +374,21 @@ configuration data is undefined:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
Environment variables interpolation in strict mode raises an exception when encounters
|
||||||
|
an undefined environment variable without a default value.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
section:
|
||||||
|
option: {$UNDEFINED}
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
try:
|
||||||
|
container.config.from_yaml('undefined_env.yml') # raise exception
|
||||||
|
except ValueError:
|
||||||
|
...
|
||||||
|
|
||||||
You can override ``.from_*()`` methods behaviour in strict mode using ``required`` argument:
|
You can override ``.from_*()`` methods behaviour in strict mode using ``required`` argument:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
2
examples/providers/configuration/config-with-env-var.yml
Normal file
2
examples/providers/configuration/config-with-env-var.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
section:
|
||||||
|
option: ${ENV_VAR}
|
|
@ -0,0 +1,19 @@
|
||||||
|
"""`Configuration` provider values loading example."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dependency_injector import containers, providers
|
||||||
|
|
||||||
|
|
||||||
|
class Container(containers.DeclarativeContainer):
|
||||||
|
|
||||||
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('ENV_VAR', 'default value')
|
||||||
|
|
||||||
|
container = Container()
|
||||||
|
container.config.from_yaml('config-with-env-var.yml')
|
||||||
|
|
||||||
|
assert container.config.section.option() == 'default value'
|
File diff suppressed because it is too large
Load Diff
|
@ -200,8 +200,8 @@ class ConfigurationOption(Provider[Any]):
|
||||||
def required(self) -> ConfigurationOption: ...
|
def required(self) -> ConfigurationOption: ...
|
||||||
def is_required(self) -> bool: ...
|
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], required: bool = False) -> None: ...
|
def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ...
|
||||||
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any]=None) -> None: ...
|
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ...
|
||||||
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: ...
|
||||||
|
@ -237,8 +237,8 @@ class Configuration(Object[Any]):
|
||||||
def set(self, selector: str, value: Any) -> OverridingContext[P]: ...
|
def set(self, selector: str, value: Any) -> OverridingContext[P]: ...
|
||||||
def reset_cache(self) -> None: ...
|
def reset_cache(self) -> None: ...
|
||||||
def update(self, value: Any) -> None: ...
|
def update(self, value: Any) -> None: ...
|
||||||
def from_ini(self, filepath: Union[Path, str], required: bool = False) -> None: ...
|
def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ...
|
||||||
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any]=None) -> None: ...
|
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ...
|
||||||
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: ...
|
||||||
|
|
|
@ -68,68 +68,60 @@ config_env_marker_pattern = re.compile(
|
||||||
r'\${(?P<name>[^}^{:]+)(?P<separator>:?)(?P<default>.*?)}',
|
r'\${(?P<name>[^}^{:]+)(?P<separator>:?)(?P<default>.*?)}',
|
||||||
)
|
)
|
||||||
|
|
||||||
def _resolve_config_env_markers(config_value):
|
def _resolve_config_env_markers(config_content, envs_required=False):
|
||||||
""""Replace environment variable markers with their values."""
|
"""Replace environment variable markers with their values."""
|
||||||
for match in reversed(list(config_env_marker_pattern.finditer(config_value))):
|
findings = list(config_env_marker_pattern.finditer(config_content))
|
||||||
|
|
||||||
|
for match in reversed(findings):
|
||||||
|
env_name = match.group('name')
|
||||||
has_default = match.group('separator') == ':'
|
has_default = match.group('separator') == ':'
|
||||||
|
|
||||||
value = os.getenv(match.group('name'))
|
value = os.getenv(env_name)
|
||||||
if value is None:
|
if value is None:
|
||||||
if not has_default:
|
if not has_default and envs_required:
|
||||||
continue
|
raise ValueError(f'Missing required environment variable "{env_name}"')
|
||||||
value = match.group('default')
|
value = match.group('default')
|
||||||
|
|
||||||
span_min, span_max = match.span()
|
span_min, span_max = match.span()
|
||||||
config_value = f'{config_value[:span_min]}{value}{config_value[span_max:]}'
|
config_content = f'{config_content[:span_min]}{value}{config_content[span_max:]}'
|
||||||
return config_value
|
return config_content
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info[0] == 3:
|
if sys.version_info[0] == 3:
|
||||||
class EnvInterpolation(iniconfigparser.BasicInterpolation):
|
def _parse_ini_file(filepath, envs_required=False):
|
||||||
"""Interpolation which expands environment variables in values."""
|
parser = iniconfigparser.ConfigParser()
|
||||||
|
|
||||||
def before_get(self, parser, section, option, value, defaults):
|
|
||||||
value = super().before_get(parser, section, option, value, defaults)
|
|
||||||
return _resolve_config_env_markers(value)
|
|
||||||
|
|
||||||
def _parse_ini_file(filepath):
|
|
||||||
parser = iniconfigparser.ConfigParser(interpolation=EnvInterpolation())
|
|
||||||
with open(filepath) as config_file:
|
with open(filepath) as config_file:
|
||||||
parser.read_file(config_file)
|
config_string = _resolve_config_env_markers(
|
||||||
|
config_file.read(),
|
||||||
|
envs_required=envs_required,
|
||||||
|
)
|
||||||
|
parser.read_string(config_string)
|
||||||
return parser
|
return parser
|
||||||
else:
|
else:
|
||||||
import StringIO
|
import StringIO
|
||||||
|
|
||||||
def _parse_ini_file(filepath):
|
def _parse_ini_file(filepath, envs_required=False):
|
||||||
parser = iniconfigparser.ConfigParser()
|
parser = iniconfigparser.ConfigParser()
|
||||||
with open(filepath) as config_file:
|
with open(filepath) as config_file:
|
||||||
config_string = _resolve_config_env_markers(config_file.read())
|
config_string = _resolve_config_env_markers(
|
||||||
|
config_file.read(),
|
||||||
|
envs_required=envs_required,
|
||||||
|
)
|
||||||
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.*
|
|
||||||
def yaml_env_marker_constructor(_, node):
|
|
||||||
""""Replace environment variable marker with its value."""
|
|
||||||
return _resolve_config_env_markers(node.value)
|
|
||||||
|
|
||||||
yaml.add_implicit_resolver('!path', config_env_marker_pattern)
|
|
||||||
yaml.add_constructor('!path', yaml_env_marker_constructor)
|
|
||||||
|
|
||||||
class YamlLoader(yaml.SafeLoader):
|
class YamlLoader(yaml.SafeLoader):
|
||||||
"""Custom YAML loader.
|
"""YAML loader.
|
||||||
|
|
||||||
Inherits ``yaml.SafeLoader`` and add environment variables interpolation.
|
This loader mimics ``yaml.SafeLoader``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
YamlLoader.add_implicit_resolver('!path', config_env_marker_pattern, None)
|
|
||||||
YamlLoader.add_constructor('!path', yaml_env_marker_constructor)
|
|
||||||
else:
|
else:
|
||||||
class YamlLoader:
|
class YamlLoader:
|
||||||
"""Custom YAML loader.
|
"""YAML loader.
|
||||||
|
|
||||||
Inherits ``yaml.SafeLoader`` and add environment variables interpolation.
|
This loader mimics ``yaml.SafeLoader``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -1535,7 +1527,7 @@ cdef class ConfigurationOption(Provider):
|
||||||
"""
|
"""
|
||||||
self.override(value)
|
self.override(value)
|
||||||
|
|
||||||
def from_ini(self, filepath, required=UNDEFINED):
|
def from_ini(self, filepath, required=UNDEFINED, envs_required=False):
|
||||||
"""Load configuration from the ini file.
|
"""Load configuration from the ini file.
|
||||||
|
|
||||||
Loaded configuration is merged recursively over existing configuration.
|
Loaded configuration is merged recursively over existing configuration.
|
||||||
|
@ -1546,10 +1538,16 @@ cdef class ConfigurationOption(Provider):
|
||||||
:param required: When required is True, raise an exception if file does not exist.
|
:param required: When required is True, raise an exception if file does not exist.
|
||||||
:type required: bool
|
:type required: bool
|
||||||
|
|
||||||
|
:param envs_required: When True, raises an error on undefined environment variable.
|
||||||
|
:type envs_required: bool
|
||||||
|
|
||||||
:rtype: None
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
parser = _parse_ini_file(filepath)
|
parser = _parse_ini_file(
|
||||||
|
filepath,
|
||||||
|
envs_required=envs_required or self._is_strict_mode_enabled(),
|
||||||
|
)
|
||||||
except IOError as exception:
|
except IOError as exception:
|
||||||
if required is not False \
|
if required is not False \
|
||||||
and (self._is_strict_mode_enabled() or required is True) \
|
and (self._is_strict_mode_enabled() or required is True) \
|
||||||
|
@ -1567,7 +1565,7 @@ cdef class ConfigurationOption(Provider):
|
||||||
current_config = {}
|
current_config = {}
|
||||||
self.override(merge_dicts(current_config, config))
|
self.override(merge_dicts(current_config, config))
|
||||||
|
|
||||||
def from_yaml(self, filepath, required=UNDEFINED, loader=None):
|
def from_yaml(self, filepath, required=UNDEFINED, loader=None, envs_required=False):
|
||||||
"""Load configuration from the yaml file.
|
"""Load configuration from the yaml file.
|
||||||
|
|
||||||
Loaded configuration is merged recursively over existing configuration.
|
Loaded configuration is merged recursively over existing configuration.
|
||||||
|
@ -1581,6 +1579,9 @@ cdef class ConfigurationOption(Provider):
|
||||||
:param loader: YAML loader, :py:class:`YamlLoader` is used if not specified.
|
:param loader: YAML loader, :py:class:`YamlLoader` is used if not specified.
|
||||||
:type loader: ``yaml.Loader``
|
:type loader: ``yaml.Loader``
|
||||||
|
|
||||||
|
:param envs_required: When True, raises an error on undefined environment variable.
|
||||||
|
:type envs_required: bool
|
||||||
|
|
||||||
:rtype: None
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
if yaml is None:
|
if yaml is None:
|
||||||
|
@ -1595,7 +1596,7 @@ cdef class ConfigurationOption(Provider):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(filepath) as opened_file:
|
with open(filepath) as opened_file:
|
||||||
config = yaml.load(opened_file, loader)
|
config_content = opened_file.read()
|
||||||
except IOError as exception:
|
except IOError as exception:
|
||||||
if required is not False \
|
if required is not False \
|
||||||
and (self._is_strict_mode_enabled() or required is True) \
|
and (self._is_strict_mode_enabled() or required is True) \
|
||||||
|
@ -1604,6 +1605,12 @@ cdef class ConfigurationOption(Provider):
|
||||||
raise
|
raise
|
||||||
return
|
return
|
||||||
|
|
||||||
|
config_content = _resolve_config_env_markers(
|
||||||
|
config_content,
|
||||||
|
envs_required=envs_required or self._is_strict_mode_enabled(),
|
||||||
|
)
|
||||||
|
config = yaml.load(config_content, loader)
|
||||||
|
|
||||||
current_config = self.__call__()
|
current_config = self.__call__()
|
||||||
if not current_config:
|
if not current_config:
|
||||||
current_config = {}
|
current_config = {}
|
||||||
|
@ -1956,7 +1963,7 @@ cdef class Configuration(Object):
|
||||||
"""
|
"""
|
||||||
self.override(value)
|
self.override(value)
|
||||||
|
|
||||||
def from_ini(self, filepath, required=UNDEFINED):
|
def from_ini(self, filepath, required=UNDEFINED, envs_required=False):
|
||||||
"""Load configuration from the ini file.
|
"""Load configuration from the ini file.
|
||||||
|
|
||||||
Loaded configuration is merged recursively over existing configuration.
|
Loaded configuration is merged recursively over existing configuration.
|
||||||
|
@ -1967,10 +1974,16 @@ cdef class Configuration(Object):
|
||||||
:param required: When required is True, raise an exception if file does not exist.
|
:param required: When required is True, raise an exception if file does not exist.
|
||||||
:type required: bool
|
:type required: bool
|
||||||
|
|
||||||
|
:param envs_required: When True, raises an error on undefined environment variable.
|
||||||
|
:type envs_required: bool
|
||||||
|
|
||||||
:rtype: None
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
parser = _parse_ini_file(filepath)
|
parser = _parse_ini_file(
|
||||||
|
filepath,
|
||||||
|
envs_required=envs_required or self._is_strict_mode_enabled(),
|
||||||
|
)
|
||||||
except IOError as exception:
|
except IOError as exception:
|
||||||
if required is not False \
|
if required is not False \
|
||||||
and (self._is_strict_mode_enabled() or required is True) \
|
and (self._is_strict_mode_enabled() or required is True) \
|
||||||
|
@ -1988,7 +2001,7 @@ cdef class Configuration(Object):
|
||||||
current_config = {}
|
current_config = {}
|
||||||
self.override(merge_dicts(current_config, config))
|
self.override(merge_dicts(current_config, config))
|
||||||
|
|
||||||
def from_yaml(self, filepath, required=UNDEFINED, loader=None):
|
def from_yaml(self, filepath, required=UNDEFINED, loader=None, envs_required=False):
|
||||||
"""Load configuration from the yaml file.
|
"""Load configuration from the yaml file.
|
||||||
|
|
||||||
Loaded configuration is merged recursively over existing configuration.
|
Loaded configuration is merged recursively over existing configuration.
|
||||||
|
@ -2002,6 +2015,9 @@ cdef class Configuration(Object):
|
||||||
:param loader: YAML loader, :py:class:`YamlLoader` is used if not specified.
|
:param loader: YAML loader, :py:class:`YamlLoader` is used if not specified.
|
||||||
:type loader: ``yaml.Loader``
|
:type loader: ``yaml.Loader``
|
||||||
|
|
||||||
|
:param envs_required: When True, raises an error on undefined environment variable.
|
||||||
|
:type envs_required: bool
|
||||||
|
|
||||||
:rtype: None
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
if yaml is None:
|
if yaml is None:
|
||||||
|
@ -2016,7 +2032,7 @@ cdef class Configuration(Object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(filepath) as opened_file:
|
with open(filepath) as opened_file:
|
||||||
config = yaml.load(opened_file, loader)
|
config_content = opened_file.read()
|
||||||
except IOError as exception:
|
except IOError as exception:
|
||||||
if required is not False \
|
if required is not False \
|
||||||
and (self._is_strict_mode_enabled() or required is True) \
|
and (self._is_strict_mode_enabled() or required is True) \
|
||||||
|
@ -2025,6 +2041,12 @@ cdef class Configuration(Object):
|
||||||
raise
|
raise
|
||||||
return
|
return
|
||||||
|
|
||||||
|
config_content = _resolve_config_env_markers(
|
||||||
|
config_content,
|
||||||
|
envs_required=envs_required or self._is_strict_mode_enabled(),
|
||||||
|
)
|
||||||
|
config = yaml.load(config_content, loader)
|
||||||
|
|
||||||
current_config = self.__call__()
|
current_config = self.__call__()
|
||||||
if not current_config:
|
if not current_config:
|
||||||
current_config = {}
|
current_config = {}
|
||||||
|
|
|
@ -629,7 +629,7 @@ class ConfigFromIniWithEnvInterpolationTests(unittest.TestCase):
|
||||||
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')
|
self.assertEqual(self.config.section1.value2(), 'test-path/path')
|
||||||
|
|
||||||
def test_missing_envs(self):
|
def test_missing_envs_not_required(self):
|
||||||
del os.environ['CONFIG_TEST_ENV']
|
del os.environ['CONFIG_TEST_ENV']
|
||||||
del os.environ['CONFIG_TEST_PATH']
|
del os.environ['CONFIG_TEST_PATH']
|
||||||
|
|
||||||
|
@ -639,20 +639,107 @@ class ConfigFromIniWithEnvInterpolationTests(unittest.TestCase):
|
||||||
self.config(),
|
self.config(),
|
||||||
{
|
{
|
||||||
'section1': {
|
'section1': {
|
||||||
'value1': '${CONFIG_TEST_ENV}',
|
'value1': '',
|
||||||
'value2': '${CONFIG_TEST_PATH}/path',
|
'value2': '/path',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.config.section1(),
|
self.config.section1(),
|
||||||
{
|
{
|
||||||
'value1': '${CONFIG_TEST_ENV}',
|
'value1': '',
|
||||||
'value2': '${CONFIG_TEST_PATH}/path',
|
'value2': '/path',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(self.config.section1.value1(), '${CONFIG_TEST_ENV}')
|
self.assertEqual(self.config.section1.value1(), '')
|
||||||
self.assertEqual(self.config.section1.value2(), '${CONFIG_TEST_PATH}/path')
|
self.assertEqual(self.config.section1.value2(), '/path')
|
||||||
|
|
||||||
|
def test_missing_envs_required(self):
|
||||||
|
with open(self.config_file, 'w') as config_file:
|
||||||
|
config_file.write(
|
||||||
|
'[section]\n'
|
||||||
|
'undefined=${UNDEFINED}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.config.from_ini(self.config_file, envs_required=True)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(context.exception),
|
||||||
|
'Missing required environment variable "UNDEFINED"',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_missing_envs_strict_mode(self):
|
||||||
|
with open(self.config_file, 'w') as config_file:
|
||||||
|
config_file.write(
|
||||||
|
'[section]\n'
|
||||||
|
'undefined=${UNDEFINED}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.config.set_strict(True)
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.config.from_ini(self.config_file)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(context.exception),
|
||||||
|
'Missing required environment variable "UNDEFINED"',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_option_missing_envs_not_required(self):
|
||||||
|
del os.environ['CONFIG_TEST_ENV']
|
||||||
|
del os.environ['CONFIG_TEST_PATH']
|
||||||
|
|
||||||
|
self.config.option.from_ini(self.config_file)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.config.option(),
|
||||||
|
{
|
||||||
|
'section1': {
|
||||||
|
'value1': '',
|
||||||
|
'value2': '/path',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.config.option.section1(),
|
||||||
|
{
|
||||||
|
'value1': '',
|
||||||
|
'value2': '/path',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(self.config.option.section1.value1(), '')
|
||||||
|
self.assertEqual(self.config.option.section1.value2(), '/path')
|
||||||
|
|
||||||
|
def test_option_missing_envs_required(self):
|
||||||
|
with open(self.config_file, 'w') as config_file:
|
||||||
|
config_file.write(
|
||||||
|
'[section]\n'
|
||||||
|
'undefined=${UNDEFINED}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.config.option.from_ini(self.config_file, envs_required=True)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(context.exception),
|
||||||
|
'Missing required environment variable "UNDEFINED"',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_option_missing_envs_strict_mode(self):
|
||||||
|
with open(self.config_file, 'w') as config_file:
|
||||||
|
config_file.write(
|
||||||
|
'[section]\n'
|
||||||
|
'undefined=${UNDEFINED}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.config.set_strict(True)
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.config.option.from_ini(self.config_file)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(context.exception),
|
||||||
|
'Missing required environment variable "UNDEFINED"',
|
||||||
|
)
|
||||||
|
|
||||||
def test_default_values(self):
|
def test_default_values(self):
|
||||||
os.environ['DEFINED'] = 'defined'
|
os.environ['DEFINED'] = 'defined'
|
||||||
|
@ -673,7 +760,7 @@ class ConfigFromIniWithEnvInterpolationTests(unittest.TestCase):
|
||||||
{
|
{
|
||||||
'defined_with_default': 'defined',
|
'defined_with_default': 'defined',
|
||||||
'undefined_with_default': 'default',
|
'undefined_with_default': 'default',
|
||||||
'complex': 'defined/path/defined/${UNDEFINED}/default',
|
'complex': 'defined/path/defined//default',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -880,7 +967,7 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
|
||||||
self.assertEqual(self.config.section1.value2(), 'test-path/path')
|
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_missing_envs(self):
|
def test_missing_envs_not_required(self):
|
||||||
del os.environ['CONFIG_TEST_ENV']
|
del os.environ['CONFIG_TEST_ENV']
|
||||||
del os.environ['CONFIG_TEST_PATH']
|
del os.environ['CONFIG_TEST_PATH']
|
||||||
|
|
||||||
|
@ -890,20 +977,112 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
|
||||||
self.config(),
|
self.config(),
|
||||||
{
|
{
|
||||||
'section1': {
|
'section1': {
|
||||||
'value1': '${CONFIG_TEST_ENV}',
|
'value1': None,
|
||||||
'value2': '${CONFIG_TEST_PATH}/path',
|
'value2': '/path',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.config.section1(),
|
self.config.section1(),
|
||||||
{
|
{
|
||||||
'value1': '${CONFIG_TEST_ENV}',
|
'value1': None,
|
||||||
'value2': '${CONFIG_TEST_PATH}/path',
|
'value2': '/path',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(self.config.section1.value1(), '${CONFIG_TEST_ENV}')
|
self.assertIsNone(self.config.section1.value1())
|
||||||
self.assertEqual(self.config.section1.value2(), '${CONFIG_TEST_PATH}/path')
|
self.assertEqual(self.config.section1.value2(), '/path')
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
|
||||||
|
def test_missing_envs_required(self):
|
||||||
|
with open(self.config_file, 'w') as config_file:
|
||||||
|
config_file.write(
|
||||||
|
'section:\n'
|
||||||
|
' undefined: ${UNDEFINED}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.config.from_yaml(self.config_file, envs_required=True)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(context.exception),
|
||||||
|
'Missing required environment variable "UNDEFINED"',
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
|
||||||
|
def test_missing_envs_strict_mode(self):
|
||||||
|
with open(self.config_file, 'w') as config_file:
|
||||||
|
config_file.write(
|
||||||
|
'section:\n'
|
||||||
|
' undefined: ${UNDEFINED}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.config.set_strict(True)
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.config.from_yaml(self.config_file)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(context.exception),
|
||||||
|
'Missing required environment variable "UNDEFINED"',
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
|
||||||
|
def test_option_missing_envs_not_required(self):
|
||||||
|
del os.environ['CONFIG_TEST_ENV']
|
||||||
|
del os.environ['CONFIG_TEST_PATH']
|
||||||
|
|
||||||
|
self.config.option.from_yaml(self.config_file)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.config.option(),
|
||||||
|
{
|
||||||
|
'section1': {
|
||||||
|
'value1': None,
|
||||||
|
'value2': '/path',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.config.option.section1(),
|
||||||
|
{
|
||||||
|
'value1': None,
|
||||||
|
'value2': '/path',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertIsNone(self.config.option.section1.value1())
|
||||||
|
self.assertEqual(self.config.option.section1.value2(), '/path')
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
|
||||||
|
def test_option_missing_envs_required(self):
|
||||||
|
with open(self.config_file, 'w') as config_file:
|
||||||
|
config_file.write(
|
||||||
|
'section:\n'
|
||||||
|
' undefined: ${UNDEFINED}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.config.option.from_yaml(self.config_file, envs_required=True)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(context.exception),
|
||||||
|
'Missing required environment variable "UNDEFINED"',
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
|
||||||
|
def test_option_missing_envs_strict_mode(self):
|
||||||
|
with open(self.config_file, 'w') as config_file:
|
||||||
|
config_file.write(
|
||||||
|
'section:\n'
|
||||||
|
' undefined: ${UNDEFINED}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.config.set_strict(True)
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.config.option.from_yaml(self.config_file)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(context.exception),
|
||||||
|
'Missing required environment variable "UNDEFINED"',
|
||||||
|
)
|
||||||
|
|
||||||
@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_default_values(self):
|
def test_default_values(self):
|
||||||
|
@ -925,7 +1104,7 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
|
||||||
{
|
{
|
||||||
'defined_with_default': 'defined',
|
'defined_with_default': 'defined',
|
||||||
'undefined_with_default': 'default',
|
'undefined_with_default': 'default',
|
||||||
'complex': 'defined/path/defined/${UNDEFINED}/default',
|
'complex': 'defined/path/defined//default',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -277,6 +277,7 @@ class TestSchemaMultipleContainersWithInlineProviders(unittest.TestCase):
|
||||||
|
|
||||||
class TestSchemaBoto3Session(unittest.TestCase):
|
class TestSchemaBoto3Session(unittest.TestCase):
|
||||||
|
|
||||||
|
@unittest.skip('Boto3 tries to connect to the internet')
|
||||||
def test(self):
|
def test(self):
|
||||||
container = containers.DynamicContainer()
|
container = containers.DynamicContainer()
|
||||||
container.from_yaml_schema(f'{_SAMPLES_DIR}/schemasample/container-boto3-session.yml')
|
container.from_yaml_schema(f'{_SAMPLES_DIR}/schemasample/container-boto3-session.yml')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user