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 1 commit
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
13 changes: 10 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=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')

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 = '',
arsenron marked this conversation as resolved.
Show resolved Hide resolved
):
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
"""
Expand Down Expand Up @@ -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) :]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a comment here explaining why we're doing this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. We are splitting an environment variable key into keys and last_key by self.env_nested_delimiter and in case of matching characters in both self.env_nested_delimiter and self.env_prefix, we were adding to keys an environment variable prefix. This was before, but now we just strip a prefix and can safely split by a delimeter because there will be no any more conflicts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, when I say "please add a comment", I mean add a comment in the code. I know why we're doing this, but we comment the code to make it easier for someone reading this in future (including me when I've forgotten this conversation).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, sorry for misunderstanding

_, *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