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 35b96970f6..6c2d1b3ffd 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 c11cdbb739..018cc5a49c 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'