← pod_r5_v1h

fastapi_14430

resolved RESOLVED PASS · 32 tool calls · 291 s · fastapi/fastapi

Task input

🐛 Fix optional sequence handling with new union syntax from Python 3.10

https://github.com/fastapi/fastapi/pull/14297 is only taking one union type into account. I can only recommend using https://github.com/pydantic/typing-inspection for these kind of checks (see https://typing-inspection.pydantic.dev/latest/usage/ for motivation).

Tool calls (33)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"Union\" fastapi/fastapi | grep \"Python 3.10\" || grep -rn \"Union\" fastapi/fastapi | head -20"
}
{"status": "ok", "stdout": "", "stderr": "grep: fastapi/fastapi: No such file or directory\ngrep: fastapi/fastapi: No such file or directory\n", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "grep: fastapi/fastapi: No such file or directory\ngrep: fastapi/fastapi: No such file or directory\n", "exit_code": 0}
2run_command
{
 "command": "ls -F"
}
{"status": "ok", "stdout": "CITATION.cff\nCONTRIBUTING.md\ndocs/\ndocs_src/\nfastapi/\nLICENSE\npdm_build.py\npyproject.toml\npytest.ini\nREADME.md\nrequirement
full
{"status": "ok", "stdout": "CITATION.cff\nCONTRIBUTING.md\ndocs/\ndocs_src/\nfastapi/\nLICENSE\npdm_build.py\npyproject.toml\npytest.ini\nREADME.md\nrequirements-docs-tests.txt\nrequirements-docs.txt\nrequirements-github-actions.txt\nrequirements-tests.txt\nrequirements-translations.txt\nrequirements.txt\nscripts/\nSECURITY.md\ntests/\n", "stderr": "", "exit_code": 0}
3run_command
{
 "command": "grep -rn \"Union\" fastapi/ | grep \"3.10\" | head -20"
}
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:838: # Check if it's a Union type (covers both typing.Union and types.UnionType in Python 3.10+)\n", "
full
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:838:    # Check if it's a Union type (covers both typing.Union and types.UnionType in Python 3.10+)\n", "stderr": "", "exit_code": 0}
4run_command
{
 "command": "sed -n '800,860p' fastapi/dependencies/utils.py"
}
{"status": "ok", "stdout": " params_to_process[key] = value\n else:\n params_to_process[key] = received_params.get(key)\n\n if single_not_embedded_field:\n fiel
full
{"status": "ok", "stdout": "                    params_to_process[key] = value\n            else:\n                params_to_process[key] = received_params.get(key)\n\n    if single_not_embedded_field:\n        field_info = first_field.field_info\n        assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (\n            \"Params must be subclasses of Param\"\n        )\n        loc: Tuple[str, ...] = (field_info.in_.value,)\n        v_, errors_ = _validate_value_with_model_field(\n            field=first_field, value=params_to_process, values=values, loc=loc\n        )\n        return {first_field.name: v_}, errors_\n\n    for field in fields:\n        value = _get_multidict_value(field, received_params)\n        field_info = field.field_info\n        assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (\n            \"Params must be subclasses of Param\"\n        )\n        loc = (field_info.in_.value, field.alias)\n        v_, errors_ = _validate_value_with_model_field(\n            field=field, value=value, values=values, loc=loc\n        )\n        if errors_:\n            errors.extend(errors_)\n        else:\n            values[field.name] = v_\n    return values, errors\n\n\ndef is_union_of_base_models(field_type: Any) -> bool:\n    \"\"\"Check if field type is a Union where all members are BaseModel subclasses.\"\"\"\n    from fastapi.types import UnionType\n\n    origin = get_origin(field_type)\n\n    # Check if it's a Union type (covers both typing.Union and types.UnionType in Python 3.10+)\n    if origin is not Union and origin is not UnionType:\n        return False\n\n    union_args = get_args(field_type)\n\n    for arg in union_args:\n        if not _is_model_class(arg):\n            return False\n\n    return True\n\n\ndef _should_embed_body_fields(fields: List[ModelField]) -> bool:\n    if not fields:\n        return False\n    # More than one dependency could have the same field, it would show up 
5run_command
{
 "command": "grep -rn \"UnionType\" fastapi/"
}
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:834: from fastapi.types import UnionType\nfastapi/dependencies/utils.py:838: # Check if it's a Union t
full
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:834:    from fastapi.types import UnionType\nfastapi/dependencies/utils.py:838:    # Check if it's a Union type (covers both typing.Union and types.UnionType in Python 3.10+)\nfastapi/dependencies/utils.py:839:    if origin is not Union and origin is not UnionType:\nfastapi/types.py:8:UnionType = getattr(types, \"UnionType\", Union)\nfastapi/_compat/shared.py:20:from fastapi.types import UnionType\nfastapi/_compat/shared.py:39:        types.UnionType,\nfastapi/_compat/shared.py:85:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:111:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:132:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:151:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:162:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:171:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:186:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:203:    if origin is Union or origin is UnionType:\n", "stderr": "", "exit_code": 0}
6run_command
{
 "command": "sed -n '10,210p' fastapi/_compat/shared.py"
}
{"status": "ok", "stdout": " List,\n Mapping,\n Sequence,\n Set,\n Tuple,\n Type,\n Union,\n)\n\nfrom fastapi._compat import may_v1\nfrom fastapi.types import U
full
{"status": "ok", "stdout": "    List,\n    Mapping,\n    Sequence,\n    Set,\n    Tuple,\n    Type,\n    Union,\n)\n\nfrom fastapi._compat import may_v1\nfrom fastapi.types import UnionType\nfrom pydantic import BaseModel\nfrom pydantic.version import VERSION as PYDANTIC_VERSION\nfrom starlette.datastructures import UploadFile\nfrom typing_extensions import Annotated, get_args, get_origin\n\n# Copy from Pydantic v2, compatible with v1\nif sys.version_info < (3, 9):\n    # Pydantic no longer supports Python 3.8, this might be incorrect, but the code\n    # this is used for is also never reached in this codebase, as it's a copy of\n    # Pydantic's lenient_issubclass, just for compatibility with v1\n    # TODO: remove when dropping support for Python 3.8\n    WithArgsTypes: Tuple[Any, ...] = ()\nelif sys.version_info < (3, 10):\n    WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias)  # type: ignore[attr-defined]\nelse:\n    WithArgsTypes: tuple[Any, ...] = (\n        typing._GenericAlias,  # type: ignore[attr-defined]\n        types.GenericAlias,\n        types.UnionType,\n    )  # pyright: ignore[reportAttributeAccessIssue]\n\nPYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(\".\")[:2])\nPYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2\n\n\nsequence_annotation_to_type = {\n    Sequence: list,\n    List: list,\n    list: list,\n    Tuple: tuple,\n    tuple: tuple,\n    Set: set,\n    set: set,\n    FrozenSet: frozenset,\n    frozenset: frozenset,\n    Deque: deque,\n    deque: deque,\n}\n\nsequence_types = tuple(sequence_annotation_to_type.keys())\n\nUrl: Type[Any]\n\n\n# Copy of Pydantic v2, compatible with v1\ndef lenient_issubclass(\n    cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...], None]\n) -> bool:\n    try:\n        return isinstance(cls, type) and issubclass(cls, class_or_tuple)  # type: ignore[arg-type]\n    except TypeError:  # pragma: no cover\n        if isinstance(cls, WithArgsTypes)
7run_command
{
 "command": "python3 - <<'PY'\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\ndef check_field_annotation_is_sequence(annotation):\n    # Simulating the logic in fastapi/_compat/shared.py\n    # sequence_types = (Sequence, List, list, Tuple, tuple, Set, set, FrozenSet, frozenset, Deque, deque)\n    from typing import Sequence, List, Tuple, Set, FrozenSet\n    from collections import deque\n    sequence_types = (Sequence, List, list, Tuple, tuple, Set, set, FrozenSet, frozenset, deque)\n    \n    def lenient_issubclass(cls, class_or_tuple):\n        try:\n            return isinstance(cls, type) and issubclass(cls, class_or_tuple)\n        except TypeError:\n            return False\n\n    def _annotation_is_sequence(annotation):\n        if lenient_issubclass(annotation, (str, bytes)):\n            return False\n        return lenient_issubclass(annotation, sequence_types)\n\n    origin = get_origin(annotation)\n    # This is the part we are testing\n    UnionType = getattr(types, \"UnionType\", typing.Union)\n    if origin is typing.Union or origin is UnionType:\n        for arg in get_args(annotation):\n            # We need to call field_annotation_is_sequence recursively, \n            # but for this test we'll just use the inner logic.\n            # Let's refine the structure to match the real function.\n            pass\n    return False\n\n# The actual function from the codebase\ndef field_annotation_is_sequence_impl(annotation, sequence_types, lenient_issubclass, _annotation_is_sequence):\n    origin = get_origin(annotation)\n    UnionType = getattr(types, \"UnionType\", typing.Union)\n    if origin is typing.Union or origin is UnionType:\n        for arg in get_args(annotation):\n            # Recursive call\n            if field_annotation_is_sequence_impl(arg, sequence_types, lenient_issubclass, _annotation_is_sequence):\n                return True\n        return False\n    return _annotation_is_sequence(annotation) or _annotation_is_sequence(\n        get_origin(annotation)\n    )\n\ndef _annotation_is_sequence_impl(annotation, sequence_types, lenient_issubclass):\n    if lenient_issubclass(annotation, (str, bytes)):\n        return False\n    return lenient_issubclass(annotation, sequence_types)\n\ndef lenient_issubclass_impl(cls, class_or_tuple):\n    try:\n        return isinstance(cls, type) and issubclass(cls, class_or_tuple)\n    except TypeError:\n        return False\n\nsequence_types = (typing.Sequence, typing.List, list, typing.Tuple, tuple, typing.Set, set, typing.FrozenSet, frozenset, \n                  # Use a dummy for deque since it might not be imported\n                  type(None)) # Just a placeholder\n\n# Need real sequence types\nfrom collections import deque\nsequence_types = (typing.Sequence, typing.List, list, typing.Tuple, tuple, typing.Set, set, typing.FrozenSet, frozenset, deque)\n\ndef test():\n    # Python 3.10+ union syntax: list[int] | None\n    # This should be recognized as a sequence.\n    try:\n        annotation = list[int] | None\n    except TypeError:\n        # Fallback for Python < 3.10\n        annotation = typing.Union[typing.List[int], None]\n\n    result = field_annotation_is_sequence_impl(\n        annotation, \n        sequence_types, \n        lenient_issubclass_impl, \n        lambda a: _annotation_is_sequence_impl(a, sequence_types, lenient_issubclass_impl)\n    )\n    print(f\"Annotation: {annotation}, Is sequence: {result}\")\n\ntest()\nPY\n"
}
{"status": "ok", "stdout": "Annotation: list[int] | None, Is sequence: True\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Annotation: list[int] | None, Is sequence: True\n", "stderr": "", "exit_code": 0}
8run_command
{
 "command": "python3 - <<'PY'\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\n# Mocking the environment as in fastapi/_compat/shared.py\nfrom collections import deque\nfrom typing import Sequence, List, Tuple, Set, FrozenSet\n\nsequence_types = (Sequence, List, list, Tuple, tuple, Set, set, FrozenSet, frozenset, deque)\n\ndef lenient_issubclass(cls, class_or_tuple):\n    try:\n        return isinstance(cls, type) and issubclass(cls, class_or_tuple)\n    except TypeError:\n        return False\n\ndef _annotation_is_sequence(annotation):\n    if lenient_issubclass(annotation, (str, bytes)):\n        return False\n    return lenient_issubclass(annotation, sequence_types)\n\ndef field_annotation_is_sequence(annotation):\n    origin = get_origin(annotation)\n    UnionType = getattr(types, \"UnionType\", typing.Union)\n    if origin is typing.Union or origin is UnionType:\n        for arg in get_args(annotation):\n            if field_annotation_is_sequence(arg):\n                return True\n        return False\n    return _annotation_is_sequence(annotation) or _annotation_is_sequence(\n        get_origin(annotation)\n    )\n\ndef test():\n    # Case 1: list[int] | None\n    ann1 = list[int] | None\n    print(f\"list[int] | None: {field_annotation_is_sequence(ann1)}\")\n    \n    # Case 2: list[int] | str\n    # This should be True because list[int] is a sequence.\n    ann2 = list[int] | str\n    print(f\"list[int] | str: {field_annotation_is_sequence(ann2)}\")\n\n    # Case 3: str | int\n    ann3 = str | int\n    print(f\"str | int: {field_annotation_is_sequence(ann3)}\")\n\ntest()\nPY\n"
}
{"status": "ok", "stdout": "list[int] | None: True\nlist[int] | str: True\nstr | int: False\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "list[int] | None: True\nlist[int] | str: True\nstr | int: False\n", "stderr": "", "exit_code": 0}
9run_command
{
 "command": "python3 - <<'PY'\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\nfrom collections import deque\nfrom typing import Sequence, List, Tuple, Set, FrozenSet\n\nsequence_types = (Sequence, List, list, Tuple, tuple, Set, set, FrozenSet, frozenset, deque)\n\ndef lenient_issubclass(cls, class_or_tuple):\n    try:\n        return isinstance(cls, type) and issubclass(cls, class_or_tuple)\n    except TypeError:\n        return False\n\ndef _annotation_is_sequence(annotation):\n    if lenient_issubclass(annotation, (str, bytes)):\n        return False\n    return lenient_issubclass(annotation, sequence_types)\n\ndef field_annotation_is_scalar(annotation):\n    # Simulation of field_annotation_is_scalar\n    # field_annotation_is_complex is what it uses.\n    # In a real scenario, it would check if it's a sequence or BaseModel etc.\n    if _annotation_is_sequence(annotation) or _annotation_is_sequence(get_origin(annotation)):\n        return False\n    return True\n\ndef field_annotation_is_scalar_sequence(annotation):\n    origin = get_origin(annotation)\n    UnionType = getattr(types, \"UnionType\", typing.Union)\n    if origin is typing.Union or origin is UnionType:\n        at_least_one_scalar_sequence = False\n        for arg in get_args(annotation):\n            if field_annotation_is_scalar_sequence(arg):\n                at_least_one_scalar_sequence = True\n                continue\n            elif not field_annotation_is_scalar(arg):\n                return False\n        return at_least_one_scalar_sequence\n    \n    # return field_annotation_is_sequence(annotation) and all(field_annotation_is_scalar(sub) for sub in get_args(annotation))\n    # Simulating field_annotation_is_sequence logic\n    is_seq = _annotation_is_sequence(annotation) or _annotation_is_sequence(origin)\n    if is_seq:\n        return all(field_annotation_is_scalar(sub) for sub in get_args(annotation))\n    return False\n\ndef test():\n    # The problematic case: list[int] | None\n    # list[int] is a scalar sequence.\n    # None is a scalar.\n    # So list[int] | None should be a scalar sequence.\n    \n    ann1 = list[int] | None\n    print(f\"list[int] | None: {field_annotation_is_scalar_sequence(ann1)}\")\n    \n    # Case: list[int] | list[str]\n    # list[int] is a scalar sequence, list[str] is a scalar sequence.\n    # This should be a scalar sequence.\n    ann2 = list[int] | list[str]\n    print(f\"list[int] | list[str]: {field_annotation_is_scalar_sequence(ann2)}\")\n    \n    # Case: list[int] | dict[str, int]\n    # dict[str, int] is complex but not a scalar sequence.\n    # So this should return False.\n    ann3 = list[int] | dict[str, int]\n    print(f\"list[int] | dict[str, int]: {field_annotation_is_scalar_sequence(ann3)}\")\n\ntest()\nPY\n"
}
{"status": "ok", "stdout": "list[int] | None: True\nlist[int] | list[str]: True\nlist[int] | dict[str, int]: True\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "list[int] | None: True\nlist[int] | list[str]: True\nlist[int] | dict[str, int]: True\n", "stderr": "", "exit_code": 0}
10run_command
{
 "command": "python3 - <<'PY'\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\nfrom collections import deque\nfrom typing import Sequence, List, Tuple, Set, FrozenSet\n\nsequence_types = (Sequence, List, list, Tuple, tuple, Set, set, FrozenSet, frozenset, deque)\n\ndef lenient_issubclass(cls, class_or_tuple):\n    try:\n        return isinstance(cls, type) and issubclass(cls, class_or_tuple)\n    except TypeError:\n        return False\n\ndef _annotation_is_sequence(annotation):\n    if lenient_issubclass(annotation, (str, bytes)):\n        return False\n    return lenient_issubclass(annotation, sequence_types)\n\ndef field_annotation_is_complex(annotation):\n    # Simulating the logic: a complex type is a sequence or a BaseModel etc.\n    # For this test, let's just say it's complex if it's a sequence or a dict.\n    origin = get_origin(annotation)\n    if origin is typing.Union or origin is getattr(types, \"UnionType\", typing.Union):\n        return any(field_annotation_is_complex(arg) for arg in get_args(annotation))\n    \n    if _annotation_is_sequence(annotation) or _annotation_is_sequence(origin):\n        return True\n    if origin is dict:\n        return True\n    return False\n\ndef field_annotation_is_scalar(annotation):\n    return not field_annotation_is_complex(annotation)\n\ndef field_annotation_is_scalar_sequence(annotation):\n    origin = get_origin(annotation)\n    UnionType = getattr(types, \"UnionType\", typing.Union)\n    if origin is typing.Union or origin is UnionType:\n        at_least_one_scalar_sequence = False\n        for arg in get_args(annotation):\n            if field_annotation_is_scalar_sequence(arg):\n                at_least_one_scalar_sequence = True\n                continue\n            elif not field_annotation_is_scalar(arg):\n                return False\n        return at_least_one_scalar_sequence\n    \n    # Simulating field_annotation_is_sequence(annotation)\n    is_seq = _annotation_is_sequence(annotation) or _annotation_is_sequence(origin)\n    if is_seq:\n        return all(field_annotation_is_scalar(sub) for sub in get_args(annotation))\n    return False\n\ndef test():\n    # list[int] is a scalar sequence.\n    # None is a scalar.\n    # list[int] | None should be True.\n    ann1 = list[int] | None\n    print(f\"list[int] | None: {field_annotation_is_scalar_sequence(ann1)}\")\n    \n    # list[int] | list[str]\n    # Both are scalar sequences.\n    # Should be True.\n    ann2 = list[int] | list[str]\n    print(f\"list[int] | list[str]: {field_annotation_is_scalar_sequence(ann2)}\")\n    \n    # list[int] | dict[str, int]\n    # list[int] is scalar sequence.\n    # dict[str, int] is complex but NOT a scalar.\n    # This should return False.\n    ann3 = list[int] | dict[str, int]\n    print(f\"list[int] | dict[str, int]: {field_annotation_is_scalar_sequence(ann3)}\")\n\ntest()\nPY\n"
}
{"status": "ok", "stdout": "list[int] | None: True\nlist[int] | list[str]: True\nlist[int] | dict[str, int]: False\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "list[int] | None: True\nlist[int] | list[str]: True\nlist[int] | dict[str, int]: False\n", "stderr": "", "exit_code": 0}
11run_command
{
 "command": "grep -rn \"typing-inspection\" ."
}
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
full
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
12run_command
{
 "command": "ls -F fastapi/types.py"
}
{"status": "ok", "stdout": "fastapi/types.py\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "fastapi/types.py\n", "stderr": "", "exit_code": 0}
13run_command
{
 "command": "cat fastapi/types.py"
}
{"status": "ok", "stdout": "import types\nfrom enum import Enum\nfrom typing import Any, Callable, Dict, Optional, Set, Tuple, Type, TypeVar, Union\n\nfrom pyda
full
{"status": "ok", "stdout": "import types\nfrom enum import Enum\nfrom typing import Any, Callable, Dict, Optional, Set, Tuple, Type, TypeVar, Union\n\nfrom pydantic import BaseModel\n\nDecoratedCallable = TypeVar(\"DecoratedCallable\", bound=Callable[..., Any])\nUnionType = getattr(types, \"UnionType\", Union)\nModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]\nIncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]\nDependencyCacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...], str]\n", "stderr": "", "exit_code": 0}
14run_command
{
 "command": "python3 - <<'PY'\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\nfrom collections import deque\nfrom typing import Sequence, List, Tuple, Set, FrozenSet\n\nsequence_types = (Sequence, List, list, Tuple, tuple, Set, set, FrozenSet, frozenset, deque)\n\ndef lenient_issubclass(cls, class_or_tuple):\n    try:\n        return isinstance(cls, type) and issubclass(cls, class_or_tuple)\n    except TypeError:\n        return False\n\ndef _annotation_is_sequence(annotation):\n    if lenient_issubclass(annotation, (str, bytes)):\n        return False\n    return lenient_issubclass(annotation, sequence_types)\n\ndef field_annotation_is_complex(annotation):\n    origin = get_origin(annotation)\n    UnionType = getattr(types, \"UnionType\", typing.Union)\n    if origin is typing.Union or origin is UnionType:\n        return any(field_annotation_is_complex(arg) for arg in get_args(annotation))\n    \n    if _annotation_is_sequence(annotation) or _annotation_is_sequence(origin):\n        return True\n    if origin is dict:\n        return True\n    return False\n\ndef field_annotation_is_scalar(annotation):\n    return not field_annotation_is_complex(annotation)\n\ndef field_annotation_is_scalar_sequence(annotation):\n    origin = get_origin(annotation)\n    UnionType = getattr(types, \"UnionType\", typing.Union)\n    if origin is typing.Union or origin is UnionType:\n        at_least_one_scalar_sequence = False\n        for arg in get_args(annotation):\n            if field_annotation_is_scalar_sequence(arg):\n                at_least_one_scalar_sequence = True\n                continue\n            elif not field_annotation_is_scalar(arg):\n                return False\n        return at_least_one_scalar_sequence\n    \n    is_seq = _annotation_is_sequence(annotation) or _annotation_is_sequence(origin)\n    if is_seq:\n        return all(field_annotation_is_scalar(sub) for sub in get_args(annotation))\n    return False\n\ndef test():\n    # list[int] | None\n    ann1 = list[int] | None\n    print(f\"list[int] | None: {field_annotation_is_scalar_sequence(ann1)}\")\n    \n    # list[int] | list[str]\n    ann2 = list[int] | list[str]\n    print(f\"list[int] | list[str]: {field_annotation_is_scalar_sequence(ann2)}\")\n    \n    # list[int] | dict[str, int]\n    ann3 = list[int] | dict[str, int]\n    print(f\"list[int] | dict[str, int]: {field_annotation_is_scalar_sequence(ann3)}\")\n\ntest()\nPY\n"
}
{"status": "ok", "stdout": "list[int] | None: True\nlist[int] | list[str]: True\nlist[int] | dict[str, int]: False\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "list[int] | None: True\nlist[int] | list[str]: True\nlist[int] | dict[str, int]: False\n", "stderr": "", "exit_code": 0}
15run_command
{
 "command": "grep -rn \"typing-inspection\" ."
}
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
full
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
16run_command
{
 "command": "python3 - <<'PY'\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\ndef test_union_types():\n    # Testing how get_origin and get_args handle Python 3.10 union syntax\n    try:\n        ann = list[int] | None\n    except TypeError:\n        ann = typing.Union[typing.List[int], None]\n        \n    print(f\"Annotation: {ann}\")\n    print(f\"Origin: {get_origin(ann)}\")\n    print(f\"Args: {get_args(ann)}\")\n    \n    UnionType = getattr(types, \"UnionType\", typing.Union)\n    print(f\"UnionType: {UnionType}\")\n    print(f\"Origin is Union: {get_origin(ann) is typing.Union}\")\n    print(f\"Origin is UnionType: {get_origin(ann) is UnionType}\")\n\ntest_union_types()\nPY\n"
}
{"status": "ok", "stdout": "Annotation: list[int] | None\nOrigin: <class 'types.UnionType'>\nArgs: (list[int], <class 'NoneType'>)\nUnionType: <class 'types.Uni
full
{"status": "ok", "stdout": "Annotation: list[int] | None\nOrigin: <class 'types.UnionType'>\nArgs: (list[int], <class 'NoneType'>)\nUnionType: <class 'types.UnionType'>\nOrigin is Union: False\nOrigin is UnionType: True\n", "stderr": "", "exit_code": 0}
17run_command
{
 "command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/_compat/shared.py\")\ns = p.read_text()\n# The issue report mentions that the current implementation is only taking one union type into account.\n# Looking at field_annotation_is_scalar_sequence:\n# if origin is Union or origin is UnionType:\n#     at_least_one_scalar_sequence = False\n#     for arg in get_args(annotation):\n#         if field_annotation_is_scalar_sequence(arg):\n#             at_least_one_scalar_sequence = True\n#             continue\n#         elif not field_annotation_is_scalar(arg):\n#             return False\n#     return at_least_one_scalar_sequence\n\n# Wait, I don't see an obvious bug in the logic I just reviewed.\n# Let's re-read the PR mentioned: https://github.com/fastapi/fastapi/pull/14297\n# Since I cannot access external links, I have to rely on the description.\n# \"is only taking one union type into account\"\n\n# Maybe there is another function that handles unions?\n# let's search for \"Union or origin is UnionType\" in shared.py\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
18run_command
{
 "command": "grep -n \"Union or origin is UnionType\" fastapi/_compat/shared.py"
}
{"status": "ok", "stdout": "85: if origin is Union or origin is UnionType:\n111: if origin is Union or origin is UnionType:\n132: if origin is Union or origin i
full
{"status": "ok", "stdout": "85:    if origin is Union or origin is UnionType:\n111:    if origin is Union or origin is UnionType:\n132:    if origin is Union or origin is UnionType:\n151:    if origin is Union or origin is UnionType:\n162:    if origin is Union or origin is UnionType:\n171:    if origin is Union or origin is UnionType:\n186:    if origin is Union or origin is UnionType:\n203:    if origin is Union or origin is UnionType:\n", "stderr": "", "exit_code": 0}
19run_command
{
 "command": "grep -rn \"typing-inspection\" ."
}
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
full
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
20run_command
{
 "command": "python3 - <<'PY'\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\ndef test_complex_union():\n    # If we have a nested union like Union[Union[int, str], float]\n    # Python 3.10+ usually flattens these.\n    ann = (int | str) | float\n    print(f\"Annotation: {ann}\")\n    print(f\"Origin: {get_origin(ann)}\")\n    print(f\"Args: {get_args(ann)}\")\n\ntest_complex_union()\nPY\n"
}
{"status": "ok", "stdout": "Annotation: int | str | float\nOrigin: <class 'types.UnionType'>\nArgs: (<class 'int'>, <class 'str'>, <class 'float'>)\n", "stderr"
full
{"status": "ok", "stdout": "Annotation: int | str | float\nOrigin: <class 'types.UnionType'>\nArgs: (<class 'int'>, <class 'str'>, <class 'float'>)\n", "stderr": "", "exit_code": 0}
21run_command
{
 "command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/_compat/shared.py\")\ns = p.read_text()\n\n# I'll check for any other place where Unions are handled.\n# Maybe there's something with Optional?\n# In Python 3.10+, Optional[T] is Union[T, None].\n\n# Let's look at the code again.\n# if origin is Union or origin is UnionType:\n#     for arg in get_args(annotation):\n#         if field_annotation_is_sequence(arg):\n#             return True\n#     return False\n\n# This looks correct for checking if ANY of the union types is a sequence.\n\n# Let's search for \"get_origin(annotation) is Union\"\n# maybe it's missing \"or origin is UnionType\" somewhere?\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
22run_command
{
 "command": "grep -rn \"origin is Union\" fastapi/ | grep -v \"UnionType\""
}
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
full
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
23run_command
{
 "command": "grep -rn \"origin is Union\" fastapi/ | head -20"
}
{"status": "ok", "stdout": "fastapi/_compat/shared.py:85: if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:111: if origin is Union or origi
full
{"status": "ok", "stdout": "fastapi/_compat/shared.py:85:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:111:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:132:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:151:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:162:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:171:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:186:    if origin is Union or origin is UnionType:\nfastapi/_compat/shared.py:203:    if origin is Union or origin is UnionType:\n", "stderr": "", "exit_code": 0}
24run_command
{
 "command": "grep -rn \"get_origin\" fastapi/ | grep -v \"UnionType\" | head -20"
}
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:78:from typing_extensions import Annotated, Literal, get_args, get_origin\nfastapi/dependencies/utils.
full
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:78:from typing_extensions import Annotated, Literal, get_args, get_origin\nfastapi/dependencies/utils.py:357:    if get_origin(use_annotation) is Annotated:\nfastapi/dependencies/utils.py:836:    origin = get_origin(field_type)\nfastapi/_compat/shared.py:24:from typing_extensions import Annotated, get_args, get_origin\nfastapi/_compat/shared.py:84:    origin = get_origin(annotation)\nfastapi/_compat/shared.py:91:        get_origin(annotation)\nfastapi/_compat/shared.py:110:    origin = get_origin(annotation)\nfastapi/_compat/shared.py:131:    origin = get_origin(annotation)\nfastapi/_compat/shared.py:150:    origin = get_origin(annotation)\nfastapi/_compat/shared.py:161:    origin = get_origin(annotation)\nfastapi/_compat/shared.py:170:    origin = get_origin(annotation)\nfastapi/_compat/shared.py:185:    origin = get_origin(annotation)\nfastapi/_compat/shared.py:202:    origin = get_origin(annotation)\nfastapi/_compat/v2.py:36:from typing_extensions import Annotated, Literal, get_args, get_origin\nfastapi/_compat/v2.py:383:    origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation\nfastapi/_compat/v2.py:389:            origin_type = get_origin(union_arg) or union_arg\nfastapi/_compat/v2.py:459:    origin = get_origin(annotation)\n", "stderr": "", "exit_code": 0}
25run_command
{
 "command": "sed -n '380,400p' fastapi/_compat/v2.py"
}
{"status": "ok", "stdout": "\n\ndef serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:\n origin_type = get_origin(field.field_info.ann
full
{"status": "ok", "stdout": "\n\ndef serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:\n    origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation\n    if origin_type is Union:  # Handle optional sequences\n        union_args = get_args(field.field_info.annotation)\n        for union_arg in union_args:\n            if union_arg is type(None):\n                continue\n            origin_type = get_origin(union_arg) or union_arg\n            break\n    assert issubclass(origin_type, shared.sequence_types)  # type: ignore[arg-type]\n    return shared.sequence_annotation_to_type[origin_type](value)  # type: ignore[no-any-return]\n\n\ndef get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:\n    error = ValidationError.from_exception_data(\n        \"Field required\", [{\"type\": \"missing\", \"loc\": loc, \"input\": {}}]\n    ).errors(include_url=False)[0]\n    error[\"input\"] = None\n    return error  # type: ignore[return-value]\n", "stderr": "", "exit_code": 0}
26run_command
{
 "command": "python3 - <<'PY'\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\ndef test_serialize_sequence_value():\n    # Simulating the logic in fastapi/_compat/v2.py\n    def serialize_sequence_value(annotation, value):\n        origin_type = get_origin(annotation) or annotation\n        \n        # This is the part we suspect is broken:\n        # if origin_type is Union:\n        if origin_type is typing.Union:\n            union_args = get_args(annotation)\n            for union_arg in union_args:\n                if union_arg is type(None):\n                    continue\n                origin_type = get_origin(union_arg) or union_arg\n                break\n        \n        print(f\"Annotation: {annotation}, Final origin_type: {origin_type}\")\n        # return issubclass(origin_type, (list, tuple, set)) # simplified\n\n    # Case 1: typing.Union[list[int], None]\n    serialize_sequence_value(typing.Union[typing.List[int], None], [1, 2])\n    \n    # Case 2: list[int] | None (Python 3.10 syntax)\n    try:\n        serialize_sequence_value(list[int] | None, [1, 2])\n    except TypeError:\n        print(\"Python < 3.10\")\n\ntest_serialize_sequence_value()\nPY\n"
}
{"status": "ok", "stdout": "Annotation: typing.Optional[typing.List[int]], Final origin_type: <class 'list'>\nAnnotation: list[int] | None, Final origin_type: <
full
{"status": "ok", "stdout": "Annotation: typing.Optional[typing.List[int]], Final origin_type: <class 'list'>\nAnnotation: list[int] | None, Final origin_type: <class 'types.UnionType'>\n", "stderr": "", "exit_code": 0}
27run_command
{
 "command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/_compat/v2.py\")\ns = p.read_text()\nold = \"\"\"    if origin_type is Union:  # Handle optional sequences\"\"\"\nnew = \"\"\"    if origin_type is Union or origin_type is UnionType:  # Handle optional sequences\"\"\"\n# Need to make sure UnionType is imported.\n# Let's check imports in v2.py\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
28run_command
{
 "command": "sed -n '1,50p' fastapi/_compat/v2.py"
}
{"status": "ok", "stdout": "import re\nimport warnings\nfrom copy import copy, deepcopy\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing i
full
{"status": "ok", "stdout": "import re\nimport warnings\nfrom copy import copy, deepcopy\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing import (\n    Any,\n    Dict,\n    List,\n    Sequence,\n    Set,\n    Tuple,\n    Type,\n    Union,\n    cast,\n)\n\nfrom fastapi._compat import may_v1, shared\nfrom fastapi.openapi.constants import REF_TEMPLATE\nfrom fastapi.types import IncEx, ModelNameMap\nfrom pydantic import BaseModel, TypeAdapter, create_model\nfrom pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError\nfrom pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation\nfrom pydantic import ValidationError as ValidationError\nfrom pydantic._internal._schema_generation_shared import (  # type: ignore[attr-defined]\n    GetJsonSchemaHandler as GetJsonSchemaHandler,\n)\nfrom pydantic._internal._typing_extra import eval_type_lenient\nfrom pydantic._internal._utils import lenient_issubclass as lenient_issubclass\nfrom pydantic.fields import FieldInfo as FieldInfo\nfrom pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema\nfrom pydantic.json_schema import JsonSchemaValue as JsonSchemaValue\nfrom pydantic_core import CoreSchema as CoreSchema\nfrom pydantic_core import PydanticUndefined, PydanticUndefinedType\nfrom pydantic_core import Url as Url\nfrom typing_extensions import Annotated, Literal, get_args, get_origin\n\ntry:\n    from pydantic_core.core_schema import (\n        with_info_plain_validator_function as with_info_plain_validator_function,\n    )\nexcept ImportError:  # pragma: no cover\n    from pydantic_core.core_schema import (\n        general_plain_validator_function as with_info_plain_validator_function,  # noqa: F401\n    )\n\nRequiredParam = PydanticUndefined\nUndefined = PydanticUndefined\nUndefinedType = PydanticUndefinedType\nevaluate_forwardref = eval_type_lenient\n", "stderr": "", "exit_code": 0}
29run_command
{
 "command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/_compat/v2.py\")\ns = p.read_text()\n\n# Add UnionType import from fastapi.types\n# and update serialize_sequence_value\n\nif \"from fastapi.types import IncEx, ModelNameMap\" in s:\n    s = s.replace(\n        \"from fastapi.types import IncEx, ModelNameMap\",\n        \"from fastapi.types import IncEx, ModelNameMap, UnionType\"\n    )\n\n# I need to find the exact lines for the replace.\n# Using the previous sed output:\n# 386:    if origin_type is Union:  # Handle optional sequences\n# Let's use a safer replacement.\nold_line = \"    if origin_type is Union:  # Handle optional sequences\"\nnew_line = \"    if origin_type is Union or origin_type is UnionType:  # Handle optional sequences\"\n\nif old_line in s:\n    s = s.replace(old_line, new_line)\n\np.write_text(s)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
30run_command
{
 "command": "python3 -m py_compile fastapi/_compat/v2.py"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
31run_command
{
 "command": "python3 - <<'PY'\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\nfrom fastapi.types import UnionType\n\ndef test_serialize_sequence_value_fixed():\n    def serialize_sequence_value_impl(annotation):\n        origin_type = get_origin(annotation) or annotation\n        \n        # Fixed logic\n        if origin_type is typing.Union or origin_type is UnionType:\n            union_args = get_args(annotation)\n            for union_arg in union_args:\n                if union_arg is type(None):\n                    continue\n                origin_type = get_origin(union_arg) or union_arg\n                break\n        \n        return origin_type\n\n    # Case 1: typing.Union[list[int], None]\n    ann1 = typing.Union[typing.List[int], None]\n    print(f\"Union[List, None] -> {serialize_sequence_value_impl(ann1)}\")\n    \n    # Case 2: list[int] | None (Python 3.10 syntax)\n    try:\n        ann2 = list[int] | None\n        print(f\"list[int] | None -> {serialize_sequence_value_impl(ann2)}\")\n    except TypeError:\n        print(\"Python < 3.10\")\n\ntest_serialize_sequence_value_fixed()\nPY\n"
}
{"status": "ok", "stdout": "Union[List, None] -> <class 'list'>\nlist[int] | None -> <class 'list'>\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Union[List, None] -> <class 'list'>\nlist[int] | None -> <class 'list'>\n", "stderr": "", "exit_code": 0}
32run_command
{
 "command": "submit_patch"
}
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bas
full
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bash: submit_patch: command not found\n", "exit_code": 127}}
33submit_patch
{}
{"status": "ok", "patch_size": 1170, "files_changed": 1}
full
{"status": "ok", "patch_size": 1170, "files_changed": 1}

Patch

diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py
index 543a42dd..0faa7d5a 100644
--- a/fastapi/_compat/v2.py
+++ b/fastapi/_compat/v2.py
@@ -17,7 +17,7 @@ from typing import (
 
 from fastapi._compat import may_v1, shared
 from fastapi.openapi.constants import REF_TEMPLATE
-from fastapi.types import IncEx, ModelNameMap
+from fastapi.types import IncEx, ModelNameMap, UnionType
 from pydantic import BaseModel, TypeAdapter, create_model
 from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
 from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation
@@ -381,7 +381,7 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
 
 def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
     origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation
-    if origin_type is Union:  # Handle optional sequences
+    if origin_type is Union or origin_type is UnionType:  # Handle optional sequences
         union_args = get_args(field.field_info.annotation)
         for union_arg in union_args:
             if union_arg is type(None):

Test output

show
.s.........ss                                                            [100%]
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
  /Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
    _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
10 passed, 3 skipped, 1 warning in 0.61s