Skip to content

Commit 9e0a944

Browse files
committed
Add default settings source.
1 parent 4840d69 commit 9e0a944

File tree

3 files changed

+92
-2
lines changed

3 files changed

+92
-2
lines changed

pydantic_settings/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .sources import (
1212
ENV_FILE_SENTINEL,
1313
CliSettingsSource,
14+
DefaultSettingsSource,
1415
DotEnvSettingsSource,
1516
DotenvType,
1617
EnvSettingsSource,
@@ -264,6 +265,7 @@ def _settings_build_values(
264265
secrets_dir = _secrets_dir if _secrets_dir is not None else self.model_config.get('secrets_dir')
265266

266267
# Configure built-in sources
268+
default_settings = DefaultSettingsSource(self.__class__)
267269
init_settings = InitSettingsSource(self.__class__, init_kwargs=init_kwargs)
268270
env_settings = EnvSettingsSource(
269271
self.__class__,
@@ -296,7 +298,7 @@ def _settings_build_values(
296298
env_settings=env_settings,
297299
dotenv_settings=dotenv_settings,
298300
file_secret_settings=file_secret_settings,
299-
)
301+
) + (default_settings,)
300302
if not any([source for source in sources if isinstance(source, CliSettingsSource)]):
301303
if cli_parse_args is not None or cli_settings_source is not None:
302304
cli_settings = (

pydantic_settings/sources.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from abc import ABC, abstractmethod
1111
from argparse import SUPPRESS, ArgumentParser, HelpFormatter, Namespace, _SubParsersAction
1212
from collections import deque
13-
from dataclasses import is_dataclass
13+
from dataclasses import asdict, is_dataclass
1414
from enum import Enum
1515
from pathlib import Path
1616
from types import FunctionType
@@ -246,6 +246,74 @@ def __call__(self) -> dict[str, Any]:
246246
pass
247247

248248

249+
class DefaultSettingsSource(PydanticBaseSettingsSource):
250+
"""
251+
Source class for loading default values.
252+
"""
253+
254+
def __init__(self, settings_cls: type[BaseSettings]):
255+
super().__init__(settings_cls)
256+
self.defaults = self._get_defaults(settings_cls)
257+
258+
def _get_defaults(self, settings_cls: type[BaseSettings]) -> dict[str, Any]:
259+
defaults: dict[str, Any] = {}
260+
if self.config.get('validate_default'):
261+
fields = (
262+
settings_cls.__pydantic_fields__ if is_pydantic_dataclass(settings_cls) else settings_cls.model_fields
263+
)
264+
for field_name, field_info in fields.items():
265+
if field_info.validate_default is not False:
266+
resolved_name = self._get_resolved_name(field_name, field_info)
267+
if field_info.default not in (PydanticUndefined, None):
268+
if is_model_class(field_info.annotation):
269+
defaults[resolved_name] = field_info.default.model_dump()
270+
elif is_dataclass(field_info.annotation):
271+
defaults[resolved_name] = asdict(field_info.default)
272+
else:
273+
defaults[resolved_name] = field_info.default
274+
elif field_info.default_factory is not None:
275+
defaults[resolved_name] = field_info.default_factory
276+
return defaults
277+
278+
def _get_resolved_name(self, field_name: str, field_info: FieldInfo) -> str:
279+
if not any((field_info.alias, field_info.validation_alias)):
280+
return field_name
281+
282+
resolved_names: list[str] = []
283+
is_alias_path_only: bool = True
284+
new_alias_paths: list[AliasPath] = []
285+
for alias in (field_info.alias, field_info.validation_alias):
286+
if alias is None:
287+
continue
288+
elif isinstance(alias, str):
289+
resolved_names.append(alias)
290+
is_alias_path_only = False
291+
elif isinstance(alias, AliasChoices):
292+
for name in alias.choices:
293+
if isinstance(name, str):
294+
resolved_names.append(name)
295+
is_alias_path_only = False
296+
else:
297+
new_alias_paths.append(name)
298+
else:
299+
new_alias_paths.append(alias)
300+
for alias_path in new_alias_paths:
301+
name = cast(str, alias_path.path[0])
302+
if not resolved_names and is_alias_path_only:
303+
resolved_names.append(name)
304+
return tuple(dict.fromkeys(resolved_names))[0]
305+
306+
def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
307+
# Nothing to do here. Only implement the return statement to make mypy happy
308+
return None, '', False
309+
310+
def __call__(self) -> dict[str, Any]:
311+
return self.defaults
312+
313+
def __repr__(self) -> str:
314+
return f'DefaultSettingsSource(init_kwargs={self.defaults!r})'
315+
316+
249317
class InitSettingsSource(PydanticBaseSettingsSource):
250318
"""
251319
Source class for loading values provided during settings class initialization.

tests/test_settings.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,26 @@ class ComplexSettings(BaseSettings):
491491
]
492492

493493

494+
def test_env_default_settings(env):
495+
class NestedA(BaseModel):
496+
v0: bool
497+
v1: bool
498+
499+
class NestedB(BaseModel):
500+
v0: bool = False
501+
v1: bool = True
502+
503+
class SettingsDefaultsA(BaseSettings, env_nested_delimiter='__'):
504+
nested: NestedB = NestedB()
505+
506+
class SettingsDefaultsB(BaseSettings, env_nested_delimiter='__'):
507+
nested: NestedA = NestedA(v0=False, v1=True)
508+
509+
env.set('NESTED__V0', 'True')
510+
assert SettingsDefaultsA().model_dump() == {'nested': {'v0': True, 'v1': True}}
511+
assert SettingsDefaultsB().model_dump() == {'nested': {'v0': True, 'v1': True}}
512+
513+
494514
def test_env_str(env):
495515
class Settings(BaseSettings):
496516
apple: str = Field(None, validation_alias='BOOM')

0 commit comments

Comments
 (0)