Add option to disable env var interpolation in configs (#861)

This commit is contained in:
ZipFile 2025-02-23 19:01:01 +02:00 committed by GitHub
parent 09efbffab1
commit 7d4ebecd19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 100 additions and 45 deletions

View File

@ -366,6 +366,19 @@ See also: :ref:`configuration-strict-mode`.
assert container.config.section.option() is None assert container.config.section.option() is None
If you want to disable environment variables interpolation, pass ``envs_required=None``:
.. code-block:: yaml
:caption: templates.yml
template_string: 'Hello, ${name}!'
.. code-block:: python
>>> container.config.from_yaml("templates.yml", envs_required=None)
>>> container.config.template_string()
'Hello, ${name}!'
Mandatory and optional sources Mandatory and optional sources
------------------------------ ------------------------------

View File

@ -225,20 +225,20 @@ class ConfigurationOption(Provider[Any]):
self, self,
filepath: Union[Path, str], filepath: Union[Path, str],
required: bool = False, required: bool = False,
envs_required: bool = False, envs_required: Optional[bool] = False,
) -> None: ... ) -> None: ...
def from_yaml( def from_yaml(
self, self,
filepath: Union[Path, str], filepath: Union[Path, str],
required: bool = False, required: bool = False,
loader: Optional[Any] = None, loader: Optional[Any] = None,
envs_required: bool = False, envs_required: Optional[bool] = False,
) -> None: ... ) -> None: ...
def from_json( def from_json(
self, self,
filepath: Union[Path, str], filepath: Union[Path, str],
required: bool = False, required: bool = False,
envs_required: bool = False, envs_required: Optional[bool] = False,
) -> None: ... ) -> None: ...
def from_pydantic( def from_pydantic(
self, settings: PydanticSettings, required: bool = False, **kwargs: Any self, settings: PydanticSettings, required: bool = False, **kwargs: Any

View File

@ -15,6 +15,7 @@ import sys
import threading import threading
import types import types
import warnings import warnings
from configparser import ConfigParser as IniConfigParser
try: try:
import contextvars import contextvars
@ -41,11 +42,6 @@ try:
except ImportError: except ImportError:
_is_coroutine = True _is_coroutine = True
try:
import ConfigParser as iniconfigparser
except ImportError:
import configparser as iniconfigparser
try: try:
import yaml import yaml
except ImportError: except ImportError:
@ -102,7 +98,7 @@ 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_content, envs_required=False): cdef str _resolve_config_env_markers(config_content: str, envs_required: bool):
"""Replace environment variable markers with their values.""" """Replace environment variable markers with their values."""
findings = list(config_env_marker_pattern.finditer(config_content)) findings = list(config_env_marker_pattern.finditer(config_content))
@ -121,28 +117,19 @@ def _resolve_config_env_markers(config_content, envs_required=False):
return config_content return config_content
if sys.version_info[0] == 3: cdef object _parse_ini_file(filepath, envs_required: bool | None):
def _parse_ini_file(filepath, envs_required=False): parser = IniConfigParser()
parser = iniconfigparser.ConfigParser()
with open(filepath) as config_file:
config_string = _resolve_config_env_markers(
config_file.read(),
envs_required=envs_required,
)
parser.read_string(config_string)
return parser
else:
import StringIO
def _parse_ini_file(filepath, envs_required=False): with open(filepath) as config_file:
parser = iniconfigparser.ConfigParser() config_string = config_file.read()
with open(filepath) as config_file:
if envs_required is not None:
config_string = _resolve_config_env_markers( config_string = _resolve_config_env_markers(
config_file.read(), config_string,
envs_required=envs_required, envs_required=envs_required,
) )
parser.readfp(StringIO.StringIO(config_string)) parser.read_string(config_string)
return parser return parser
if yaml: if yaml:
@ -1717,7 +1704,7 @@ cdef class ConfigurationOption(Provider):
try: try:
parser = _parse_ini_file( parser = _parse_ini_file(
filepath, filepath,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
) )
except IOError as exception: except IOError as exception:
if required is not False \ if required is not False \
@ -1776,10 +1763,11 @@ cdef class ConfigurationOption(Provider):
raise raise
return return
config_content = _resolve_config_env_markers( if envs_required is not None:
config_content, config_content = _resolve_config_env_markers(
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), config_content,
) envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = yaml.load(config_content, loader) config = yaml.load(config_content, loader)
current_config = self.__call__() current_config = self.__call__()
@ -1814,10 +1802,11 @@ cdef class ConfigurationOption(Provider):
raise raise
return return
config_content = _resolve_config_env_markers( if envs_required is not None:
config_content, config_content = _resolve_config_env_markers(
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), config_content,
) envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = json.loads(config_content) config = json.loads(config_content)
current_config = self.__call__() current_config = self.__call__()
@ -2270,7 +2259,7 @@ cdef class Configuration(Object):
try: try:
parser = _parse_ini_file( parser = _parse_ini_file(
filepath, filepath,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
) )
except IOError as exception: except IOError as exception:
if required is not False \ if required is not False \
@ -2329,10 +2318,11 @@ cdef class Configuration(Object):
raise raise
return return
config_content = _resolve_config_env_markers( if envs_required is not None:
config_content, config_content = _resolve_config_env_markers(
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), config_content,
) envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = yaml.load(config_content, loader) config = yaml.load(config_content, loader)
current_config = self.__call__() current_config = self.__call__()
@ -2367,10 +2357,11 @@ cdef class Configuration(Object):
raise raise
return return
config_content = _resolve_config_env_markers( if envs_required is not None:
config_content, config_content = _resolve_config_env_markers(
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), config_content,
) envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = json.loads(config_content) config = json.loads(config_content)
current_config = self.__call__() current_config = self.__call__()

View File

@ -5,6 +5,23 @@ import os
from pytest import mark, raises from pytest import mark, raises
def test_no_env_variable_interpolation(config, ini_config_file_3):
config.from_ini(ini_config_file_3, envs_required=None)
assert config() == {
"section1": {
"value1": "${CONFIG_TEST_ENV}",
"value2": "${CONFIG_TEST_PATH}/path",
},
}
assert config.section1() == {
"value1": "${CONFIG_TEST_ENV}",
"value2": "${CONFIG_TEST_PATH}/path",
}
assert config.section1.value1() == "${CONFIG_TEST_ENV}"
assert config.section1.value2() == "${CONFIG_TEST_PATH}/path"
def test_env_variable_interpolation(config, ini_config_file_3): def test_env_variable_interpolation(config, ini_config_file_3):
config.from_ini(ini_config_file_3) config.from_ini(ini_config_file_3)

View File

@ -6,6 +6,23 @@ import os
from pytest import mark, raises from pytest import mark, raises
def test_no_env_variable_interpolation(config, json_config_file_3):
config.from_json(json_config_file_3, envs_required=None)
assert config() == {
"section1": {
"value1": "${CONFIG_TEST_ENV}",
"value2": "${CONFIG_TEST_PATH}/path",
},
}
assert config.section1() == {
"value1": "${CONFIG_TEST_ENV}",
"value2": "${CONFIG_TEST_PATH}/path",
}
assert config.section1.value1() == "${CONFIG_TEST_ENV}"
assert config.section1.value2() == "${CONFIG_TEST_PATH}/path"
def test_env_variable_interpolation(config, json_config_file_3): def test_env_variable_interpolation(config, json_config_file_3):
config.from_json(json_config_file_3) config.from_json(json_config_file_3)

View File

@ -6,6 +6,23 @@ import yaml
from pytest import mark, raises from pytest import mark, raises
def test_no_env_variable_interpolation(config, yaml_config_file_3):
config.from_yaml(yaml_config_file_3, envs_required=None)
assert config() == {
"section1": {
"value1": "${CONFIG_TEST_ENV}",
"value2": "${CONFIG_TEST_PATH}/path",
},
}
assert config.section1() == {
"value1": "${CONFIG_TEST_ENV}",
"value2": "${CONFIG_TEST_PATH}/path",
}
assert config.section1.value1() == "${CONFIG_TEST_ENV}"
assert config.section1.value2() == "${CONFIG_TEST_PATH}/path"
def test_env_variable_interpolation(config, yaml_config_file_3): def test_env_variable_interpolation(config, yaml_config_file_3):
config.from_yaml(yaml_config_file_3) config.from_yaml(yaml_config_file_3)