mirror of https://github.com/tiangolo/fastapi.git
parent
a2cef707e3
commit
8f4d2665a9
|
|
@ -1,3 +1,5 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from .main import BaseConfig as BaseConfig
|
||||
from .main import PydanticSchemaGenerationError as PydanticSchemaGenerationError
|
||||
from .main import RequiredParam as RequiredParam
|
||||
|
|
@ -34,6 +36,7 @@ from .may_v1 import CoreSchema as CoreSchema
|
|||
from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
|
||||
from .may_v1 import JsonSchemaValue as JsonSchemaValue
|
||||
from .may_v1 import _normalize_errors as _normalize_errors
|
||||
from .may_v1 import display_errors as display_errors
|
||||
from .model_field import ModelField as ModelField
|
||||
from .shared import PYDANTIC_V2 as PYDANTIC_V2
|
||||
from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
|
||||
|
|
@ -48,3 +51,6 @@ from .shared import (
|
|||
from .shared import lenient_issubclass as lenient_issubclass
|
||||
from .shared import sequence_types as sequence_types
|
||||
from .shared import value_is_sequence as value_is_sequence
|
||||
|
||||
if TYPE_CHECKING: # pragma: nocover
|
||||
from .may_v1 import ErrorDict as ErrorDict
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import sys
|
||||
from typing import Any, Dict, List, Literal, Sequence, Tuple, Type, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
|
||||
from fastapi.types import ModelNameMap
|
||||
|
||||
|
|
@ -58,6 +69,23 @@ if sys.version_info >= (3, 14):
|
|||
|
||||
from .v2 import ValidationError, create_model
|
||||
|
||||
def _display_error_loc(error: "ErrorDict") -> str:
|
||||
return " -> ".join(str(e) for e in error["loc"])
|
||||
|
||||
def _display_error_type_and_ctx(error: "ErrorDict") -> str:
|
||||
t = "type=" + error["type"]
|
||||
ctx = error.get("ctx")
|
||||
if ctx: # pragma: no cover
|
||||
return t + "".join(f"; {k}={v}" for k, v in ctx.items())
|
||||
else:
|
||||
return t
|
||||
|
||||
def display_errors(errors: List["ErrorDict"]) -> str:
|
||||
return "\n".join(
|
||||
f"{_display_error_loc(e)}\n {e['msg']} ({_display_error_type_and_ctx(e)})"
|
||||
for e in errors
|
||||
)
|
||||
|
||||
def get_definitions(
|
||||
*,
|
||||
fields: List[ModelField],
|
||||
|
|
@ -71,6 +99,17 @@ if sys.version_info >= (3, 14):
|
|||
]:
|
||||
return {}, {} # pragma: no cover
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
Loc = Tuple[Union[int, str], ...]
|
||||
|
||||
class _ErrorDictRequired(TypedDict):
|
||||
loc: Loc
|
||||
msg: str
|
||||
type: str
|
||||
|
||||
class ErrorDict(_ErrorDictRequired, total=False):
|
||||
ctx: Dict[str, Any]
|
||||
|
||||
|
||||
else:
|
||||
from .v1 import AnyUrl as AnyUrl
|
||||
|
|
@ -91,8 +130,12 @@ else:
|
|||
from .v1 import UndefinedType as UndefinedType
|
||||
from .v1 import Url as Url
|
||||
from .v1 import ValidationError, create_model
|
||||
from .v1 import display_errors as display_errors
|
||||
from .v1 import get_definitions as get_definitions
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from .v1 import ErrorDict as ErrorDict
|
||||
|
||||
|
||||
RequestErrorModel: Type[BaseModel] = create_model("Request")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from copy import copy
|
|||
from dataclasses import dataclass, is_dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
|
|
@ -33,6 +34,7 @@ if not PYDANTIC_V2:
|
|||
from pydantic.class_validators import Validator as Validator
|
||||
from pydantic.color import Color as Color
|
||||
from pydantic.error_wrappers import ErrorWrapper as ErrorWrapper
|
||||
from pydantic.error_wrappers import display_errors as display_errors
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import ( # type: ignore[attr-defined]
|
||||
SHAPE_FROZENSET,
|
||||
|
|
@ -69,6 +71,8 @@ if not PYDANTIC_V2:
|
|||
from pydantic.typing import evaluate_forwardref as evaluate_forwardref
|
||||
from pydantic.utils import lenient_issubclass as lenient_issubclass
|
||||
|
||||
if TYPE_CHECKING: # pragma: nocover
|
||||
from pydantic.error_wrappers import ErrorDict as ErrorDict
|
||||
|
||||
else:
|
||||
from pydantic.v1 import BaseConfig as BaseConfig # type: ignore[assignment]
|
||||
|
|
@ -80,6 +84,7 @@ else:
|
|||
from pydantic.v1.class_validators import Validator as Validator
|
||||
from pydantic.v1.color import Color as Color # type: ignore[assignment]
|
||||
from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper
|
||||
from pydantic.v1.error_wrappers import display_errors as display_errors
|
||||
from pydantic.v1.errors import MissingError
|
||||
from pydantic.v1.fields import (
|
||||
SHAPE_FROZENSET,
|
||||
|
|
@ -122,6 +127,9 @@ else:
|
|||
from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref
|
||||
from pydantic.v1.utils import lenient_issubclass as lenient_issubclass
|
||||
|
||||
if TYPE_CHECKING: # pragma: nocover
|
||||
from pydantic.v1.error_wrappers import ErrorDict as ErrorDict
|
||||
|
||||
|
||||
GetJsonSchemaHandler = Any
|
||||
JsonSchemaValue = Dict[str, Any]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
from typing import Any, Dict, Optional, Sequence, Type, TypedDict, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
|
||||
from annotated_doc import Doc
|
||||
from fastapi._compat import display_errors
|
||||
from pydantic import BaseModel, create_model
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.exceptions import WebSocketException as StarletteWebSocketException
|
||||
from typing_extensions import Annotated
|
||||
|
||||
if TYPE_CHECKING: # pragma: nocover
|
||||
from fastapi._compat import ErrorDict
|
||||
|
||||
|
||||
class EndpointContext(TypedDict, total=False):
|
||||
function: str
|
||||
|
|
@ -164,7 +177,7 @@ class DependencyScopeError(FastAPIError):
|
|||
class ValidationException(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
errors: Sequence[Any],
|
||||
errors: Sequence["ErrorDict"],
|
||||
*,
|
||||
endpoint_ctx: Optional[EndpointContext] = None,
|
||||
) -> None:
|
||||
|
|
@ -177,7 +190,7 @@ class ValidationException(Exception):
|
|||
self.endpoint_file = ctx.get("file")
|
||||
self.endpoint_line = ctx.get("line")
|
||||
|
||||
def errors(self) -> Sequence[Any]:
|
||||
def errors(self) -> Sequence["ErrorDict"]:
|
||||
return self._errors
|
||||
|
||||
def _format_endpoint_context(self) -> str:
|
||||
|
|
@ -193,8 +206,7 @@ class ValidationException(Exception):
|
|||
|
||||
def __str__(self) -> str:
|
||||
message = f"{len(self._errors)} validation error{'s' if len(self._errors) != 1 else ''}:\n"
|
||||
for err in self._errors:
|
||||
message += f" {err}\n"
|
||||
message += display_errors(self._errors)
|
||||
message += self._format_endpoint_context()
|
||||
return message.rstrip()
|
||||
|
||||
|
|
@ -202,7 +214,7 @@ class ValidationException(Exception):
|
|||
class RequestValidationError(ValidationException):
|
||||
def __init__(
|
||||
self,
|
||||
errors: Sequence[Any],
|
||||
errors: Sequence["ErrorDict"],
|
||||
*,
|
||||
body: Any = None,
|
||||
endpoint_ctx: Optional[EndpointContext] = None,
|
||||
|
|
@ -224,7 +236,7 @@ class WebSocketRequestValidationError(ValidationException):
|
|||
class ResponseValidationError(ValidationException):
|
||||
def __init__(
|
||||
self,
|
||||
errors: Sequence[Any],
|
||||
errors: Sequence["ErrorDict"],
|
||||
*,
|
||||
body: Any = None,
|
||||
endpoint_ctx: Optional[EndpointContext] = None,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,46 @@
|
|||
from fastapi.testclient import TestClient
|
||||
import inspect
|
||||
|
||||
from docs_src.handling_errors.tutorial004 import app
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
import docs_src.handling_errors.tutorial004 as tutorial004
|
||||
from tests.utils import pydantic_snapshot
|
||||
|
||||
app = tutorial004.app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get_validation_error():
|
||||
source_file = inspect.getsourcefile(tutorial004.read_item)
|
||||
line_number = inspect.getsourcelines(tutorial004.read_item)[1]
|
||||
func_name = tutorial004.read_item.__name__
|
||||
|
||||
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 response.text == pydantic_snapshot(
|
||||
v2=snapshot(
|
||||
"\n".join(
|
||||
[
|
||||
"1 validation error:",
|
||||
"path -> item_id",
|
||||
" Input should be a valid integer, unable to parse string as an integer (type=int_parsing)",
|
||||
f' File "{source_file}", line {line_number}, in {func_name}',
|
||||
" GET /items/{item_id}",
|
||||
]
|
||||
)
|
||||
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
|
||||
),
|
||||
v1=snapshot(
|
||||
"\n".join(
|
||||
[
|
||||
"1 validation error:",
|
||||
"path -> item_id",
|
||||
" value is not a valid integer (type=type_error.integer)",
|
||||
f' File "{source_file}", line {line_number}, in {func_name}',
|
||||
" GET /items/{item_id}",
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from dirty_equals import IsDict
|
|||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from tests.utils import needs_py39, needs_py310
|
||||
from tests.utils import needs_py39, needs_py310, pydantic_snapshot
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
|
|
@ -62,10 +62,10 @@ def test_header_param_model_defaults(client: TestClient):
|
|||
def test_header_param_model_invalid(client: TestClient):
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == snapshot(
|
||||
assert response.json() == pydantic_snapshot(
|
||||
v2=snapshot(
|
||||
{
|
||||
"detail": [
|
||||
IsDict(
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "save_data"],
|
||||
|
|
@ -78,18 +78,21 @@ def test_header_param_model_invalid(client: TestClient):
|
|||
"connection": "keep-alive",
|
||||
"user-agent": "testclient",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
),
|
||||
v1=snapshot(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "value_error.missing",
|
||||
"loc": ["header", "save_data"],
|
||||
"msg": "field required",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from dirty_equals import IsDict
|
|||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from tests.utils import needs_py39, needs_py310
|
||||
from tests.utils import needs_py39, needs_py310, pydantic_snapshot
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
|
|
@ -59,10 +59,10 @@ def test_header_param_model_no_underscore(client: TestClient):
|
|||
],
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == snapshot(
|
||||
assert response.json() == pydantic_snapshot(
|
||||
v2=snapshot(
|
||||
{
|
||||
"detail": [
|
||||
IsDict(
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "save_data"],
|
||||
|
|
@ -80,17 +80,20 @@ def test_header_param_model_no_underscore(client: TestClient):
|
|||
"x-tag": ["one", "two"],
|
||||
},
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
],
|
||||
},
|
||||
),
|
||||
v1=snapshot(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "value_error.missing",
|
||||
"loc": ["header", "save_data"],
|
||||
"msg": "field required",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -109,10 +112,10 @@ def test_header_param_model_defaults(client: TestClient):
|
|||
def test_header_param_model_invalid(client: TestClient):
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == snapshot(
|
||||
assert response.json() == pydantic_snapshot(
|
||||
v2=snapshot(
|
||||
{
|
||||
"detail": [
|
||||
IsDict(
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "save_data"],
|
||||
|
|
@ -126,17 +129,20 @@ def test_header_param_model_invalid(client: TestClient):
|
|||
"user-agent": "testclient",
|
||||
},
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
],
|
||||
},
|
||||
),
|
||||
v1=snapshot(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "value_error.missing",
|
||||
"loc": ["header", "save_data"],
|
||||
"msg": "field required",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue