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

Improve docstrings #72

Merged
merged 4 commits into from
Jun 8, 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
26 changes: 14 additions & 12 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ from pydantic_settings import BaseSettings


class Settings(BaseSettings):
redis_host: str = 'localhost'

model_config = ConfigDict(case_sensitive=True)

redis_host: str = 'localhost'
```

When `case_sensitive` is `True`, the environment variable names must match field names (optionally with a prefix),
Expand Down Expand Up @@ -163,11 +163,11 @@ class SubModel(BaseModel):


class Settings(BaseSettings):
model_config = ConfigDict(env_nested_delimiter='__')

v0: str
sub_model: SubModel

model_config = ConfigDict(env_nested_delimiter='__')


print(Settings().model_dump())
"""
Expand Down Expand Up @@ -259,9 +259,10 @@ in a `BaseSettings` class:

```py test="skip" lint="skip"
class Settings(BaseSettings):
model_config = ConfigDict(env_file='.env', env_file_encoding = 'utf-8')

...

model_config = ConfigDict(env_file='.env', env_file_encoding = 'utf-8')
```

**2.** instantiating a `BaseSettings` derived class with the `_env_file` keyword argument
Expand Down Expand Up @@ -297,12 +298,12 @@ from pydantic_settings import BaseSettings


class Settings(BaseSettings):
...

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 @@ -334,10 +335,11 @@ Once you have your secret files, *pydantic* supports loading it in two ways:

```py test="skip" lint="skip"
class Settings(BaseSettings):
model_config = ConfigDict(secrets_dir='/var/run')

...
database_password: str

model_config = ConfigDict(secrets_dir='/var/run')
```

**2.** instantiating a `BaseSettings` derived class with the `_secrets_dir` keyword argument:
Expand Down Expand Up @@ -366,9 +368,9 @@ and using secrets in Docker see the official
First, define your Settings
```py test="skip" lint="skip"
class Settings(BaseSettings):
my_secret_data: str

model_config = ConfigDict(secrets_dir='/run/secrets')

my_secret_data: str
```
!!! note
By default Docker uses `/run/secrets` as the target mount point. If you want to use a different location, change
Expand Down Expand Up @@ -495,10 +497,10 @@ class JsonConfigSettingsSource(PydanticBaseSettingsSource):


class Settings(BaseSettings):
foobar: str

model_config = ConfigDict(env_file_encoding='utf-8')

foobar: str

@classmethod
def settings_customise_sources(
cls,
Expand Down
21 changes: 21 additions & 0 deletions pydantic_settings/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ class BaseSettings(BaseModel):

This is useful in production for secrets you do not wish to save in code, it plays nicely with docker(-compose),
Heroku and any 12 factor app design.

All the bellow attributes can be set via `model_config`.

Args:
_env_file: The env file(s) to load settings values from. Defaults to `Path('')`.
_env_file_encoding: The env file encoding. e.g. `'latin-1'`. Defaults to `None`.
_env_nested_delimiter: The nested env values delimiter. Defaults to `None`.
_secrets_dir: The secret files directory. Defaults to `None`.
"""

def __init__(
Expand Down Expand Up @@ -64,6 +72,19 @@ def settings_customise_sources(
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
"""
Define the sources and their order for loading the settings values.

Args:
settings_cls: The Settings class.
init_settings: The `InitSettingsSource` instance.
env_settings: The `EnvSettingsSource` instance.
dotenv_settings: The `DotEnvSettingsSource` instance.
file_secret_settings: The `SecretsSettingsSource` instance.

Returns:
A tuple containing the sources and their order for loading the settings values.
"""
return init_settings, env_settings, dotenv_settings, file_secret_settings

def _settings_build_values(
Expand Down
109 changes: 88 additions & 21 deletions pydantic_settings/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str,
This is an abstract method that should be overrided in every settings source classes.

Args:
field (FieldInfo): The field.
field_name (str): The field name.
field: The field.
field_name: The field name.

Returns:
tuple[str, Any, bool]: The key, value and a flag to determine whether value is complex.
A tuple contains the key, value and a flag to determine whether value is complex.
"""
pass

Expand All @@ -58,10 +58,10 @@ def field_is_complex(self, field: FieldInfo) -> bool:
Checks whether a field is complex, in which case it will attempt to be parsed as JSON.

Args:
field (FieldInfo): The field.
field: The field.

Returns:
bool: Whether the field is complex.
Whether the field is complex.
"""
return _annotation_is_complex(field.annotation)

Expand All @@ -70,13 +70,13 @@ def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, val
Prepares the value of a field.

Args:
field_name (str): The field name.
field (FieldInfo): The field.
value (Any): The value of the field that has to be prepared.
field_name: The field name.
field: The field.
value: The value of the field that has to be prepared.
value_is_complex: A flag to determine whether value is complex.

Returns:
Any: The prepared value.
The prepared value.
"""
if self.field_is_complex(field) or value_is_complex:
return json.loads(value)
Expand All @@ -88,6 +88,10 @@ def __call__(self) -> dict[str, Any]:


class InitSettingsSource(PydanticBaseSettingsSource):
"""
Source class for loading values provided during settings class initialization.
"""

def __init__(self, settings_cls: type[BaseSettings], init_kwargs: dict[str, Any]):
self.init_kwargs = init_kwargs
super().__init__(settings_cls)
Expand Down Expand Up @@ -236,6 +240,10 @@ def __call__(self) -> dict[str, Any]:


class SecretsSettingsSource(PydanticBaseEnvSettingsSource):
"""
Source class for loading settings values from secret files.
"""

def __init__(self, settings_cls: type[BaseSettings], secrets_dir: str | Path | None):
self.secrets_dir = secrets_dir
super().__init__(settings_cls)
Expand Down Expand Up @@ -264,6 +272,14 @@ def __call__(self) -> dict[str, Any]:
def find_case_path(cls, dir_path: Path, file_name: str, case_sensitive: bool) -> Path | None:
"""
Find a file within path's directory matching filename, optionally ignoring case.

Args:
dir_path: Directory path.
file_name: File name.
case_sensitive: Whether to search for file name case sensitively.

Returns:
Whether file path or `None` if file does not exist in directory.
"""
for f in dir_path.iterdir():
if f.name == file_name:
Expand All @@ -273,6 +289,18 @@ def find_case_path(cls, dir_path: Path, file_name: str, case_sensitive: bool) ->
return None

def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
"""
Gets the value for field from secret file and a flag to determine whether value is complex.

Args:
field: The field.
field_name: The field name.

Returns:
A tuple contains the key, value if the file exists otherwise `None`, and
a flag to determine whether value is complex.
"""

for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name):
path = self.find_case_path(
self.secrets_path, env_name, self.settings_cls.model_config.get('case_sensitive', False)
Expand All @@ -296,6 +324,10 @@ def __repr__(self) -> str:


class EnvSettingsSource(PydanticBaseEnvSettingsSource):
"""
Source class for loading settings values from environment variables.
"""

def __init__(
self,
settings_cls: type[BaseSettings],
Expand All @@ -315,6 +347,18 @@ def _load_env_vars(self) -> Mapping[str, str | None]:
return {k.lower(): v for k, v in os.environ.items()}

def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
"""
Gets the value for field from environment variables and a flag to determine whether value is complex.

Args:
field: The field.
field_name: The field name.

Returns:
A tuple contains the key, value if the file exists otherwise `None`, and
a flag to determine whether value is complex.
"""

env_val: str | None = None
for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name):
env_val = self.env_vars.get(env_name)
Expand All @@ -324,6 +368,22 @@ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str,
return env_val, field_key, value_is_complex

def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any:
"""
Prepare value for the field.

* Extract value for nested field.
* Deserialize value to python object for complex field.

Args:
field: The field.
field_name: The field name.

Returns:
A tuple contains prepared value for the field.

Raises:
ValuesError: When There is an error in deserializing value for complex field.
"""
is_complex, allow_parse_failure = self._field_is_complex(field)
if is_complex or value_is_complex:
if value is None:
Expand Down Expand Up @@ -382,6 +442,13 @@ class Cfg(BaseSettings):
Then:
next_field(sub_model, 'vals') Returns the `vals` field of `SubModel` class
next_field(sub_model, 'sub_sub_model') Returns `sub_sub_model` field of `SubModel` class

Args:
field: The field.
key: The key (env name).

Returns:
Field if it finds the next field otherwise `None`.
"""
if not field or origin_is_union(get_origin(field.annotation)):
# no support for Unions of complex BaseSettings fields
Expand All @@ -396,6 +463,14 @@ def explode_env_vars(self, field_name: str, field: FieldInfo, env_vars: Mapping[
Process env_vars and extract the values of keys containing env_nested_delimiter into nested dictionaries.

This is applied to a single field, hence filtering by env_var prefix.

Args:
field_name: The field name.
field: The field.
env_vars: Environment variables.

Returns:
A dictionaty contains extracted values from nested env values.
"""
prefixes = [
f'{env_name}{self.env_nested_delimiter}' for _, env_name, _ in self._extract_field_info(field, field_name)
Expand Down Expand Up @@ -437,6 +512,10 @@ def __repr__(self) -> str:


class DotEnvSettingsSource(EnvSettingsSource):
"""
Source class for loading settings values from env files.
"""

def __init__(
self,
settings_cls: type[BaseSettings],
Expand Down Expand Up @@ -514,18 +593,6 @@ def read_env_file(
return file_vars


def find_case_path(dir_path: Path, file_name: str, case_sensitive: bool) -> Path | None:
"""
Find a file within path's directory matching filename, optionally ignoring case.
"""
for f in dir_path.iterdir():
if f.name == file_name:
return f
elif not case_sensitive and f.name.lower() == file_name.lower():
return f
return None


def _annotation_is_complex(annotation: type[Any] | None) -> bool:
origin = get_origin(annotation)
return (
Expand Down