Skip to content

Commit 8b8803d

Browse files
authored
feat: Enable access to the current state in settings sources (#326)
1 parent 229319c commit 8b8803d

File tree

4 files changed

+146
-1
lines changed

4 files changed

+146
-1
lines changed

docs/index.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,36 @@ print(Settings())
14861486
#> foobar='test'
14871487
```
14881488

1489+
#### Accesing the result of previous sources
1490+
1491+
Each source of settings can access the output of the previous ones.
1492+
1493+
```python
1494+
from typing import Any, Dict, Tuple
1495+
1496+
from pydantic.fields import FieldInfo
1497+
1498+
from pydantic_settings import PydanticBaseSettingsSource
1499+
1500+
1501+
class MyCustomSource(PydanticBaseSettingsSource):
1502+
def get_field_value(
1503+
self, field: FieldInfo, field_name: str
1504+
) -> Tuple[Any, str, bool]: ...
1505+
1506+
def __call__(self) -> Dict[str, Any]:
1507+
# Retrieve the aggregated settings from previous sources
1508+
current_state = self.current_state
1509+
current_state.get('some_setting')
1510+
1511+
# Retrive settings from all sources individually
1512+
# self.settings_sources_data["SettingsSourceName"]: Dict[str, Any]
1513+
settings_sources_data = self.settings_sources_data
1514+
settings_sources_data['SomeSettingsSource'].get('some_setting')
1515+
1516+
# Your code here...
1517+
```
1518+
14891519
### Removing sources
14901520

14911521
You might also want to disable a source:

pydantic_settings/main.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,19 @@ def _settings_build_values(
308308
)
309309
sources = (cli_settings,) + sources
310310
if sources:
311-
return deep_update(*reversed([source() for source in sources]))
311+
state: dict[str, Any] = {}
312+
states: dict[str, dict[str, Any]] = {}
313+
for source in sources:
314+
if isinstance(source, PydanticBaseSettingsSource):
315+
source._set_current_state(state)
316+
source._set_settings_sources_data(states)
317+
318+
source_name = source.__name__ if hasattr(source, '__name__') else type(source).__name__
319+
source_state = source()
320+
321+
states[source_name] = source_state
322+
state = deep_update(source_state, state)
323+
return state
312324
else:
313325
# no one should mean to do this, but I think returning an empty dict is marginally preferable
314326
# to an informative error and much better than a confusing error

pydantic_settings/sources.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,36 @@ class PydanticBaseSettingsSource(ABC):
126126
def __init__(self, settings_cls: type[BaseSettings]):
127127
self.settings_cls = settings_cls
128128
self.config = settings_cls.model_config
129+
self._current_state: dict[str, Any] = {}
130+
self._settings_sources_data: dict[str, dict[str, Any]] = {}
131+
132+
def _set_current_state(self, state: dict[str, Any]) -> None:
133+
"""
134+
Record the state of settings from the previous settings sources. This should
135+
be called right before __call__.
136+
"""
137+
self._current_state = state
138+
139+
def _set_settings_sources_data(self, states: dict[str, dict[str, Any]]) -> None:
140+
"""
141+
Record the state of settings from all previous settings sources. This should
142+
be called right before __call__.
143+
"""
144+
self._settings_sources_data = states
145+
146+
@property
147+
def current_state(self) -> dict[str, Any]:
148+
"""
149+
The current state of the settings, populated by the previous settings sources.
150+
"""
151+
return self._current_state
152+
153+
@property
154+
def settings_sources_data(self) -> dict[str, dict[str, Any]]:
155+
"""
156+
The state of all previous settings sources.
157+
"""
158+
return self._settings_sources_data
129159

130160
@abstractmethod
131161
def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:

tests/test_settings.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4025,3 +4025,76 @@ class Settings(BaseSettings):
40254025
env.set('nested__FOO', '["string1", "string2"]')
40264026
s = Settings()
40274027
assert s.model_dump() == {'nested': {'FOO': ['string1', 'string2']}}
4028+
4029+
4030+
def test_settings_source_current_state(env):
4031+
class SettingsSource(PydanticBaseSettingsSource):
4032+
def get_field_value(self, field: FieldInfo, field_name: str) -> Any:
4033+
pass
4034+
4035+
def __call__(self) -> Dict[str, Any]:
4036+
current_state = self.current_state
4037+
if current_state.get('one') == '1':
4038+
return {'two': '1'}
4039+
4040+
return {}
4041+
4042+
class Settings(BaseSettings):
4043+
one: bool = False
4044+
two: bool = False
4045+
4046+
@classmethod
4047+
def settings_customise_sources(
4048+
cls,
4049+
settings_cls: Type[BaseSettings],
4050+
init_settings: PydanticBaseSettingsSource,
4051+
env_settings: PydanticBaseSettingsSource,
4052+
dotenv_settings: PydanticBaseSettingsSource,
4053+
file_secret_settings: PydanticBaseSettingsSource,
4054+
) -> Tuple[PydanticBaseSettingsSource, ...]:
4055+
return (env_settings, SettingsSource(settings_cls))
4056+
4057+
env.set('one', '1')
4058+
s = Settings()
4059+
assert s.two is True
4060+
4061+
4062+
def test_settings_source_settings_sources_data(env):
4063+
class SettingsSource(PydanticBaseSettingsSource):
4064+
def get_field_value(self, field: FieldInfo, field_name: str) -> Any:
4065+
pass
4066+
4067+
def __call__(self) -> Dict[str, Any]:
4068+
settings_sources_data = self.settings_sources_data
4069+
if settings_sources_data == {
4070+
'InitSettingsSource': {'one': True, 'two': True},
4071+
'EnvSettingsSource': {'one': '1'},
4072+
'function_settings_source': {'three': 'false'},
4073+
}:
4074+
return {'four': '1'}
4075+
4076+
return {}
4077+
4078+
def function_settings_source():
4079+
return {'three': 'false'}
4080+
4081+
class Settings(BaseSettings):
4082+
one: bool = False
4083+
two: bool = False
4084+
three: bool = False
4085+
four: bool = False
4086+
4087+
@classmethod
4088+
def settings_customise_sources(
4089+
cls,
4090+
settings_cls: Type[BaseSettings],
4091+
init_settings: PydanticBaseSettingsSource,
4092+
env_settings: PydanticBaseSettingsSource,
4093+
dotenv_settings: PydanticBaseSettingsSource,
4094+
file_secret_settings: PydanticBaseSettingsSource,
4095+
) -> Tuple[PydanticBaseSettingsSource, ...]:
4096+
return (env_settings, init_settings, function_settings_source, SettingsSource(settings_cls))
4097+
4098+
env.set('one', '1')
4099+
s = Settings(one=True, two=True)
4100+
assert s.four is True

0 commit comments

Comments
 (0)