pydantic / pydantic-settings

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

Field alias AttributeError: 'AliasChoices' object has no attribute 'lower' - CliSettingsSource #303

Closed SantiagoG closed 1 month ago

SantiagoG commented 1 month ago

Description

Field aliases fail when subclassing a model with BaseSettings and using AliasChoices.

Pytest output ```shell /usr/local/lib/python3.11/dist-packages/pydantic_settings/main.py:141: in __init__ **__pydantic_self__._settings_build_values( /usr/local/lib/python3.11/dist-packages/pydantic_settings/main.py:260: in _settings_build_values CliSettingsSource( /usr/local/lib/python3.11/dist-packages/pydantic_settings/sources.py:902: in __init__ self._connect_root_parser( /usr/local/lib/python3.11/dist-packages/pydantic_settings/sources.py:1236: in _connect_root_parser self._add_parser_args( /usr/local/lib/python3.11/dist-packages/pydantic_settings/sources.py:1255: in _add_parser_args for field_name, resolved_name, field_info in self._sort_arg_fields(model): /usr/local/lib/python3.11/dist-packages/pydantic_settings/sources.py:1154: in _sort_arg_fields resolved_name = resolved_name.lower() if not self.case_sensitive else resolved_name E AttributeError: 'AliasChoices' object has no attribute 'lower'\ self = EnvSettingsSource(env_nested_delimiter='.', env_prefix_len=0), model = .Check'> def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, str, FieldInfo]]: positional_args, subcommand_args, optional_args = [], [], [] fields = model.__pydantic_fields__ if is_pydantic_dataclass(model) else model.model_fields for field_name, field_info in fields.items(): resolved_name = field_name if field_info.alias is None else field_info.alias > resolved_name = resolved_name.lower() if not self.case_sensitive else resolved_name E AttributeError: 'AliasChoices' object has no attribute 'lower' ```

Example Code

import pytest

from pydantic import AliasChoices, Field
# from pydantic import BaseModel
from pydantic_settings import BaseSettings

def test_settings():
    class Check(BaseSettings):
    # class Check(BaseModel):  # This works
        field: str = Field(
            "foo",
            alias=AliasChoices(
                "ALIAS_NAME"
            )
        )

    assert Check().field == 'foo'
    assert Check(ALIAS_NAME="bar").field == 'bar'

Expect

For this test to pass with BaseSettings as it does so with BaseModel.

Platform: Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Python version: Python 3.11.9 (main, Apr  6 2024, 17:59:24) [GCC 11.4.0]

pydantic           2.7.3
pydantic_core      2.18.4
pydantic-settings  2.3.0
hramezani commented 1 month ago

Thanks @SantiagoG for reporting this.

This is probably related to pydantic-settings new CLI source.

@kschwab Could you please take a look at this? this shouldn't happen because the code does not enable the CLI source and we mentioned in the doc

To enable CLI parsing, we simply set the `cli_parse_args` flag to a valid value, which retains similar conotations as
defined in `argparse`.

Probably we need to check cli_parse_args soon and return fast in CliSettingsSource.__init__ or even don't initialize it

kschwab commented 1 month ago

@hramezani yes agreed. It's currently getting initialized; we can move it to only initialize if parsing is necessary.

The above is also a bug in CliSettingsSource related to AliasChoices which we can address separately.

hramezani commented 3 weeks ago

the fix has been released in new pydantic-settings 2.3.2