mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2025-02-11 17:10:57 +03:00
Make prototype with enterpolation before parsing
This commit is contained in:
parent
9abf34cb88
commit
b1d00915fd
File diff suppressed because it is too large
Load Diff
|
@ -201,7 +201,7 @@ class ConfigurationOption(Provider[Any]):
|
||||||
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) -> 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: ...
|
||||||
|
@ -238,7 +238,7 @@ class Configuration(Object[Any]):
|
||||||
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) -> 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,15 +68,18 @@ 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_value, 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_value))
|
||||||
|
|
||||||
|
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()
|
||||||
|
@ -85,17 +88,11 @@ def _resolve_config_env_markers(config_value):
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info[0] == 3:
|
if sys.version_info[0] == 3:
|
||||||
class EnvInterpolation(iniconfigparser.BasicInterpolation):
|
|
||||||
"""Interpolation which expands environment variables in values."""
|
|
||||||
|
|
||||||
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):
|
def _parse_ini_file(filepath):
|
||||||
parser = iniconfigparser.ConfigParser(interpolation=EnvInterpolation())
|
parser = iniconfigparser.ConfigParser()
|
||||||
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())
|
||||||
|
parser.read_string(config_string)
|
||||||
return parser
|
return parser
|
||||||
else:
|
else:
|
||||||
import StringIO
|
import StringIO
|
||||||
|
@ -109,27 +106,16 @@ else:
|
||||||
|
|
||||||
|
|
||||||
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``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -1567,7 +1553,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 +1567,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 +1584,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 +1593,12 @@ cdef class ConfigurationOption(Provider):
|
||||||
raise
|
raise
|
||||||
return
|
return
|
||||||
|
|
||||||
|
config_content = _resolve_config_env_markers(
|
||||||
|
config_content,
|
||||||
|
envs_required=envs_required,
|
||||||
|
)
|
||||||
|
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 = {}
|
||||||
|
@ -1988,7 +1983,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 +1997,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 +2014,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 +2023,12 @@ cdef class Configuration(Object):
|
||||||
raise
|
raise
|
||||||
return
|
return
|
||||||
|
|
||||||
|
config_content = _resolve_config_env_markers(
|
||||||
|
config_content,
|
||||||
|
envs_required=envs_required,
|
||||||
|
)
|
||||||
|
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 = {}
|
||||||
|
|
|
@ -639,20 +639,20 @@ 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_default_values(self):
|
def test_default_values(self):
|
||||||
os.environ['DEFINED'] = 'defined'
|
os.environ['DEFINED'] = 'defined'
|
||||||
|
@ -673,7 +673,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',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -890,20 +890,20 @@ 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')
|
@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 +925,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',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user