resolved RESOLVED PASS · 9 tool calls · 17 s · fastapi/fastapi
🐛 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 | Arguments | Result |
|---|---|---|---|
| 1 | run_command | {
"command": "grep -rn \"Union\" --include=*.py fastapi | grep \"sequence\" | head -20"
} | {"status": "ok", "stdout": "fastapi/_compat/shared.py:77:def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:\nfastapi/_compat/shared.py:83:full{"status": "ok", "stdout": "fastapi/_compat/shared.py:77:def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:\nfastapi/_compat/shared.py:83:def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:\nfastapi/_compat/shared.py:130:def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:\nfastapi/_compat/v2.py:384: if origin_type is Union: # Handle optional sequences\n", "stderr": "", "exit_code": 0} |
| 2 | run_command | {
"command": "cat 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 ifull{"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\nValidator = Any\n\n\nclass BaseConfig:\n pass\n\n\nclass ErrorWrapper(Exception):\n pass\n\n\n@data |
| 3 | run_command | {
"command": "grep -n \"if origin_type is Union:\" fastapi/_compat/v2.py"
} | {"status": "ok", "stdout": "384: if origin_type is Union: # Handle optional sequences\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "384: if origin_type is Union: # Handle optional sequences\n", "stderr": "", "exit_code": 0} |
| 4 | run_command | {
"command": "sed -n '370,400p' fastapi/_compat/v2.py"
} | {"status": "ok", "stdout": " return shared.is_bytes_sequence_annotation(field.type_)\n\n\ndef copy_field_info(*, field_info: FieldInfo, annotation: Any) -> Fielfull{"status": "ok", "stdout": " 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", "stderr": "", "exit_code": 0} |
| 5 | run_command | {
"command": "cat fastapi/_compat/shared.py"
} | {"status": "ok", "stdout": "import sys\nimport types\nimport typing\nfrom collections import deque\nfrom dataclasses import is_dataclass\nfrom typing import (\nfull{"status": "ok", "stdout": "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], ...], None]\n) -> bool:\n try:\n return |
| 6 | run_command | {
"command": "grep -n \"UnionType\" fastapi/_compat/shared.py"
} | {"status": "ok", "stdout": "20:from fastapi.types import UnionType\n39: types.UnionType,\n85: if origin is Union or origin is UnionType:\n111: if origin is Uniofull{"status": "ok", "stdout": "20:from fastapi.types import UnionType\n39: types.UnionType,\n85: 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} |
| 7 | run_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 pydafull{"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} |
| 8 | run_command | {
"command": "sed -n '380,395p' 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.annfull{"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", "stderr": "", "exit_code": 0} |
| 9 | edit_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\nfull{"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} |
| 10 | submit_patch | {} | {"status": "ok", "patch_size": 710, "files_changed": 1}full{"status": "ok", "patch_size": 710, "files_changed": 1} |
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):
.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.75s