1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
|
"""
Test pydantic_settings.JsonConfigSettingsSource.
"""
import importlib.resources
import json
import sys
if sys.version_info < (3, 11):
from importlib.abc import Traversable
else:
from importlib.resources.abc import Traversable
from pathlib import Path
import pytest
from pydantic import BaseModel
from pydantic_settings import (
BaseSettings,
JsonConfigSettingsSource,
PydanticBaseSettingsSource,
SettingsConfigDict,
)
def test_repr() -> None:
source = JsonConfigSettingsSource(BaseSettings, Path('config.json'))
assert repr(source) == 'JsonConfigSettingsSource(json_file=config.json)'
def test_json_file(tmp_path):
p = tmp_path / '.env'
p.write_text(
"""
{"foobar": "Hello", "nested": {"nested_field": "world!"}, "null_field": null}
"""
)
class Nested(BaseModel):
nested_field: str
class Settings(BaseSettings):
model_config = SettingsConfigDict(json_file=p)
foobar: str
nested: Nested
null_field: str | None
@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()
assert s.foobar == 'Hello'
assert s.nested.nested_field == 'world!'
def test_json_no_file():
class Settings(BaseSettings):
model_config = SettingsConfigDict(json_file=None)
@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()
assert s.model_dump() == {}
def test_multiple_file_json(tmp_path):
p5 = tmp_path / '.env.json5'
p6 = tmp_path / '.env.json6'
with open(p5, 'w') as f5:
json.dump({'json5': 5}, f5)
with open(p6, 'w') as f6:
json.dump({'json6': 6}, f6)
class Settings(BaseSettings):
json5: int
json6: int
@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, json_file=[p5, p6]),)
s = Settings()
assert s.model_dump() == {'json5': 5, 'json6': 6}
@pytest.mark.parametrize('deep_merge', [False, True])
def test_multiple_file_json_merge(tmp_path, deep_merge):
p5 = tmp_path / '.env.json5'
p6 = tmp_path / '.env.json6'
with open(p5, 'w') as f5:
json.dump({'hello': 'world', 'nested': {'foo': 1, 'bar': 2}}, f5)
with open(p6, 'w') as f6:
json.dump({'nested': {'foo': 3}}, f6)
class Nested(BaseModel):
foo: int
bar: int = 0
class Settings(BaseSettings):
hello: str
nested: Nested
@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, json_file=[p5, p6], deep_merge=deep_merge),)
s = Settings()
assert s.model_dump() == {'hello': 'world', 'nested': {'foo': 3, 'bar': 2 if deep_merge else 0}}
class TestTraversableSupport:
FILENAME = 'example_test_config.json'
@pytest.fixture(params=['importlib_resources', 'custom', 'custom_with_path'])
def json_config_path(self, request, tmp_path):
tests_package_dir = importlib.resources.files('tests')
if request.param == 'importlib_resources':
# get Traversable object using importlib.resources
return tests_package_dir / self.FILENAME
# Create a custom Traversable implementation
class CustomTraversable(Traversable):
def __init__(self, path):
self._path = path
def __truediv__(self, child):
return CustomTraversable(self._path / child)
def is_file(self):
return self._path.is_file()
def is_dir(self):
return self._path.is_dir()
def iterdir(self):
raise NotImplementedError('iterdir not implemented for this test')
def open(self, mode='r', *args, **kwargs):
return self._path.open(mode, *args, **kwargs)
def read_bytes(self):
return self._path.read_bytes()
def read_text(self, encoding=None):
return self._path.read_text(encoding=encoding)
@property
def name(self):
return self._path.name
def joinpath(self, *descendants):
return CustomTraversable(self._path.joinpath(*descendants))
if request.param == 'custom':
custom_traversable = CustomTraversable(tests_package_dir)
return custom_traversable / self.FILENAME
filepath = tmp_path / self.FILENAME
with filepath.open('w') as f:
json.dump({'foobar': 'test'}, f)
return CustomTraversable(filepath)
def test_traversable_support(self, json_config_path: Traversable):
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'
|