Skip to content

Commit 411b88f

Browse files
Add pytest-examples and make examples in docs testable (#56)
Co-authored-by: Samuel Colvin <[email protected]>
1 parent dc9efb3 commit 411b88f

File tree

13 files changed

+251
-101
lines changed

13 files changed

+251
-101
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ _build/
2424
/sandbox/
2525
/.ghtopdep_cache/
2626
/worktrees/
27+
/.ruff_cache/

Makefile

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ install:
99

1010
.PHONY: format
1111
format:
12-
pyupgrade --py37-plus --exit-zero-even-if-changed `find $(sources) -name "*.py" -type f`
13-
isort $(sources)
1412
black $(sources)
13+
ruff --fix $(sources)
1514

1615
.PHONY: lint
1716
lint:
18-
flake8 $(sources)
19-
isort $(sources) --check-only --df
17+
ruff $(sources)
2018
black $(sources) --check --diff
2119

2220
.PHONY: mypy

docs/index.md

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ from typing import Any, Callable, Set
1616

1717
from pydantic import (
1818
AliasChoices,
19+
AmqpDsn,
1920
BaseModel,
2021
ConfigDict,
22+
Field,
2123
ImportString,
22-
RedisDsn,
2324
PostgresDsn,
24-
AmqpDsn,
25-
Field,
25+
RedisDsn,
2626
)
27+
2728
from pydantic_settings import BaseSettings
2829

2930

@@ -38,7 +39,7 @@ class Settings(BaseSettings):
3839

3940
redis_dsn: RedisDsn = Field(
4041
'redis://user:pass@localhost:6379/1',
41-
validation_alias=AliasChoices('service_redis_dsn', 'redis_url')
42+
validation_alias=AliasChoices('service_redis_dsn', 'redis_url'),
4243
)
4344
pg_dsn: PostgresDsn = 'postgres://user:pass@localhost:5432/foobar'
4445
amqp_dsn: AmqpDsn = 'amqp://user:pass@localhost:5672/'
@@ -53,7 +54,8 @@ class Settings(BaseSettings):
5354
# export my_prefix_more_settings='{"foo": "x", "apple": 1}'
5455
more_settings: SubModel = SubModel()
5556

56-
model_config = ConfigDict(env_prefix = 'my_prefix_') # defaults to no prefix, i.e. ""
57+
model_config = ConfigDict(env_prefix='my_prefix_') # defaults to no prefix, i.e. ""
58+
5759

5860
print(Settings().model_dump())
5961
"""
@@ -63,7 +65,7 @@ print(Settings().model_dump())
6365
'redis_dsn': Url('redis://user:pass@localhost:6379/1'),
6466
'pg_dsn': Url('postgres://user:pass@localhost:5432/foobar'),
6567
'amqp_dsn': Url('amqp://user:pass@localhost:5672/'),
66-
'special_function': <built-in function cos>,
68+
'special_function': math.cos,
6769
'domains': set(),
6870
'more_settings': {'foo': 'bar', 'apple': 1},
6971
}
@@ -92,6 +94,7 @@ Case-sensitivity can be turned on through the `model_config`:
9294

9395
```py
9496
from pydantic import ConfigDict
97+
9598
from pydantic_settings import BaseSettings
9699

97100

@@ -144,6 +147,7 @@ You could load a settings module thus:
144147

145148
```py
146149
from pydantic import BaseModel, ConfigDict
150+
147151
from pydantic_settings import BaseSettings
148152

149153

@@ -169,12 +173,7 @@ print(Settings().model_dump())
169173
"""
170174
{
171175
'v0': '0',
172-
'sub_model': {
173-
'v1': 'json-1',
174-
'v2': b'nested-2',
175-
'v3': 3,
176-
'deep': {'v4': 'v4'},
177-
},
176+
'sub_model': {'v1': 'json-1', 'v2': b'nested-2', 'v3': 3, 'deep': {'v4': 'v4'}},
178177
}
179178
"""
180179
```
@@ -196,11 +195,18 @@ import os
196195
from typing import Any, List, Tuple, Type
197196

198197
from pydantic.fields import FieldInfo
199-
from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource
198+
199+
from pydantic_settings import (
200+
BaseSettings,
201+
EnvSettingsSource,
202+
PydanticBaseSettingsSource,
203+
)
200204

201205

202206
class MyCustomSource(EnvSettingsSource):
203-
def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any:
207+
def prepare_field_value(
208+
self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool
209+
) -> Any:
204210
if field_name == 'numbers':
205211
return [int(x) for x in value.split(',')]
206212
return json.loads(value)
@@ -251,7 +257,7 @@ Once you have your `.env` file filled with variables, *pydantic* supports loadin
251257
**1.** setting `env_file` (and `env_file_encoding` if you don't want the default encoding of your OS) on `model_config`
252258
in a `BaseSettings` class:
253259

254-
```py
260+
```py test="skip" lint="skip"
255261
class Settings(BaseSettings):
256262
...
257263

@@ -261,7 +267,7 @@ class Settings(BaseSettings):
261267
**2.** instantiating a `BaseSettings` derived class with the `_env_file` keyword argument
262268
(and the `_env_file_encoding` if needed):
263269

264-
```py
270+
```py test="skip" lint="skip"
265271
settings = Settings(_env_file='prod.env', _env_file_encoding='utf-8')
266272
```
267273

@@ -285,12 +291,18 @@ If you need to load multiple dotenv files, you can pass the file paths as a `lis
285291
Later files in the list/tuple will take priority over earlier files.
286292

287293
```py
288-
from pydantic import BaseSettings
294+
from pydantic import ConfigDict
295+
296+
from pydantic_settings import BaseSettings
297+
289298

290299
class Settings(BaseSettings):
291300
...
292301

293-
model_config = ConfigDict(env_file=('.env', '.env.prod')) # `.env.prod` takes priority over `.env`
302+
model_config = ConfigDict(
303+
# `.env.prod` takes priority over `.env`
304+
env_file=('.env', '.env.prod')
305+
)
294306
```
295307

296308
You can also use the keyword argument override to tell Pydantic not to load any file at all (even if one is set in
@@ -320,7 +332,7 @@ Once you have your secret files, *pydantic* supports loading it in two ways:
320332

321333
**1.** setting `secrets_dir` on `model_config` in a `BaseSettings` class to the directory where your secret files are stored:
322334

323-
```py
335+
```py test="skip" lint="skip"
324336
class Settings(BaseSettings):
325337
...
326338
database_password: str
@@ -330,7 +342,7 @@ class Settings(BaseSettings):
330342

331343
**2.** instantiating a `BaseSettings` derived class with the `_secrets_dir` keyword argument:
332344

333-
```py
345+
```py test="skip" lint="skip"
334346
settings = Settings(_secrets_dir='/var/run')
335347
```
336348

@@ -352,7 +364,7 @@ and using secrets in Docker see the official
352364
[Docker documentation](https://docs.docker.com/engine/reference/commandline/secret/).
353365

354366
First, define your Settings
355-
```py
367+
```py test="skip" lint="skip"
356368
class Settings(BaseSettings):
357369
my_secret_data: str
358370

@@ -399,7 +411,9 @@ The order of the returned callables decides the priority of inputs; first item i
399411

400412
```py
401413
from typing import Tuple, Type
414+
402415
from pydantic import PostgresDsn
416+
403417
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource
404418

405419

@@ -436,6 +450,7 @@ from typing import Any, Dict, Tuple, Type
436450

437451
from pydantic import ConfigDict
438452
from pydantic.fields import FieldInfo
453+
439454
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource
440455

441456

@@ -448,22 +463,31 @@ class JsonConfigSettingsSource(PydanticBaseSettingsSource):
448463
when reading `config.json`
449464
"""
450465

451-
def get_field_value(self, field: FieldInfo, field_name: str) -> Tuple[Any, str, bool]:
466+
def get_field_value(
467+
self, field: FieldInfo, field_name: str
468+
) -> Tuple[Any, str, bool]:
452469
encoding = self.config.get('env_file_encoding')
453-
file_content_json = json.loads(Path('config.json').read_text(encoding))
470+
file_content_json = json.loads(
471+
Path('tests/example_test_config.json').read_text(encoding)
472+
)
454473
fiel_value = file_content_json.get(field_name)
455474
return fiel_value, field_name, False
456475

457-
458-
def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any:
476+
def prepare_field_value(
477+
self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool
478+
) -> Any:
459479
return value
460480

461481
def __call__(self) -> Dict[str, Any]:
462482
d: Dict[str, Any] = {}
463483

464484
for field_name, field in self.settings_cls.model_fields.items():
465-
field_value, field_key, value_is_complex = self.get_field_value(field, field_name)
466-
field_value = self.prepare_field_value(field_name, field, field_value, value_is_complex)
485+
field_value, field_key, value_is_complex = self.get_field_value(
486+
field, field_name
487+
)
488+
field_value = self.prepare_field_value(
489+
field_name, field, field_value, value_is_complex
490+
)
467491
if field_value is not None:
468492
d[field_key] = field_value
469493

@@ -493,7 +517,7 @@ class Settings(BaseSettings):
493517

494518

495519
print(Settings())
496-
#> foobar='spam'
520+
#> foobar='test'
497521
```
498522

499523
### Removing sources
@@ -503,6 +527,8 @@ You might also want to disable a source:
503527
```py
504528
from typing import Tuple, Type
505529

530+
from pydantic import ValidationError
531+
506532
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource
507533

508534

@@ -522,6 +548,14 @@ class Settings(BaseSettings):
522548
return env_settings, file_secret_settings
523549

524550

525-
print(Settings(my_api_key='this is ignored'))
526-
# requires: `MY_API_KEY` env variable to be set, e.g. `export MY_API_KEY=xxx`
551+
try:
552+
Settings(my_api_key='this is ignored')
553+
except ValidationError as exc_info:
554+
print(exc_info)
555+
"""
556+
1 validation error for Settings
557+
my_api_key
558+
Field required [type=missing, input_value={}, input_type=dict]
559+
For further information visit https://errors.pydantic.dev/2/v/missing
560+
"""
527561
```

pydantic_settings/sources.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ def _read_env_files(self, case_sensitive: bool) -> Mapping[str, str | None]:
470470
def __call__(self) -> dict[str, Any]:
471471
data: dict[str, Any] = super().__call__()
472472

473-
data_lower_keys: List[str] = []
473+
data_lower_keys: list[str] = []
474474
if not self.settings_cls.model_config.get('case_sensitive', False):
475475
data_lower_keys = [x.lower() for x in data.keys()]
476476

pyproject.toml

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,6 @@ filterwarnings = [
5959
'ignore:This is a placeholder until pydantic-settings.*:UserWarning',
6060
]
6161

62-
[tool.flake8]
63-
max_line_length = 120
64-
max_complexity = 14
65-
inline_quotes = 'single'
66-
multiline_quotes = 'double'
67-
ignore = ['E203', 'W503']
68-
6962
[tool.coverage.run]
7063
source = ['pydantic_settings']
7164
branch = true
@@ -86,21 +79,20 @@ source = [
8679
'pydantic_settings/',
8780
]
8881

82+
[tool.ruff]
83+
line-length = 120
84+
extend-select = ['Q', 'RUF100', 'C90', 'UP', 'I']
85+
flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'}
86+
mccabe = { max-complexity = 14 }
87+
isort = { known-first-party = ['pydantic_settings', 'tests'] }
88+
target-version = 'py37'
89+
8990
[tool.black]
9091
color = true
9192
line-length = 120
9293
target-version = ['py310']
9394
skip-string-normalization = true
9495

95-
[tool.isort]
96-
line_length = 120
97-
known_first_party = 'pydantic_settings'
98-
known_third_party = 'pydantic'
99-
multi_line_output = 3
100-
include_trailing_comma = true
101-
force_grid_wrap = 0
102-
combine_as_imports = true
103-
10496
[tool.mypy]
10597
python_version = '3.10'
10698
show_error_codes = true

requirements/linting.in

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
black
2-
flake8
3-
flake8-quotes
4-
flake8-pyproject
5-
isort
2+
ruff
63
pyupgrade
74
mypy
85
pre-commit

requirements/linting.txt

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
2-
# This file is autogenerated by pip-compile with python 3.10
3-
# To update, run:
2+
# This file is autogenerated by pip-compile with Python 3.11
3+
# by the following command:
44
#
55
# pip-compile --output-file=requirements/linting.txt requirements/linting.in
66
#
@@ -14,21 +14,8 @@ distlib==0.3.6
1414
# via virtualenv
1515
filelock==3.12.0
1616
# via virtualenv
17-
flake8==6.0.0
18-
# via
19-
# -r requirements/linting.in
20-
# flake8-pyproject
21-
# flake8-quotes
22-
flake8-pyproject==1.2.3
23-
# via -r requirements/linting.in
24-
flake8-quotes==3.3.2
25-
# via -r requirements/linting.in
2617
identify==2.5.24
2718
# via pre-commit
28-
isort==5.12.0
29-
# via -r requirements/linting.in
30-
mccabe==0.7.0
31-
# via flake8
3219
mypy==1.3.0
3320
# via -r requirements/linting.in
3421
mypy-extensions==1.0.0
@@ -47,21 +34,14 @@ platformdirs==3.5.1
4734
# virtualenv
4835
pre-commit==3.3.1
4936
# via -r requirements/linting.in
50-
pycodestyle==2.10.0
51-
# via flake8
52-
pyflakes==3.0.1
53-
# via flake8
5437
pyupgrade==3.4.0
5538
# via -r requirements/linting.in
5639
pyyaml==6.0
5740
# via pre-commit
41+
ruff==0.0.265
42+
# via -r requirements/linting.in
5843
tokenize-rt==5.0.0
5944
# via pyupgrade
60-
tomli==2.0.1
61-
# via
62-
# black
63-
# flake8-pyproject
64-
# mypy
6545
typing-extensions==4.5.0
6646
# via mypy
6747
virtualenv==20.23.0

0 commit comments

Comments
 (0)