Skip to content
Open
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
8 changes: 7 additions & 1 deletion pydantic_settings/sources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,13 @@ def _read_files(self, files: PathType | None, deep_merge: bool = False) -> dict[
files = [files]
vars: dict[str, Any] = {}
for file in files:
file_path = Path(file).expanduser()
if isinstance(file, str):
file_path = Path(file)
else:
file_path = file
Comment on lines +203 to +206
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic has a bug when handling Traversable objects. The check at line 199 isinstance(files, (str, os.PathLike)) doesn't catch Traversable objects (they don't implement os.PathLike). This means a single Traversable object won't be converted to a list, and the iteration at line 202 will fail or behave incorrectly.

Consider changing the logic to check if files is not a sequence (or more specifically, check if it has the file-like interface you need) before wrapping it in a list. For example:

if not isinstance(files, Sequence) or isinstance(files, (str, os.PathLike)):
    files = [files]

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sube-py would be great if you could handle this

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

if isinstance(file_path, Path):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this if here? As we convert str to Path above?

file_path = file_path.expanduser()

if not file_path.is_file():
continue

Expand Down
2 changes: 1 addition & 1 deletion pydantic_settings/sources/providers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(
super().__init__(settings_cls, self.json_data)

def _read_file(self, file_path: Path) -> dict[str, Any]:
with open(file_path, encoding=self.json_file_encoding) as json_file:
with file_path.open(encoding=self.json_file_encoding) as json_file:
return json.load(json_file)

def __repr__(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion pydantic_settings/sources/providers/toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def __init__(

def _read_file(self, file_path: Path) -> dict[str, Any]:
import_toml()
with open(file_path, mode='rb') as toml_file:
with file_path.open(mode='rb') as toml_file:
if sys.version_info < (3, 11):
return tomli.load(toml_file)
return tomllib.load(toml_file)
Expand Down
2 changes: 1 addition & 1 deletion pydantic_settings/sources/providers/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __init__(

def _read_file(self, file_path: Path) -> dict[str, Any]:
import_yaml()
with open(file_path, encoding=self.yaml_file_encoding) as yaml_file:
with file_path.open(encoding=self.yaml_file_encoding) as yaml_file:
return yaml.safe_load(yaml_file) or {}

def __repr__(self) -> str:
Expand Down
31 changes: 31 additions & 0 deletions tests/test_source_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Test pydantic_settings.JsonConfigSettingsSource.
"""

import importlib.resources
import json
from pathlib import Path

Expand Down Expand Up @@ -132,3 +133,33 @@ def settings_customise_sources(

s = Settings()
assert s.model_dump() == {'hello': 'world', 'nested': {'foo': 3, 'bar': 2 if deep_merge else 0}}


def test_traversable_support():
# get Traversable object
tests_package_dir = importlib.resources.files('tests')
json_config_path = tests_package_dir / 'example_test_config.json'
assert json_config_path.is_file()

class Settings(BaseSettings):
foobar: str

model_config = SettingsConfigDict(
# Traversable is not added in annotation, but is supported
json_file=json_config_path,
)

@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 (JsonConfigSettingsSource(settings_cls),)

s = Settings()
# "test" value in file
assert s.foobar == 'test'