Skip to content

Add option to disable env var interpolation in configs #861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 23, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/providers/configuration.rst
Original file line number Diff line number Diff line change
@@ -366,6 +366,19 @@ See also: :ref:`configuration-strict-mode`.

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
------------------------------

6 changes: 3 additions & 3 deletions src/dependency_injector/providers.pyi
Original file line number Diff line number Diff line change
@@ -225,20 +225,20 @@ class ConfigurationOption(Provider[Any]):
self,
filepath: Union[Path, str],
required: bool = False,
envs_required: bool = False,
envs_required: Optional[bool] = False,
) -> None: ...
def from_yaml(
self,
filepath: Union[Path, str],
required: bool = False,
loader: Optional[Any] = None,
envs_required: bool = False,
envs_required: Optional[bool] = False,
) -> None: ...
def from_json(
self,
filepath: Union[Path, str],
required: bool = False,
envs_required: bool = False,
envs_required: Optional[bool] = False,
) -> None: ...
def from_pydantic(
self, settings: PydanticSettings, required: bool = False, **kwargs: Any
75 changes: 33 additions & 42 deletions src/dependency_injector/providers.pyx
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ import sys
import threading
import types
import warnings
from configparser import ConfigParser as IniConfigParser

try:
import contextvars
@@ -41,11 +42,6 @@ try:
except ImportError:
_is_coroutine = True

try:
import ConfigParser as iniconfigparser
except ImportError:
import configparser as iniconfigparser

try:
import yaml
except ImportError:
@@ -102,7 +98,7 @@ config_env_marker_pattern = re.compile(
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."""
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


if sys.version_info[0] == 3:
def _parse_ini_file(filepath, envs_required=False):
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
cdef object _parse_ini_file(filepath, envs_required: bool | None):
parser = IniConfigParser()

with open(filepath) as config_file:
config_string = config_file.read()

def _parse_ini_file(filepath, envs_required=False):
parser = iniconfigparser.ConfigParser()
with open(filepath) as config_file:
if envs_required is not None:
config_string = _resolve_config_env_markers(
config_file.read(),
config_string,
envs_required=envs_required,
)
parser.readfp(StringIO.StringIO(config_string))
return parser
parser.read_string(config_string)
return parser


if yaml:
@@ -1717,7 +1704,7 @@ cdef class ConfigurationOption(Provider):
try:
parser = _parse_ini_file(
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:
if required is not False \
@@ -1776,10 +1763,11 @@ cdef class ConfigurationOption(Provider):
raise
return

config_content = _resolve_config_env_markers(
config_content,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
if envs_required is not None:
config_content = _resolve_config_env_markers(
config_content,
envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = yaml.load(config_content, loader)

current_config = self.__call__()
@@ -1814,10 +1802,11 @@ cdef class ConfigurationOption(Provider):
raise
return

config_content = _resolve_config_env_markers(
config_content,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
if envs_required is not None:
config_content = _resolve_config_env_markers(
config_content,
envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = json.loads(config_content)

current_config = self.__call__()
@@ -2270,7 +2259,7 @@ cdef class Configuration(Object):
try:
parser = _parse_ini_file(
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:
if required is not False \
@@ -2329,10 +2318,11 @@ cdef class Configuration(Object):
raise
return

config_content = _resolve_config_env_markers(
config_content,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
if envs_required is not None:
config_content = _resolve_config_env_markers(
config_content,
envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = yaml.load(config_content, loader)

current_config = self.__call__()
@@ -2367,10 +2357,11 @@ cdef class Configuration(Object):
raise
return

config_content = _resolve_config_env_markers(
config_content,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
if envs_required is not None:
config_content = _resolve_config_env_markers(
config_content,
envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = json.loads(config_content)

current_config = self.__call__()
Original file line number Diff line number Diff line change
@@ -5,6 +5,23 @@
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):
config.from_ini(ini_config_file_3)

Original file line number Diff line number Diff line change
@@ -6,6 +6,23 @@
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):
config.from_json(json_config_file_3)

Original file line number Diff line number Diff line change
@@ -6,6 +6,23 @@
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):
config.from_yaml(yaml_config_file_3)