Compare commits

..

7 Commits

Author SHA1 Message Date
github-actions[bot]
4473a0cd91 📝 Update release notes
[skip ci]
2026-06-15 14:31:50 +00:00
Sebastián Ramírez
76876e5a81 🔧 Add ty configs to check docs sources (#15769) 2026-06-15 14:31:14 +00:00
Sebastián Ramírez
a82e5f2fac 🔖 Release version 0.137.1 (#15766)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-15 11:26:48 +00:00
github-actions[bot]
edd1461589 📝 Update release notes
[skip ci]
2026-06-15 11:20:24 +00:00
Sebastián Ramírez
b78c82262f 🚨 Fix typing checks for APIRoute (#15765) 2026-06-15 13:19:51 +02:00
github-actions[bot]
e0f8cadf09 📝 Update release notes
[skip ci]
2026-06-15 10:55:32 +00:00
Sebastián Ramírez
d8aad201eb 🐛 Fix bug, allow empty path in path operation in prefixless router (#15763) 2026-06-15 12:55:06 +02:00
17 changed files with 173 additions and 29 deletions

View File

@@ -45,7 +45,7 @@ repos:
- id: local-ty
name: ty check
entry: uv run ty check fastapi
entry: uv run ty check fastapi docs_src --force-exclude
require_serial: true
language: unsupported
pass_filenames: false

View File

@@ -7,6 +7,17 @@ hide:
## Latest Changes
### Internal
* 🔧 Add ty configs to check docs sources. PR [#15769](https://github.com/fastapi/fastapi/pull/15769) by [@tiangolo](https://github.com/tiangolo).
## 0.137.1 (2026-06-15)
### Fixes
* 🚨 Fix typing checks for APIRoute. PR [#15765](https://github.com/fastapi/fastapi/pull/15765) by [@tiangolo](https://github.com/tiangolo).
* 🐛 Fix bug, allow empty path in path operation in prefixless router. PR [#15763](https://github.com/fastapi/fastapi/pull/15763) by [@tiangolo](https://github.com/tiangolo).
## 0.137.0 (2026-06-14)
### Breaking Changes

View File

@@ -479,7 +479,7 @@ item: Item
* 來自不同來源的**參數**宣告:例如 **headers**、**cookies**、**form fields** 和 **files**。
* 如何設定**驗證限制**,如 `maximum_length` 或 `regex`。
* 一個非常強大且易用的 **<dfn title="也稱為:元件、資源、提供者、服務、可注入物">依賴注入</dfn>** 系統。
* 一個非常強大且易用的 **<dfn title="也稱為:components、resources、providers、services、injectables">依賴注入</dfn>** 系統。
* 安全與驗證,包含支援 **OAuth2** 搭配 **JWT tokens** 與 **HTTP Basic** 驗證。
* 宣告**深度巢狀 JSON 模型**的進階(但同樣簡單)技巧(感謝 Pydantic
* 與 [Strawberry](https://strawberry.rocks) 及其他函式庫的 **GraphQL** 整合。
@@ -492,7 +492,9 @@ item: Item
### 部署你的應用(可選) { #deploy-your-app-optional }
你可以選擇只用一個指令,將 FastAPI 應用部署到 [FastAPI Cloud](https://fastapicloud.com)。🚀
可以選擇將 FastAPI 應用部署到 [FastAPI Cloud](https://fastapicloud.com),如果你還沒加入,去登記等候名單吧。🚀
如果你已經有 **FastAPI Cloud** 帳號(我們已從等候名單邀請你 😉),你可以用一個指令部署你的應用。
<div class="termy">
@@ -508,8 +510,6 @@ Deploying to FastAPI Cloud...
</div>
CLI 會自動偵測你的 FastAPI 應用並將其部署到雲端。若你尚未登入,系統會開啟瀏覽器以完成驗證流程。
就這樣!現在你可以在該 URL 造訪你的應用。✨
#### 關於 FastAPI Cloud { #about-fastapi-cloud }

View File

@@ -108,7 +108,7 @@ q: str | None = None
{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *}
/// note | 注意
/// info | 注意
`Body` 也具有與 `Query``Path` 以及之後你會看到的其他工具相同的額外驗證與中繼資料參數。
@@ -123,7 +123,7 @@ q: str | None = None
但如果你想讓它像宣告多個 Body 參數時那樣,期望一個帶有 `item` 鍵、其內含模型內容的 JSON你可以使用 `Body` 的特殊參數 `embed`
```Python
item: Annotated[Item, Body(embed=True)]
item: Item = Body(embed=True)
```
如下:

View File

@@ -24,19 +24,19 @@
///
/// note
/// info
要宣告 cookies你需要使用 `Cookie`否則參數會被當作查詢參數query parameters來解析。
///
/// note
/// info
請注意,由於**瀏覽器以特殊方式並在背後處理 cookies**,因此**不**容易讓 **JavaScript** 觸碰到它們。
請注意,由於瀏覽器以特殊在背後處理的方式管理 cookies,它們通常不允許 JavaScript 輕易存取它們。
如果你前往位於 `/docs`**API 文件介面**,你可以在你的*路徑操作path operations*中看到 cookies 的**文件**
如果你前往位於 `/docs` 的 API 文件介面你可以在你的路徑操作path operations的文件中看到 cookies 的說明
但即使你**填入資料**並點擊「Execute」由於該文件介面是以 **JavaScript** 運作cookies 不會被送出,你會看到一則**錯誤**訊息,就好像你沒有填任何值一樣。
但即使你填入資料並點擊「Execute」由於該文件介面是以 JavaScript 運作cookies 不會被送出,你會看到一則錯誤訊息,就好像你沒有填任何值一樣。
///

View File

@@ -72,7 +72,7 @@ from myapp import app
就不會被執行。
/// note
/// info | 說明
想了解更多,參考 [Python 官方文件](https://docs.python.org/3/library/__main__.html)。

View File

@@ -72,13 +72,13 @@
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *}
/// note | 注意
/// info | 資訊
請注意,`response_description` 專指回應,而 `description` 則是針對整個「路徑操作」的一般描述。
///
/// tip
/// check | 檢查
OpenAPI 規範要求每個「路徑操作」都必須有一個回應描述。

View File

@@ -8,7 +8,7 @@
{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *}
/// note
/// info
FastAPI 在 0.95.0 版加入並開始推薦使用 `Annotated`
@@ -131,7 +131,7 @@ Python 不會對這個 `*` 做任何事,但它會知道後續的所有參數
* `lt`:小於(`l`ess `t`han
* `le`:小於或等於(`l`ess than or `e`qual
/// note
/// info
你之後會看到的 `Query``Path` 與其他類別,都是共同父類別 `Param` 的子類別。

View File

@@ -2,7 +2,7 @@
你可以使用 `File` 定義由用戶端上傳的檔案。
/// note
/// info
若要接收上傳的檔案,請先安裝 [`python-multipart`](https://github.com/Kludex/python-multipart)。
@@ -28,7 +28,7 @@ $ pip install python-multipart
{* ../../docs_src/request_files/tutorial001_an_py310.py hl[9] *}
/// note
/// info
`File` 是直接繼承自 `Form` 的類別。

View File

@@ -2,7 +2,7 @@
你可以使用 `File``Form` 同時定義檔案與表單欄位。
/// note
/// info
要接收上傳的檔案與/或表單資料,請先安裝 [`python-multipart`](https://github.com/Kludex/python-multipart)。

View File

@@ -18,7 +18,7 @@
參數 `status_code` 接受一個數字作為 HTTP 狀態碼。
/// note | 注意
/// info | 資訊
`status_code` 也可以接收一個 `IntEnum`,例如 Python 的 [`http.HTTPStatus`](https://docs.python.org/3/library/http.html#http.HTTPStatus)。
@@ -27,7 +27,7 @@
它會:
* 在回應中傳回該狀態碼。
* 在 OpenAPI 構中如此記錄(因此也會反映在使用者介面中):
* 在 OpenAPI 構中如此記錄(因此也會反映在使用者介面中):
<img src="/img/tutorial/response-status-code/image01.png">

View File

@@ -8,7 +8,7 @@
## 使用 `TestClient` { #using-testclient }
/// note
/// info
要使用 `TestClient`,請先安裝 [`httpx`](https://www.python-httpx.org)。
@@ -144,7 +144,7 @@ $ pip install httpx
關於如何把資料傳給後端(使用 `httpx` 或 `TestClient`),更多資訊請參考 [HTTPX 文件](https://www.python-httpx.org)。
/// note
/// info
請注意,`TestClient` 接收的是可轉為 JSON 的資料,而不是 Pydantic models。

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.137.0"
__version__ = "0.137.1"
from starlette import status as status

View File

@@ -1062,6 +1062,41 @@ def _populate_api_route_state(
class APIRoute(routing.Route):
stream_item_type: Any | None
response_model: Any
summary: str | None
response_description: str
deprecated: bool | None
operation_id: str | None
response_model_include: IncEx | None
response_model_exclude: IncEx | None
response_model_by_alias: bool
response_model_exclude_unset: bool
response_model_exclude_defaults: bool
response_model_exclude_none: bool
include_in_schema: bool
response_class: type[Response] | DefaultPlaceholder
dependency_overrides_provider: Any | None
callbacks: list[BaseRoute] | None
openapi_extra: dict[str, Any] | None
generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder
strict_content_type: bool | DefaultPlaceholder
tags: list[str | Enum]
responses: dict[int | str, dict[str, Any]]
unique_id: str
status_code: int | None
response_field: ModelField | None
stream_item_field: ModelField | None
dependencies: list[params.Depends]
description: str
response_fields: dict[int | str, ModelField]
dependant: Dependant
_flat_dependant: Dependant
_embed_body_fields: bool
body_field: ModelField | None
is_sse_stream: bool
is_json_stream: bool
def __init__(
self,
path: str,
@@ -2435,9 +2470,16 @@ class APIRouter(routing.Router):
"A path prefix must not end with '/', as the routes will start with '/'"
)
else:
for r in _iter_included_route_candidates(router.routes):
path = getattr(r, "path", None)
name = getattr(r, "name", "unknown")
for route, route_context in _iter_routes_with_context(router.routes):
if route_context is None:
path = getattr(route, "path", None)
name = getattr(route, "name", "unknown")
elif route_context.starlette_route is not None:
path = getattr(route_context.starlette_route, "path", None)
name = getattr(route_context.starlette_route, "name", "unknown")
else:
path = route_context.path
name = route_context.name
if path is not None and not path:
raise FastAPIError(
f"Prefix and path cannot be both empty (path operation: {name})"

View File

@@ -349,5 +349,41 @@ havin = "havin"
Ines = "Ines"
ser = "ser"
[tool.ty.src]
exclude = [
# These docs examples are intentionally partial, dynamic, environment-driven,
# deprecated, or currently require broader tutorial rewrites to satisfy ty.
"docs_src/additional_status_codes/",
"docs_src/app_testing/tutorial003_py310.py",
"docs_src/body_multiple_params/",
"docs_src/body_updates/tutorial002_py310.py",
"docs_src/custom_docs_ui/",
"docs_src/custom_response/tutorial001_py310.py",
"docs_src/custom_response/tutorial001b_py310.py",
"docs_src/custom_response/tutorial009c_py310.py",
"docs_src/dependencies/tutorial007_py310.py",
"docs_src/dependencies/tutorial008_an_py310.py",
"docs_src/dependencies/tutorial008_py310.py",
"docs_src/dependencies/tutorial010_py310.py",
"docs_src/events/",
"docs_src/extending_openapi/tutorial001_py310.py",
"docs_src/path_params_numeric_validations/",
"docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py",
"docs_src/python_types/tutorial003_py310.py",
"docs_src/python_types/tutorial011_py310.py",
"docs_src/query_params_str_validations/",
"docs_src/response_model/tutorial006_py310.py",
"docs_src/security/tutorial003_an_py310.py",
"docs_src/security/tutorial003_py310.py",
"docs_src/security/tutorial004_an_py310.py",
"docs_src/security/tutorial004_py310.py",
"docs_src/security/tutorial005_an_py310.py",
"docs_src/security/tutorial005_py310.py",
"docs_src/settings/",
"docs_src/sql_databases/",
"docs_src/using_request_directly/tutorial001_py310.py",
"docs_src/wsgi/tutorial001_py310.py",
]
[tool.ty.terminal]
error-on-warning = true

View File

@@ -4,6 +4,6 @@ set -e
set -x
mypy fastapi
ty check fastapi
ty check fastapi docs_src --force-exclude
ruff check fastapi tests docs_src scripts
ruff format fastapi tests --check

View File

@@ -2,6 +2,7 @@ from typing import Annotated, cast
import pytest
from fastapi import APIRouter, Body, Depends, FastAPI, Request
from fastapi.exceptions import FastAPIError
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.routing import (
APIRoute,
@@ -807,6 +808,60 @@ def test_no_prefix_include_validation_sees_effective_starlette_route_candidates(
assert cast(Route, candidates[0]).path == "/child/items"
def test_no_prefix_include_validation_sees_effective_api_route_path():
leaf_router = APIRouter()
@leaf_router.get("")
def read_items():
return []
parent_router = APIRouter()
parent_router.include_router(leaf_router, prefix="/items")
# for coverage
candidates = list(_iter_included_route_candidates(parent_router.routes))
assert cast(APIRoute, candidates[0]).path == ""
app = FastAPI()
app.include_router(parent_router)
client = TestClient(app)
response = client.get("/items")
assert response.status_code == 200, response.text
assert response.json() == []
def test_no_prefix_include_validation_sees_effective_starlette_route_path():
def endpoint(request):
return PlainTextResponse("ok")
child_router = APIRouter(routes=[Route("/items", endpoint, name="read_items")])
parent_router = APIRouter()
parent_router.include_router(child_router, prefix="/child")
app = FastAPI()
app.include_router(parent_router)
client = TestClient(app)
response = client.get("/child/items")
assert response.status_code == 200, response.text
assert response.text == "ok"
def test_no_prefix_include_validation_rejects_empty_effective_api_route_path():
router = APIRouter()
@router.get("")
def read_items(): # pragma: no cover
return []
app = FastAPI()
with pytest.raises(FastAPIError):
app.include_router(router)
def test_apirouter_matches_fallback_without_include_context():
router = APIRouter()