diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 227ad837d..6a1d6b2e3 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -19,17 +19,20 @@ from typing import ( cast, ) -from fastapi.exceptions import RequestErrorModel +from fastapi.exceptions import ( + PYDANTIC_V2 as PYDANTIC_V2, +) +from fastapi.exceptions import ( + PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE, +) +from fastapi.exceptions import ( + RequestErrorModel, +) from fastapi.types import IncEx, ModelNameMap, UnionType from pydantic import BaseModel, create_model -from pydantic.version import VERSION as PYDANTIC_VERSION from starlette.datastructures import UploadFile from typing_extensions import Annotated, Literal, get_args, get_origin -PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 - - sequence_annotation_to_type = { Sequence: list, List: list, diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 44d4ada86..204ad427a 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,10 +1,27 @@ -from typing import Any, Dict, Optional, Sequence, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Type, Union, cast from pydantic import BaseModel, create_model +from pydantic.version import VERSION as PYDANTIC_VERSION from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import WebSocketException as StarletteWebSocketException from typing_extensions import Annotated, Doc +PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) +PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 + +if PYDANTIC_V2: + from pydantic.v1.error_wrappers import display_errors + + if TYPE_CHECKING: # pragma: nocover + from pydantic.v1.error_wrappers import ErrorDict +else: + from pydantic.error_wrappers import ( # type: ignore[no-redef] + display_errors, + ) + + if TYPE_CHECKING: # pragma: nocover + from pydantic.error_wrappers import ErrorDict # type: ignore[no-redef] + class HTTPException(StarletteHTTPException): """ @@ -159,6 +176,14 @@ class RequestValidationError(ValidationException): super().__init__(errors) self.body = body + def __str__(self) -> str: + errors = cast(List["ErrorDict"], self.errors()) + no_errors = len(errors) + return ( + f"{no_errors} validation error{'' if no_errors == 1 else 's'}\n" + f"{display_errors(errors)}" + ) + class WebSocketRequestValidationError(ValidationException): pass diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 217159a59..a5e307133 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -1,3 +1,4 @@ +from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE from fastapi.testclient import TestClient from docs_src.handling_errors.tutorial004 import app @@ -8,18 +9,19 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 400, response.text - # TODO: remove when deprecating Pydantic v1 - assert ( - # TODO: remove when deprecating Pydantic v1 - "path -> item_id" in response.text - or "'loc': ('path', 'item_id')" in response.text - ) - assert ( - # TODO: remove when deprecating Pydantic v1 - "value is not a valid integer" in response.text - or "Input should be a valid integer, unable to parse string as an integer" - in response.text - ) + if PYDANTIC_VERSION_MINOR_TUPLE < (2, 0): + assert response.text == ( + "1 validation error\n" + "path -> item_id\n" + " value is not a valid integer (type=type_error.integer)" + ) + else: + assert response.text == ( + "1 validation error\n" + "path -> item_id\n" + " Input should be a valid integer, unable to parse string as an integer " + "(type=int_parsing)" + ) def test_get_http_error():