diff --git a/graphene_pydantic/converters.py b/graphene_pydantic/converters.py index 1e3297c..0654d8e 100644 --- a/graphene_pydantic/converters.py +++ b/graphene_pydantic/converters.py @@ -209,6 +209,13 @@ def find_graphene_type( if isinstance(type_, UnionType): type_ = T.Union[type_.__args__] + # Unwrap Annotated[T, ...] -> T. Pydantic v2 strips top-level Annotated + # from FieldInfo.annotation, but does NOT strip inside Union arms or + # generic containers, so e.g. Optional[PositiveFloat] reaches us with + # the inner arm still wrapped as Annotated[float, Gt(gt=0)]. + if T.get_origin(type_) is T.Annotated: + type_ = T.get_args(type_)[0] + if type_ == uuid.UUID: return UUID elif type_ in (str, bytes): diff --git a/tests/test_converters.py b/tests/test_converters.py index 6c18450..a209c96 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -9,11 +9,15 @@ import graphene import graphene.types import pytest -from pydantic import BaseModel +from pydantic import BaseModel, PositiveFloat, PositiveInt from pydantic import create_model import graphene_pydantic.converters as converters -from graphene_pydantic.converters import ConversionError, convert_pydantic_field +from graphene_pydantic.converters import ( + ConversionError, + convert_pydantic_field, + find_graphene_type, +) from graphene_pydantic.objecttype import PydanticObjectType from graphene_pydantic.registry import Placeholder, get_global_registry @@ -212,6 +216,30 @@ def test_unresolved_placeholders(): ) +def test_annotated_unwrapped_at_top_level(): + # Pydantic strips top-level Annotated from FieldInfo.annotation, but + # find_graphene_type may still receive a raw Annotated[T, ...] from + # callers; verify it resolves to the underlying type. + field = _get_field_from_spec("attr", (PositiveFloat, 1.0)) + assert find_graphene_type(PositiveFloat, field, None) is graphene.Float + + +def test_annotated_inside_optional(): + # Regression: Optional[PositiveFloat] reaches find_graphene_type as + # Union[Annotated[float, Gt(gt=0)], None]; the inner arm is still + # Annotated and previously raised ConversionError. + field = _get_field_from_spec("attr", (T.Optional[PositiveFloat], 1.0)) + # Optional[T] decomposes to T in graphene_pydantic's union handling. + assert find_graphene_type(field.annotation, field, None) is graphene.Float + + +def test_annotated_inside_list(): + field = _get_field_from_spec("attr", (T.List[PositiveInt], [1])) + result = find_graphene_type(field.annotation, field, None) + assert isinstance(result, graphene.List) + assert result.of_type is graphene.Int + + def test_self_referencing(): class NodeModel(BaseModel): id: int