diff --git a/docs/index.md b/docs/index.md index e27f0fcc..19f0400b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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), @@ -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()) """ @@ -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 @@ -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 @@ -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: @@ -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 @@ -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, diff --git a/pydantic_settings/main.py b/pydantic_settings/main.py index 88478f16..dbe90f0c 100644 --- a/pydantic_settings/main.py +++ b/pydantic_settings/main.py @@ -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__( @@ -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( diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 3631e5fe..da5d90ef 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -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 @@ -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) @@ -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) @@ -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) @@ -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) @@ -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: @@ -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) @@ -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], @@ -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) @@ -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: @@ -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 @@ -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) @@ -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], @@ -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 (