← eval4_submission_v1b

fastapi_14430

resolved RESOLVED PASS · 23 tool calls · 220 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 (24)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"Union\" fastapi/params.py"
}
{"status": "ok", "stdout": "4:from typing import Any, Callable, Dict, List, Optional, Sequence, Union\n33: default_factory: Union[Callable[[], Any], None] = _Un
full
{"status": "ok", "stdout": "4:from typing import Any, Callable, Dict, List, Optional, Sequence, Union\n33:        default_factory: Union[Callable[[], Any], None] = _Unset,\n36:        alias_priority: Union[int, None] = _Unset,\n39:        validation_alias: Union[str, None] = None,\n40:        serialization_alias: Union[str, None] = None,\n56:        discriminator: Union[str, None] = None,\n57:        strict: Union[bool, None] = _Unset,\n58:        multiple_of: Union[float, None] = _Unset,\n59:        allow_inf_nan: Union[bool, None] = _Unset,\n60:        max_digits: Union[int, None] = _Unset,\n61:        decimal_places: Union[int, None] = _Unset,\n71:        deprecated: Union[deprecated, str, bool, None] = None,\n73:        json_schema_extra: Union[Dict[str, Any], None] = None,\n147:        default_factory: Union[Callable[[], Any], None] = _Unset,\n150:        alias_priority: Union[int, None] = _Unset,\n153:        validation_alias: Union[str, None] = None,\n154:        serialization_alias: Union[str, None] = None,\n170:        discriminator: Union[str, None] = None,\n171:        strict: Union[bool, None] = _Unset,\n172:        multiple_of: Union[float, None] = _Unset,\n173:        allow_inf_nan: Union[bool, None] = _Unset,\n174:        max_digits: Union[int, None] = _Unset,\n175:        decimal_places: Union[int, None] = _Unset,\n185:        deprecated: Union[deprecated, str, bool, None] = None,\n187:        json_schema_extra: Union[Dict[str, Any], None] = None,\n233:        default_factory: Union[Callable[[], Any], None] = _Unset,\n236:        alias_priority: Union[int, None] = _Unset,\n239:        validation_alias: Union[str, None] = None,\n240:        serialization_alias: Union[str, None] = None,\n256:        discriminator: Union[str, None] = None,\n257:        strict: Union[bool, None] = _Unset,\n258:        multiple_of: Union[float, None] = _Unset,\n259:        allow_inf_nan: Union[bool, None] = _Unset,\n260:        max_digits: Union[int, None] = _Unset,\n261:
2run_command
{
 "command": "grep -rn \"Union\" fastapi/utils.py | head -20"
}
{"status": "ok", "stdout": "12: Union,\n44:def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:\n69: \"field (e.g. Union[Response, d
full
{"status": "ok", "stdout": "12:    Union,\n44:def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:\n69:    \"field (e.g. Union[Response, dict, None]) you can disable generating the \"\n81:    required: Union[bool, UndefinedType] = Undefined,\n82:    model_config: Union[Type[BaseConfig], None] = None,\n246:    first_item: Union[DefaultPlaceholder, DefaultType],\n247:    *extra_items: Union[DefaultPlaceholder, DefaultType],\n248:) -> Union[DefaultPlaceholder, DefaultType]:\n", "exit_code": 0}
3run_command
{
 "command": "grep -rn \"Optional\" fastapi/utils.py | head -20"
}
{"status": "ok", "stdout": "9: Optional,\n79: class_validators: Optional[Dict[str, Validator]] = None,\n80: default: Optional[Any] = Undefined,\n83: field_info:
full
{"status": "ok", "stdout": "9:    Optional,\n79:    class_validators: Optional[Dict[str, Validator]] = None,\n80:    default: Optional[Any] = Undefined,\n83:    field_info: Optional[FieldInfo] = None,\n84:    alias: Optional[str] = None,\n144:    cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,\n", "exit_code": 0}
4run_command
{
 "command": "grep -rn \"get_type_hints\" fastapi/ | head -20"
}
{"status": "ok", "stdout": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "exit_code": 0}
5run_command
{
 "command": "grep -rn \"types.UnionType\" fastapi/ | 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+)\nfast
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+)\nfastapi/_compat/shared.py:39:        types.UnionType,\n", "exit_code": 0}
6read_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": 830,
 "end_line": 860
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n\ndef is_union_of_base_models(field_type: Any) -> bool:\n \"\"\"Check if field type
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\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 as multiple\n    # fields but it's the same one, so count them by name\n    body_param_names_set = {field.name for field in fields}\n    # A top level field has to be a single field, not multiple\n    if len(body_param_names_set) > 1:\n        return True\n    first_field = fields[0]\n", "start_line": 830, "end_line": 860, "total_lines": 1035, "is_truncated": false}
7run_command
{
 "command": "grep -rn \"Optional\" fastapi/dependencies/utils.py | head -20"
}
{"status": "ok", "stdout": "14: Optional,\n143: visited: Optional[List[DependencyCacheKey]] = None,\n233: name: Optional[str] = None,\n234: own_oauth_scopes: Op
full
{"status": "ok", "stdout": "14:    Optional,\n143:    visited: Optional[List[DependencyCacheKey]] = None,\n233:    name: Optional[str] = None,\n234:    own_oauth_scopes: Optional[List[str]] = None,\n235:    parent_oauth_scopes: Optional[List[str]] = None,\n313:) -> Optional[bool]:\n338:    depends: Optional[params.Depends]\n339:    field: Optional[ModelField]\n560:    background_tasks: Optional[StarletteBackgroundTasks]\n569:    body: Optional[Union[Dict[str, Any], FormData]] = None,\n570:    background_tasks: Optional[StarletteBackgroundTasks] = None,\n571:    response: Optional[Response] = None,\n572:    dependency_overrides_provider: Optional[Any] = None,\n573:    dependency_cache: Optional[Dict[DependencyCacheKey, Any]] = None,\n924:    received_body: Optional[Union[Dict[str, Any], FormData]],\n954:        value: Optional[Any] = None\n974:) -> Optional[ModelField]:\n", "exit_code": 0}
8run_command
{
 "command": "grep -rn \"is_optional\" fastapi/ | head -20"
}
{"status": "ok", "stdout": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "exit_code": 0}
9run_command
{
 "command": "grep -rn \"get_args\" fastapi/ | 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:358:        annotated_args = get_args(annotation)\nfastapi/dependencies/utils.py:842:    union_args = get_args(field_type)\nfastapi/_compat/shared.py:24:from typing_extensions import Annotated, get_args, get_origin\nfastapi/_compat/shared.py:86:        for arg in get_args(annotation):\nfastapi/_compat/shared.py:112:        return any(field_annotation_is_complex(arg) for arg in get_args(annotation))\nfastapi/_compat/shared.py:115:        return field_annotation_is_complex(get_args(annotation)[0])\nfastapi/_compat/shared.py:134:        for arg in get_args(annotation):\nfastapi/_compat/shared.py:143:        for sub_annotation in get_args(annotation)\nfastapi/_compat/shared.py:152:        for arg in get_args(annotation):\nfastapi/_compat/shared.py:163:        for arg in get_args(annotation):\nfastapi/_compat/shared.py:173:        for arg in get_args(annotation):\nfastapi/_compat/shared.py:180:        for sub_annotation in get_args(annotation)\nfastapi/_compat/shared.py:188:        for arg in get_args(annotation):\nfastapi/_compat/shared.py:195:        for sub_annotation in get_args(annotation)\nfastapi/_compat/shared.py:204:        for arg in get_args(annotation):\nfastapi/_compat/shared.py:208:        for sub_annotation in get_args(annotation):\nfastapi/_compat/v2.py:36:from typing_extensions import Annotated, Literal, get_args, get_origin\nfastapi/_compat/v2.py:385:        union_args = get_args(field.field_info.annotation)\nfastapi/_compat/v2.py:461:        for arg in get_args(annotation):\n", "exit_code": 0}
10read_file
{
 "end_line": 220,
 "filepath": "fastapi/_compat/shared.py",
 "start_line": 1
}
{"status": "ok", "filepath": "fastapi/_compat/shared.py", "content": "import sys\nimport types\nimport typing\nfrom collections import deque\nfrom dataclasses i
full
{"status": "ok", "filepath": "fastapi/_compat/shared.py", "content": "import sys\nimport types\nimport typing\nfrom collections import deque\nfrom dataclasses import is_dataclass\nfrom typing import (\n    Any,\n    Deque,\n    FrozenSet,\n    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], ...], N
11read_file
{
 "end_line": 150,
 "filepath": "fastapi/_compat/shared.py",
 "start_line": 100
}
{"status": "ok", "filepath": "fastapi/_compat/shared.py", "content": " return (\n lenient_issubclass(\n annotation, (BaseModel, may_v1.BaseModel, Mapping, Uploa
full
{"status": "ok", "filepath": "fastapi/_compat/shared.py", "content": "    return (\n        lenient_issubclass(\n            annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile)\n        )\n        or _annotation_is_sequence(annotation)\n        or is_dataclass(annotation)\n    )\n\n\ndef field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:\n    origin = get_origin(annotation)\n    if origin is Union or origin is UnionType:\n        return any(field_annotation_is_complex(arg) for arg in get_args(annotation))\n\n    if origin is Annotated:\n        return field_annotation_is_complex(get_args(annotation)[0])\n\n    return (\n        _annotation_is_complex(annotation)\n        or _annotation_is_complex(origin)\n        or hasattr(origin, \"__pydantic_core_schema__\")\n        or hasattr(origin, \"__get_pydantic_core_schema__\")\n    )\n\n\ndef field_annotation_is_scalar(annotation: Any) -> bool:\n    # handle Ellipsis here to make tuple[int, ...] work nicely\n    return annotation is Ellipsis or not field_annotation_is_complex(annotation)\n\n\ndef field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:\n    origin = get_origin(annotation)\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    return field_annotation_is_sequence(annotation) and all(\n        field_annotation_is_scalar(sub_annotation)\n        for sub_annotation in get_args(annotation)\n    )\n\n\ndef is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:\n    if lenient_issubclass(annotation, bytes):\n        return True\n    origin = get_origin(annotation)\n", "start_line": 100, "end_line": 150
12run_command
{
 "command": "python3 -c \"\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\ndef field_annotation_is_sequence(annotation):\n    # Simplified version of the function in fastapi/_compat/shared.py\n    origin = get_origin(annotation)\n    # Mocking UnionType for Python < 3.10 if necessary, but we are in 3.10+\n    UnionType = getattr(types, 'UnionType', None)\n    if origin is typing.Union or (UnionType and origin is UnionType):\n        for arg in get_args(annotation):\n            # In real code, this calls itself recursively\n            if arg is list or arg is typing.List:\n                return True\n        return False\n    return annotation is list or annotation is typing.List\n\n# Test case: Optional[list[int]] which is Union[list[int], None]\n# In Python 3.10+, list[int] | None\nprint(f'Test 1 (Union[list[int], None]): {field_annotation_is_sequence(typing.Union[typing.List[int], None])}')\nprint(f'Test 2 (list[int] | None): {field_annotation_is_sequence(list[int] | None)}')\n\""
}
{"status": "ok", "stdout": "Test 1 (Union[list[int], None]): False\nTest 2 (list[int] | None): False\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Test 1 (Union[list[int], None]): False\nTest 2 (list[int] | None): False\n", "exit_code": 0}
13run_command
{
 "command": "python3 -c \"\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\ndef _annotation_is_sequence(annotation):\n    # Simplified version of the function in fastapi/_compat/shared.py\n    # In real code it uses lenient_issubclass(annotation, sequence_types)\n    # sequence_types = (Sequence, List, list, Tuple, tuple, Set, set, FrozenSet, frozenset, Deque, deque)\n    sequence_types = (typing.Sequence, typing.List, list, typing.Tuple, tuple, typing.Set, set, typing.FrozenSet, frozenset)\n    try:\n        return isinstance(annotation, type) and issubclass(annotation, sequence_types)\n    except TypeError:\n        return False\n\ndef field_annotation_is_sequence(annotation):\n    origin = get_origin(annotation)\n    UnionType = getattr(types, 'UnionType', None)\n    if origin is typing.Union or (UnionType and 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(origin)\n\n# Test case: Optional[list[int]] which is Union[list[int], None]\n# In Python 3.10+, list[int] | None\nprint(f'Test 1 (Union[list[int], None]): {field_annotation_is_sequence(typing.Union[typing.List[int], None])}')\nprint(f'Test 2 (list[int] | None): {field_annotation_is_sequence(list[int] | None)}')\n\""
}
{"status": "ok", "stdout": "Test 1 (Union[list[int], None]): True\nTest 2 (list[int] | None): True\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Test 1 (Union[list[int], None]): True\nTest 2 (list[int] | None): True\n", "exit_code": 0}
14run_command
{
 "command": "grep -rn \"field_annotation_is_scalar_sequence\" fastapi/ | head -20"
}
{"status": "ok", "stdout": "fastapi/_compat/shared.py:130:def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:\nfastapi/_compat/
full
{"status": "ok", "stdout": "fastapi/_compat/shared.py:130:def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:\nfastapi/_compat/shared.py:135:            if field_annotation_is_scalar_sequence(arg):\nfastapi/_compat/v2.py:362:    return shared.field_annotation_is_scalar_sequence(field.field_info.annotation)\n", "exit_code": 0}
15read_file
{
 "end_line": 145,
 "filepath": "fastapi/_compat/shared.py",
 "start_line": 125
}
{"status": "ok", "filepath": "fastapi/_compat/shared.py", "content": "def field_annotation_is_scalar(annotation: Any) -> bool:\n # handle Ellipsis here to make
full
{"status": "ok", "filepath": "fastapi/_compat/shared.py", "content": "def field_annotation_is_scalar(annotation: Any) -> bool:\n    # handle Ellipsis here to make tuple[int, ...] work nicely\n    return annotation is Ellipsis or not field_annotation_is_complex(annotation)\n\n\ndef field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:\n    origin = get_origin(annotation)\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    return field_annotation_is_sequence(annotation) and all(\n        field_annotation_is_scalar(sub_annotation)\n        for sub_annotation in get_args(annotation)\n    )\n\n", "start_line": 125, "end_line": 145, "total_lines": 211, "is_truncated": false}
16run_command
{
 "command": "python3 -c \"\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\ndef field_annotation_is_complex(annotation):\n    # Simplified\n    return annotation is typing.List or annotation is list\n\ndef field_annotation_is_scalar(annotation):\n    return not field_annotation_is_complex(annotation)\n\ndef field_annotation_is_sequence(annotation):\n    # Simplified\n    return annotation is typing.List or annotation is list or get_origin(annotation) is list or get_origin(annotation) is typing.List\n\ndef field_annotation_is_scalar_sequence(annotation):\n    origin = get_origin(annotation)\n    UnionType = getattr(types, 'UnionType', None)\n    if origin is typing.Union or (UnionType and 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    return field_annotation_is_sequence(annotation) and all(\n        field_annotation_is_scalar(sub_annotation)\n        for sub_annotation in get_args(annotation)\n    )\n\n# Test: list[int] | None\n# list[int] is a scalar sequence. None is a scalar.\n# So list[int] | None should be a scalar sequence.\nprint(f'Test 1 (list[int] | None): {field_annotation_is_scalar_sequence(list[int] | None)}')\n\n# Test: list[int] | list[str]\n# Both are scalar sequences.\nprint(f'Test 2 (list[int] | list[str]): {field_annotation_is_scalar_sequence(list[int] | list[str])}')\n\n# Test: list[int] | dict[str, int]\n# dict[str, int] is complex and not a scalar sequence.\n# field_annotation_is_scalar(dict[str, int]) should be False.\n# So this should return False.\nprint(f'Test 3 (list[int] | dict[str, int]): {field_annotation_is_scalar_sequence(list[int] | typing.Dict[str, int])}')\n\""
}
{"status": "ok", "stdout": "Test 1 (list[int] | None): True\nTest 2 (list[int] | list[str]): True\nTest 3 (list[int] | dict[str, int]): True\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Test 1 (list[int] | None): True\nTest 2 (list[int] | list[str]): True\nTest 3 (list[int] | dict[str, int]): True\n", "exit_code": 0}
17run_command
{
 "command": "python3 -c \"\nimport typing\nimport types\nfrom typing_extensions import get_origin, get_args\n\ndef field_annotation_is_complex(annotation):\n    # Simplified: list is complex, dict is complex, int is scalar\n    if annotation is typing.List or annotation is list or get_origin(annotation) is list or get_origin(annotation) is typing.List:\n        return True\n    if annotation is typing.Dict or annotation is dict or get_origin(annotation) is dict or get_origin(annotation) is typing.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_sequence(annotation):\n    if annotation is typing.List or annotation is list or get_origin(annotation) is list or get_origin(annotation) is typing.List:\n        return True\n    return False\n\ndef field_annotation_is_scalar_sequence(annotation):\n    origin = get_origin(annotation)\n    UnionType = getattr(types, 'UnionType', None)\n    if origin is typing.Union or (UnionType and 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    return field_annotation_is_sequence(annotation) and all(\n        field_annotation_is_scalar(sub_annotation)\n        for sub_annotation in get_args(annotation)\n    )\n\n# Test 3 again: list[int] | dict[str, int]\n# arg1 = list[int] -> field_annotation_is_scalar_sequence(list[int]) -> True. at_least_one_scalar_sequence = True.\n# arg2 = dict[str, int] -> field_annotation_is_scalar_sequence(dict[str, int]) -> False.\n#   Check: not field_annotation_is_scalar(dict[str, int]) -> not False -> True.\n#   Returns False.\nprint(f'Test 3 (list[int] | dict[str, int]): {field_annotation_is_scalar_sequence(list[int] | typing.Dict[str, int])}')\n\""
}
{"status": "ok", "stdout": "Test 3 (list[int] | dict[str, int]): False\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Test 3 (list[int] | dict[str, int]): False\n", "exit_code": 0}
18run_command
{
 "command": "grep -rn \"Union\" fastapi/params.py | head -20"
}
{"status": "ok", "stdout": "4:from typing import Any, Callable, Dict, List, Optional, Sequence, Union\n33: default_factory: Union[Callable[[], Any], None] = _Un
full
{"status": "ok", "stdout": "4:from typing import Any, Callable, Dict, List, Optional, Sequence, Union\n33:        default_factory: Union[Callable[[], Any], None] = _Unset,\n36:        alias_priority: Union[int, None] = _Unset,\n39:        validation_alias: Union[str, None] = None,\n40:        serialization_alias: Union[str, None] = None,\n56:        discriminator: Union[str, None] = None,\n57:        strict: Union[bool, None] = _Unset,\n58:        multiple_of: Union[float, None] = _Unset,\n59:        allow_inf_nan: Union[bool, None] = _Unset,\n60:        max_digits: Union[int, None] = _Unset,\n61:        decimal_places: Union[int, None] = _Unset,\n71:        deprecated: Union[deprecated, str, bool, None] = None,\n73:        json_schema_extra: Union[Dict[str, Any], None] = None,\n147:        default_factory: Union[Callable[[], Any], None] = _Unset,\n150:        alias_priority: Union[int, None] = _Unset,\n153:        validation_alias: Union[str, None] = None,\n154:        serialization_alias: Union[str, None] = None,\n170:        discriminator: Union[str, None] = None,\n171:        strict: Union[bool, None] = _Unset,\n172:        multiple_of: Union[float, None] = _Unset,\n", "exit_code": 0}
19run_command
{
 "command": "grep -rn \"Union\" fastapi/params.py | grep -v \"Union\\[\" | head -20"
}
{"status": "ok", "stdout": "4:from typing import Any, Callable, Dict, List, Optional, Sequence, Union\n", "exit_code": 0}
full
{"status": "ok", "stdout": "4:from typing import Any, Callable, Dict, List, Optional, Sequence, Union\n", "exit_code": 0}
20run_command
{
 "command": "grep -rn \"Union\" fastapi/ | grep -v \"Union\\[\" | head -40"
}
{"status": "ok", "stdout": "fastapi/dependencies/models.py:5:from typing import Any, Callable, List, Optional, Sequence, Union\nfastapi/dependencies/utils.py:18
full
{"status": "ok", "stdout": "fastapi/dependencies/models.py:5:from typing import Any, Callable, List, Optional, Sequence, Union\nfastapi/dependencies/utils.py:18:    Union,\nfastapi/dependencies/utils.py:833:    \"\"\"Check if field type is a Union where all members are BaseModel subclasses.\"\"\"\nfastapi/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/_compat/may_v1.py:2:from typing import Any, Dict, List, Literal, Sequence, Tuple, Type, Union\nfastapi/_compat/shared.py:16:    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:\nfastapi/_compat/v1.py:13:    Union,\nfastapi/_compat/model_field.py:6:    Union,\nfastapi/_compat/v2.py:14:    Union,\nfastapi/_compat/v2.py:384:    if origin_type is Union:  # Handle optional sequences\nfastapi/applications.py:13:    Union,\nfastapi/exceptions.py:1:from typing import Any, Dict, Optional, Sequence, Type, Union\nfastapi/temp_pydantic_v1_params.py:2:from typing import Any, Callable, Dict, List, Optional, Union\nfastapi/security/api_key.py:1:from typing import Optional, Union\nfastapi/security/oau
21read_file
{
 "end_line": 390,
 "filepath": "fastapi/_compat/v2.py",
 "start_line": 370
}
{"status": "ok", "filepath": "fastapi/_compat/v2.py", "content": " return shared.is_bytes_sequence_annotation(field.type_)\n\n\ndef copy_field_info(*, field_inf
full
{"status": "ok", "filepath": "fastapi/_compat/v2.py", "content": "    return shared.is_bytes_sequence_annotation(field.type_)\n\n\ndef copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:\n    cls = type(field_info)\n    merged_field_info = cls.from_annotation(annotation)\n    new_field_info = copy(field_info)\n    new_field_info.metadata = merged_field_info.metadata\n    new_field_info.annotation = merged_field_info.annotation\n    return new_field_info\n\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", "start_line": 370, "end_line": 390, "total_lines": 496, "is_truncated": false}
22read_file
{
 "end_line": 400,
 "filepath": "fastapi/_compat/v2.py",
 "start_line": 370
}
{"status": "ok", "filepath": "fastapi/_compat/v2.py", "content": " return shared.is_bytes_sequence_annotation(field.type_)\n\n\ndef copy_field_info(*, field_inf
full
{"status": "ok", "filepath": "fastapi/_compat/v2.py", "content": "    return shared.is_bytes_sequence_annotation(field.type_)\n\n\ndef copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:\n    cls = type(field_info)\n    merged_field_info = cls.from_annotation(annotation)\n    new_field_info = copy(field_info)\n    new_field_info.metadata = merged_field_info.metadata\n    new_field_info.annotation = merged_field_info.annotation\n    return new_field_info\n\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", "start_line": 370, "end_line": 400, "total_lines": 496, "is_truncated": false}
23edit_file
{
 "filepath": "fastapi/_compat/v2.py",
 "new_string": "    if origin_type is Union or origin_type is shared.UnionType:  # Handle optional sequences",
 "old_string": "    if origin_type is Union:  # Handle optional sequences"
}
{"status": "ok", "filepath": "fastapi/_compat/v2.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/_compat/v2.py\n+++ b/fastapi/_compat/v2.py\n
full
{"status": "ok", "filepath": "fastapi/_compat/v2.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/_compat/v2.py\n+++ b/fastapi/_compat/v2.py\n@@ -381,7 +381,7 @@\n \n def 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+    if origin_type is Union or origin_type is shared.UnionType:  # 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", "is_truncated": false}
24submit_patch
[]
{"status": "ok", "patch_size": 710, "files_changed": 1}
full
{"status": "ok", "patch_size": 710, "files_changed": 1}

Patch

diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py
index 543a42dd..fec85d9d 100644
--- a/fastapi/_compat/v2.py
+++ b/fastapi/_compat/v2.py
@@ -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 shared.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 ===============================
../../../../../../kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
  /kaggle/tmp/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.72s