pydantic/pydantic-settings

pydantic-settings new version can't handle scenario

Closed this issue · 2 comments

Hi All:

Recently, we get an error when we define "discriminated union" for one field within BaseSettings, and use _env_file to construct the class which was working fine before.

Enviroment

(.venv_pydantic_settings_2_5_2) user@jupyter:/abehsu/pydantic_testing$ python --version
Python 3.10.12
(.venv_pydantic_settings_2_5_2) user@jupyter:
/abehsu/pydantic_testing$ pip list | grep pydantic
pydantic 2.9.2
pydantic_core 2.23.4
pydantic-settings 2.5.2

Error message

(.venv_pydantic_settings_2_5_2) user@abehsu-us-vscode:~/abehsu/pydantic_testing$ python test.py 
Traceback (most recent call last):
  File "/home/user/abehsu/pydantic_testing/test.py", line 40, in <module>
    result = Html(_env_file="/home/user/abehsu/pydantic_testing/test.env")
  File "/home/user/abehsu/pydantic_testing/.venv_pydantic_settings_2_5_2/lib/python3.10/site-packages/pydantic_settings/main.py", line 152, in __init__
    super().__init__(
  File "/home/user/abehsu/pydantic_testing/.venv_pydantic_settings_2_5_2/lib/python3.10/site-packages/pydantic/main.py", line 212, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 2 validation errors for Html
contents__el_type
  Extra inputs are not permitted [type=extra_forbidden, input_value='input', input_type=str]
    For further information visit https://errors.pydantic.dev/2.9/v/extra_forbidden
contents__class_name
  Extra inputs are not permitted [type=extra_forbidden, input_value='123', input_type=str]
    For further information visit https://errors.pydantic.dev/2.9/v/extra_forbidden

Code

from typing import Any, Literal

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

class DivModel(BaseModel):
    el_type: Literal['div'] = 'div'
    class_name: str | None = None
    children: list[Any] | None = None


class SpanModel(BaseModel):
    el_type: Literal['span'] = 'span'
    class_name: str | None = None
    contents: str | None = None


class ButtonModel(BaseModel):
    el_type: Literal['button'] = 'button'
    class_name: str | None = None
    contents: str | None = None


class InputModel(BaseModel):
    el_type: Literal['input'] = 'input'
    class_name: str | None = None
    value: str | None = None


class Html(BaseSettings):
    contents: DivModel | SpanModel | ButtonModel | InputModel = Field(
        discriminator='el_type'
    )

    model_config = SettingsConfigDict(
        env_nested_delimiter="__",
    )


result = Html(_env_file="/home/user/abehsu/pydantic_testing/test.env")
print(result)
contents__el_type=input
contents__class_name=123

==================================================

The same code can successfully if i use pydantic-settings 2.4.0 version.

(.venv_pydantic_settings_2_4_0) user@abehsu-us-vscode:~/abehsu/pydantic_testing$ python --version
Python 3.10.12
(.venv_pydantic_settings_2_4_0) user@abehsu-us-vscode:~/abehsu/pydantic_testing$ pip list | grep pydantic
pydantic             2.8.2
pydantic_core        2.20.1
pydantic-extra-types 2.9.0
pydantic-settings    2.4.0
(.venv_pydantic_settings_2_4_0) user@abehsu-us-vscode:~/abehsu/pydantic_testing$ python test.py 
contents=InputModel(el_type='input', class_name='123', value=None)

After comparison, i find out it is because this line be added from last week's release (https://github.com/pydantic/pydantic-settings/blob/main/pydantic_settings/sources.py#L989) by this PR (287cb22), are there any suggestion?

https://github.com/pydantic/pydantic/blob/c9190eedd8c536cd1445876c4a078c29b04d3aa1/pydantic/_internal/_utils.py#L76-L82

def lenient_issubclass(cls: Any, class_or_tuple: Any) -> bool:  # pragma: no cover
    try:
        return isinstance(cls, type) and issubclass(cls, class_or_tuple)
    except TypeError:
        if isinstance(cls, _typing_extra.WithArgsTypes):
            return False
        raise  # pragma: no cover
(Pdb) l
 75  
 76     def lenient_issubclass(cls: Any, class_or_tuple: Any) -> bool:  # pragma: no cover
 77         try:
 78             if cls != {}:
 79                 breakpoint()
 80  ->         return isinstance(cls, type) and issubclass(cls, class_or_tuple)
 81         except TypeError:
 82             if isinstance(cls, _typing_extra.WithArgsTypes):
 83                 return False
 84             raise  # pragma: no cover
 85  
(Pdb) cls
__main__.DivModel | __main__.SpanModel | __main__.ButtonModel | __main__.InputModel
(Pdb) isinstance(cls, type)
False
(Pdb) type(cls)
<class 'types.UnionType'>

(Pdb) issubclass(cls, class_or_tuple)
*** TypeError: issubclass() arg 1 must be a class

Thanks @hsuyuming for reporting this bug.

I think it is a duplicate of #420

I created #423 as a fix. Could you please confirm the fix?

Fixed in f3a25f2