Skip to content

Commit dff3f02

Browse files
Support loading a specific nested key from YAML in YamlConfigSettingsSource (#603)
Co-authored-by: Hasan Ramezani <[email protected]>
1 parent 2ac8eac commit dff3f02

File tree

3 files changed

+85
-0
lines changed

3 files changed

+85
-0
lines changed

pydantic_settings/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ class SettingsConfigDict(ConfigDict, total=False):
6262
json_file_encoding: str | None
6363
yaml_file: PathType | None
6464
yaml_file_encoding: str | None
65+
yaml_config_section: str | None
66+
"""
67+
Specifies the top-level key in a YAML file from which to load the settings.
68+
If provided, the settings will be loaded from the nested section under this key.
69+
This is useful when the YAML file contains multiple configuration sections
70+
and you only want to load a specific subset into your settings model.
71+
"""
72+
6573
pyproject_toml_depth: int
6674
"""
6775
Number of levels **up** from the current working directory to attempt to find a pyproject.toml
@@ -446,6 +454,7 @@ def _settings_build_values(
446454
json_file_encoding=None,
447455
yaml_file=None,
448456
yaml_file_encoding=None,
457+
yaml_config_section=None,
449458
toml_file=None,
450459
secrets_dir=None,
451460
protected_namespaces=('model_validate', 'model_dump', 'settings_customise_sources'),

pydantic_settings/sources/providers/yaml.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,28 @@ def __init__(
3939
settings_cls: type[BaseSettings],
4040
yaml_file: PathType | None = DEFAULT_PATH,
4141
yaml_file_encoding: str | None = None,
42+
yaml_config_section: str | None = None,
4243
):
4344
self.yaml_file_path = yaml_file if yaml_file != DEFAULT_PATH else settings_cls.model_config.get('yaml_file')
4445
self.yaml_file_encoding = (
4546
yaml_file_encoding
4647
if yaml_file_encoding is not None
4748
else settings_cls.model_config.get('yaml_file_encoding')
4849
)
50+
self.yaml_config_section = (
51+
yaml_config_section
52+
if yaml_config_section is not None
53+
else settings_cls.model_config.get('yaml_config_section')
54+
)
4955
self.yaml_data = self._read_files(self.yaml_file_path)
56+
57+
if self.yaml_config_section:
58+
try:
59+
self.yaml_data = self.yaml_data[self.yaml_config_section]
60+
except KeyError:
61+
raise KeyError(
62+
f'yaml_config_section key "{self.yaml_config_section}" not found in {self.yaml_file_path}'
63+
)
5064
super().__init__(settings_cls, self.yaml_data)
5165

5266
def _read_file(self, file_path: Path) -> dict[str, Any]:

tests/test_source_yaml.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,65 @@ def settings_customise_sources(
166166

167167
s = Settings()
168168
assert s.model_dump() == {'yaml3': 3, 'yaml4': 4}
169+
170+
171+
@pytest.mark.skipif(yaml is None, reason='pyYAML is not installed')
172+
def test_yaml_config_section(tmp_path):
173+
p = tmp_path / '.env'
174+
p.write_text(
175+
"""
176+
foobar: "Hello"
177+
nested:
178+
nested_field: "world!"
179+
"""
180+
)
181+
182+
class Settings(BaseSettings):
183+
nested_field: str
184+
185+
model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='nested')
186+
187+
@classmethod
188+
def settings_customise_sources(
189+
cls,
190+
settings_cls: type[BaseSettings],
191+
init_settings: PydanticBaseSettingsSource,
192+
env_settings: PydanticBaseSettingsSource,
193+
dotenv_settings: PydanticBaseSettingsSource,
194+
file_secret_settings: PydanticBaseSettingsSource,
195+
) -> tuple[PydanticBaseSettingsSource, ...]:
196+
return (YamlConfigSettingsSource(settings_cls),)
197+
198+
s = Settings()
199+
assert s.nested_field == 'world!'
200+
201+
202+
@pytest.mark.skipif(yaml is None, reason='pyYAML is not installed')
203+
def test_invalid_yaml_config_section(tmp_path):
204+
p = tmp_path / '.env'
205+
p.write_text(
206+
"""
207+
foobar: "Hello"
208+
nested:
209+
nested_field: "world!"
210+
"""
211+
)
212+
213+
class Settings(BaseSettings):
214+
nested_field: str
215+
216+
model_config = SettingsConfigDict(yaml_file=p, yaml_config_section='invalid_key')
217+
218+
@classmethod
219+
def settings_customise_sources(
220+
cls,
221+
settings_cls: type[BaseSettings],
222+
init_settings: PydanticBaseSettingsSource,
223+
env_settings: PydanticBaseSettingsSource,
224+
dotenv_settings: PydanticBaseSettingsSource,
225+
file_secret_settings: PydanticBaseSettingsSource,
226+
) -> tuple[PydanticBaseSettingsSource, ...]:
227+
return (YamlConfigSettingsSource(settings_cls),)
228+
229+
with pytest.raises(KeyError, match='yaml_config_section key "invalid_key" not found in .+'):
230+
Settings()

0 commit comments

Comments
 (0)