Skip to content
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

Add pytest-examples and make examples in docs testable #56

Merged
merged 7 commits into from
May 17, 2023
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ _build/
/sandbox/
/.ghtopdep_cache/
/worktrees/
/.ruff_cache/
6 changes: 2 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ install:

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

.PHONY: lint
lint:
flake8 $(sources)
isort $(sources) --check-only --df
ruff $(sources)
black $(sources) --check --diff

.PHONY: mypy
Expand Down
94 changes: 64 additions & 30 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ from typing import Any, Callable, Set

from pydantic import (
AliasChoices,
AmqpDsn,
BaseModel,
ConfigDict,
Field,
ImportString,
RedisDsn,
PostgresDsn,
AmqpDsn,
Field,
RedisDsn,
)

from pydantic_settings import BaseSettings


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

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

model_config = ConfigDict(env_prefix = 'my_prefix_') # defaults to no prefix, i.e. ""
model_config = ConfigDict(env_prefix='my_prefix_') # defaults to no prefix, i.e. ""


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

```py
from pydantic import ConfigDict

from pydantic_settings import BaseSettings


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

```py
from pydantic import BaseModel, ConfigDict

from pydantic_settings import BaseSettings


Expand All @@ -169,12 +173,7 @@ print(Settings().model_dump())
"""
{
'v0': '0',
'sub_model': {
'v1': 'json-1',
'v2': b'nested-2',
'v3': 3,
'deep': {'v4': 'v4'},
},
'sub_model': {'v1': 'json-1', 'v2': b'nested-2', 'v3': 3, 'deep': {'v4': 'v4'}},
}
"""
```
Expand All @@ -196,11 +195,18 @@ import os
from typing import Any, List, Tuple, Type

from pydantic.fields import FieldInfo
from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource

from pydantic_settings import (
BaseSettings,
EnvSettingsSource,
PydanticBaseSettingsSource,
)


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

```py
```py test="skip" lint="skip"
class Settings(BaseSettings):
...

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

```py
```py test="skip" lint="skip"
settings = Settings(_env_file='prod.env', _env_file_encoding='utf-8')
```

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

```py
from pydantic import BaseSettings
from pydantic import ConfigDict

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
...

model_config = ConfigDict(env_file=('.env', '.env.prod')) # `.env.prod` takes priority over `.env`
model_config = ConfigDict(
# `.env.prod` takes priority over `.env`
env_file=('.env', '.env.prod')
)
```

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

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

```py
```py test="skip" lint="skip"
class Settings(BaseSettings):
...
database_password: str
Expand All @@ -330,7 +342,7 @@ class Settings(BaseSettings):

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

```py
```py test="skip" lint="skip"
settings = Settings(_secrets_dir='/var/run')
```

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

First, define your Settings
```py
```py test="skip" lint="skip"
class Settings(BaseSettings):
my_secret_data: str

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

```py
from typing import Tuple, Type

from pydantic import PostgresDsn

from pydantic_settings import BaseSettings, PydanticBaseSettingsSource


Expand Down Expand Up @@ -436,6 +450,7 @@ from typing import Any, Dict, Tuple, Type

from pydantic import ConfigDict
from pydantic.fields import FieldInfo

from pydantic_settings import BaseSettings, PydanticBaseSettingsSource


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

def get_field_value(self, field: FieldInfo, field_name: str) -> Tuple[Any, str, bool]:
def get_field_value(
self, field: FieldInfo, field_name: str
) -> Tuple[Any, str, bool]:
encoding = self.config.get('env_file_encoding')
file_content_json = json.loads(Path('config.json').read_text(encoding))
file_content_json = json.loads(
Path('tests/example_test_config.json').read_text(encoding)
)
fiel_value = file_content_json.get(field_name)
return fiel_value, field_name, False


def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any:
def prepare_field_value(
self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool
) -> Any:
return value

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

