mirror of
https://github.com/fastapi/fastapi.git
synced 2026-02-24 10:46:42 -05:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b423b73c35 | ||
|
|
70e8558352 | ||
|
|
48e9835732 | ||
|
|
2e62fb1513 | ||
|
|
eb544e704c | ||
|
|
bc06e4296d | ||
|
|
590a5e5355 | ||
|
|
1e78a36b73 | ||
|
|
f921de6495 | ||
|
|
4ab8138554 | ||
|
|
468d5173ed | ||
|
|
c9455d5400 | ||
|
|
69ae1d0f28 | ||
|
|
083b6ebe9e | ||
|
|
1b9a351ee8 | ||
|
|
f55ab7e020 |
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@@ -29,8 +29,6 @@ internal:
|
||||
- scripts/**
|
||||
- .gitignore
|
||||
- .pre-commit-config.yaml
|
||||
- pdm_build.py
|
||||
- requirements*.txt
|
||||
- uv.lock
|
||||
- docs/en/data/sponsors.yml
|
||||
- docs/en/overrides/main.html
|
||||
|
||||
10
.github/workflows/publish.yml
vendored
10
.github/workflows/publish.yml
vendored
@@ -8,11 +8,6 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package:
|
||||
- fastapi
|
||||
- fastapi-slim
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
@@ -26,14 +21,9 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
# Issue ref: https://github.com/actions/setup-python/issues/436
|
||||
# cache: "pip"
|
||||
# cache-dependency-path: pyproject.toml
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Build distribution
|
||||
run: uv build
|
||||
env:
|
||||
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }}
|
||||
- name: Publish
|
||||
run: uv publish
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Custom Response - HTML, Stream, File, others { #custom-response-html-stream-file-others }
|
||||
|
||||
By default, **FastAPI** will return the responses using `JSONResponse`.
|
||||
By default, **FastAPI** will return JSON responses.
|
||||
|
||||
You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}.
|
||||
|
||||
@@ -10,43 +10,27 @@ But you can also declare the `Response` that you want to be used (e.g. any `Resp
|
||||
|
||||
The contents that you return from your *path operation function* will be put inside of that `Response`.
|
||||
|
||||
And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*.
|
||||
|
||||
/// note
|
||||
|
||||
If you use a response class with no media type, FastAPI will expect your response to have no content, so it will not document the response format in its generated OpenAPI docs.
|
||||
|
||||
///
|
||||
|
||||
## Use `ORJSONResponse` { #use-orjsonresponse }
|
||||
## JSON Responses { #json-responses }
|
||||
|
||||
For example, if you are squeezing performance, you can install and use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> and set the response to be `ORJSONResponse`.
|
||||
By default FastAPI returns JSON responses.
|
||||
|
||||
Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*.
|
||||
If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic.
|
||||
|
||||
For large responses, returning a `Response` directly is much faster than returning a dictionary.
|
||||
If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`.
|
||||
|
||||
This is because by default, FastAPI will inspect every item inside and make sure it is serializable as JSON, using the same [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} explained in the tutorial. This is what allows you to return **arbitrary objects**, for example database models.
|
||||
If you declare a `response_class` with a JSON media type (`application/json`), like is the case with the `JSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*. But the data won't be serialized to JSON bytes with Pydantic, instead it will be converted with the `jsonable_encoder` and then passed to the `JSONResponse` class, which will serialize it to bytes using the standard JSON library in Python.
|
||||
|
||||
But if you are certain that the content that you are returning is **serializable with JSON**, you can pass it directly to the response class and avoid the extra overhead that FastAPI would have by passing your return content through the `jsonable_encoder` before passing it to the response class.
|
||||
### JSON Performance { #json-performance }
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial001b_py310.py hl[2,7] *}
|
||||
In short, if you want the maximum performance, use a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} and don't declare a `response_class` in the *path operation decorator*.
|
||||
|
||||
/// info
|
||||
|
||||
The parameter `response_class` will also be used to define the "media type" of the response.
|
||||
|
||||
In this case, the HTTP header `Content-Type` will be set to `application/json`.
|
||||
|
||||
And it will be documented as such in OpenAPI.
|
||||
|
||||
///
|
||||
|
||||
/// tip
|
||||
|
||||
The `ORJSONResponse` is only available in FastAPI, not in Starlette.
|
||||
|
||||
///
|
||||
{* ../../docs_src/response_model/tutorial001_01_py310.py ln[15:17] hl[16] *}
|
||||
|
||||
## HTML Response { #html-response }
|
||||
|
||||
@@ -154,40 +138,6 @@ Takes some data and returns an `application/json` encoded response.
|
||||
|
||||
This is the default response used in **FastAPI**, as you read above.
|
||||
|
||||
### `ORJSONResponse` { #orjsonresponse }
|
||||
|
||||
A fast alternative JSON response using <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, as you read above.
|
||||
|
||||
/// info
|
||||
|
||||
This requires installing `orjson` for example with `pip install orjson`.
|
||||
|
||||
///
|
||||
|
||||
### `UJSONResponse` { #ujsonresponse }
|
||||
|
||||
An alternative JSON response using <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a>.
|
||||
|
||||
/// info
|
||||
|
||||
This requires installing `ujson` for example with `pip install ujson`.
|
||||
|
||||
///
|
||||
|
||||
/// warning
|
||||
|
||||
`ujson` is less careful than Python's built-in implementation in how it handles some edge-cases.
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial001_py310.py hl[2,7] *}
|
||||
|
||||
/// tip
|
||||
|
||||
It's possible that `ORJSONResponse` might be a faster alternative.
|
||||
|
||||
///
|
||||
|
||||
### `RedirectResponse` { #redirectresponse }
|
||||
|
||||
Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default.
|
||||
@@ -268,7 +218,7 @@ In this case, you can return the file path directly from your *path operation* f
|
||||
|
||||
You can create your own custom response class, inheriting from `Response` and using it.
|
||||
|
||||
For example, let's say that you want to use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, but with some custom settings not used in the included `ORJSONResponse` class.
|
||||
For example, let's say that you want to use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> with some settings.
|
||||
|
||||
Let's say you want it to return indented and formatted JSON, so you want to use the orjson option `orjson.OPT_INDENT_2`.
|
||||
|
||||
@@ -292,13 +242,21 @@ Now instead of returning:
|
||||
|
||||
Of course, you will probably find much better ways to take advantage of this than formatting JSON. 😉
|
||||
|
||||
### `orjson` or Response Model { #orjson-or-response-model }
|
||||
|
||||
If what you are looking for is performance, you are probably better off using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than an `orjson` response.
|
||||
|
||||
With a response model, FastAPI will use Pydantic to serialize the data to JSON, without using intermediate steps, like converting it with `jsonable_encoder`, which would happen in any other case.
|
||||
|
||||
And under the hood, Pydantic uses the same underlying Rust mechanisms as `orjson` to serialize to JSON, so you will already get the best performance with a response model.
|
||||
|
||||
## Default response class { #default-response-class }
|
||||
|
||||
When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default.
|
||||
|
||||
The parameter that defines this is `default_response_class`.
|
||||
|
||||
In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`.
|
||||
In the example below, **FastAPI** will use `HTMLResponse` by default, in all *path operations*, instead of JSON.
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial010_py310.py hl[2,4] *}
|
||||
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
|
||||
When you create a **FastAPI** *path operation* you can normally return any data from it: a `dict`, a `list`, a Pydantic model, a database model, etc.
|
||||
|
||||
By default, **FastAPI** would automatically convert that return value to JSON using the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}.
|
||||
If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic.
|
||||
|
||||
Then, behind the scenes, it would put that JSON-compatible data (e.g. a `dict`) inside of a `JSONResponse` that would be used to send the response to the client.
|
||||
If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`.
|
||||
|
||||
But you can return a `JSONResponse` directly from your *path operations*.
|
||||
You could also create a `JSONResponse` directly and return it.
|
||||
|
||||
It might be useful, for example, to return custom headers or cookies.
|
||||
/// tip
|
||||
|
||||
You will normally have much better performance using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than returning a `JSONResponse` directly, as that way it serializes the data using Pydantic, in Rust.
|
||||
|
||||
///
|
||||
|
||||
## Return a `Response` { #return-a-response }
|
||||
|
||||
In fact, you can return any `Response` or any sub-class of it.
|
||||
You can return any `Response` or any sub-class of it.
|
||||
|
||||
/// tip
|
||||
/// info
|
||||
|
||||
`JSONResponse` itself is a sub-class of `Response`.
|
||||
|
||||
@@ -56,6 +60,18 @@ You could put your XML content in a string, put that in a `Response`, and return
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial002_py310.py hl[1,18] *}
|
||||
|
||||
## How a Response Model Works { #how-a-response-model-works }
|
||||
|
||||
When you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} in a path operation, **FastAPI** will use it to serialize the data to JSON, using Pydantic.
|
||||
|
||||
{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *}
|
||||
|
||||
As that will happen on the Rust side, the performance will be much better than if it was done with regular Python and the `JSONResponse` class.
|
||||
|
||||
When using a response model FastAPI won't use the `jsonable_encoder` to convert the data (which would be slower) nor the `JSONResponse` class.
|
||||
|
||||
Instead it takes the JSON bytes generated with Pydantic using the response model and returns a `Response` with the right media type for JSON directly (`application/json`).
|
||||
|
||||
## Notes { #notes }
|
||||
|
||||
When you return a `Response` directly its data is not validated, converted (serialized), or documented automatically.
|
||||
|
||||
@@ -6,6 +6,10 @@ Here are several pointers to other places in the docs, for general or frequent q
|
||||
|
||||
To ensure that you don't return more data than you should, read the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}.
|
||||
|
||||
## Optimize Response Performance - Response Model - Return Type { #optimize-response-performance-response-model-return-type }
|
||||
|
||||
To optimize performance when returning JSON data, use a return type or response model, that way Pydantic will handle the serialization to JSON on the Rust side, without going through Python. Read more in the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}.
|
||||
|
||||
## Documentation Tags - OpenAPI { #documentation-tags-openapi }
|
||||
|
||||
To add tags to your *path operations*, and group them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}.
|
||||
|
||||
@@ -22,7 +22,13 @@ from fastapi.responses import (
|
||||
|
||||
## FastAPI Responses
|
||||
|
||||
There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance.
|
||||
There were a couple of custom FastAPI response classes that were intended to optimize JSON performance.
|
||||
|
||||
However, they are now deprecated as you will now get better performance by using a [Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
That way, Pydantic will serialize the data into JSON bytes on the Rust side, which will achieve better performance than these custom JSON responses.
|
||||
|
||||
Read more about it in [Custom Response - HTML, Stream, File, others - `orjson` or Response Model](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model).
|
||||
|
||||
::: fastapi.responses.UJSONResponse
|
||||
options:
|
||||
|
||||
@@ -7,6 +7,29 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.131.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* 🗑️ Deprecate `ORJSONResponse` and `UJSONResponse`. PR [#14964](https://github.com/fastapi/fastapi/pull/14964) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.130.0
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Serialize JSON response with Pydantic (in Rust), when there's a Pydantic return type or response model. PR [#14962](https://github.com/fastapi/fastapi/pull/14962) by [@tiangolo](https://github.com/tiangolo).
|
||||
* This results in 2x (or more) performance increase for JSON responses.
|
||||
* New docs: [Custom Response - JSON Performance](https://fastapi.tiangolo.com/advanced/custom-response/#json-performance).
|
||||
|
||||
## 0.129.2
|
||||
|
||||
### Internal
|
||||
|
||||
* ⬆️ Upgrade pytest. PR [#14959](https://github.com/fastapi/fastapi/pull/14959) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Fix CI, do not attempt to publish `fastapi-slim`. PR [#14958](https://github.com/fastapi/fastapi/pull/14958) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ➖ Drop support for `fastapi-slim`, no more versions will be released, use only `"fastapi[standard]"` or `fastapi`. PR [#14957](https://github.com/fastapi/fastapi/pull/14957) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🔧 Update pyproject.toml, remove unneeded lines. PR [#14956](https://github.com/fastapi/fastapi/pull/14956) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.129.1
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -13,6 +13,7 @@ FastAPI will use this return type to:
|
||||
* Add a **JSON Schema** for the response, in the OpenAPI *path operation*.
|
||||
* This will be used by the **automatic docs**.
|
||||
* It will also be used by automatic client code generation tools.
|
||||
* **Serialize** the returned data to JSON using Pydantic, which is written in **Rust**, so it will be **much faster**.
|
||||
|
||||
But most importantly:
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
app = FastAPI(default_response_class=HTMLResponse)
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items():
|
||||
return [{"item_id": "Foo"}]
|
||||
return "<h1>Items</h1><p>This is a list of items.</p>"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
|
||||
return item_a, item_b, item_c, item_d, item_e
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.129.1"
|
||||
__version__ = "0.131.0"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -199,6 +199,32 @@ class ModelField:
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
def serialize_json(
|
||||
self,
|
||||
value: Any,
|
||||
*,
|
||||
include: IncEx | None = None,
|
||||
exclude: IncEx | None = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
) -> bytes:
|
||||
# What calls this code passes a value that already called
|
||||
# self._type_adapter.validate_python(value)
|
||||
# This uses Pydantic's dump_json() which serializes directly to JSON
|
||||
# bytes in one pass (via Rust), avoiding the intermediate Python dict
|
||||
# step of dump_python(mode="json") + json.dumps().
|
||||
return self._type_adapter.dump_json(
|
||||
value,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Each ModelField is unique for our purposes, to allow making a dict from
|
||||
# ModelField to its JSON Schema.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from starlette.responses import FileResponse as FileResponse # noqa
|
||||
from starlette.responses import HTMLResponse as HTMLResponse # noqa
|
||||
from starlette.responses import JSONResponse as JSONResponse # noqa
|
||||
@@ -7,6 +8,7 @@ from starlette.responses import PlainTextResponse as PlainTextResponse # noqa
|
||||
from starlette.responses import RedirectResponse as RedirectResponse # noqa
|
||||
from starlette.responses import Response as Response # noqa
|
||||
from starlette.responses import StreamingResponse as StreamingResponse # noqa
|
||||
from typing_extensions import deprecated
|
||||
|
||||
try:
|
||||
import ujson
|
||||
@@ -20,12 +22,29 @@ except ImportError: # pragma: nocover
|
||||
orjson = None # type: ignore
|
||||
|
||||
|
||||
@deprecated(
|
||||
"UJSONResponse is deprecated, FastAPI now serializes data directly to JSON "
|
||||
"bytes via Pydantic when a return type or response model is set, which is "
|
||||
"faster and doesn't need a custom response class. Read more in the FastAPI "
|
||||
"docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model "
|
||||
"and https://fastapi.tiangolo.com/tutorial/response-model/",
|
||||
category=FastAPIDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
class UJSONResponse(JSONResponse):
|
||||
"""
|
||||
JSON response using the high-performance ujson library to serialize data to JSON.
|
||||
"""JSON response using the ujson library to serialize data to JSON.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
**Deprecated**: `UJSONResponse` is deprecated. FastAPI now serializes data
|
||||
directly to JSON bytes via Pydantic when a return type or response model is
|
||||
set, which is faster and doesn't need a custom response class.
|
||||
|
||||
Read more in the
|
||||
[FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model)
|
||||
and the
|
||||
[FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
**Note**: `ujson` is not included with FastAPI and must be installed
|
||||
separately, e.g. `pip install ujson`.
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
@@ -33,12 +52,29 @@ class UJSONResponse(JSONResponse):
|
||||
return ujson.dumps(content, ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
@deprecated(
|
||||
"ORJSONResponse is deprecated, FastAPI now serializes data directly to JSON "
|
||||
"bytes via Pydantic when a return type or response model is set, which is "
|
||||
"faster and doesn't need a custom response class. Read more in the FastAPI "
|
||||
"docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model "
|
||||
"and https://fastapi.tiangolo.com/tutorial/response-model/",
|
||||
category=FastAPIDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
class ORJSONResponse(JSONResponse):
|
||||
"""
|
||||
JSON response using the high-performance orjson library to serialize data to JSON.
|
||||
"""JSON response using the orjson library to serialize data to JSON.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
**Deprecated**: `ORJSONResponse` is deprecated. FastAPI now serializes data
|
||||
directly to JSON bytes via Pydantic when a return type or response model is
|
||||
set, which is faster and doesn't need a custom response class.
|
||||
|
||||
Read more in the
|
||||
[FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model)
|
||||
and the
|
||||
[FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).
|
||||
|
||||
**Note**: `orjson` is not included with FastAPI and must be installed
|
||||
separately, e.g. `pip install orjson`.
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
|
||||
@@ -271,6 +271,7 @@ async def serialize_response(
|
||||
exclude_none: bool = False,
|
||||
is_coroutine: bool = True,
|
||||
endpoint_ctx: EndpointContext | None = None,
|
||||
dump_json: bool = False,
|
||||
) -> Any:
|
||||
if field:
|
||||
if is_coroutine:
|
||||
@@ -286,8 +287,8 @@ async def serialize_response(
|
||||
body=response_content,
|
||||
endpoint_ctx=ctx,
|
||||
)
|
||||
|
||||
return field.serialize(
|
||||
serializer = field.serialize_json if dump_json else field.serialize
|
||||
return serializer(
|
||||
value,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
@@ -443,6 +444,14 @@ def get_request_handler(
|
||||
response_args["status_code"] = current_status_code
|
||||
if solved_result.response.status_code:
|
||||
response_args["status_code"] = solved_result.response.status_code
|
||||
# Use the fast path (dump_json) when no custom response
|
||||
# class was set and a response field with a TypeAdapter
|
||||
# exists. Serializes directly to JSON bytes via Pydantic's
|
||||
# Rust core, skipping the intermediate Python dict +
|
||||
# json.dumps() step.
|
||||
use_dump_json = response_field is not None and isinstance(
|
||||
response_class, DefaultPlaceholder
|
||||
)
|
||||
content = await serialize_response(
|
||||
field=response_field,
|
||||
response_content=raw_response,
|
||||
@@ -454,8 +463,16 @@ def get_request_handler(
|
||||
exclude_none=response_model_exclude_none,
|
||||
is_coroutine=is_coroutine,
|
||||
endpoint_ctx=endpoint_ctx,
|
||||
dump_json=use_dump_json,
|
||||
)
|
||||
response = actual_response_class(content, **response_args)
|
||||
if use_dump_json:
|
||||
response = Response(
|
||||
content=content,
|
||||
media_type="application/json",
|
||||
**response_args,
|
||||
)
|
||||
else:
|
||||
response = actual_response_class(content, **response_args)
|
||||
if not is_body_allowed_for_status_code(response.status_code):
|
||||
response.body = b""
|
||||
response.headers.raw.extend(solved_result.response.headers.raw)
|
||||
|
||||
40
pdm_build.py
40
pdm_build.py
@@ -1,40 +0,0 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from pdm.backend.hooks import Context
|
||||
|
||||
TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE")
|
||||
|
||||
|
||||
def pdm_build_initialize(context: Context) -> None:
|
||||
metadata = context.config.metadata
|
||||
# Get main version
|
||||
version = metadata["version"]
|
||||
# Get custom config for the current package, from the env var
|
||||
all_configs_config: dict[str, Any] = context.config.data["tool"]["tiangolo"][
|
||||
"_internal-slim-build"
|
||||
]["packages"]
|
||||
|
||||
if TIANGOLO_BUILD_PACKAGE not in all_configs_config:
|
||||
return
|
||||
|
||||
config = all_configs_config[TIANGOLO_BUILD_PACKAGE]
|
||||
project_config: dict[str, Any] = config["project"]
|
||||
# Override main [project] configs with custom configs for this package
|
||||
for key, value in project_config.items():
|
||||
metadata[key] = value
|
||||
# Get custom build config for the current package
|
||||
build_config: dict[str, Any] = (
|
||||
config.get("tool", {}).get("pdm", {}).get("build", {})
|
||||
)
|
||||
# Override PDM build config with custom build config for this package
|
||||
for key, value in build_config.items():
|
||||
context.config.build_config[key] = value
|
||||
# Get main dependencies
|
||||
dependencies: list[str] = metadata.get("dependencies", [])
|
||||
# Sync versions in dependencies
|
||||
new_dependencies = []
|
||||
for dep in dependencies:
|
||||
new_dep = f"{dep}>={version}"
|
||||
new_dependencies.append(new_dep)
|
||||
metadata["dependencies"] = new_dependencies
|
||||
249
pyproject.toml
249
pyproject.toml
@@ -57,7 +57,6 @@ Issues = "https://github.com/fastapi/fastapi/issues"
|
||||
Changelog = "https://fastapi.tiangolo.com/release-notes/"
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
standard = [
|
||||
"fastapi-cli[standard] >=0.0.8",
|
||||
# For the test client
|
||||
@@ -106,10 +105,6 @@ all = [
|
||||
"itsdangerous >=1.1.0",
|
||||
# For Starlette's schema generation, would not be used with FastAPI
|
||||
"pyyaml >=5.3.1",
|
||||
# For UJSONResponse
|
||||
"ujson >=5.8.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.9.3",
|
||||
# To validate email fields
|
||||
"email-validator >=2.0.0",
|
||||
# Uvicorn with uvloop
|
||||
@@ -152,6 +147,10 @@ docs = [
|
||||
docs-tests = [
|
||||
"httpx >=0.23.0,<1.0.0",
|
||||
"ruff >=0.14.14",
|
||||
# For UJSONResponse
|
||||
"ujson >=5.8.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.9.3",
|
||||
]
|
||||
github-actions = [
|
||||
"httpx >=0.27.0,<1.0.0",
|
||||
@@ -171,7 +170,7 @@ tests = [
|
||||
"mypy >=1.14.1",
|
||||
"pwdlib[argon2] >=0.2.1",
|
||||
"pyjwt >=2.9.0",
|
||||
"pytest >=7.1.3,<9.0.0",
|
||||
"pytest >=9.0.0",
|
||||
"pytest-codspeed >=4.2.0",
|
||||
"pyyaml >=5.3.1,<7.0.0",
|
||||
"sqlmodel >=0.0.31",
|
||||
@@ -199,32 +198,6 @@ source-includes = [
|
||||
"docs/en/docs/img/favicon.png",
|
||||
]
|
||||
|
||||
[tool.tiangolo._internal-slim-build.packages.fastapi-slim.project]
|
||||
name = "fastapi-slim"
|
||||
readme = "fastapi-slim/README.md"
|
||||
dependencies = [
|
||||
"fastapi",
|
||||
]
|
||||
optional-dependencies = {}
|
||||
scripts = {}
|
||||
|
||||
[tool.tiangolo._internal-slim-build.packages.fastapi-slim.tool.pdm.build]
|
||||
# excludes needs to explicitly exclude the top level python packages,
|
||||
# otherwise PDM includes them by default
|
||||
# A "*" glob pattern can't be used here because in PDM internals, the patterns are put
|
||||
# in a set (unordered, order varies) and each excluded file is assigned one of the
|
||||
# glob patterns that matches, as the set is unordered, the matched pattern could be "*"
|
||||
# independent of the order here. And then the internal code would give it a lower score
|
||||
# than the one for a default included file.
|
||||
# By not using "*" and explicitly excluding the top level packages, they get a higher
|
||||
# score than the default inclusion
|
||||
excludes = ["fastapi", "tests", "pdm_build.py"]
|
||||
# source-includes needs to explicitly define some value because PDM will check the
|
||||
# truthy value of the list, and if empty, will include some defaults, including "tests",
|
||||
# an empty string doesn't match anything, but makes the list truthy, so that PDM
|
||||
# doesn't override it during the build.
|
||||
source-includes = [""]
|
||||
|
||||
[tool.mypy]
|
||||
plugins = ["pydantic.mypy"]
|
||||
strict = true
|
||||
@@ -245,25 +218,16 @@ disallow_incomplete_defs = false
|
||||
disallow_untyped_defs = false
|
||||
disallow_untyped_calls = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
[tool.pytest]
|
||||
minversion = "9.0"
|
||||
addopts = [
|
||||
"--strict-config",
|
||||
"--strict-markers",
|
||||
"--ignore=docs_src",
|
||||
]
|
||||
xfail_strict = true
|
||||
junit_family = "xunit2"
|
||||
strict_xfail = true
|
||||
filterwarnings = [
|
||||
"error",
|
||||
# see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28
|
||||
"ignore:You seem to already have a custom.*:RuntimeWarning:trio",
|
||||
# TODO: remove after upgrading SQLAlchemy to a version that includes the following changes
|
||||
# https://github.com/sqlalchemy/sqlalchemy/commit/59521abcc0676e936b31a523bd968fc157fef0c2
|
||||
'ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:sqlalchemy',
|
||||
# Trio 24.1.0 raises a warning from attrs
|
||||
# Ref: https://github.com/python-trio/trio/pull/3054
|
||||
# Remove once there's a new version of Trio
|
||||
'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio',
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
@@ -280,7 +244,6 @@ dynamic_context = "test_function"
|
||||
omit = [
|
||||
"docs_src/response_model/tutorial003_04_py39.py",
|
||||
"docs_src/response_model/tutorial003_04_py310.py",
|
||||
"docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock
|
||||
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?
|
||||
"docs_src/dependencies/tutorial014_an_py310.py", # temporary code example?
|
||||
# Pydantic v1 migration, no longer tested
|
||||
@@ -288,202 +251,6 @@ omit = [
|
||||
"docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py",
|
||||
"docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py",
|
||||
"docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py",
|
||||
# TODO: remove all the ignores below when all translations use the new Python 3.10 files
|
||||
"docs_src/additional_responses/tutorial001_py39.py",
|
||||
"docs_src/additional_responses/tutorial003_py39.py",
|
||||
"docs_src/advanced_middleware/tutorial001_py39.py",
|
||||
"docs_src/advanced_middleware/tutorial002_py39.py",
|
||||
"docs_src/advanced_middleware/tutorial003_py39.py",
|
||||
"docs_src/app_testing/app_a_py39/main.py",
|
||||
"docs_src/app_testing/app_a_py39/test_main.py",
|
||||
"docs_src/app_testing/tutorial001_py39.py",
|
||||
"docs_src/app_testing/tutorial002_py39.py",
|
||||
"docs_src/app_testing/tutorial003_py39.py",
|
||||
"docs_src/app_testing/tutorial004_py39.py",
|
||||
"docs_src/async_tests/app_a_py39/main.py",
|
||||
"docs_src/async_tests/app_a_py39/test_main.py",
|
||||
"docs_src/authentication_error_status_code/tutorial001_an_py39.py",
|
||||
"docs_src/background_tasks/tutorial001_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial001_01_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial001_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial002_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial003_py39.py",
|
||||
"docs_src/behind_a_proxy/tutorial004_py39.py",
|
||||
"docs_src/bigger_applications/app_an_py39/dependencies.py",
|
||||
"docs_src/bigger_applications/app_an_py39/internal/admin.py",
|
||||
"docs_src/bigger_applications/app_an_py39/main.py",
|
||||
"docs_src/bigger_applications/app_an_py39/routers/items.py",
|
||||
"docs_src/bigger_applications/app_an_py39/routers/users.py",
|
||||
"docs_src/bigger_applications/app_py39/dependencies.py",
|
||||
"docs_src/bigger_applications/app_py39/main.py",
|
||||
"docs_src/body_nested_models/tutorial008_py39.py",
|
||||
"docs_src/body_nested_models/tutorial009_py39.py",
|
||||
"docs_src/conditional_openapi/tutorial001_py39.py",
|
||||
"docs_src/configure_swagger_ui/tutorial001_py39.py",
|
||||
"docs_src/configure_swagger_ui/tutorial002_py39.py",
|
||||
"docs_src/configure_swagger_ui/tutorial003_py39.py",
|
||||
"docs_src/cors/tutorial001_py39.py",
|
||||
"docs_src/custom_docs_ui/tutorial001_py39.py",
|
||||
"docs_src/custom_docs_ui/tutorial002_py39.py",
|
||||
"docs_src/custom_response/tutorial001_py39.py",
|
||||
"docs_src/custom_response/tutorial001b_py39.py",
|
||||
"docs_src/custom_response/tutorial002_py39.py",
|
||||
"docs_src/custom_response/tutorial003_py39.py",
|
||||
"docs_src/custom_response/tutorial004_py39.py",
|
||||
"docs_src/custom_response/tutorial005_py39.py",
|
||||
"docs_src/custom_response/tutorial006_py39.py",
|
||||
"docs_src/custom_response/tutorial006b_py39.py",
|
||||
"docs_src/custom_response/tutorial006c_py39.py",
|
||||
"docs_src/custom_response/tutorial007_py39.py",
|
||||
"docs_src/custom_response/tutorial008_py39.py",
|
||||
"docs_src/custom_response/tutorial009_py39.py",
|
||||
"docs_src/custom_response/tutorial009b_py39.py",
|
||||
"docs_src/custom_response/tutorial009c_py39.py",
|
||||
"docs_src/custom_response/tutorial010_py39.py",
|
||||
"docs_src/debugging/tutorial001_py39.py",
|
||||
"docs_src/dependencies/tutorial006_an_py39.py",
|
||||
"docs_src/dependencies/tutorial006_py39.py",
|
||||
"docs_src/dependencies/tutorial007_py39.py",
|
||||
"docs_src/dependencies/tutorial008_py39.py",
|
||||
"docs_src/dependencies/tutorial008b_an_py39.py",
|
||||
"docs_src/dependencies/tutorial008b_py39.py",
|
||||
"docs_src/dependencies/tutorial008c_an_py39.py",
|
||||
"docs_src/dependencies/tutorial008c_py39.py",
|
||||
"docs_src/dependencies/tutorial008d_an_py39.py",
|
||||
"docs_src/dependencies/tutorial008d_py39.py",
|
||||
"docs_src/dependencies/tutorial008e_an_py39.py",
|
||||
"docs_src/dependencies/tutorial008e_py39.py",
|
||||
"docs_src/dependencies/tutorial010_py39.py",
|
||||
"docs_src/dependencies/tutorial011_an_py39.py",
|
||||
"docs_src/dependencies/tutorial011_py39.py",
|
||||
"docs_src/dependencies/tutorial012_an_py39.py",
|
||||
"docs_src/dependencies/tutorial012_py39.py",
|
||||
"docs_src/events/tutorial001_py39.py",
|
||||
"docs_src/events/tutorial002_py39.py",
|
||||
"docs_src/events/tutorial003_py39.py",
|
||||
"docs_src/extending_openapi/tutorial001_py39.py",
|
||||
"docs_src/extra_models/tutorial004_py39.py",
|
||||
"docs_src/extra_models/tutorial005_py39.py",
|
||||
"docs_src/first_steps/tutorial001_py39.py",
|
||||
"docs_src/first_steps/tutorial003_py39.py",
|
||||
"docs_src/generate_clients/tutorial001_py39.py",
|
||||
"docs_src/generate_clients/tutorial002_py39.py",
|
||||
"docs_src/generate_clients/tutorial003_py39.py",
|
||||
"docs_src/generate_clients/tutorial004_py39.py",
|
||||
"docs_src/graphql_/tutorial001_py39.py",
|
||||
"docs_src/handling_errors/tutorial001_py39.py",
|
||||
"docs_src/handling_errors/tutorial002_py39.py",
|
||||
"docs_src/handling_errors/tutorial003_py39.py",
|
||||
"docs_src/handling_errors/tutorial004_py39.py",
|
||||
"docs_src/handling_errors/tutorial005_py39.py",
|
||||
"docs_src/handling_errors/tutorial006_py39.py",
|
||||
"docs_src/metadata/tutorial001_1_py39.py",
|
||||
"docs_src/metadata/tutorial001_py39.py",
|
||||
"docs_src/metadata/tutorial002_py39.py",
|
||||
"docs_src/metadata/tutorial003_py39.py",
|
||||
"docs_src/metadata/tutorial004_py39.py",
|
||||
"docs_src/middleware/tutorial001_py39.py",
|
||||
"docs_src/openapi_webhooks/tutorial001_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial001_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial002_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial003_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial005_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial006_py39.py",
|
||||
"docs_src/path_operation_advanced_configuration/tutorial007_py39.py",
|
||||
"docs_src/path_operation_configuration/tutorial002b_py39.py",
|
||||
"docs_src/path_operation_configuration/tutorial006_py39.py",
|
||||
"docs_src/path_params/tutorial001_py39.py",
|
||||
"docs_src/path_params/tutorial002_py39.py",
|
||||
"docs_src/path_params/tutorial003_py39.py",
|
||||
"docs_src/path_params/tutorial003b_py39.py",
|
||||
"docs_src/path_params/tutorial004_py39.py",
|
||||
"docs_src/path_params/tutorial005_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial002_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial002_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial003_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial003_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial004_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial004_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial005_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial005_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial006_an_py39.py",
|
||||
"docs_src/path_params_numeric_validations/tutorial006_py39.py",
|
||||
"docs_src/python_types/tutorial001_py39.py",
|
||||
"docs_src/python_types/tutorial002_py39.py",
|
||||
"docs_src/python_types/tutorial003_py39.py",
|
||||
"docs_src/python_types/tutorial004_py39.py",
|
||||
"docs_src/python_types/tutorial005_py39.py",
|
||||
"docs_src/python_types/tutorial006_py39.py",
|
||||
"docs_src/python_types/tutorial007_py39.py",
|
||||
"docs_src/python_types/tutorial008_py39.py",
|
||||
"docs_src/python_types/tutorial008b_py39.py",
|
||||
"docs_src/python_types/tutorial009_py39.py",
|
||||
"docs_src/python_types/tutorial009b_py39.py",
|
||||
"docs_src/python_types/tutorial009c_py39.py",
|
||||
"docs_src/python_types/tutorial010_py39.py",
|
||||
"docs_src/python_types/tutorial013_py39.py",
|
||||
"docs_src/query_params/tutorial001_py39.py",
|
||||
"docs_src/query_params/tutorial005_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial005_an_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial005_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial006_an_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial006_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial012_an_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial012_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial013_an_py39.py",
|
||||
"docs_src/query_params_str_validations/tutorial013_py39.py",
|
||||
"docs_src/request_files/tutorial001_03_an_py39.py",
|
||||
"docs_src/request_files/tutorial001_03_py39.py",
|
||||
"docs_src/request_files/tutorial001_an_py39.py",
|
||||
"docs_src/request_files/tutorial001_py39.py",
|
||||
"docs_src/request_files/tutorial002_an_py39.py",
|
||||
"docs_src/request_files/tutorial002_py39.py",
|
||||
"docs_src/request_files/tutorial003_an_py39.py",
|
||||
"docs_src/request_files/tutorial003_py39.py",
|
||||
"docs_src/request_form_models/tutorial001_an_py39.py",
|
||||
"docs_src/request_form_models/tutorial001_py39.py",
|
||||
"docs_src/request_form_models/tutorial002_an_py39.py",
|
||||
"docs_src/request_form_models/tutorial002_py39.py",
|
||||
"docs_src/request_forms/tutorial001_an_py39.py",
|
||||
"docs_src/request_forms/tutorial001_py39.py",
|
||||
"docs_src/request_forms_and_files/tutorial001_an_py39.py",
|
||||
"docs_src/request_forms_and_files/tutorial001_py39.py",
|
||||
"docs_src/response_change_status_code/tutorial001_py39.py",
|
||||
"docs_src/response_cookies/tutorial001_py39.py",
|
||||
"docs_src/response_cookies/tutorial002_py39.py",
|
||||
"docs_src/response_directly/tutorial002_py39.py",
|
||||
"docs_src/response_headers/tutorial001_py39.py",
|
||||
"docs_src/response_headers/tutorial002_py39.py",
|
||||
"docs_src/response_model/tutorial003_02_py39.py",
|
||||
"docs_src/response_model/tutorial003_03_py39.py",
|
||||
"docs_src/response_status_code/tutorial001_py39.py",
|
||||
"docs_src/response_status_code/tutorial002_py39.py",
|
||||
"docs_src/security/tutorial001_an_py39.py",
|
||||
"docs_src/security/tutorial001_py39.py",
|
||||
"docs_src/security/tutorial006_an_py39.py",
|
||||
"docs_src/security/tutorial006_py39.py",
|
||||
"docs_src/security/tutorial007_an_py39.py",
|
||||
"docs_src/security/tutorial007_py39.py",
|
||||
"docs_src/settings/app01_py39/config.py",
|
||||
"docs_src/settings/app01_py39/main.py",
|
||||
"docs_src/settings/app02_an_py39/config.py",
|
||||
"docs_src/settings/app02_an_py39/main.py",
|
||||
"docs_src/settings/app02_an_py39/test_main.py",
|
||||
"docs_src/settings/app02_py39/config.py",
|
||||
"docs_src/settings/app02_py39/main.py",
|
||||
"docs_src/settings/app02_py39/test_main.py",
|
||||
"docs_src/settings/app03_an_py39/config.py",
|
||||
"docs_src/settings/app03_an_py39/main.py",
|
||||
"docs_src/settings/app03_py39/config.py",
|
||||
"docs_src/settings/app03_py39/main.py",
|
||||
"docs_src/settings/tutorial001_py39.py",
|
||||
"docs_src/static_files/tutorial001_py39.py",
|
||||
"docs_src/sub_applications/tutorial001_py39.py",
|
||||
"docs_src/templates/tutorial001_py39.py",
|
||||
"docs_src/using_request_directly/tutorial001_py39.py",
|
||||
"docs_src/websockets/tutorial001_py39.py",
|
||||
"docs_src/websockets/tutorial003_py39.py",
|
||||
"docs_src/wsgi/tutorial001_py39.py",
|
||||
]
|
||||
|
||||
[tool.coverage.report]
|
||||
|
||||
73
tests/test_deprecated_responses.py
Normal file
73
tests/test_deprecated_responses.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.responses import ORJSONResponse, UJSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
# ORJSON
|
||||
|
||||
|
||||
def _make_orjson_app() -> FastAPI:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
|
||||
@app.get("/items")
|
||||
def get_items() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_orjson_response_returns_correct_data():
|
||||
app = _make_orjson_app()
|
||||
client = TestClient(app)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
|
||||
|
||||
def test_orjson_response_emits_deprecation_warning():
|
||||
with pytest.warns(FastAPIDeprecationWarning, match="ORJSONResponse is deprecated"):
|
||||
ORJSONResponse(content={"hello": "world"})
|
||||
|
||||
|
||||
# UJSON
|
||||
|
||||
|
||||
def _make_ujson_app() -> FastAPI:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=UJSONResponse)
|
||||
|
||||
@app.get("/items")
|
||||
def get_items() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_ujson_response_returns_correct_data():
|
||||
app = _make_ujson_app()
|
||||
client = TestClient(app)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
|
||||
|
||||
def test_ujson_response_emits_deprecation_warning():
|
||||
with pytest.warns(FastAPIDeprecationWarning, match="UJSONResponse is deprecated"):
|
||||
UJSONResponse(content={"hello": "world"})
|
||||
51
tests/test_dump_json_fast_path.py
Normal file
51
tests/test_dump_json_fast_path.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/default")
|
||||
def get_default() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
|
||||
@app.get("/explicit", response_class=JSONResponse)
|
||||
def get_explicit() -> Item:
|
||||
return Item(name="widget", price=9.99)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_default_response_class_skips_json_dumps():
|
||||
"""When no response_class is set, the fast path serializes directly to
|
||||
JSON bytes via Pydantic's dump_json and never calls json.dumps."""
|
||||
with patch(
|
||||
"starlette.responses.json.dumps", wraps=__import__("json").dumps
|
||||
) as mock_dumps:
|
||||
response = client.get("/default")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
mock_dumps.assert_not_called()
|
||||
|
||||
|
||||
def test_explicit_response_class_uses_json_dumps():
|
||||
"""When response_class is explicitly set to JSONResponse, the normal path
|
||||
is used and json.dumps is called via JSONResponse.render()."""
|
||||
with patch(
|
||||
"starlette.responses.json.dumps", wraps=__import__("json").dumps
|
||||
) as mock_dumps:
|
||||
response = client.get("/explicit")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "widget", "price": 9.99}
|
||||
mock_dumps.assert_called_once()
|
||||
@@ -1,9 +1,14 @@
|
||||
import warnings
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
|
||||
|
||||
@app.get("/orjson_non_str_keys")
|
||||
@@ -16,6 +21,8 @@ client = TestClient(app)
|
||||
|
||||
|
||||
def test_orjson_non_str_keys():
|
||||
with client:
|
||||
response = client.get("/orjson_non_str_keys")
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
with client:
|
||||
response = client.get("/orjson_non_str_keys")
|
||||
assert response.json() == {"msg": "Hello World", "1": 1}
|
||||
|
||||
@@ -9,7 +9,6 @@ from inline_snapshot import snapshot
|
||||
name="client",
|
||||
params=[
|
||||
pytest.param("tutorial001_py310"),
|
||||
pytest.param("tutorial010_py310"),
|
||||
],
|
||||
)
|
||||
def get_client(request: pytest.FixtureRequest):
|
||||
@@ -18,12 +17,14 @@ def get_client(request: pytest.FixtureRequest):
|
||||
return client
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_get_custom_response(client: TestClient):
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi.exceptions import FastAPIDeprecationWarning
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from docs_src.custom_response.tutorial001b_py310 import app
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", FastAPIDeprecationWarning)
|
||||
from docs_src.custom_response.tutorial001b_py310 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_get_custom_response():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
50
tests/test_tutorial/test_custom_response/test_tutorial010.py
Normal file
50
tests/test_tutorial/test_custom_response/test_tutorial010.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
name="client",
|
||||
params=[
|
||||
pytest.param("tutorial010_py310"),
|
||||
],
|
||||
)
|
||||
def get_client(request: pytest.FixtureRequest):
|
||||
mod = importlib.import_module(f"docs_src.custom_response.{request.param}")
|
||||
client = TestClient(mod.app)
|
||||
return client
|
||||
|
||||
|
||||
def test_get_custom_response(client: TestClient):
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.text == snapshot("<h1>Items</h1><p>This is a list of items.</p>")
|
||||
|
||||
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == snapshot(
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"text/html": {"schema": {"type": "string"}}
|
||||
},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
30
uv.lock
generated
30
uv.lock
generated
@@ -1083,12 +1083,10 @@ all = [
|
||||
{ name = "httpx" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pydantic-extra-types" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "ujson" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
standard = [
|
||||
@@ -1134,6 +1132,7 @@ dev = [
|
||||
{ name = "mkdocs-redirects" },
|
||||
{ name = "mkdocstrings", extra = ["python"] },
|
||||
{ name = "mypy" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pillow" },
|
||||
{ name = "playwright" },
|
||||
{ name = "prek" },
|
||||
@@ -1151,6 +1150,7 @@ dev = [
|
||||
{ name = "typer" },
|
||||
{ name = "types-orjson" },
|
||||
{ name = "types-ujson" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "black" },
|
||||
@@ -1165,15 +1165,19 @@ docs = [
|
||||
{ name = "mkdocs-material" },
|
||||
{ name = "mkdocs-redirects" },
|
||||
{ name = "mkdocstrings", extra = ["python"] },
|
||||
{ name = "orjson" },
|
||||
{ name = "pillow" },
|
||||
{ name = "python-slugify" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "ruff" },
|
||||
{ name = "typer" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
docs-tests = [
|
||||
{ name = "httpx" },
|
||||
{ name = "orjson" },
|
||||
{ name = "ruff" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
github-actions = [
|
||||
{ name = "httpx" },
|
||||
@@ -1192,6 +1196,7 @@ tests = [
|
||||
{ name = "httpx" },
|
||||
{ name = "inline-snapshot" },
|
||||
{ name = "mypy" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pwdlib", extra = ["argon2"] },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pytest" },
|
||||
@@ -1202,6 +1207,7 @@ tests = [
|
||||
{ name = "strawberry-graphql" },
|
||||
{ name = "types-orjson" },
|
||||
{ name = "types-ujson" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython" },
|
||||
@@ -1225,7 +1231,6 @@ requires-dist = [
|
||||
{ name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" },
|
||||
{ name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" },
|
||||
{ name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" },
|
||||
{ name = "orjson", marker = "extra == 'all'", specifier = ">=3.9.3" },
|
||||
{ name = "pydantic", specifier = ">=2.7.0" },
|
||||
{ name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" },
|
||||
@@ -1240,7 +1245,6 @@ requires-dist = [
|
||||
{ name = "starlette", specifier = ">=0.40.0,<1.0.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.8.0" },
|
||||
{ name = "typing-inspection", specifier = ">=0.4.2" },
|
||||
{ name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" },
|
||||
@@ -1269,6 +1273,7 @@ dev = [
|
||||
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||
{ name = "mypy", specifier = ">=1.14.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pillow", specifier = ">=11.3.0" },
|
||||
{ name = "playwright", specifier = ">=1.57.0" },
|
||||
{ name = "prek", specifier = ">=0.2.22" },
|
||||
@@ -1276,7 +1281,7 @@ dev = [
|
||||
{ name = "pydantic-ai", specifier = ">=0.4.10" },
|
||||
{ name = "pygithub", specifier = ">=2.8.1" },
|
||||
{ name = "pyjwt", specifier = ">=2.9.0" },
|
||||
{ name = "pytest", specifier = ">=7.1.3,<9.0.0" },
|
||||
{ name = "pytest", specifier = ">=9.0.0" },
|
||||
{ name = "pytest-codspeed", specifier = ">=4.2.0" },
|
||||
{ name = "python-slugify", specifier = ">=8.0.4" },
|
||||
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
||||
@@ -1286,6 +1291,7 @@ dev = [
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "black", specifier = ">=25.1.0" },
|
||||
@@ -1300,15 +1306,19 @@ docs = [
|
||||
{ name = "mkdocs-material", specifier = ">=9.7.0" },
|
||||
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
|
||||
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pillow", specifier = ">=11.3.0" },
|
||||
{ name = "python-slugify", specifier = ">=8.0.4" },
|
||||
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
docs-tests = [
|
||||
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
github-actions = [
|
||||
{ name = "httpx", specifier = ">=0.27.0,<1.0.0" },
|
||||
@@ -1327,9 +1337,10 @@ tests = [
|
||||
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" },
|
||||
{ name = "inline-snapshot", specifier = ">=0.21.1" },
|
||||
{ name = "mypy", specifier = ">=1.14.1" },
|
||||
{ name = "orjson", specifier = ">=3.9.3" },
|
||||
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" },
|
||||
{ name = "pyjwt", specifier = ">=2.9.0" },
|
||||
{ name = "pytest", specifier = ">=7.1.3,<9.0.0" },
|
||||
{ name = "pytest", specifier = ">=9.0.0" },
|
||||
{ name = "pytest-codspeed", specifier = ">=4.2.0" },
|
||||
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
@@ -1337,6 +1348,7 @@ tests = [
|
||||
{ name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" },
|
||||
{ name = "types-orjson", specifier = ">=3.6.2" },
|
||||
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
|
||||
{ name = "ujson", specifier = ">=5.8.0" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython", specifier = ">=3.1.46" },
|
||||
@@ -4330,7 +4342,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -4341,9 +4353,9 @@ dependencies = [
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user