Skip to content

Add support for toml #210 #212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pydantic_settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
InitSettingsSource,
PydanticBaseSettingsSource,
SecretsSettingsSource,
TomlSettingsSource,
)
from .version import VERSION

Expand All @@ -16,6 +17,7 @@
'PydanticBaseSettingsSource',
'SecretsSettingsSource',
'SettingsConfigDict',
'TomlSettingsSource',
'__version__',
)

Expand Down
47 changes: 47 additions & 0 deletions pydantic_settings/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,19 @@

from pydantic_settings.utils import path_type_label

try:
import tomllib # type:ignore
except ImportError:
import tomli as tomllib

if TYPE_CHECKING:
from pydantic_settings.main import BaseSettings


DotenvType = Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]
PathType = Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]

DEFAULT_PATH: PathType = Path('')

# This is used as default value for `_env_file` in the `BaseSettings` class and
# `env_file` in `DotEnvSettingsSource` so the default can be distinguished from `None`.
Expand Down Expand Up @@ -716,3 +724,42 @@ def _annotation_is_complex_inner(annotation: type[Any] | None) -> bool:
return lenient_issubclass(annotation, (BaseModel, Mapping, Sequence, tuple, set, frozenset, deque)) or is_dataclass(
annotation
)


class TomlSettingsSource(InitSettingsSource):
"""
Source class for loading values provided through TOML files.
"""

def __init__(self, settings_cls: type[BaseSettings], toml_file: PathType | None = DEFAULT_PATH):
self.toml_file = toml_file if toml_file != DEFAULT_PATH else settings_cls.model_config.get('toml_file')
self.toml_data = self._load_toml_data()
super().__init__(settings_cls, self.toml_data)

def _load_toml_data(self) -> dict[str, Any | None]:
return self._read_toml_files()

def _read_toml_files(self) -> dict[str, Any | None]:
toml_files = self.toml_file
if toml_files is None:
return {}
if isinstance(toml_files, (str, os.PathLike)):
toml_files = [toml_files]
toml_vars: dict[str, Any | None] = {}
for toml_file in toml_files: # type: ignore[attr-defined]
toml_path = Path(toml_file).expanduser()
if toml_path.is_file():
toml_vars.update(self._read_toml_file(toml_path))
return toml_vars

def _read_toml_file(self, toml_path: Path) -> dict[str, Any | None]:
toml_data = {}
with open(toml_path, mode='rb') as fp:
try:
toml_data = tomllib.load(fp)
except Exception as e:
warnings.warn(f'Failed to load "{toml_path} - {e}"')
return toml_data

def __repr__(self) -> str:
return f'TomlSettingsSource(toml_file={self.toml_file!r})'
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ requires-python = '>=3.8'
dependencies = [
'pydantic>=2.3.0',
'python-dotenv>=0.21.0',
'tomli>=1.1.0; python_version < "3.11"',
]
dynamic = ['version']

Expand Down
36 changes: 35 additions & 1 deletion tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
PydanticBaseSettingsSource,
SecretsSettingsSource,
SettingsConfigDict,
TomlSettingsSource,
)
from pydantic_settings.sources import SettingsError, read_env_file

Expand Down Expand Up @@ -1910,8 +1911,41 @@ def test_dotenv_optional_json_field(tmp_path):

class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=p)

data: Optional[Json[Dict[str, str]]] = Field(default=None)

s = Settings()
assert s.data == {'foo': 'bar'}


def test_toml_file(tmp_path):
p = tmp_path / 'settings.toml'
toml_content = '''
foobar = "foobarval"

[foo]
bar = "nested-foo-bar"'''
p.write_text(toml_content)

class Settings(BaseSettings):
model_config = SettingsConfigDict(toml_file=p)

class Foo(BaseSettings):
bar: str

foobar: str
foo: Foo

@classmethod
def settings_customise_sources(
cls,
settings_cls: Type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return init_settings, env_settings, dotenv_settings, file_secret_settings, TomlSettingsSource(settings_cls)

s = Settings()
assert s.foobar == 'foobarval'
assert s.foo.bar == 'nested-foo-bar'