mirror of https://github.com/tiangolo/fastapi.git
Merge branch 'master' into feature-branch-name
This commit is contained in:
commit
659032db2e
|
|
@ -14,7 +14,7 @@ GitHub-Repository: <a href="https://github.com/tiangolo/full-stack-fastapi-templ
|
||||||
- 💾 [PostgreSQL](https://www.postgresql.org) als SQL-Datenbank.
|
- 💾 [PostgreSQL](https://www.postgresql.org) als SQL-Datenbank.
|
||||||
- 🚀 [React](https://react.dev) für das Frontend.
|
- 🚀 [React](https://react.dev) für das Frontend.
|
||||||
- 💃 Verwendung von TypeScript, Hooks, [Vite](https://vitejs.dev) und anderen Teilen eines modernen Frontend-Stacks.
|
- 💃 Verwendung von TypeScript, Hooks, [Vite](https://vitejs.dev) und anderen Teilen eines modernen Frontend-Stacks.
|
||||||
- 🎨 [Chakra UI](https://chakra-ui.com) für die Frontend-Komponenten.
|
- 🎨 [Tailwind CSS](https://tailwindcss.com) und [shadcn/ui](https://ui.shadcn.com) für die Frontend-Komponenten.
|
||||||
- 🤖 Ein automatisch generierter Frontend-Client.
|
- 🤖 Ein automatisch generierter Frontend-Client.
|
||||||
- 🧪 [Playwright](https://playwright.dev) für End-to-End-Tests.
|
- 🧪 [Playwright](https://playwright.dev) für End-to-End-Tests.
|
||||||
- 🦇 Unterstützung des Dunkelmodus.
|
- 🦇 Unterstützung des Dunkelmodus.
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,18 @@ GitHub Repository: <a href="https://github.com/tiangolo/full-stack-fastapi-templ
|
||||||
## Full Stack FastAPI Template - Technology Stack and Features { #full-stack-fastapi-template-technology-stack-and-features }
|
## Full Stack FastAPI Template - Technology Stack and Features { #full-stack-fastapi-template-technology-stack-and-features }
|
||||||
|
|
||||||
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) for the Python backend API.
|
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) for the Python backend API.
|
||||||
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) for the Python SQL database interactions (ORM).
|
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) for the Python SQL database interactions (ORM).
|
||||||
- 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management.
|
- 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management.
|
||||||
- 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database.
|
- 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database.
|
||||||
- 🚀 [React](https://react.dev) for the frontend.
|
- 🚀 [React](https://react.dev) for the frontend.
|
||||||
- 💃 Using TypeScript, hooks, [Vite](https://vitejs.dev), and other parts of a modern frontend stack.
|
- 💃 Using TypeScript, hooks, Vite, and other parts of a modern frontend stack.
|
||||||
- 🎨 [Chakra UI](https://chakra-ui.com) for the frontend components.
|
- 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for the frontend components.
|
||||||
- 🤖 An automatically generated frontend client.
|
- 🤖 An automatically generated frontend client.
|
||||||
- 🧪 [Playwright](https://playwright.dev) for End-to-End testing.
|
- 🧪 [Playwright](https://playwright.dev) for End-to-End testing.
|
||||||
- 🦇 Dark mode support.
|
- 🦇 Dark mode support.
|
||||||
- 🐋 [Docker Compose](https://www.docker.com) for development and production.
|
- 🐋 [Docker Compose](https://www.docker.com) for development and production.
|
||||||
- 🔒 Secure password hashing by default.
|
- 🔒 Secure password hashing by default.
|
||||||
- 🔑 JWT token authentication.
|
- 🔑 JWT (JSON Web Token) authentication.
|
||||||
- 📫 Email based password recovery.
|
- 📫 Email based password recovery.
|
||||||
- ✅ Tests with [Pytest](https://pytest.org).
|
- ✅ Tests with [Pytest](https://pytest.org).
|
||||||
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
|
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,31 @@ hide:
|
||||||
|
|
||||||
## Latest Changes
|
## Latest Changes
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
* 📝 Update tech stack in project generation docs. PR [#14472](https://github.com/fastapi/fastapi/pull/14472) by [@alejsdev](https://github.com/alejsdev).
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
|
||||||
|
* ✅ Add test for Pydantic v2, dataclasses, UUID, and `__annotations__`. PR [#14477](https://github.com/fastapi/fastapi/pull/14477) by [@tiangolo](https://github.com/tiangolo).
|
||||||
|
|
||||||
|
## 0.124.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🚸 Improve tracebacks by adding endpoint metadata. PR [#14306](https://github.com/fastapi/fastapi/pull/14306) by [@savannahostrowski](https://github.com/savannahostrowski).
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
|
||||||
|
* ✏️ Fix typo in `scripts/mkdocs_hooks.py`. PR [#14457](https://github.com/fastapi/fastapi/pull/14457) by [@yujiteshima](https://github.com/yujiteshima).
|
||||||
|
|
||||||
|
## 0.123.10
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix using class (not instance) dependency that has `__call__` method. PR [#14458](https://github.com/fastapi/fastapi/pull/14458) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||||
|
* 🐛 Fix `separate_input_output_schemas=False` with `computed_field`. PR [#14453](https://github.com/fastapi/fastapi/pull/14453) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||||
|
|
||||||
## 0.123.9
|
## 0.123.9
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ Repositório GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-tem
|
||||||
- 💾 [PostgreSQL](https://www.postgresql.org) como banco de dados SQL.
|
- 💾 [PostgreSQL](https://www.postgresql.org) como banco de dados SQL.
|
||||||
- 🚀 [React](https://react.dev) para o frontend.
|
- 🚀 [React](https://react.dev) para o frontend.
|
||||||
- 💃 Usando TypeScript, hooks, [Vite](https://vitejs.dev), e outras partes de uma _stack_ frontend moderna.
|
- 💃 Usando TypeScript, hooks, [Vite](https://vitejs.dev), e outras partes de uma _stack_ frontend moderna.
|
||||||
- 🎨 [Chakra UI](https://chakra-ui.com) para os componentes de frontend.
|
- 🎨 [Tailwind CSS](https://tailwindcss.com) e [shadcn/ui](https://ui.shadcn.com) para os componentes de frontend.
|
||||||
- 🤖 Um cliente frontend automaticamente gerado.
|
- 🤖 Um cliente frontend automaticamente gerado.
|
||||||
- 🧪 [Playwright](https://playwright.dev) para testes Ponta-a-Ponta.
|
- 🧪 [Playwright](https://playwright.dev) para testes Ponta-a-Ponta.
|
||||||
- 🦇 Suporte para modo escuro.
|
- 🦇 Suporte para modo escuro.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||||
|
|
||||||
__version__ = "0.123.9"
|
__version__ = "0.124.0"
|
||||||
|
|
||||||
from starlette import status as status
|
from starlette import status as status
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,13 @@ def _get_model_config(model: BaseModel) -> Any:
|
||||||
return model.model_config
|
return model.model_config
|
||||||
|
|
||||||
|
|
||||||
|
def _has_computed_fields(field: ModelField) -> bool:
|
||||||
|
computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
|
||||||
|
"computed_fields", []
|
||||||
|
)
|
||||||
|
return len(computed_fields) > 0
|
||||||
|
|
||||||
|
|
||||||
def get_schema_from_model_field(
|
def get_schema_from_model_field(
|
||||||
*,
|
*,
|
||||||
field: ModelField,
|
field: ModelField,
|
||||||
|
|
@ -180,12 +187,9 @@ def get_schema_from_model_field(
|
||||||
],
|
],
|
||||||
separate_input_output_schemas: bool = True,
|
separate_input_output_schemas: bool = True,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
|
|
||||||
"computed_fields", []
|
|
||||||
)
|
|
||||||
override_mode: Union[Literal["validation"], None] = (
|
override_mode: Union[Literal["validation"], None] = (
|
||||||
None
|
None
|
||||||
if (separate_input_output_schemas or len(computed_fields) > 0)
|
if (separate_input_output_schemas or _has_computed_fields(field))
|
||||||
else "validation"
|
else "validation"
|
||||||
)
|
)
|
||||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||||
|
|
@ -208,15 +212,7 @@ def get_definitions(
|
||||||
Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
|
Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
|
||||||
Dict[str, Dict[str, Any]],
|
Dict[str, Dict[str, Any]],
|
||||||
]:
|
]:
|
||||||
has_computed_fields: bool = any(
|
|
||||||
field._type_adapter.core_schema.get("schema", {}).get("computed_fields", [])
|
|
||||||
for field in fields
|
|
||||||
)
|
|
||||||
|
|
||||||
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
||||||
override_mode: Union[Literal["validation"], None] = (
|
|
||||||
None if (separate_input_output_schemas or has_computed_fields) else "validation"
|
|
||||||
)
|
|
||||||
validation_fields = [field for field in fields if field.mode == "validation"]
|
validation_fields = [field for field in fields if field.mode == "validation"]
|
||||||
serialization_fields = [field for field in fields if field.mode == "serialization"]
|
serialization_fields = [field for field in fields if field.mode == "serialization"]
|
||||||
flat_validation_models = get_flat_models_from_fields(
|
flat_validation_models = get_flat_models_from_fields(
|
||||||
|
|
@ -246,9 +242,16 @@ def get_definitions(
|
||||||
unique_flat_model_fields = {
|
unique_flat_model_fields = {
|
||||||
f for f in flat_model_fields if f.type_ not in input_types
|
f for f in flat_model_fields if f.type_ not in input_types
|
||||||
}
|
}
|
||||||
|
|
||||||
inputs = [
|
inputs = [
|
||||||
(field, override_mode or field.mode, field._type_adapter.core_schema)
|
(
|
||||||
|
field,
|
||||||
|
(
|
||||||
|
field.mode
|
||||||
|
if (separate_input_output_schemas or _has_computed_fields(field))
|
||||||
|
else "validation"
|
||||||
|
),
|
||||||
|
field._type_adapter.core_schema,
|
||||||
|
)
|
||||||
for field in list(fields) + list(unique_flat_model_fields)
|
for field in list(fields) + list(unique_flat_model_fields)
|
||||||
]
|
]
|
||||||
field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs)
|
field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs)
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,8 @@ class Dependant:
|
||||||
_impartial(self.call)
|
_impartial(self.call)
|
||||||
) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):
|
) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):
|
||||||
return True
|
return True
|
||||||
|
if inspect.isclass(_unwrapped_call(self.call)):
|
||||||
|
return False
|
||||||
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
||||||
if dunder_call is None:
|
if dunder_call is None:
|
||||||
return False # pragma: no cover
|
return False # pragma: no cover
|
||||||
|
|
@ -134,6 +136,8 @@ class Dependant:
|
||||||
_impartial(self.call)
|
_impartial(self.call)
|
||||||
) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):
|
) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):
|
||||||
return True
|
return True
|
||||||
|
if inspect.isclass(_unwrapped_call(self.call)):
|
||||||
|
return False
|
||||||
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
||||||
if dunder_call is None:
|
if dunder_call is None:
|
||||||
return False # pragma: no cover
|
return False # pragma: no cover
|
||||||
|
|
@ -162,6 +166,8 @@ class Dependant:
|
||||||
_unwrapped_call(self.call)
|
_unwrapped_call(self.call)
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
if inspect.isclass(_unwrapped_call(self.call)):
|
||||||
|
return False
|
||||||
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
||||||
if dunder_call is None:
|
if dunder_call is None:
|
||||||
return False # pragma: no cover
|
return False # pragma: no cover
|
||||||
|
|
@ -176,7 +182,6 @@ class Dependant:
|
||||||
_impartial(dunder_unwrapped_call)
|
_impartial(dunder_unwrapped_call)
|
||||||
) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)):
|
) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)):
|
||||||
return True
|
return True
|
||||||
# if inspect.isclass(self.call): False, covered by default return
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Any, Dict, Optional, Sequence, Type, Union
|
from typing import Any, Dict, Optional, Sequence, Type, TypedDict, Union
|
||||||
|
|
||||||
from annotated_doc import Doc
|
from annotated_doc import Doc
|
||||||
from pydantic import BaseModel, create_model
|
from pydantic import BaseModel, create_model
|
||||||
|
|
@ -7,6 +7,13 @@ from starlette.exceptions import WebSocketException as StarletteWebSocketExcepti
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointContext(TypedDict, total=False):
|
||||||
|
function: str
|
||||||
|
path: str
|
||||||
|
file: str
|
||||||
|
line: int
|
||||||
|
|
||||||
|
|
||||||
class HTTPException(StarletteHTTPException):
|
class HTTPException(StarletteHTTPException):
|
||||||
"""
|
"""
|
||||||
An HTTP exception you can raise in your own code to show errors to the client.
|
An HTTP exception you can raise in your own code to show errors to the client.
|
||||||
|
|
@ -155,30 +162,72 @@ class DependencyScopeError(FastAPIError):
|
||||||
|
|
||||||
|
|
||||||
class ValidationException(Exception):
|
class ValidationException(Exception):
|
||||||
def __init__(self, errors: Sequence[Any]) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
errors: Sequence[Any],
|
||||||
|
*,
|
||||||
|
endpoint_ctx: Optional[EndpointContext] = None,
|
||||||
|
) -> None:
|
||||||
self._errors = errors
|
self._errors = errors
|
||||||
|
self.endpoint_ctx = endpoint_ctx
|
||||||
|
|
||||||
|
ctx = endpoint_ctx or {}
|
||||||
|
self.endpoint_function = ctx.get("function")
|
||||||
|
self.endpoint_path = ctx.get("path")
|
||||||
|
self.endpoint_file = ctx.get("file")
|
||||||
|
self.endpoint_line = ctx.get("line")
|
||||||
|
|
||||||
def errors(self) -> Sequence[Any]:
|
def errors(self) -> Sequence[Any]:
|
||||||
return self._errors
|
return self._errors
|
||||||
|
|
||||||
|
def _format_endpoint_context(self) -> str:
|
||||||
|
if not (self.endpoint_file and self.endpoint_line and self.endpoint_function):
|
||||||
|
if self.endpoint_path:
|
||||||
|
return f"\n Endpoint: {self.endpoint_path}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
context = f'\n File "{self.endpoint_file}", line {self.endpoint_line}, in {self.endpoint_function}'
|
||||||
|
if self.endpoint_path:
|
||||||
|
context += f"\n {self.endpoint_path}"
|
||||||
|
return context
|
||||||
|
|
||||||
|
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 += self._format_endpoint_context()
|
||||||
|
return message.rstrip()
|
||||||
|
|
||||||
|
|
||||||
class RequestValidationError(ValidationException):
|
class RequestValidationError(ValidationException):
|
||||||
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
def __init__(
|
||||||
super().__init__(errors)
|
self,
|
||||||
|
errors: Sequence[Any],
|
||||||
|
*,
|
||||||
|
body: Any = None,
|
||||||
|
endpoint_ctx: Optional[EndpointContext] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(errors, endpoint_ctx=endpoint_ctx)
|
||||||
self.body = body
|
self.body = body
|
||||||
|
|
||||||
|
|
||||||
class WebSocketRequestValidationError(ValidationException):
|
class WebSocketRequestValidationError(ValidationException):
|
||||||
pass
|
def __init__(
|
||||||
|
self,
|
||||||
|
errors: Sequence[Any],
|
||||||
|
*,
|
||||||
|
endpoint_ctx: Optional[EndpointContext] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(errors, endpoint_ctx=endpoint_ctx)
|
||||||
|
|
||||||
|
|
||||||
class ResponseValidationError(ValidationException):
|
class ResponseValidationError(ValidationException):
|
||||||
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
def __init__(
|
||||||
super().__init__(errors)
|
self,
|
||||||
|
errors: Sequence[Any],
|
||||||
|
*,
|
||||||
|
body: Any = None,
|
||||||
|
endpoint_ctx: Optional[EndpointContext] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(errors, endpoint_ctx=endpoint_ctx)
|
||||||
self.body = body
|
self.body = body
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
message = f"{len(self._errors)} validation errors:\n"
|
|
||||||
for err in self._errors:
|
|
||||||
message += f" {err}\n"
|
|
||||||
return message
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ from fastapi.dependencies.utils import (
|
||||||
)
|
)
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.exceptions import (
|
from fastapi.exceptions import (
|
||||||
|
EndpointContext,
|
||||||
FastAPIError,
|
FastAPIError,
|
||||||
RequestValidationError,
|
RequestValidationError,
|
||||||
ResponseValidationError,
|
ResponseValidationError,
|
||||||
|
|
@ -212,6 +213,33 @@ def _merge_lifespan_context(
|
||||||
return merged_lifespan # type: ignore[return-value]
|
return merged_lifespan # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
|
# Cache for endpoint context to avoid re-extracting on every request
|
||||||
|
_endpoint_context_cache: Dict[int, EndpointContext] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_endpoint_context(func: Any) -> EndpointContext:
|
||||||
|
"""Extract endpoint context with caching to avoid repeated file I/O."""
|
||||||
|
func_id = id(func)
|
||||||
|
|
||||||
|
if func_id in _endpoint_context_cache:
|
||||||
|
return _endpoint_context_cache[func_id]
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx: EndpointContext = {}
|
||||||
|
|
||||||
|
if (source_file := inspect.getsourcefile(func)) is not None:
|
||||||
|
ctx["file"] = source_file
|
||||||
|
if (line_number := inspect.getsourcelines(func)[1]) is not None:
|
||||||
|
ctx["line"] = line_number
|
||||||
|
if (func_name := getattr(func, "__name__", None)) is not None:
|
||||||
|
ctx["function"] = func_name
|
||||||
|
except Exception:
|
||||||
|
ctx = EndpointContext()
|
||||||
|
|
||||||
|
_endpoint_context_cache[func_id] = ctx
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
async def serialize_response(
|
async def serialize_response(
|
||||||
*,
|
*,
|
||||||
field: Optional[ModelField] = None,
|
field: Optional[ModelField] = None,
|
||||||
|
|
@ -223,6 +251,7 @@ async def serialize_response(
|
||||||
exclude_defaults: bool = False,
|
exclude_defaults: bool = False,
|
||||||
exclude_none: bool = False,
|
exclude_none: bool = False,
|
||||||
is_coroutine: bool = True,
|
is_coroutine: bool = True,
|
||||||
|
endpoint_ctx: Optional[EndpointContext] = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if field:
|
if field:
|
||||||
errors = []
|
errors = []
|
||||||
|
|
@ -245,8 +274,11 @@ async def serialize_response(
|
||||||
elif errors_:
|
elif errors_:
|
||||||
errors.append(errors_)
|
errors.append(errors_)
|
||||||
if errors:
|
if errors:
|
||||||
|
ctx = endpoint_ctx or EndpointContext()
|
||||||
raise ResponseValidationError(
|
raise ResponseValidationError(
|
||||||
errors=_normalize_errors(errors), body=response_content
|
errors=_normalize_errors(errors),
|
||||||
|
body=response_content,
|
||||||
|
endpoint_ctx=ctx,
|
||||||
)
|
)
|
||||||
|
|
||||||
if hasattr(field, "serialize"):
|
if hasattr(field, "serialize"):
|
||||||
|
|
@ -318,6 +350,18 @@ def get_request_handler(
|
||||||
"fastapi_middleware_astack not found in request scope"
|
"fastapi_middleware_astack not found in request scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Extract endpoint context for error messages
|
||||||
|
endpoint_ctx = (
|
||||||
|
_extract_endpoint_context(dependant.call)
|
||||||
|
if dependant.call
|
||||||
|
else EndpointContext()
|
||||||
|
)
|
||||||
|
|
||||||
|
if dependant.path:
|
||||||
|
# For mounted sub-apps, include the mount path prefix
|
||||||
|
mount_path = request.scope.get("root_path", "").rstrip("/")
|
||||||
|
endpoint_ctx["path"] = f"{request.method} {mount_path}{dependant.path}"
|
||||||
|
|
||||||
# Read body and auto-close files
|
# Read body and auto-close files
|
||||||
try:
|
try:
|
||||||
body: Any = None
|
body: Any = None
|
||||||
|
|
@ -355,6 +399,7 @@ def get_request_handler(
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
body=e.doc,
|
body=e.doc,
|
||||||
|
endpoint_ctx=endpoint_ctx,
|
||||||
)
|
)
|
||||||
raise validation_error from e
|
raise validation_error from e
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
@ -414,6 +459,7 @@ def get_request_handler(
|
||||||
exclude_defaults=response_model_exclude_defaults,
|
exclude_defaults=response_model_exclude_defaults,
|
||||||
exclude_none=response_model_exclude_none,
|
exclude_none=response_model_exclude_none,
|
||||||
is_coroutine=is_coroutine,
|
is_coroutine=is_coroutine,
|
||||||
|
endpoint_ctx=endpoint_ctx,
|
||||||
)
|
)
|
||||||
response = actual_response_class(content, **response_args)
|
response = actual_response_class(content, **response_args)
|
||||||
if not is_body_allowed_for_status_code(response.status_code):
|
if not is_body_allowed_for_status_code(response.status_code):
|
||||||
|
|
@ -421,7 +467,7 @@ def get_request_handler(
|
||||||
response.headers.raw.extend(solved_result.response.headers.raw)
|
response.headers.raw.extend(solved_result.response.headers.raw)
|
||||||
if errors:
|
if errors:
|
||||||
validation_error = RequestValidationError(
|
validation_error = RequestValidationError(
|
||||||
_normalize_errors(errors), body=body
|
_normalize_errors(errors), body=body, endpoint_ctx=endpoint_ctx
|
||||||
)
|
)
|
||||||
raise validation_error
|
raise validation_error
|
||||||
|
|
||||||
|
|
@ -438,6 +484,15 @@ def get_websocket_app(
|
||||||
embed_body_fields: bool = False,
|
embed_body_fields: bool = False,
|
||||||
) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:
|
) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:
|
||||||
async def app(websocket: WebSocket) -> None:
|
async def app(websocket: WebSocket) -> None:
|
||||||
|
endpoint_ctx = (
|
||||||
|
_extract_endpoint_context(dependant.call)
|
||||||
|
if dependant.call
|
||||||
|
else EndpointContext()
|
||||||
|
)
|
||||||
|
if dependant.path:
|
||||||
|
# For mounted sub-apps, include the mount path prefix
|
||||||
|
mount_path = websocket.scope.get("root_path", "").rstrip("/")
|
||||||
|
endpoint_ctx["path"] = f"WS {mount_path}{dependant.path}"
|
||||||
async_exit_stack = websocket.scope.get("fastapi_inner_astack")
|
async_exit_stack = websocket.scope.get("fastapi_inner_astack")
|
||||||
assert isinstance(async_exit_stack, AsyncExitStack), (
|
assert isinstance(async_exit_stack, AsyncExitStack), (
|
||||||
"fastapi_inner_astack not found in request scope"
|
"fastapi_inner_astack not found in request scope"
|
||||||
|
|
@ -451,7 +506,8 @@ def get_websocket_app(
|
||||||
)
|
)
|
||||||
if solved_result.errors:
|
if solved_result.errors:
|
||||||
raise WebSocketRequestValidationError(
|
raise WebSocketRequestValidationError(
|
||||||
_normalize_errors(solved_result.errors)
|
_normalize_errors(solved_result.errors),
|
||||||
|
endpoint_ctx=endpoint_ctx,
|
||||||
)
|
)
|
||||||
assert dependant.call is not None, "dependant.call must be a function"
|
assert dependant.call is not None, "dependant.call must be a function"
|
||||||
await dependant.call(**solved_result.values)
|
await dependant.call(**solved_result.values)
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ def on_pre_page(page: Page, *, config: MkDocsConfig, files: Files) -> Page:
|
||||||
def on_page_markdown(
|
def on_page_markdown(
|
||||||
markdown: str, *, page: Page, config: MkDocsConfig, files: Files
|
markdown: str, *, page: Page, config: MkDocsConfig, files: Files
|
||||||
) -> str:
|
) -> str:
|
||||||
# Set matadata["social"]["cards_layout_options"]["title"] to clean title (without
|
# Set metadata["social"]["cards_layout_options"]["title"] to clean title (without
|
||||||
# permalink)
|
# permalink)
|
||||||
title = page.title
|
title = page.title
|
||||||
clean_title = title.split("{ #")[0]
|
clean_title = title.split("{ #")[0]
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,34 @@ async_callable_gen_dependency = AsyncCallableGenDependency()
|
||||||
methods_dependency = MethodsDependency()
|
methods_dependency = MethodsDependency()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/callable-dependency-class")
|
||||||
|
async def get_callable_dependency_class(
|
||||||
|
value: str, instance: CallableDependency = Depends()
|
||||||
|
):
|
||||||
|
return instance(value)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/callable-gen-dependency-class")
|
||||||
|
async def get_callable_gen_dependency_class(
|
||||||
|
value: str, instance: CallableGenDependency = Depends()
|
||||||
|
):
|
||||||
|
return next(instance(value))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/async-callable-dependency-class")
|
||||||
|
async def get_async_callable_dependency_class(
|
||||||
|
value: str, instance: AsyncCallableDependency = Depends()
|
||||||
|
):
|
||||||
|
return await instance(value)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/async-callable-gen-dependency-class")
|
||||||
|
async def get_async_callable_gen_dependency_class(
|
||||||
|
value: str, instance: AsyncCallableGenDependency = Depends()
|
||||||
|
):
|
||||||
|
return await instance(value).__anext__()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/callable-dependency")
|
@app.get("/callable-dependency")
|
||||||
async def get_callable_dependency(value: str = Depends(callable_dependency)):
|
async def get_callable_dependency(value: str = Depends(callable_dependency)):
|
||||||
return value
|
return value
|
||||||
|
|
@ -114,6 +142,10 @@ client = TestClient(app)
|
||||||
("/synchronous-method-gen-dependency", "synchronous-method-gen-dependency"),
|
("/synchronous-method-gen-dependency", "synchronous-method-gen-dependency"),
|
||||||
("/asynchronous-method-dependency", "asynchronous-method-dependency"),
|
("/asynchronous-method-dependency", "asynchronous-method-dependency"),
|
||||||
("/asynchronous-method-gen-dependency", "asynchronous-method-gen-dependency"),
|
("/asynchronous-method-gen-dependency", "asynchronous-method-gen-dependency"),
|
||||||
|
("/callable-dependency-class", "callable-dependency-class"),
|
||||||
|
("/callable-gen-dependency-class", "callable-gen-dependency-class"),
|
||||||
|
("/async-callable-dependency-class", "async-callable-dependency-class"),
|
||||||
|
("/async-callable-gen-dependency-class", "async-callable-gen-dependency-class"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_class_dependency(route, value):
|
def test_class_dependency(route, value):
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,18 @@ class Item(BaseModel):
|
||||||
model_config = {"json_schema_serialization_defaults_required": True}
|
model_config = {"json_schema_serialization_defaults_required": True}
|
||||||
|
|
||||||
|
|
||||||
|
if PYDANTIC_V2:
|
||||||
|
from pydantic import computed_field
|
||||||
|
|
||||||
|
class WithComputedField(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def computed_field(self) -> str:
|
||||||
|
return f"computed {self.name}"
|
||||||
|
|
||||||
|
|
||||||
def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
|
def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
|
||||||
app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)
|
app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)
|
||||||
|
|
||||||
|
|
@ -46,6 +58,14 @@ def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
|
||||||
Item(name="Plumbus"),
|
Item(name="Plumbus"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if PYDANTIC_V2:
|
||||||
|
|
||||||
|
@app.post("/with-computed-field/")
|
||||||
|
def create_with_computed_field(
|
||||||
|
with_computed_field: WithComputedField,
|
||||||
|
) -> WithComputedField:
|
||||||
|
return with_computed_field
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
@ -131,6 +151,23 @@ def test_read_items():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@needs_pydanticv2
|
||||||
|
def test_with_computed_field():
|
||||||
|
client = get_app_client()
|
||||||
|
client_no = get_app_client(separate_input_output_schemas=False)
|
||||||
|
response = client.post("/with-computed-field/", json={"name": "example"})
|
||||||
|
response2 = client_no.post("/with-computed-field/", json={"name": "example"})
|
||||||
|
assert response.status_code == response2.status_code == 200, response.text
|
||||||
|
assert (
|
||||||
|
response.json()
|
||||||
|
== response2.json()
|
||||||
|
== {
|
||||||
|
"name": "example",
|
||||||
|
"computed_field": "computed example",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@needs_pydanticv2
|
@needs_pydanticv2
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
client = get_app_client()
|
client = get_app_client()
|
||||||
|
|
@ -245,6 +282,44 @@ def test_openapi_schema():
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/with-computed-field/": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Create With Computed Field",
|
||||||
|
"operationId": "create_with_computed_field_with_computed_field__post",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WithComputedField-Input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WithComputedField-Output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
|
@ -333,6 +408,25 @@ def test_openapi_schema():
|
||||||
"required": ["subname", "sub_description", "tags"],
|
"required": ["subname", "sub_description", "tags"],
|
||||||
"title": "SubItem",
|
"title": "SubItem",
|
||||||
},
|
},
|
||||||
|
"WithComputedField-Input": {
|
||||||
|
"properties": {"name": {"type": "string", "title": "Name"}},
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
|
"title": "WithComputedField",
|
||||||
|
},
|
||||||
|
"WithComputedField-Output": {
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string", "title": "Name"},
|
||||||
|
"computed_field": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Computed Field",
|
||||||
|
"readOnly": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "computed_field"],
|
||||||
|
"title": "WithComputedField",
|
||||||
|
},
|
||||||
"ValidationError": {
|
"ValidationError": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"loc": {
|
"loc": {
|
||||||
|
|
@ -458,6 +552,44 @@ def test_openapi_schema_no_separate():
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/with-computed-field/": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Create With Computed Field",
|
||||||
|
"operationId": "create_with_computed_field_with_computed_field__post",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WithComputedField-Input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WithComputedField-Output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
|
@ -508,6 +640,25 @@ def test_openapi_schema_no_separate():
|
||||||
"required": ["subname"],
|
"required": ["subname"],
|
||||||
"title": "SubItem",
|
"title": "SubItem",
|
||||||
},
|
},
|
||||||
|
"WithComputedField-Input": {
|
||||||
|
"properties": {"name": {"type": "string", "title": "Name"}},
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
|
"title": "WithComputedField",
|
||||||
|
},
|
||||||
|
"WithComputedField-Output": {
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string", "title": "Name"},
|
||||||
|
"computed_field": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Computed Field",
|
||||||
|
"readOnly": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "computed_field"],
|
||||||
|
"title": "WithComputedField",
|
||||||
|
},
|
||||||
"ValidationError": {
|
"ValidationError": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"loc": {
|
"loc": {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from dirty_equals import IsUUID
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from inline_snapshot import snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Item:
|
||||||
|
id: uuid.UUID
|
||||||
|
name: str
|
||||||
|
price: float
|
||||||
|
tags: List[str] = field(default_factory=list)
|
||||||
|
description: Union[str, None] = None
|
||||||
|
tax: Union[float, None] = None
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/item", response_model=Item)
|
||||||
|
async def read_item():
|
||||||
|
return {
|
||||||
|
"id": uuid.uuid4(),
|
||||||
|
"name": "Island In The Moon",
|
||||||
|
"price": 12.99,
|
||||||
|
"description": "A place to be be playin' and havin' fun",
|
||||||
|
"tags": ["breater"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotations():
|
||||||
|
response = client.get("/item")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == snapshot(
|
||||||
|
{
|
||||||
|
"id": IsUUID(),
|
||||||
|
"name": "Island In The Moon",
|
||||||
|
"price": 12.99,
|
||||||
|
"tags": ["breater"],
|
||||||
|
"description": "A place to be be playin' and havin' fun",
|
||||||
|
"tax": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
from fastapi import FastAPI, Request, WebSocket
|
||||||
|
from fastapi.exceptions import (
|
||||||
|
RequestValidationError,
|
||||||
|
ResponseValidationError,
|
||||||
|
WebSocketRequestValidationError,
|
||||||
|
)
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Item(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionCapture:
|
||||||
|
def __init__(self):
|
||||||
|
self.exception = None
|
||||||
|
|
||||||
|
def capture(self, exc):
|
||||||
|
self.exception = exc
|
||||||
|
return exc
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
sub_app = FastAPI()
|
||||||
|
captured_exception = ExceptionCapture()
|
||||||
|
|
||||||
|
app.mount(path="/sub", app=sub_app)
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
@sub_app.exception_handler(RequestValidationError)
|
||||||
|
async def request_validation_handler(request: Request, exc: RequestValidationError):
|
||||||
|
captured_exception.capture(exc)
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(ResponseValidationError)
|
||||||
|
@sub_app.exception_handler(ResponseValidationError)
|
||||||
|
async def response_validation_handler(_: Request, exc: ResponseValidationError):
|
||||||
|
captured_exception.capture(exc)
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(WebSocketRequestValidationError)
|
||||||
|
@sub_app.exception_handler(WebSocketRequestValidationError)
|
||||||
|
async def websocket_validation_handler(
|
||||||
|
websocket: WebSocket, exc: WebSocketRequestValidationError
|
||||||
|
):
|
||||||
|
captured_exception.capture(exc)
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/users/{user_id}")
|
||||||
|
def get_user(user_id: int):
|
||||||
|
return {"user_id": user_id} # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/", response_model=Item)
|
||||||
|
def get_item():
|
||||||
|
return {"name": "Widget"}
|
||||||
|
|
||||||
|
|
||||||
|
@sub_app.get("/items/", response_model=Item)
|
||||||
|
def get_sub_item():
|
||||||
|
return {"name": "Widget"} # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws/{item_id}")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket, item_id: int):
|
||||||
|
await websocket.accept() # pragma: no cover
|
||||||
|
await websocket.send_text(f"Item: {item_id}") # pragma: no cover
|
||||||
|
await websocket.close() # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
@sub_app.websocket("/ws/{item_id}")
|
||||||
|
async def subapp_websocket_endpoint(websocket: WebSocket, item_id: int):
|
||||||
|
await websocket.accept() # pragma: no cover
|
||||||
|
await websocket.send_text(f"Item: {item_id}") # pragma: no cover
|
||||||
|
await websocket.close() # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_validation_error_includes_endpoint_context():
|
||||||
|
captured_exception.exception = None
|
||||||
|
try:
|
||||||
|
client.get("/users/invalid")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert captured_exception.exception is not None
|
||||||
|
error_str = str(captured_exception.exception)
|
||||||
|
assert "get_user" in error_str
|
||||||
|
assert "/users/" in error_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_response_validation_error_includes_endpoint_context():
|
||||||
|
captured_exception.exception = None
|
||||||
|
try:
|
||||||
|
client.get("/items/")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert captured_exception.exception is not None
|
||||||
|
error_str = str(captured_exception.exception)
|
||||||
|
assert "get_item" in error_str
|
||||||
|
assert "/items/" in error_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_websocket_validation_error_includes_endpoint_context():
|
||||||
|
captured_exception.exception = None
|
||||||
|
try:
|
||||||
|
with client.websocket_connect("/ws/invalid"):
|
||||||
|
pass # pragma: no cover
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert captured_exception.exception is not None
|
||||||
|
error_str = str(captured_exception.exception)
|
||||||
|
assert "websocket_endpoint" in error_str
|
||||||
|
assert "/ws/" in error_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_subapp_request_validation_error_includes_endpoint_context():
|
||||||
|
captured_exception.exception = None
|
||||||
|
try:
|
||||||
|
client.get("/sub/items/")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert captured_exception.exception is not None
|
||||||
|
error_str = str(captured_exception.exception)
|
||||||
|
assert "get_sub_item" in error_str
|
||||||
|
assert "/sub/items/" in error_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_subapp_websocket_validation_error_includes_endpoint_context():
|
||||||
|
captured_exception.exception = None
|
||||||
|
try:
|
||||||
|
with client.websocket_connect("/sub/ws/invalid"):
|
||||||
|
pass # pragma: no cover
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert captured_exception.exception is not None
|
||||||
|
error_str = str(captured_exception.exception)
|
||||||
|
assert "subapp_websocket_endpoint" in error_str
|
||||||
|
assert "/sub/ws/" in error_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_error_with_only_path():
|
||||||
|
errors = [{"type": "missing", "loc": ("body", "name"), "msg": "Field required"}]
|
||||||
|
exc = RequestValidationError(errors, endpoint_ctx={"path": "GET /api/test"})
|
||||||
|
error_str = str(exc)
|
||||||
|
assert "Endpoint: GET /api/test" in error_str
|
||||||
|
assert 'File "' not in error_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_error_with_no_context():
|
||||||
|
errors = [{"type": "missing", "loc": ("body", "name"), "msg": "Field required"}]
|
||||||
|
exc = RequestValidationError(errors, endpoint_ctx={})
|
||||||
|
error_str = str(exc)
|
||||||
|
assert "1 validation error:" in error_str
|
||||||
|
assert "Endpoint" not in error_str
|
||||||
|
assert 'File "' not in error_str
|
||||||
Loading…
Reference in New Issue