Skip to content
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

Fix #3807: matching characters in nested env delimeter and env prefix #3975

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/3975-arsenron.md
@@ -0,0 +1 @@
Remove undefined behaviour when `env_prefix` had characters in common with `env_nested_delimiter`
14 changes: 11 additions & 3 deletions pydantic/env_settings.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
"""
Expand Down Expand Up @@ -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, {})
Expand Down
27 changes: 27 additions & 0 deletions tests/test_settings.py
Expand Up @@ -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'
Expand Down