pydantic / pydantic-settings

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

Feature request: Add `_init_ignore_none` to BaseSettings #247

Open jjeff07 opened 4 months ago

jjeff07 commented 4 months ago

I would like to keep the default order of init_settings and then env_settings but if I pass None in the init then it will error.

Example:

# .env
username=user
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="allow")
    username: str

settings = Settings(username=None)
username
  Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
    For further information visit https://errors.pydantic.dev/2.6/v/string_type, <traceback object at 0x0000014EA7D5E680>)

Implementation:

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="allow")
    username: str

settings = Settings(username=None, _env_ignore_none=True)
print(settings.username)
>>> user
hramezani commented 4 months ago

Thanks @jjeff07 for this feature request.

You are setting explicitly the value for username in init and it is the correct behavior. As this is a rare case, I would like not to accept this feature because adding more flags makes the code base complex. You can implement your custom InitSettingsSource and have your desired behavior there.

jjeff07 commented 4 months ago

If I am allowed to set _env_ignore_empty=True to ignore empty environment variables shouldn't there be a way to do it with init args? For example:

import os
import httpx
from pydantic import BaseModel
from pydantic_settings import BaseSettings

os.environ['password'] = 'world'

class Settings(BaseSettings):
    url: str
    username: str
    password: str

class Client(Settings, BaseModel):
    def __init__(self, url=None, username=None, password=None):
        super().__init__(url=url, username=username, password=password)
        self.client = httpx.Client(base_url=self.url, auth=(self.username, self.password))

client = Client(url='google.com', username='Hello')

"""
(<class 'pydantic_core._pydantic_core.ValidationError'>, 1 validation error for Client
password
  Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
    For further information visit https://errors.pydantic.dev/2.6/v/string_type, <traceback object at 0x00000226F8500D80>)
"""
hramezani commented 4 months ago

You can have your desired behavior like:

from typing import Any, Tuple, Type

from pydantic_settings import BaseSettings, InitSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict

class MyInitSettingsSource(InitSettingsSource):
    def __init__(self, settings_cls: type[BaseSettings], init_kwargs: dict[str, Any]):
        init_kwargs = {k: v for k, v in init_kwargs.items() if v is not None}
        super().__init__(settings_cls, init_kwargs)

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="allow")
    username: str

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: Type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> Tuple[PydanticBaseSettingsSource, ...]:
        return MyInitSettingsSource(settings_cls, init_settings.init_kwargs), env_settings, dotenv_settings, file_secret_settings

settings = Settings(username=None)
jjeff07 commented 4 months ago

It would be nice if there were no Unresolved References though. Probably why I couldn't figure out a solution as there are no documented examples.

image