From c788a084ea58f081533cfe242bd71d940f296dc5 Mon Sep 17 00:00:00 2001 From: Konstantinos Tselepakis Date: Fri, 27 Jun 2025 12:39:58 +0300 Subject: [PATCH 1/4] Fix handle type alias declared with type statement --- pydantic_settings/sources/base.py | 7 ++++++- pydantic_settings/sources/utils.py | 2 ++ tests/test_settings.py | 17 ++++++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/pydantic_settings/sources/base.py b/pydantic_settings/sources/base.py index b2c4d166..be3c1647 100644 --- a/pydantic_settings/sources/base.py +++ b/pydantic_settings/sources/base.py @@ -16,6 +16,7 @@ from pydantic._internal._utils import is_model_class from pydantic.fields import FieldInfo from typing_extensions import get_args +from typing_inspection import typing_objects from typing_inspection.introspection import is_union_origin from ..exceptions import SettingsError @@ -25,6 +26,7 @@ _annotation_is_complex, _get_alias_names, _get_model_fields, + _strip_annotated, _union_is_complex, ) @@ -353,7 +355,10 @@ def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[s field_info.append((v_alias, self._apply_case_sensitive(v_alias), False)) if not v_alias or self.config.get('populate_by_name', False): - if is_union_origin(get_origin(field.annotation)) and _union_is_complex(field.annotation, field.metadata): + annotation = field.annotation + if typing_objects.is_typealiastype(annotation): + annotation = _strip_annotated(annotation.__value__) + if is_union_origin(get_origin(annotation)) and _union_is_complex(annotation, field.metadata): field_info.append((field_name, self._apply_case_sensitive(self.env_prefix + field_name), True)) else: field_info.append((field_name, self._apply_case_sensitive(self.env_prefix + field_name), False)) diff --git a/pydantic_settings/sources/utils.py b/pydantic_settings/sources/utils.py index 270a8c17..9b14f44f 100644 --- a/pydantic_settings/sources/utils.py +++ b/pydantic_settings/sources/utils.py @@ -43,6 +43,8 @@ def parse_env_vars( def _annotation_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool: # If the model is a root model, the root annotation should be used to # evaluate the complexity. + if typing_objects.is_typealiastype(annotation): + annotation = annotation.__value__ if annotation is not None and _lenient_issubclass(annotation, RootModel) and annotation is not RootModel: annotation = cast('type[RootModel[Any]]', annotation) root_annotation = annotation.model_fields['root'].annotation diff --git a/tests/test_settings.py b/tests/test_settings.py index a7bd86c3..2f3e18a1 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -34,7 +34,7 @@ dataclasses as pydantic_dataclasses, ) from pydantic.fields import FieldInfo -from typing_extensions import override +from typing_extensions import TypeAliasType, override from pydantic_settings import ( BaseSettings, @@ -474,6 +474,21 @@ class AnnotatedComplexSettings(BaseSettings): ] +def test_annotated_with_type(env): + """https://github.com/pydantic/pydantic-settings/issues/536. + + PEP 695 type aliases need to be analyzed when determining if an annotation is complex. + """ + MinLenList = TypeAliasType('MinLenList', Annotated[Union[list[str], list[int]], MinLen(2)]) + + class AnnotatedComplexSettings(BaseSettings): + apples: MinLenList + + env.set('apples', '["russet", "granny smith"]') + s = AnnotatedComplexSettings() + assert s.apples == ['russet', 'granny smith'] + + def test_set_dict_model(env): env.set('bananas', '[1, 2, 3, 3]') env.set('CARROTS', '{"a": null, "b": 4}') From 823f4ae860b4dbc6cad0f6ffe689283974538c08 Mon Sep 17 00:00:00 2001 From: Konstantinos Tselepakis Date: Wed, 2 Jul 2025 23:36:24 +0300 Subject: [PATCH 2/4] Handle generic type alias --- pydantic_settings/sources/base.py | 4 +++- pydantic_settings/sources/utils.py | 4 +++- tests/test_settings.py | 9 +++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pydantic_settings/sources/base.py b/pydantic_settings/sources/base.py index be3c1647..6dceafa6 100644 --- a/pydantic_settings/sources/base.py +++ b/pydantic_settings/sources/base.py @@ -356,7 +356,9 @@ def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[s if not v_alias or self.config.get('populate_by_name', False): annotation = field.annotation - if typing_objects.is_typealiastype(annotation): + if annotation and ( + typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)) + ): annotation = _strip_annotated(annotation.__value__) if is_union_origin(get_origin(annotation)) and _union_is_complex(annotation, field.metadata): field_info.append((field_name, self._apply_case_sensitive(self.env_prefix + field_name), True)) diff --git a/pydantic_settings/sources/utils.py b/pydantic_settings/sources/utils.py index 9b14f44f..c5e3fa64 100644 --- a/pydantic_settings/sources/utils.py +++ b/pydantic_settings/sources/utils.py @@ -43,7 +43,9 @@ def parse_env_vars( def _annotation_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool: # If the model is a root model, the root annotation should be used to # evaluate the complexity. - if typing_objects.is_typealiastype(annotation): + if annotation and ( + typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)) + ): annotation = annotation.__value__ if annotation is not None and _lenient_issubclass(annotation, RootModel) and annotation is not RootModel: annotation = cast('type[RootModel[Any]]', annotation) diff --git a/tests/test_settings.py b/tests/test_settings.py index 2f3e18a1..8e845ff1 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -488,6 +488,15 @@ class AnnotatedComplexSettings(BaseSettings): s = AnnotatedComplexSettings() assert s.apples == ['russet', 'granny smith'] + T = TypeVar('T') + MinLenList = TypeAliasType('MinLenList', Annotated[Union[list[T], tuple[T]], MinLen(2)], type_params=(T,)) + + class AnnotatedComplexSettings(BaseSettings): + apples: MinLenList[str] + + s = AnnotatedComplexSettings() + assert s.apples == ['russet', 'granny smith'] + def test_set_dict_model(env): env.set('bananas', '[1, 2, 3, 3]') From d3b7fec7c60e57315e2efd96b56bc4a790038d2d Mon Sep 17 00:00:00 2001 From: Konstantinos Tselepakis Date: Thu, 3 Jul 2025 12:16:09 +0300 Subject: [PATCH 3/4] Update _annotation_is_complex signature --- pydantic_settings/sources/utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pydantic_settings/sources/utils.py b/pydantic_settings/sources/utils.py index c5e3fa64..a8501568 100644 --- a/pydantic_settings/sources/utils.py +++ b/pydantic_settings/sources/utils.py @@ -40,12 +40,10 @@ def parse_env_vars( } -def _annotation_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool: +def _annotation_is_complex(annotation: Any, metadata: list[Any]) -> bool: # If the model is a root model, the root annotation should be used to # evaluate the complexity. - if annotation and ( - typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)) - ): + if typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)): annotation = annotation.__value__ if annotation is not None and _lenient_issubclass(annotation, RootModel) and annotation is not RootModel: annotation = cast('type[RootModel[Any]]', annotation) From ff4f433cd2dedcb108874cb9c6768398b73f5412 Mon Sep 17 00:00:00 2001 From: Konstantinos Tselepakis Date: Sun, 6 Jul 2025 18:10:24 +0300 Subject: [PATCH 4/4] Ignore mypy error --- pydantic_settings/sources/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pydantic_settings/sources/base.py b/pydantic_settings/sources/base.py index 6dceafa6..4ead04aa 100644 --- a/pydantic_settings/sources/base.py +++ b/pydantic_settings/sources/base.py @@ -356,10 +356,8 @@ def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[s if not v_alias or self.config.get('populate_by_name', False): annotation = field.annotation - if annotation and ( - typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)) - ): - annotation = _strip_annotated(annotation.__value__) + if typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)): + annotation = _strip_annotated(annotation.__value__) # type: ignore[union-attr] if is_union_origin(get_origin(annotation)) and _union_is_complex(annotation, field.metadata): field_info.append((field_name, self._apply_case_sensitive(self.env_prefix + field_name), True)) else: