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