From a7a4076b392bb6c0e2135e93087ed8d3a8e21dc6 Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Tue, 17 Dec 2024 21:36:02 -0700 Subject: [PATCH 1/5] Add use_preferred_alias flag to get_field_value. --- pydantic_settings/sources.py | 45 +++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 656a32f1..258cd653 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -268,7 +268,9 @@ def settings_sources_data(self) -> dict[str, dict[str, Any]]: return self._settings_sources_data @abstractmethod - def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + def get_field_value( + self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False + ) -> tuple[Any, str, bool]: """ Gets the value, the key for model creation, and a flag to determine whether value is complex. @@ -277,9 +279,10 @@ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, Args: field: The field. field_name: The field name. + use_preferred_alias: Use the preferred alias name when resolving key. Defaults to `False`. Returns: - A tuple contains the key, value and a flag to determine whether value is complex. + A tuple contains the value, key and a flag to determine whether value is complex. """ pass @@ -364,7 +367,9 @@ def __init__(self, settings_cls: type[BaseSettings], nested_model_default_partia elif is_model_class(type(field_info.default)): self.defaults[preferred_alias] = field_info.default.model_dump() - def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + def get_field_value( + self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False + ) -> tuple[Any, str, bool]: # Nothing to do here. Only implement the return statement to make mypy happy return None, '', False @@ -396,7 +401,9 @@ def __init__( else self.config.get('nested_model_default_partial_update', False) ) - def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + def get_field_value( + self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False + ) -> tuple[Any, str, bool]: # Nothing to do here. Only implement the return statement to make mypy happy return None, '', False @@ -567,7 +574,9 @@ def __call__(self) -> dict[str, Any]: for field_name, field in self.settings_cls.model_fields.items(): try: - field_value, field_key, value_is_complex = self.get_field_value(field, field_name) + field_value, field_key, value_is_complex = self.get_field_value( + field, field_name, use_preferred_alias=True + ) except Exception as e: raise SettingsError( f'error getting value for field "{field_name}" from source "{self.__class__.__name__}"' @@ -666,16 +675,19 @@ def find_case_path(cls, dir_path: Path, file_name: str, case_sensitive: bool) -> return f return None - def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + def get_field_value( + self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False + ) -> tuple[Any, str, bool]: """ Gets the value for field from secret file and a flag to determine whether value is complex. Args: field: The field. field_name: The field name. + use_preferred_alias: Use the preferred alias name when resolving key. Defaults to `False`. Returns: - A tuple contains the key, value if the file exists otherwise `None`, and + A tuple that contains the value (`None` if the file does not exist), key, and a flag to determine whether value is complex. """ @@ -690,7 +702,11 @@ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, continue if path.is_file(): - if value_is_complex or (self.config.get('populate_by_name', False) and (field_key == field_name)): + if ( + not use_preferred_alias + or value_is_complex + or (self.config.get('populate_by_name', False) and (field_key == field_name)) + ): preferred_key = field_key return path.read_text().strip(), preferred_key, value_is_complex else: @@ -733,16 +749,19 @@ def __init__( def _load_env_vars(self) -> Mapping[str, str | None]: return parse_env_vars(os.environ, self.case_sensitive, self.env_ignore_empty, self.env_parse_none_str) - def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + def get_field_value( + self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False + ) -> tuple[Any, str, bool]: """ Gets the value for field from environment variables and a flag to determine whether value is complex. Args: field: The field. field_name: The field name. + use_preferred_alias: Use the preferred alias name when resolving key. Defaults to `False`. Returns: - A tuple contains the key, value if the file exists otherwise `None`, and + A tuple that contains the value (`None` if not found), key, and a flag to determine whether value is complex. """ @@ -752,7 +771,11 @@ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, for field_key, env_name, value_is_complex in field_infos: env_val = self.env_vars.get(env_name) if env_val is not None: - if value_is_complex or (self.config.get('populate_by_name', False) and (field_key == field_name)): + if ( + not use_preferred_alias + or value_is_complex + or (self.config.get('populate_by_name', False) and (field_key == field_name)) + ): preferred_key = field_key break From f6151fa0b595d42a528e4c3f66db84af4749c8b0 Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Fri, 20 Dec 2024 06:57:28 -0700 Subject: [PATCH 2/5] Make preferred alias resolution private. --- pydantic_settings/sources.py | 63 +++++++++++++----------------------- 1 file changed, 22 insertions(+), 41 deletions(-) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 258cd653..59638321 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -268,9 +268,7 @@ def settings_sources_data(self) -> dict[str, dict[str, Any]]: return self._settings_sources_data @abstractmethod - def get_field_value( - self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False - ) -> tuple[Any, str, bool]: + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """ Gets the value, the key for model creation, and a flag to determine whether value is complex. @@ -279,10 +277,9 @@ def get_field_value( Args: field: The field. field_name: The field name. - use_preferred_alias: Use the preferred alias name when resolving key. Defaults to `False`. Returns: - A tuple contains the value, key and a flag to determine whether value is complex. + A tuple that contains the value, key and a flag to determine whether value is complex. """ pass @@ -367,9 +364,7 @@ def __init__(self, settings_cls: type[BaseSettings], nested_model_default_partia elif is_model_class(type(field_info.default)): self.defaults[preferred_alias] = field_info.default.model_dump() - def get_field_value( - self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False - ) -> tuple[Any, str, bool]: + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: # Nothing to do here. Only implement the return statement to make mypy happy return None, '', False @@ -401,9 +396,7 @@ def __init__( else self.config.get('nested_model_default_partial_update', False) ) - def get_field_value( - self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False - ) -> tuple[Any, str, bool]: + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: # Nothing to do here. Only implement the return statement to make mypy happy return None, '', False @@ -569,12 +562,22 @@ def _replace_env_none_type_values(self, field_value: dict[str, Any]) -> dict[str return values + def _get_resolved_field_value( + self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False + ) -> tuple[Any, str, bool]: + field_value, field_key, value_is_complex = self.get_field_value(field, field_name) + if not (value_is_complex or (self.config.get('populate_by_name', False) and (field_key == field_name))): + field_infos = self._extract_field_info(field, field_name) + preferred_key, *_ = field_infos[0] + return field_value, preferred_key, value_is_complex + return field_value, field_key, value_is_complex + def __call__(self) -> dict[str, Any]: data: dict[str, Any] = {} for field_name, field in self.settings_cls.model_fields.items(): try: - field_value, field_key, value_is_complex = self.get_field_value( + field_value, field_key, value_is_complex = self._get_resolved_field_value( field, field_name, use_preferred_alias=True ) except Exception as e: @@ -675,25 +678,20 @@ def find_case_path(cls, dir_path: Path, file_name: str, case_sensitive: bool) -> return f return None - def get_field_value( - self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False - ) -> tuple[Any, str, bool]: + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """ Gets the value for field from secret file and a flag to determine whether value is complex. Args: field: The field. field_name: The field name. - use_preferred_alias: Use the preferred alias name when resolving key. Defaults to `False`. Returns: A tuple that contains the value (`None` if the file does not exist), key, and a flag to determine whether value is complex. """ - field_infos = self._extract_field_info(field, field_name) - preferred_key, *_ = field_infos[0] - for field_key, env_name, value_is_complex in field_infos: + for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): # paths reversed to match the last-wins behaviour of `env_file` for secrets_path in reversed(self.secrets_paths): path = self.find_case_path(secrets_path, env_name, self.case_sensitive) @@ -702,20 +700,14 @@ def get_field_value( continue if path.is_file(): - if ( - not use_preferred_alias - or value_is_complex - or (self.config.get('populate_by_name', False) and (field_key == field_name)) - ): - preferred_key = field_key - return path.read_text().strip(), preferred_key, value_is_complex + return path.read_text().strip(), field_key, value_is_complex else: warnings.warn( f'attempted to load secret file "{path}" but found a {path_type_label(path)} instead.', stacklevel=4, ) - return None, preferred_key, value_is_complex + return None, field_key, value_is_complex def __repr__(self) -> str: return f'{self.__class__.__name__}(secrets_dir={self.secrets_dir!r})' @@ -749,16 +741,13 @@ def __init__( def _load_env_vars(self) -> Mapping[str, str | None]: return parse_env_vars(os.environ, self.case_sensitive, self.env_ignore_empty, self.env_parse_none_str) - def get_field_value( - self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False - ) -> tuple[Any, str, bool]: + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """ Gets the value for field from environment variables and a flag to determine whether value is complex. Args: field: The field. field_name: The field name. - use_preferred_alias: Use the preferred alias name when resolving key. Defaults to `False`. Returns: A tuple that contains the value (`None` if not found), key, and @@ -766,20 +755,12 @@ def get_field_value( """ env_val: str | None = None - field_infos = self._extract_field_info(field, field_name) - preferred_key, *_ = field_infos[0] - for field_key, env_name, value_is_complex in field_infos: + for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): env_val = self.env_vars.get(env_name) if env_val is not None: - if ( - not use_preferred_alias - or value_is_complex - or (self.config.get('populate_by_name', False) and (field_key == field_name)) - ): - preferred_key = field_key break - return env_val, preferred_key, value_is_complex + return env_val, field_key, value_is_complex def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: """ From 306fea263b55b7b40097a6d52b4b777da664466e Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Mon, 30 Dec 2024 06:51:35 -0700 Subject: [PATCH 3/5] Add docstring with V3 note. --- pydantic_settings/sources.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 59638321..f73ad1a8 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -565,6 +565,21 @@ def _replace_env_none_type_values(self, field_value: dict[str, Any]) -> dict[str def _get_resolved_field_value( self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False ) -> tuple[Any, str, bool]: + """ + Gets the value, the key for model creation, and a flag to determine whether value is complex. + + Note: + In V3, this method should either be made public, or, this method should be removed and the + abstract method get_field_value should be updated to include a "use_preferred_alias" flag. + + Args: + field: The field. + field_name: The field name. + use_preferred_alias: Use the preferred alias name when resolving key. Defaults to `False`. + + Returns: + A tuple that contains the value, key and a flag to determine whether value is complex. + """ field_value, field_key, value_is_complex = self.get_field_value(field, field_name) if not (value_is_complex or (self.config.get('populate_by_name', False) and (field_key == field_name))): field_infos = self._extract_field_info(field, field_name) From 9ba24a6536e724d10dcc0b7e56fc5bc00d650999 Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Mon, 30 Dec 2024 06:55:56 -0700 Subject: [PATCH 4/5] Remove unused param. --- pydantic_settings/sources.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index f73ad1a8..11efd308 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -562,9 +562,7 @@ def _replace_env_none_type_values(self, field_value: dict[str, Any]) -> dict[str return values - def _get_resolved_field_value( - self, field: FieldInfo, field_name: str, use_preferred_alias: bool = False - ) -> tuple[Any, str, bool]: + def _get_resolved_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """ Gets the value, the key for model creation, and a flag to determine whether value is complex. @@ -575,7 +573,6 @@ def _get_resolved_field_value( Args: field: The field. field_name: The field name. - use_preferred_alias: Use the preferred alias name when resolving key. Defaults to `False`. Returns: A tuple that contains the value, key and a flag to determine whether value is complex. @@ -592,9 +589,7 @@ def __call__(self) -> dict[str, Any]: for field_name, field in self.settings_cls.model_fields.items(): try: - field_value, field_key, value_is_complex = self._get_resolved_field_value( - field, field_name, use_preferred_alias=True - ) + field_value, field_key, value_is_complex = self._get_resolved_field_value(field, field_name) except Exception as e: raise SettingsError( f'error getting value for field "{field_name}" from source "{self.__class__.__name__}"' From 8bf9d56be87e3602592684abd097ef3be5f151ed Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Mon, 30 Dec 2024 06:57:34 -0700 Subject: [PATCH 5/5] Nit. --- pydantic_settings/sources.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 11efd308..5e64164d 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -564,7 +564,8 @@ def _replace_env_none_type_values(self, field_value: dict[str, Any]) -> dict[str def _get_resolved_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: """ - Gets the value, the key for model creation, and a flag to determine whether value is complex. + Gets the value, the preferred alias key for model creation, and a flag to determine whether value + is complex. Note: In V3, this method should either be made public, or, this method should be removed and the @@ -575,7 +576,7 @@ def _get_resolved_field_value(self, field: FieldInfo, field_name: str) -> tuple[ field_name: The field name. Returns: - A tuple that contains the value, key and a flag to determine whether value is complex. + A tuple that contains the value, preferred key and a flag to determine whether value is complex. """ field_value, field_key, value_is_complex = self.get_field_value(field, field_name) if not (value_is_complex or (self.config.get('populate_by_name', False) and (field_key == field_name))):