Skip to content

Commit 5042b3a

Browse files
authored
Merge branch 'main' into sources-repr
2 parents 04b7d0d + 9583896 commit 5042b3a

File tree

3 files changed

+71
-46
lines changed

3 files changed

+71
-46
lines changed

docs/index.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,15 @@ print(Settings().model_dump())
9090
2. The environment variable name is overridden using `alias`. In this case, the environment variable
9191
`my_api_key` will be used for both validation and serialization instead of `api_key`.
9292

93-
Check the [`Field` documentation](fields.md#field-aliases) for more information.
93+
Check the [`Field` documentation](fields.md#field-aliases) for more information.
9494

95-
3. The `AliasChoices` class allows to have multiple environment variable names for a single field.
95+
3. The [`AliasChoices`][pydantic.AliasChoices] class allows to have multiple environment variable names for a single field.
9696
The first environment variable that is found will be used.
9797

98-
Check the [`AliasChoices`](alias.md#aliaspath-and-aliaschoices) for more information.
98+
Check the [documentation on alias choices](alias.md#aliaspath-and-aliaschoices) for more information.
9999

100-
4. The `ImportString` class allows to import an object from a string.
101-
In this case, the environment variable `special_function` will be read and the function `math.cos` will be imported.
100+
4. The [`ImportString`][pydantic.types.ImportString] class allows to import an object from a string.
101+
In this case, the environment variable `special_function` will be read and the function [`math.cos`][] will be imported.
102102

103103
5. The `env_prefix` config setting allows to set a prefix for all environment variables.
104104

pydantic_settings/sources.py

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
from dotenv import dotenv_values
4040
from pydantic import AliasChoices, AliasPath, BaseModel, Json, RootModel, TypeAdapter
4141
from pydantic._internal._repr import Representation
42-
from pydantic._internal._signature import _field_name_for_signature
4342
from pydantic._internal._typing_extra import WithArgsTypes, origin_is_union, typing_base
4443
from pydantic._internal._utils import deep_update, is_model_class, lenient_issubclass
4544
from pydantic.dataclasses import is_pydantic_dataclass
@@ -336,10 +335,12 @@ def __init__(self, settings_cls: type[BaseSettings], nested_model_default_partia
336335
)
337336
if self.nested_model_default_partial_update:
338337
for field_name, field_info in settings_cls.model_fields.items():
338+
alias_names, *_ = _get_alias_names(field_name, field_info)
339+
preferred_alias = alias_names[0]
339340
if is_dataclass(type(field_info.default)):
340-
self.defaults[_field_name_for_signature(field_name, field_info)] = asdict(field_info.default)
341+
self.defaults[preferred_alias] = asdict(field_info.default)
341342
elif is_model_class(type(field_info.default)):
342-
self.defaults[_field_name_for_signature(field_name, field_info)] = field_info.default.model_dump()
343+
self.defaults[preferred_alias] = field_info.default.model_dump()
343344

344345
def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
345346
# Nothing to do here. Only implement the return statement to make mypy happy
@@ -1424,41 +1425,6 @@ def _get_sub_models(self, model: type[BaseModel], field_name: str, field_info: F
14241425
sub_models.append(type_) # type: ignore
14251426
return sub_models
14261427

1427-
def _get_alias_names(
1428-
self, field_name: str, field_info: FieldInfo, alias_path_args: dict[str, str]
1429-
) -> tuple[tuple[str, ...], bool]:
1430-
alias_names: list[str] = []
1431-
is_alias_path_only: bool = True
1432-
if not any((field_info.alias, field_info.validation_alias)):
1433-
alias_names += [field_name]
1434-
is_alias_path_only = False
1435-
else:
1436-
new_alias_paths: list[AliasPath] = []
1437-
for alias in (field_info.alias, field_info.validation_alias):
1438-
if alias is None:
1439-
continue
1440-
elif isinstance(alias, str):
1441-
alias_names.append(alias)
1442-
is_alias_path_only = False
1443-
elif isinstance(alias, AliasChoices):
1444-
for name in alias.choices:
1445-
if isinstance(name, str):
1446-
alias_names.append(name)
1447-
is_alias_path_only = False
1448-
else:
1449-
new_alias_paths.append(name)
1450-
else:
1451-
new_alias_paths.append(alias)
1452-
for alias_path in new_alias_paths:
1453-
name = cast(str, alias_path.path[0])
1454-
name = name.lower() if not self.case_sensitive else name
1455-
alias_path_args[name] = 'dict' if len(alias_path.path) > 2 else 'list'
1456-
if not alias_names and is_alias_path_only:
1457-
alias_names.append(name)
1458-
if not self.case_sensitive:
1459-
alias_names = [alias_name.lower() for alias_name in alias_names]
1460-
return tuple(dict.fromkeys(alias_names)), is_alias_path_only
1461-
14621428
def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str, field_info: FieldInfo) -> None:
14631429
if _CliImplicitFlag in field_info.metadata:
14641430
cli_flag_name = 'CliImplicitFlag'
@@ -1483,7 +1449,7 @@ def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]
14831449
if not field_info.is_required():
14841450
raise SettingsError(f'subcommand argument {model.__name__}.{field_name} has a default value')
14851451
else:
1486-
alias_names, *_ = self._get_alias_names(field_name, field_info, {})
1452+
alias_names, *_ = _get_alias_names(field_name, field_info)
14871453
if len(alias_names) > 1:
14881454
raise SettingsError(f'subcommand argument {model.__name__}.{field_name} has multiple aliases')
14891455
field_types = [type_ for type_ in get_args(field_info.annotation) if type_ is not type(None)]
@@ -1497,7 +1463,7 @@ def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]
14971463
if not field_info.is_required():
14981464
raise SettingsError(f'positional argument {model.__name__}.{field_name} has a default value')
14991465
else:
1500-
alias_names, *_ = self._get_alias_names(field_name, field_info, {})
1466+
alias_names, *_ = _get_alias_names(field_name, field_info)
15011467
if len(alias_names) > 1:
15021468
raise SettingsError(f'positional argument {model.__name__}.{field_name} has multiple aliases')
15031469
positional_args.append((field_name, field_info))
@@ -1599,7 +1565,9 @@ def _add_parser_args(
15991565
alias_path_args: dict[str, str] = {}
16001566
for field_name, field_info in self._sort_arg_fields(model):
16011567
sub_models: list[type[BaseModel]] = self._get_sub_models(model, field_name, field_info)
1602-
alias_names, is_alias_path_only = self._get_alias_names(field_name, field_info, alias_path_args)
1568+
alias_names, is_alias_path_only = _get_alias_names(
1569+
field_name, field_info, alias_path_args=alias_path_args, case_sensitive=self.case_sensitive
1570+
)
16031571
preferred_alias = alias_names[0]
16041572
if _CliSubCommand in field_info.metadata:
16051573
for model in sub_models:
@@ -2252,5 +2220,41 @@ def _get_model_fields(model_cls: type[Any]) -> dict[str, FieldInfo]:
22522220
raise SettingsError(f'Error: {model_cls.__name__} is not subclass of BaseModel or pydantic.dataclasses.dataclass')
22532221

22542222

2223+
def _get_alias_names(
2224+
field_name: str, field_info: FieldInfo, alias_path_args: dict[str, str] = {}, case_sensitive: bool = True
2225+
) -> tuple[tuple[str, ...], bool]:
2226+
alias_names: list[str] = []
2227+
is_alias_path_only: bool = True
2228+
if not any((field_info.alias, field_info.validation_alias)):
2229+
alias_names += [field_name]
2230+
is_alias_path_only = False
2231+
else:
2232+
new_alias_paths: list[AliasPath] = []
2233+
for alias in (field_info.alias, field_info.validation_alias):
2234+
if alias is None:
2235+
continue
2236+
elif isinstance(alias, str):
2237+
alias_names.append(alias)
2238+
is_alias_path_only = False
2239+
elif isinstance(alias, AliasChoices):
2240+
for name in alias.choices:
2241+
if isinstance(name, str):
2242+
alias_names.append(name)
2243+
is_alias_path_only = False
2244+
else:
2245+
new_alias_paths.append(name)
2246+
else:
2247+
new_alias_paths.append(alias)
2248+
for alias_path in new_alias_paths:
2249+
name = cast(str, alias_path.path[0])
2250+
name = name.lower() if not case_sensitive else name
2251+
alias_path_args[name] = 'dict' if len(alias_path.path) > 2 else 'list'
2252+
if not alias_names and is_alias_path_only:
2253+
alias_names.append(name)
2254+
if not case_sensitive:
2255+
alias_names = [alias_name.lower() for alias_name in alias_names]
2256+
return tuple(dict.fromkeys(alias_names)), is_alias_path_only
2257+
2258+
22552259
def _is_function(obj: Any) -> bool:
22562260
return isinstance(obj, (FunctionType, BuiltinFunctionType))

tests/test_settings.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from annotated_types import MinLen
1414
from pydantic import (
1515
AliasChoices,
16+
AliasGenerator,
1617
AliasPath,
1718
BaseModel,
1819
Discriminator,
@@ -621,6 +622,26 @@ def settings_customise_sources(
621622
assert s.model_dump() == s_final
622623

623624

625+
def test_alias_nested_model_default_partial_update():
626+
class SubModel(BaseModel):
627+
v1: str = 'default'
628+
v2: bytes = b'hello'
629+
v3: int
630+
631+
class Settings(BaseSettings):
632+
model_config = SettingsConfigDict(
633+
nested_model_default_partial_update=True, alias_generator=AliasGenerator(lambda s: s.replace('_', '-'))
634+
)
635+
636+
v0: str = 'ok'
637+
sub_model: SubModel = SubModel(v1='top default', v3=33)
638+
639+
assert Settings(**{'sub-model': {'v1': 'cli'}}).model_dump() == {
640+
'v0': 'ok',
641+
'sub_model': {'v1': 'cli', 'v2': b'hello', 'v3': 33},
642+
}
643+
644+
624645
def test_env_str(env):
625646
class Settings(BaseSettings):
626647
apple: str = Field(None, validation_alias='BOOM')

0 commit comments

Comments
 (0)