Skip to content

EnvSettingsSource does not respect populate_by_name for aliased fields  #452

@hozn

Description

@hozn

It looks like the logic to extract fields from the environment (e.g. os.environ) only respects the alias for aliased Fields vs. also allowing for env vars to be set by name when populate_by_name=True in the model config.

Simple reproduce script:

import os
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class AppSettings(BaseSettings):
    SETTING_NAME: str = Field("default", alias="ALIAS_NAME")
    model_config = SettingsConfigDict(populate_by_name=True, case_sensitive=True)


if __name__ == "__main__":
    os.environ['SETTING_NAME'] = 'by-name'
    settings = AppSettings()
    print(f"{settings.SETTING_NAME=}")

    os.environ['ALIAS_NAME'] = 'by-alias'
    settings = AppSettings()
    print(f"{settings.SETTING_NAME=}")

# Outputs:
# settings.SETTING_NAME='default'
# settings.SETTING_NAME='by-alias'

Note that changing the env source to use a custom source does appear to address this, though I'm not confident this is the correct approach:

import os
from pydantic import Field
from pydantic.fields import FieldInfo
from pydantic_settings import BaseSettings, SettingsConfigDict, EnvSettingsSource, PydanticBaseSettingsSource, \
    DotEnvSettingsSource


class BetterAliasEnvSettingsSource(EnvSettingsSource):
    """
    Overrides the base env settings source to also respect populate_by_name when settings vars are aliased.
    """

    def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]:
        """
        Overrides super method to add better support for aliased fields.
        """

        field_info = super()._extract_field_info(field, field_name)

        if field.validation_alias:
            # Also add the column name if configured to allow setting by name
            if self.config.get("populate_by_name"):
                field_info.append((field_name, self._apply_case_sensitive(self.env_prefix + field_name), False))

        return field_info


class AppSettings(BaseSettings):
    SETTING_NAME: str = Field("default", alias="ALIAS_NAME")
    model_config = SettingsConfigDict(populate_by_name=True, case_sensitive=True)

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: EnvSettingsSource,
        dotenv_settings: DotEnvSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:

        fixed_env_settings = BetterAliasEnvSettingsSource(
            settings_cls,
            case_sensitive=env_settings.case_sensitive,
            env_prefix=env_settings.env_prefix,
            env_nested_delimiter=env_settings.env_nested_delimiter,
            env_ignore_empty=env_settings.env_ignore_empty,
            env_parse_none_str=env_settings.env_parse_none_str,
            env_parse_enums=env_settings.env_parse_enums,
        )

        return (
            init_settings,
            fixed_env_settings,
            dotenv_settings,
            file_secret_settings,
        )
    

if __name__ == "__main__":
    os.environ['SETTING_NAME'] = 'by-name'
    settings = AppSettings()
    print(f"{settings.SETTING_NAME=}")

    os.environ['ALIAS_NAME'] = 'by-alias'
    settings = AppSettings()
    print(f"{settings.SETTING_NAME=}")

# Outputs:
# settings.SETTING_NAME='by-name'
# settings.SETTING_NAME='by-alias'

I suspect this issue may also apply to DotEnvSettingsSource, but I have not experimented as comprehensively with this one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions