pydantic / pydantic-settings

Settings management using pydantic
https://docs.pydantic.dev/latest/usage/pydantic_settings/
MIT License
555 stars 55 forks source link

Nested Settings with instances of models as defaults #345

Closed mrijken closed 1 month ago

mrijken commented 1 month ago

When I have the next Models:

from pydantic import BaseModel
from pydantic_settings import BaseSettings

class DatabaseSettings(BaseModel):
    name: str = "test"
    env: str = "test"

class Settings(BaseSettings):
    db1: DatabaseSettings = DatabaseSettings(name="database1")
    db2: DatabaseSettings = DatabaseSettings(name="database2")

print(Settings(db1={"env": "prd"}))

I will get:

db1=DatabaseSettings(name='test', env='prd') db2=DatabaseSettings(name='database2', env='test')

However, I would expect:

db1=DatabaseSettings(name='database1', env='prd') db2=DatabaseSettings(name='database2', env='test')

I have written a small PydanticBaseSettingsSource fixes this behavior:

class FromClsSettings(pydantic_settings.PydanticBaseSettingsSource):
    def get_field_value(self, **kwargs) -> None:  # type: ignore[override]  # noqa: ANN003
        pass

    def __call__(self) -> dict[str, Any]:
        data = {}
        for field, field_info in self.settings_cls.model_fields.items():
            if isinstance(field_info.default, pydantic.BaseModel):
                data[field] = field_info.default.model_dump()
        return data

Is the issue the expected behavior? Is the proposed solution the correct one? Shall I make a PR?

hramezani commented 1 month ago

Thanks @mrijken for reporting this.

Actually, this is the correct behavior. you have db1: DatabaseSettings = DatabaseSettings(name="database1") in your model. it means the default value for db1 is DatabaseSettings(name="database1", env="test"). when you pass {"env": "prd"}, it will override the default with DatabaseSettings(name='test', env='prd')

kschwab commented 2 weeks ago

Hi @mrijken, I've added a flag nested_model_default_partial_update that enables partial updates for default model objects. Could you help verify it resolves your issue and post a simple test for your use case? I believe the case posted in the description is now resolved:

from pydantic import BaseModel
from pydantic_settings import BaseSettings

class DatabaseSettings(BaseModel):
    name: str = 'test'
    env: str = 'test'

class Settings(BaseSettings, nested_model_default_partial_update=True):
    db1: DatabaseSettings = DatabaseSettings(name='database1')
    db2: DatabaseSettings = DatabaseSettings(name='database2')

print(Settings(db1={'env': 'prd'}))
#> db1=DatabaseSettings(name='database1', env='prd') db2=DatabaseSettings(name='database2', env='test')