for field_name, field in self.settings_cls.model_fields.items():
field_value, field_key, value_is_complex = self.get_field_value(field, field_name)
field_value = self.prepare_field_value(field_name, field, field_value, value_is_complex)
field_value, field_key, value_is_complex = self.get_field_value(
field, field_name
)
field_value = self.prepare_field_value(
field_name, field, field_value, value_is_complex
)
if field_value is not None:
d[field_key] = field_value

Expand Down Expand Up @@ -493,7 +517,7 @@ class Settings(BaseSettings):


print(Settings())
#> foobar='spam'
#> foobar='test'
```

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

from pydantic import ValidationError

from pydantic_settings import BaseSettings, PydanticBaseSettingsSource


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


print(Settings(my_api_key='this is ignored'))
# requires: `MY_API_KEY` env variable to be set, e.g. `export MY_API_KEY=xxx`
try:
Settings(my_api_key='this is ignored')
except ValidationError as exc_info:
print(exc_info)
"""
1 validation error for Settings
my_api_key
Field required [type=missing, input_value={}, input_type=dict]
For further information visit https://errors.pydantic.dev/2/v/missing
"""
```
2 changes: 1 addition & 1 deletion pydantic_settings/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ def _read_env_files(self, case_sensitive: bool) -> Mapping[str, str | None]:
def __call__(self) -> dict[str, Any]:
data: dict[str, Any] = super().__call__()

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

Expand Down
24 changes: 8 additions & 16 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,6 @@ filterwarnings = [
'ignore:This is a placeholder until pydantic-settings.*:UserWarning',
]

[tool.flake8]
max_line_length = 120
max_complexity = 14
inline_quotes = 'single'
multiline_quotes = 'double'
ignore = ['E203', 'W503']

[tool.coverage.run]
source = ['pydantic_settings']
branch = true
Expand All @@ -86,21 +79,20 @@ source = [
'pydantic_settings/',
]

[tool.ruff]
line-length = 120
extend-select = ['Q', 'RUF100', 'C90', 'UP', 'I']
flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'}
mccabe = { max-complexity = 14 }
isort = { known-first-party = ['pydantic_settings', 'tests'] }
target-version = 'py37'

[tool.black]
color = true
line-length = 120
target-version = ['py310']
skip-string-normalization = true

[tool.isort]
line_length = 120
known_first_party = 'pydantic_settings'
known_third_party = 'pydantic'
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
combine_as_imports = true

[tool.mypy]
python_version = '3.10'
show_error_codes = true
Expand Down
5 changes: 1 addition & 4 deletions requirements/linting.in
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
black
flake8
flake8-quotes
flake8-pyproject
isort
ruff
pyupgrade
mypy
pre-commit
28 changes: 4 additions & 24 deletions requirements/linting.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --output-file=requirements/linting.txt requirements/linting.in
#
Expand All @@ -14,21 +14,8 @@ distlib==0.3.6
# via virtualenv
filelock==3.12.0
# via virtualenv
flake8==6.0.0
# via
# -r requirements/linting.in
# flake8-pyproject
# flake8-quotes
flake8-pyproject==1.2.3
# via -r requirements/linting.in
flake8-quotes==3.3.2
# via -r requirements/linting.in
identify==2.5.24
# via pre-commit
isort==5.12.0
# via -r requirements/linting.in
mccabe==0.7.0
# via flake8
mypy==1.3.0
# via -r requirements/linting.in
mypy-extensions==1.0.0
Expand All @@ -47,21 +34,14 @@ platformdirs==3.5.1
# virtualenv
pre-commit==3.3.1
# via -r requirements/linting.in
pycodestyle==2.10.0
# via flake8
pyflakes==3.0.1
# via flake8
pyupgrade==3.4.0
# via -r requirements/linting.in
pyyaml==6.0
# via pre-commit
ruff==0.0.265
# via -r requirements/linting.in
tokenize-rt==5.0.0
# via pyupgrade
tomli==2.0.1
# via
# black
# flake8-pyproject
# mypy
typing-extensions==4.5.0
# via mypy
virtualenv==20.23.0
Expand Down
Loading