From e0f3feb9c6b3fb7259b2cdd5f447878edf4317f8 Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Mon, 12 Aug 2024 21:06:15 -0600 Subject: [PATCH] Doc updates. --- docs/index.md | 66 ++++++++++++++++++++++++++++++++++++ pydantic_settings/sources.py | 14 ++++---- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 85d059d..56ed960 100644 --- a/docs/index.md +++ b/docs/index.md @@ -464,6 +464,72 @@ class Settings(BaseSettings): So if you provide extra values in a dotenv file, whether they start with `env_prefix` or not, a `ValidationError` will be raised. +## Parsing default objects + +Pydantic settings uses copy-by-reference when merging default (`BaseModel`) objects from different sources. This ensures +the original object reference is maintained in the final object instantiation. However, due to an internal limitation, +it is not possible to partially update a nested sub model field in a default object when using copy-by-reference; the +entirety of the sub model must be provided. + +This behavior can be overriden by setting the `default_objects_copy_by_value` flag to `True`, which will allow partial +updates to sub model fields. Note of course the original default object reference will not be retained. + +```py +import os + +from pydantic import BaseModel, ValidationError + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class SubModel(BaseModel): + val: int = 0 + flag: bool = False + + +ORIGINAL_OBJECT = SubModel() + + +class SettingsCopyByReference(BaseSettings): + model_config = SettingsConfigDict(env_nested_delimiter='__') + + default_object: SubModel = ORIGINAL_OBJECT + + +class SettingsCopyByValue(BaseSettings): + model_config = SettingsConfigDict( + env_nested_delimiter='__', default_objects_copy_by_value=True + ) + + default_object: SubModel = ORIGINAL_OBJECT + + +by_ref = SettingsCopyByReference() +assert by_ref.default_object is ORIGINAL_OBJECT + +# Apply a partial update to the default object using environment variables +os.environ['DEFAULT_OBJECT__FLAG'] = 'True' + +try: + # Copy by reference will fail + SettingsCopyByReference() +except ValidationError as err: + print(err) + """ + 1 validation error for SettingsCopyByReference + nested.val + Field required [type=missing, input_value={'flag': 'TRUE'}, input_type=dict] + For further information visit https://errors.pydantic.dev/2/v/missing + """ + +# Copy by value will pass +by_val = SettingsCopyByValue() +assert by_val.default_object is not ORIGINAL_OBJECT + +print(by_val.model_dump()) +#> {'default_object': {'val': 0, 'flag': True}} +``` + ## Command Line Support Pydantic settings provides integrated CLI support, making it easy to quickly define CLI applications using Pydantic diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 7b02872..3a5ff44 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -262,12 +262,14 @@ def __init__(self, settings_cls: type[BaseSettings], default_objects_copy_by_val if default_objects_copy_by_value is not None else self.config.get('default_objects_copy_by_value', False) ) - if self.default_objects_copy_by_value: - for field_name, field_info in settings_cls.model_fields.items(): - if is_dataclass(type(field_info.default)): - self.defaults[_field_name_for_signature(field_name, field_info)] = asdict(field_info.default) - elif is_model_class(type(field_info.default)): - self.defaults[_field_name_for_signature(field_name, field_info)] = field_info.default.model_dump() + for field_name, field_info in settings_cls.model_fields.items(): + if not self.default_objects_copy_by_value: + if is_dataclass(type(field_info.default)) or is_model_class(type(field_info.default)): + self.defaults[_field_name_for_signature(field_name, field_info)] = field_info.default + elif is_dataclass(type(field_info.default)): + self.defaults[_field_name_for_signature(field_name, field_info)] = asdict(field_info.default) + elif is_model_class(type(field_info.default)): + self.defaults[_field_name_for_signature(field_name, field_info)] = field_info.default.model_dump() def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: # Nothing to do here. Only implement the return statement to make mypy happy