From 6650edfe8f23aecf40fb58c0243a0be8a8e32a7c Mon Sep 17 00:00:00 2001 From: arsenron <33022971+arsenron@users.noreply.github.com> Date: Thu, 11 Aug 2022 13:35:03 +0300 Subject: [PATCH] Fix #3807: matching characters in nested env delimeter and env prefix (#3975) * fix nested env delimeter with matching prefix * Update env_settings and add change readme * fix change and add comment Co-authored-by: Samuel Colvin --- changes/3975-arsenron.md | 1 + pydantic/env_settings.py | 14 +++++++++++--- tests/test_settings.py | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 changes/3975-arsenron.md diff --git a/changes/3975-arsenron.md b/changes/3975-arsenron.md new file mode 100644 index 0000000000..7aaea271cb --- /dev/null +++ b/changes/3975-arsenron.md @@ -0,0 +1 @@ +Remove undefined behaviour when `env_prefix` had characters in common with `env_nested_delimiter` diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py index 73945e31eb..e1fe064279 100644 --- a/pydantic/env_settings.py +++ b/pydantic/env_settings.py @@ -63,6 +63,7 @@ def _build_values( env_nested_delimiter=( _env_nested_delimiter if _env_nested_delimiter is not None else self.__config__.env_nested_delimiter ), + env_prefix_len=len(self.__config__.env_prefix), ) file_secret_settings = SecretsSettingsSource(secrets_dir=_secrets_dir or self.__config__.secrets_dir) # Provide a hook to set built-in sources priority and add / remove sources @@ -142,14 +143,19 @@ def __repr__(self) -> str: class EnvSettingsSource: - __slots__ = ('env_file', 'env_file_encoding', 'env_nested_delimiter') + __slots__ = ('env_file', 'env_file_encoding', 'env_nested_delimiter', 'env_prefix_len') def __init__( - self, env_file: Optional[StrPath], env_file_encoding: Optional[str], env_nested_delimiter: Optional[str] = None + self, + env_file: Optional[StrPath], + env_file_encoding: Optional[str], + env_nested_delimiter: Optional[str] = None, + env_prefix_len: int = 0, ): self.env_file: Optional[StrPath] = env_file self.env_file_encoding: Optional[str] = env_file_encoding self.env_nested_delimiter: Optional[str] = env_nested_delimiter + self.env_prefix_len: int = env_prefix_len def __call__(self, settings: BaseSettings) -> Dict[str, Any]: # noqa C901 """ @@ -228,7 +234,9 @@ def explode_env_vars(self, field: ModelField, env_vars: Mapping[str, Optional[st for env_name, env_val in env_vars.items(): if not any(env_name.startswith(prefix) for prefix in prefixes): continue - _, *keys, last_key = env_name.split(self.env_nested_delimiter) + # we remove the prefix before splitting in case the prefix has characters in common with the delimiter + env_name_without_prefix = env_name[self.env_prefix_len :] + _, *keys, last_key = env_name_without_prefix.split(self.env_nested_delimiter) env_var = result for key in keys: env_var = env_var.setdefault(key, {}) diff --git a/tests/test_settings.py b/tests/test_settings.py index d23fb893d0..29a529e2e1 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -134,6 +134,33 @@ class Config: } +def test_nested_env_delimiter_with_prefix(env): + class Subsettings(BaseSettings): + banana: str + + class Settings(BaseSettings): + subsettings: Subsettings + + class Config: + env_nested_delimiter = '_' + env_prefix = 'myprefix_' + + env.set('myprefix_subsettings_banana', 'banana') + s = Settings() + assert s.subsettings.banana == 'banana' + + class Settings(BaseSettings): + subsettings: Subsettings + + class Config: + env_nested_delimiter = '_' + env_prefix = 'myprefix__' + + env.set('myprefix__subsettings_banana', 'banana') + s = Settings() + assert s.subsettings.banana == 'banana' + + def test_nested_env_delimiter_complex_required(env): class Cfg(BaseSettings): v: str = 'default'