Skip to content

Commit e49dbbd

Browse files
author
Julian Vanden Broeck
committed
Use SettingsErrors when file can't be used as source
Some file formats (at least yaml) allow to represent non dictionnary values. In such situation we can't add the values read from those files. Instead of raising a ValueError, we now raise a SettingsError and indicate we can't parse the related config file.
1 parent 0d605d0 commit e49dbbd

File tree

4 files changed

+88
-1
lines changed

4 files changed

+88
-1
lines changed

pydantic_settings/sources.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1912,7 +1912,15 @@ def _read_files(self, files: PathType | None) -> dict[str, Any]:
19121912
for file in files:
19131913
file_path = Path(file).expanduser()
19141914
if file_path.is_file():
1915-
vars.update(self._read_file(file_path))
1915+
try:
1916+
settings = self._read_file(file_path)
1917+
except ValueError as e:
1918+
raise SettingsError(f'Failed to parse settings from {file_path}, {e}')
1919+
if not isinstance(settings, dict):
1920+
raise SettingsError(
1921+
f'Failed to parse settings from {file_path}, expecting an object (valid dictionnary)'
1922+
)
1923+
vars.update(settings)
19161924
return vars
19171925

19181926
@abstractmethod

tests/test_source_json.py

+29
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import json
66
from typing import Tuple, Type, Union
77

8+
import pytest
89
from pydantic import BaseModel
910

1011
from pydantic_settings import (
1112
BaseSettings,
1213
JsonConfigSettingsSource,
1314
PydanticBaseSettingsSource,
1415
SettingsConfigDict,
16+
SettingsError,
1517
)
1618

1719

@@ -67,6 +69,33 @@ def settings_customise_sources(
6769
assert s.model_dump() == {}
6870

6971

72+
def test_nondict_json_file(tmp_path):
73+
p = tmp_path / '.env'
74+
p.write_text(
75+
"""
76+
"noway"
77+
"""
78+
)
79+
80+
class Settings(BaseSettings):
81+
foobar: str
82+
model_config = SettingsConfigDict(json_file=p)
83+
84+
@classmethod
85+
def settings_customise_sources(
86+
cls,
87+
settings_cls: Type[BaseSettings],
88+
init_settings: PydanticBaseSettingsSource,
89+
env_settings: PydanticBaseSettingsSource,
90+
dotenv_settings: PydanticBaseSettingsSource,
91+
file_secret_settings: PydanticBaseSettingsSource,
92+
) -> Tuple[PydanticBaseSettingsSource, ...]:
93+
return (JsonConfigSettingsSource(settings_cls),)
94+
95+
with pytest.raises(SettingsError, match=f'Failed to parse settings from {p}, expecting an object'):
96+
s = Settings()
97+
98+
7099
def test_multiple_file_json(tmp_path):
71100
p5 = tmp_path / '.env.json5'
72101
p6 = tmp_path / '.env.json6'

tests/test_source_toml.py

+25
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
BaseSettings,
1313
PydanticBaseSettingsSource,
1414
SettingsConfigDict,
15+
SettingsError,
1516
TomlConfigSettingsSource,
1617
)
1718

@@ -77,6 +78,30 @@ def settings_customise_sources(
7778
assert s.model_dump() == {}
7879

7980

81+
@pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed')
82+
def test_pyproject_nondict_toml(cd_tmp_path):
83+
pyproject = cd_tmp_path / 'pyproject.toml'
84+
pyproject.write_text(
85+
"""
86+
[tool.pydantic-settings]
87+
foobar
88+
"""
89+
)
90+
91+
class Settings(BaseSettings):
92+
foobar: str
93+
model_config = SettingsConfigDict()
94+
95+
@classmethod
96+
def settings_customise_sources(
97+
cls, settings_cls: Type[BaseSettings], **_kwargs: PydanticBaseSettingsSource
98+
) -> Tuple[PydanticBaseSettingsSource, ...]:
99+
return (TomlConfigSettingsSource(settings_cls, pyproject),)
100+
101+
with pytest.raises(SettingsError, match=f'Failed to parse settings from {pyproject}'):
102+
Settings()
103+
104+
80105
@pytest.mark.skipif(sys.version_info <= (3, 11) and tomli is None, reason='tomli/tomllib is not installed')
81106
def test_multiple_file_toml(tmp_path):
82107
p1 = tmp_path / '.env.toml1'

tests/test_source_yaml.py

+25
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
BaseSettings,
1212
PydanticBaseSettingsSource,
1313
SettingsConfigDict,
14+
SettingsError,
1415
YamlConfigSettingsSource,
1516
)
1617

@@ -85,6 +86,30 @@ def settings_customise_sources(
8586
assert s.nested.nested_field == 'world!'
8687

8788

89+
@pytest.mark.skipif(yaml is None, reason='pyYaml is not installed')
90+
def test_nondict_yaml_file(tmp_path):
91+
p = tmp_path / '.env'
92+
p.write_text('test invalid yaml')
93+
94+
class Settings(BaseSettings):
95+
foobar: str
96+
model_config = SettingsConfigDict(yaml_file=p)
97+
98+
@classmethod
99+
def settings_customise_sources(
100+
cls,
101+
settings_cls: Type[BaseSettings],
102+
init_settings: PydanticBaseSettingsSource,
103+
env_settings: PydanticBaseSettingsSource,
104+
dotenv_settings: PydanticBaseSettingsSource,
105+
file_secret_settings: PydanticBaseSettingsSource,
106+
) -> Tuple[PydanticBaseSettingsSource, ...]:
107+
return (YamlConfigSettingsSource(settings_cls),)
108+
109+
with pytest.raises(SettingsError, match=f'Failed to parse settings from {p}, expecting an object'):
110+
Settings()
111+
112+
88113
@pytest.mark.skipif(yaml is None, reason='pyYaml is not installed')
89114
def test_yaml_no_file():
90115
class Settings(BaseSettings):

0 commit comments

Comments
 (0)