From fe47f7614a47a727ca04211c489333437546b21c Mon Sep 17 00:00:00 2001 From: arsenron Date: Mon, 4 Apr 2022 22:39:10 +0300 Subject: [PATCH 1/3] fix nested env delimeter with matching prefix --- pydantic/env_settings.py | 13 ++++++++++--- tests/test_settings.py | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py index 35b96970f6..5468046c10 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=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') 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: str = '', ): 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: str = env_prefix def __call__(self, settings: BaseSettings) -> Dict[str, Any]: # noqa C901 """ @@ -228,7 +234,8 @@ 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) + env_name_without_prefix = env_name[len(self.env_prefix) :] + _, *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' From d0b22551156522088a8814f0a4cce0f751ef8eed Mon Sep 17 00:00:00 2001 From: arsenron Date: Mon, 8 Aug 2022 20:14:54 +0300 Subject: [PATCH 2/3] Update env_settings and add change readme --- changes/3975-arsenron.md | 1 + pydantic/env_settings.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 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..0ada967ac0 --- /dev/null +++ b/changes/3975-arsenron.md @@ -0,0 +1 @@ +fix 3807 - Remove undefined behaviour when `env_prefix` had characters in common with `env_nested_delimiter` \ No newline at end of file diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py index 5468046c10..281334acc4 100644 --- a/pydantic/env_settings.py +++ b/pydantic/env_settings.py @@ -63,7 +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=self.__config__.env_prefix, + 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 @@ -143,19 +143,19 @@ def __repr__(self) -> str: class EnvSettingsSource: - __slots__ = ('env_file', 'env_file_encoding', 'env_nested_delimiter', 'env_prefix') + __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, - env_prefix: str = '', + 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: str = env_prefix + self.env_prefix_len: int = env_prefix_len def __call__(self, settings: BaseSettings) -> Dict[str, Any]: # noqa C901 """ @@ -234,7 +234,7 @@ 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 - env_name_without_prefix = env_name[len(self.env_prefix) :] + 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: From 5193d83ba6f53abaeb2b48426eaf1b645fb1a962 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 9 Aug 2022 16:56:23 +0100 Subject: [PATCH 3/3] fix change and add comment --- changes/3975-arsenron.md | 2 +- pydantic/env_settings.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changes/3975-arsenron.md b/changes/3975-arsenron.md index 0ada967ac0..7aaea271cb 100644 --- a/changes/3975-arsenron.md +++ b/changes/3975-arsenron.md @@ -1 +1 @@ -fix 3807 - Remove undefined behaviour when `env_prefix` had characters in common with `env_nested_delimiter` \ No newline at end of file +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 281334acc4..6c2d1b3ffd 100644 --- a/pydantic/env_settings.py +++ b/pydantic/env_settings.py @@ -234,6 +234,7 @@ 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 + # 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