Compare commits

...

17 Commits

Author SHA1 Message Date
Yurii Motov
e15cff7376 Print pure text in non-interactive mode 2025-12-24 16:19:18 +01:00
Yurii Motov
844ded6b43 Add script to compare fixed elements in translated page with En page 2025-12-23 22:46:48 +01:00
github-actions[bot]
e55f223b46 📝 Update release notes
[skip ci]
2025-12-23 11:17:37 +00:00
Sebastián Ramírez
a329baaa54 👷 Update secrets check (#14592) 2025-12-23 11:17:16 +00:00
github-actions[bot]
a7a0aee984 📝 Update release notes
[skip ci]
2025-12-21 17:52:08 +00:00
Sebastián Ramírez
6539b80d9f 👷 Run CodSpeed tests in parallel to other tests to speed up CI (#14586) 2025-12-21 18:51:45 +01:00
github-actions[bot]
e1bd9f3e33 📝 Update release notes
[skip ci]
2025-12-21 17:40:41 +00:00
Sebastián Ramírez
b9b2793bda 🔨 Update scripts and pre-commit to autofix files (#14585) 2025-12-21 17:40:17 +00:00
Sebastián Ramírez
c4a1ab5036 🔖 Release version 0.127.0 2025-12-21 17:45:43 +01:00
github-actions[bot]
22c7200ebb 📝 Update release notes
[skip ci]
2025-12-21 16:44:32 +00:00
Sebastián Ramírez
6e42bcd8ce 🔊 Add deprecation warnings when using pydantic.v1 (#14583) 2025-12-21 17:44:10 +01:00
github-actions[bot]
6513d4daa1 📝 Update release notes
[skip ci]
2025-12-21 08:06:42 +00:00
Sebastián Ramírez
1d93d531bc ⬆️ Upgrade OpenAI model for translations to gpt-5.2 (#14579) 2025-12-21 08:06:22 +00:00
github-actions[bot]
c2c1cc8aec 📝 Update release notes
[skip ci]
2025-12-20 17:32:31 +00:00
Sebastián Ramírez
5289259275 🔧 Add LLM prompt file for Korean, generated from the existing translations (#14546)
Co-authored-by: hy.lee <rurouni24@gmail.com>
2025-12-20 18:32:05 +01:00
github-actions[bot]
5783910d0c 📝 Update release notes
[skip ci]
2025-12-20 17:31:11 +00:00
Sebastián Ramírez
026b43e5d3 🔧 Add LLM prompt file for Japanese, generated from the existing translations (#14545)
Co-authored-by: Maruo.S <raspi-maru2004@outlook.jp>
2025-12-20 18:30:52 +01:00
31 changed files with 1638 additions and 629 deletions

View File

@@ -60,8 +60,6 @@ jobs:
pyproject.toml
- name: Install docs extras
run: uv pip install -r requirements-docs.txt
- name: Verify Docs
run: python ./scripts/docs.py verify-docs
- name: Export Language Codes
id: show-langs
run: |

View File

@@ -7,7 +7,8 @@ on:
- synchronize
env:
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
# Forks and Dependabot don't have access to secrets
HAS_SECRETS: ${{ secrets.PRE_COMMIT != '' }}
jobs:
pre-commit:
@@ -19,7 +20,7 @@ jobs:
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v5
name: Checkout PR for own repo
if: env.IS_FORK == 'false'
if: env.HAS_SECRETS == 'true'
with:
# To be able to commit it needs to fetch the head of the branch, not the
# merge commit
@@ -31,7 +32,7 @@ jobs:
# pre-commit lite ci needs the default checkout configs to work
- uses: actions/checkout@v5
name: Checkout PR for fork
if: env.IS_FORK == 'true'
if: env.HAS_SECRETS == 'false'
with:
# To be able to commit it needs the head branch of the PR, the remote one
ref: ${{ github.event.pull_request.head.sha }}
@@ -56,7 +57,7 @@ jobs:
run: uvx prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure
continue-on-error: true
- name: Commit and push changes
if: env.IS_FORK == 'false'
if: env.HAS_SECRETS == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
@@ -68,7 +69,7 @@ jobs:
git push
fi
- uses: pre-commit-ci/lite-action@v1.1.0
if: env.IS_FORK == 'true'
if: env.HAS_SECRETS == 'false'
with:
msg: 🎨 Auto format
- name: Error out on pre-commit errors

View File

@@ -54,10 +54,14 @@ jobs:
- os: windows-latest
python-version: "3.12"
coverage: coverage
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
- os: ubuntu-latest
python-version: "3.13"
coverage: coverage
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
- os: ubuntu-latest
python-version: "3.13"
coverage: coverage
codspeed: codspeed
- os: ubuntu-latest
python-version: "3.14"
coverage: coverage
@@ -85,12 +89,13 @@ jobs:
run: uv pip install -r requirements-tests.txt
- run: mkdir coverage
- name: Test
if: matrix.codspeed != 'codspeed'
run: bash scripts/test.sh
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
- name: CodSpeed benchmarks
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
if: matrix.codspeed == 'codspeed'
uses: CodSpeedHQ/action@v4
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}

View File

@@ -21,10 +21,28 @@ repos:
- id: ruff-format
- repo: local
hooks:
- id: local-script
- id: add-permalinks-pages
language: unsupported
name: local script
name: add-permalinks-pages
entry: uv run ./scripts/docs.py add-permalinks-pages
args:
- --update-existing
files: ^docs/en/docs/.*\.md$
- id: generate-readme
language: unsupported
name: generate README.md from index.md
entry: uv run ./scripts/docs.py generate-readme
files: ^docs/en/docs/index\.md|docs/en/data/sponsors\.yml|scripts/docs\.py$
pass_filenames: false
- id: update-languages
language: unsupported
name: update languages
entry: uv run ./scripts/docs.py update-languages
files: ^docs/.*|scripts/docs\.py$
pass_filenames: false
- id: ensure-non-translated
language: unsupported
name: ensure non-translated files are not modified
entry: uv run ./scripts/docs.py ensure-non-translated
files: ^docs/(?!en/).*|^scripts/docs\.py$
pass_filenames: false

View File

@@ -7,6 +7,27 @@ hide:
## Latest Changes
### Internal
* 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo).
* 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Update scripts and pre-commit to autofix files. PR [#14585](https://github.com/fastapi/fastapi/pull/14585) by [@tiangolo](https://github.com/tiangolo).
## 0.127.0
### Breaking Changes
* 🔊 Add deprecation warnings when using `pydantic.v1`. PR [#14583](https://github.com/fastapi/fastapi/pull/14583) by [@tiangolo](https://github.com/tiangolo).
### Translations
* 🔧 Add LLM prompt file for Korean, generated from the existing translations. PR [#14546](https://github.com/fastapi/fastapi/pull/14546) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Add LLM prompt file for Japanese, generated from the existing translations. PR [#14545](https://github.com/fastapi/fastapi/pull/14545) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⬆️ Upgrade OpenAI model for translations to gpt-5.2. PR [#14579](https://github.com/fastapi/fastapi/pull/14579) by [@tiangolo](https://github.com/tiangolo).
## 0.126.0
### Upgrades

47
docs/ja/llm-prompt.md Normal file
View File

@@ -0,0 +1,47 @@
### Target language
Translate to Japanese (日本語).
Language code: ja.
### Grammar and tone
1) Use polite, instructional Japanese (です/ます調).
2) Keep the tone concise and technical (match existing Japanese FastAPI docs).
### Headings
1) Follow the existing Japanese style: short, descriptive headings (often noun phrases), e.g. 「チェック」.
2) Do not add a trailing period at the end of headings.
### Quotes
1) Prefer Japanese corner brackets 「」 in normal prose when quoting a term.
2) Do not change quotes inside inline code, code blocks, URLs, or file paths.
### Ellipsis
1) Keep ellipsis style consistent with existing Japanese docs (commonly `...`).
2) Never change `...` in code, URLs, or CLI examples.
### Preferred translations / glossary
Use the following preferred translations when they apply in documentation prose:
- request (HTTP): リクエスト
- response (HTTP): レスポンス
- path operation: パスオペレーション
- path operation function: パスオペレーション関数
### `///` admonitions
1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.).
2) If a title is present, prefer these canonical titles:
- `/// note | 備考`
- `/// note | 技術詳細`
- `/// tip | 豆知識`
- `/// warning | 注意`
- `/// info | 情報`
- `/// check | 確認`
- `/// danger | 警告`

51
docs/ko/llm-prompt.md Normal file
View File

@@ -0,0 +1,51 @@
### Target language
Translate to Korean (한국어).
Language code: ko.
### Grammar and tone
1) Use polite, instructional Korean (e.g. 합니다/하세요 style).
2) Keep the tone consistent with the existing Korean FastAPI docs.
### Headings
1) Follow existing Korean heading style (short, action-oriented headings like “확인하기”).
2) Do not add trailing punctuation to headings.
### Quotes
1) Keep quote style consistent with the existing Korean docs.
2) Never change quotes inside inline code, code blocks, URLs, or file paths.
### Ellipsis
1) Keep ellipsis style consistent with existing Korean docs (often `...`).
2) Never change `...` in code, URLs, or CLI examples.
### Preferred translations / glossary
Use the following preferred translations when they apply in documentation prose:
- request (HTTP): 요청
- response (HTTP): 응답
- path operation: 경로 처리
- path operation function: 경로 처리 함수
### `///` admonitions
1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.).
2) If a title is present, prefer these canonical titles:
- `/// note | 참고`
- `/// tip | 팁`
- `/// warning | 경고`
- `/// info | 정보`
- `/// danger | 위험`
- `/// note Technical Details | 기술 세부사항`
- `/// check | 확인`
Notes:
- `details` blocks exist in Korean docs; keep `/// details` as-is and translate only the title after `|`.
- Example canonical title used: `/// details | 상세 설명`

View File

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

View File

@@ -1,6 +1,7 @@
import dataclasses
import inspect
import sys
import warnings
from collections.abc import Coroutine, Mapping, Sequence
from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy
@@ -322,6 +323,13 @@ def get_dependant(
)
continue
assert param_details.field is not None
if isinstance(param_details.field, may_v1.ModelField):
warnings.warn(
"pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
f" Please update the param {param_name}: {param_details.type_annotation!r}.",
category=DeprecationWarning,
stacklevel=5,
)
if isinstance(
param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body)
):

View File

@@ -2,6 +2,7 @@ import email.message
import functools
import inspect
import json
import warnings
from collections.abc import (
AsyncIterator,
Awaitable,
@@ -28,6 +29,7 @@ from fastapi._compat import (
_get_model_config,
_model_dump,
_normalize_errors,
annotation_is_pydantic_v1,
lenient_issubclass,
may_v1,
)
@@ -634,6 +636,13 @@ class APIRoute(routing.Route):
f"Status code {status_code} must not have a response body"
)
response_name = "Response_" + self.unique_id
if annotation_is_pydantic_v1(self.response_model):
warnings.warn(
"pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
f" Please update the response model {self.response_model!r}.",
category=DeprecationWarning,
stacklevel=4,
)
self.response_field = create_model_field(
name=response_name,
type_=self.response_model,
@@ -667,6 +676,13 @@ class APIRoute(routing.Route):
f"Status code {additional_status_code} must not have a response body"
)
response_name = f"Response_{additional_status_code}_{self.unique_id}"
if annotation_is_pydantic_v1(model):
warnings.warn(
"pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
f" In responses={{}}, please update {model}.",
category=DeprecationWarning,
stacklevel=4,
)
response_field = create_model_field(
name=response_name, type_=model, mode="serialization"
)

694
scripts/cmpr.py Normal file
View File

@@ -0,0 +1,694 @@
import os
import platform
import re
import subprocess
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated, Literal, cast
import typer
ROOT = Path("../") # assuming this script is in the scripts directory
DOCS_ROOT = os.getenv("DOCS_ROOT", "docs")
TMP_DOCS_PATH = os.getenv("TMP_DOCS_PATH", "non-git/translations")
VSCODE_COMMAND = os.getenv(
"VSCODE_COMMAND", "code.cmd" if platform.system() == "Windows" else "code"
)
# TBD: `Literal` is not supported in typer 0.16.0, which is the
# version given in the requirements-docs.txt.
# Shall we upgrade that requirement to 0.20.0?
LANGS = Literal["es", "de", "ru", "pt", "uk", "fr"]
non_translated_sections = (
f"reference{os.sep}",
"release-notes.md",
"fastapi-people.md",
"external-links.md",
"newsletter.md",
"management-tasks.md",
"management.md",
"contributing.md",
)
class Retry(Exception):
pass
class CompareError(Exception):
pass
@dataclass
class Config:
lang: LANGS
interactive: bool = True
check_code_includes: bool = True
check_multiline_blocks: bool = True
check_headers_and_permalinks: bool = True
check_markdown_links: bool = True
check_html_links: bool = True
full_paths: bool = False
# ===================================================================================
# Code includes
CODE_INCLUDE_RE = re.compile(r"^\{\*\s*(\S+)\s*(.*)\*\}$")
def extract_code_includes(lines: list[str]) -> list[tuple[str, str, str, int]]:
includes = []
for line_no, line in enumerate(lines, start=1):
if CODE_INCLUDE_RE.match(line):
includes.append((line_no, line))
return includes
def replace_code_includes(source_text: str, target_text: str) -> str:
target_lines = target_text.splitlines()
source_code_includes = extract_code_includes(source_text.splitlines())
target_code_includes = extract_code_includes(target_lines)
if len(source_code_includes) != len(target_code_includes):
raise CompareError(
f"Number of code includes differs: "
f"{len(source_code_includes)} in source vs {len(target_code_includes)} in target."
)
for src_include, tgt_include in zip(source_code_includes, target_code_includes):
_, src_line = src_include
tgt_line_no, _ = tgt_include
target_lines[tgt_line_no - 1] = src_line
target_lines.append("") # To preserve the empty line in the end of the file
return "\n".join(target_lines)
# ===================================================================================
# Multiline code blocks
LANG_RE = re.compile(r"^```([\w-]*)", re.MULTILINE)
def get_code_block_lang(line: str) -> str:
match = LANG_RE.match(line)
if match:
return match.group(1)
return ""
def extract_multiline_blocks(text: str) -> list[tuple[str, int, str]]:
lines = text.splitlines()
blocks = []
in_code_block3 = False
in_code_block4 = False
current_block_lang = ""
current_block_start_line = -1
current_block_lines = []
for line_no, line in enumerate(lines, start=1):
stripped = line.lstrip()
# --- Detect opening fence ---
if not (in_code_block3 or in_code_block4):
if stripped.startswith("```"):
current_block_start_line = line_no
count = len(stripped) - len(stripped.lstrip("`"))
if count == 3:
in_code_block3 = True
current_block_lang = get_code_block_lang(stripped)
current_block_lines = [line]
continue
elif count >= 4:
in_code_block4 = True
current_block_lang = get_code_block_lang(stripped)
current_block_lines = [line]
continue
# --- Detect closing fence ---
elif in_code_block3:
if stripped.startswith("```"):
count = len(stripped) - len(stripped.lstrip("`"))
if count == 3:
current_block_lines.append(line)
blocks.append(
(
current_block_lang,
current_block_start_line,
"\n".join(current_block_lines),
)
)
in_code_block3 = False
current_block_lang = ""
current_block_start_line = -1
continue
current_block_lines.append(line)
elif in_code_block4:
if stripped.startswith("````"):
count = len(stripped) - len(stripped.lstrip("`"))
if count >= 4:
current_block_lines.append(line)
blocks.append(
(
current_block_lang,
current_block_start_line,
"\n".join(current_block_lines),
)
)
in_code_block4 = False
current_block_lang = ""
current_block_start_line = -1
continue
current_block_lines.append(line)
return blocks
def replace_blocks(source_text: str, target_text: str) -> str:
source_blocks = extract_multiline_blocks(source_text)
target_blocks = extract_multiline_blocks(target_text)
if len(source_blocks) != len(target_blocks):
raise CompareError(
f"Number of code blocks differs: "
f"{len(source_blocks)} in source vs {len(target_blocks)} in target."
)
for i, ((src_lang, *_), (tgt_lang, tgt_line_no, *_)) in enumerate(
zip(source_blocks, target_blocks), 1
):
if src_lang != tgt_lang:
raise CompareError(
f"Type mismatch in block #{i} (line {tgt_line_no}): "
f"'{src_lang or '(no lang)'}' vs '{tgt_lang or '(no lang)'}'"
)
# Sequentially replace each block in target with the one from source
result = target_text
for (*_, src_block), (*_, tgt_block) in zip(source_blocks, target_blocks):
result = result.replace(tgt_block, src_block, 1)
return result
# ===================================================================================
# Headers and permalinks
header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})?\s*$")
def extract_headers_and_permalinks(lines: list[str]) -> list[tuple[str, int, str]]:
headers = []
in_code_block3 = False
in_code_block4 = False
for line_no, line in enumerate(lines, start=1):
if not (in_code_block3 or in_code_block4):
if line.startswith("```"):
count = len(line) - len(line.lstrip("`"))
if count == 3:
in_code_block3 = True
continue
elif count >= 4:
in_code_block4 = True
continue
header_match = header_with_permalink_pattern.match(line)
if header_match:
hashes, _title, permalink = header_match.groups()
headers.append((hashes, line_no, permalink))
elif in_code_block3:
if line.startswith("```"):
count = len(line) - len(line.lstrip("`"))
if count == 3:
in_code_block3 = False
continue
elif in_code_block4:
if line.startswith("````"):
count = len(line) - len(line.lstrip("`"))
if count >= 4:
in_code_block4 = False
continue
return headers
def replace_headers_and_permalinks(source_text: str, target_text: str) -> str:
target_lines = target_text.splitlines()
source_headers = extract_headers_and_permalinks(source_text.splitlines())
target_headers = extract_headers_and_permalinks(target_lines)
if len(source_headers) != len(target_headers):
raise CompareError(
f"Number of headers differs: "
f"{len(source_headers)} in source vs {len(target_headers)} in target."
)
for i, ((src_hashes, *_), (tgt_hashes, tgt_line_no, *_)) in enumerate(
zip(source_headers, target_headers), 1
):
if src_hashes != tgt_hashes:
raise CompareError(
f"Header level mismatch in #{i} (line {tgt_line_no}): "
"'{src_hashes}' vs '{tgt_hashes}'"
)
# Sequentially replace each header permalink in target with the one from source
for src_header, tgt_header in zip(source_headers, target_headers):
src_permalink = src_header[2]
tgt_line_no = tgt_header[1] - 1 # Convert from 1-based to 0-based
header_match = header_with_permalink_pattern.match(target_lines[tgt_line_no])
if header_match:
hashes, title, _ = header_match.groups()
target_lines[tgt_line_no] = (
f"{hashes} {title}{src_permalink or ' (ERROR - MISSING PERMALINK)'}"
)
target_lines.append("") # To preserve the empty line in the end of the file
return "\n".join(target_lines)
# ===================================================================================
# Links
MARKDOWN_LINK_RE = re.compile(
r"(?<!\!)" # not an image ![...]
r"\[(?P<text>.*?)\]" # link text (non-greedy)
r"\("
r"(?P<url>\S+?)" # url (no spaces, non-greedy)
r'(?:\s+["\'](?P<title>.*?)["\'])?' # optional title in "" or ''
r"\)"
)
def extract_markdown_links(lines: list[str]) -> list[tuple[str, int]]:
links = []
for line_no, line in enumerate(lines, start=1):
for m in MARKDOWN_LINK_RE.finditer(line):
url = m.group("url")
links.append((url, line_no))
return links
def replace_markdown_links(source_text: str, target_text: str, lang: str) -> str:
target_lines = target_text.splitlines()
source_links = extract_markdown_links(source_text.splitlines())
target_links = extract_markdown_links(target_lines)
if len(source_links) != len(target_links):
raise CompareError(
f"Number of markdown links differs: "
f"{len(source_links)} in source vs {len(target_links)} in target."
)
# Sequentially replace each link URL in target with the one from source
for (src_link, _), (tgt_link, tgt_line_no) in zip(source_links, target_links):
real_line_no = tgt_line_no - 1 # Convert to zero-based
line = target_lines[real_line_no]
link_replace = add_lang_code_if_needed(src_link, tgt_link, lang)
target_lines[real_line_no] = line.replace(tgt_link, link_replace)
target_lines.append("") # To preserve the empty line in the end of the file
return "\n".join(target_lines)
HTML_LINK_RE = re.compile(r"<a\s+[^>]*>.*?</a>")
HTML_LINK_TEXT = re.compile(r"<a\b([^>]*)>(.*?)</a>")
HTML_LINK_OPEN_TAG_RE = re.compile(r"<a\b([^>]*)>")
HTML_ATTR_RE = re.compile(r'(\w+)\s*=\s*([\'"])(.*?)\2')
def extract_html_links(
lines: list[str],
) -> list[tuple[tuple[str, list[tuple[str, str, str]], str], int]]:
links = []
for line_no, line in enumerate(lines, start=1):
for html_link in HTML_LINK_RE.finditer(line):
link_str = html_link.group(0)
link_text = cast(re.Match, HTML_LINK_TEXT.match(link_str)).group(2)
link_data = (link_str, [], link_text)
link_open_tag = cast(re.Match, HTML_LINK_OPEN_TAG_RE.match(link_str)).group(
1
)
attributes = re.findall(HTML_ATTR_RE, link_open_tag)
for attr_data in attributes:
link_data[1].append(attr_data)
links.append((link_data, line_no))
return links
TIANGOLO_COM = "https://fastapi.tiangolo.com"
def add_lang_code_if_needed(url: str, prev_url: str, lang_code: str) -> str:
if url.startswith(TIANGOLO_COM):
if prev_url.startswith(f"{TIANGOLO_COM}/{lang_code}"):
url = url.replace(TIANGOLO_COM, f"{TIANGOLO_COM}/{lang_code}")
return url
def reconstruct_html_link(
attributes: list[tuple[str, str, str]],
link_text: str,
prev_attributes: list[tuple[str, str, str]],
lang_code: str,
) -> str:
prev_attributes_dict = {attr[0]: attr[2] for attr in prev_attributes}
prev_url = prev_attributes_dict["href"]
attributes_upd = []
for attr_name, attr_quotes, attr_value in attributes:
if attr_name == "href":
attr_value = add_lang_code_if_needed(attr_value, prev_url, lang_code)
attributes_upd.append((attr_name, attr_quotes, attr_value))
attrs_str = " ".join(
f"{name}={quetes}{value}{quetes}" for name, quetes, value in attributes_upd
)
return f"<a {attrs_str}>{link_text}</a>"
def replace_html_links(source_text: str, target_text: str, lang: str) -> str:
target_lines = target_text.splitlines()
source_links = extract_html_links(source_text.splitlines())
target_links = extract_html_links(target_lines)
if len(source_links) != len(target_links):
raise CompareError(
f"Number of HTML links differs: "
f"{len(source_links)} in source vs {len(target_links)} in target."
)
# Sequentially replace attributes of each link URL in target with the one from source
for (src_link_data, _), (tgt_link_data, tgt_line_no) in zip(
source_links, target_links
):
real_line_no = tgt_line_no - 1 # Convert to zero-based
line = target_lines[real_line_no]
tgt_link_text = tgt_link_data[2]
tgt_link_original = tgt_link_data[0]
tgt_link_override = reconstruct_html_link(
src_link_data[1], tgt_link_text, tgt_link_data[1], lang
)
target_lines[real_line_no] = line.replace(tgt_link_original, tgt_link_override)
target_lines.append("") # To preserve the empty line in the end of the file
return "\n".join(target_lines)
# ===================================================================================
# Images
# ===================================================================================
# Helper functions
def get_lang_doc_root_dir(lang: str) -> Path:
return ROOT / DOCS_ROOT / lang / "docs"
def iter_all_lang_paths(lang_path_root: Path) -> Iterable[Path]:
"""
Iterate on the markdown files to translate in order of priority.
"""
first_dirs = [
lang_path_root / "learn",
lang_path_root / "tutorial",
lang_path_root / "advanced",
lang_path_root / "about",
lang_path_root / "how-to",
]
first_parent = lang_path_root
yield from first_parent.glob("*.md")
for dir_path in first_dirs:
yield from dir_path.rglob("*.md")
first_dirs_str = tuple(str(d) for d in first_dirs)
for path in lang_path_root.rglob("*.md"):
if str(path).startswith(first_dirs_str):
continue
if path.parent == first_parent:
continue
yield path
def get_all_paths(lang: str):
res: list[str] = []
lang_docs_root = get_lang_doc_root_dir(lang)
for path in iter_all_lang_paths(lang_docs_root):
relpath = path.relative_to(lang_docs_root)
if not str(relpath).startswith(non_translated_sections):
res.append(str(relpath))
return res
# ===================================================================================
# Main
def process_one_file_with_retry(document_path: str, config: Config) -> bool:
en_docs_root_path = Path(get_lang_doc_root_dir("en"))
lang_docs_root_path = Path(get_lang_doc_root_dir(config.lang))
while True:
try:
return process_one_file(
en_docs_root_path / document_path,
lang_docs_root_path / document_path,
config=config,
)
except Retry: # Retry is only raised in interactive mode
pass
def process_one_file(
en_doc_path_str: Path, lang_doc_path_str: Path, config: Config
) -> bool:
en_doc_path = Path(en_doc_path_str)
lang_doc_path = Path(lang_doc_path_str)
if not en_doc_path.exists():
print(
f"{'❌🔎 ' if config.interactive else ''}{en_doc_path_str} - doesn't exist"
)
return False
en_doc_text = en_doc_path.read_text(encoding="utf-8")
lang_doc_text = lang_doc_path.read_text(encoding="utf-8")
lang_doc_text_orig = lang_doc_text
try:
if config.check_code_includes:
lang_doc_text = replace_code_includes(
source_text=en_doc_text,
target_text=lang_doc_text,
)
if config.check_multiline_blocks:
lang_doc_text = replace_blocks(
source_text=en_doc_text,
target_text=lang_doc_text,
)
if config.check_headers_and_permalinks:
lang_doc_text = replace_headers_and_permalinks(
source_text=en_doc_text,
target_text=lang_doc_text,
)
if config.check_markdown_links:
lang_doc_text = replace_markdown_links(
source_text=en_doc_text,
target_text=lang_doc_text,
lang=config.lang,
)
if config.check_html_links:
lang_doc_text = replace_html_links(
source_text=en_doc_text,
target_text=lang_doc_text,
lang=config.lang,
)
except CompareError as e:
print(f"{'❔❌ ' if config.interactive else ''}{lang_doc_path_str} Error: {e}")
if not config.interactive:
return False
subprocess.run([VSCODE_COMMAND, "--diff", lang_doc_path_str, en_doc_path_str])
resp = ""
while resp not in ("f", "e"):
resp = input(
" Check the diff, fix the problem, and then type F if it's fixed or E to mark as invalid and skip: "
)
if resp.lower() == "e":
print(f"{lang_doc_path_str} skipped with error")
return
print(f"Check {lang_doc_path_str} again")
raise Retry() from None
if lang_doc_text_orig != lang_doc_text:
print(
f"{'❔🆚 ' if config.interactive else ''}{lang_doc_path_str} - non-empty diff"
)
if not config.interactive:
return False
tmp_path = ROOT / TMP_DOCS_PATH / Path(lang_doc_path_str)
tmp_path.parent.mkdir(parents=True, exist_ok=True)
tmp_path.write_text(lang_doc_text, encoding="utf-8")
subprocess.run(
[VSCODE_COMMAND, "--diff", str(lang_doc_path_str), str(tmp_path)]
)
resp = ""
while resp not in ("f", "e"):
resp = input(
" Check the diff, fix the problem, and then type F to mark it as fixed or E to to mark as invalid and skip: "
).lower()
if resp == "e":
print(f"{lang_doc_path_str} skipped with non-empty diff")
return
print(f"{'' if config.interactive else ''}{lang_doc_path_str} - Ok")
return True
# ===================================================================================
# Typer app
cli = typer.Typer()
@cli.callback()
def callback():
pass
@cli.callback()
def main(
ctx: typer.Context,
lang: Annotated[LANGS, typer.Option()],
interactive: Annotated[
bool,
typer.Option(
help="If True, will open VSCode diffs for each change to fix and confirm.",
),
] = True,
full_paths: Annotated[
bool,
typer.Option(
help="If True, the provided document paths are treated as full paths.",
),
] = False,
check_code_includes: Annotated[
bool,
typer.Option(
help="If True, will compare code includes blocks.",
),
] = True,
check_multiline_blocks: Annotated[
bool,
typer.Option(
help="If True, will compare multiline code blocks.",
),
] = True,
check_headers_and_permalinks: Annotated[
bool,
typer.Option(
help="If True, will compare headers and permalinks.",
),
] = True,
check_markdown_links: Annotated[
bool,
typer.Option(
help="If True, will compare markdown links.",
),
] = True,
check_html_links: Annotated[
bool,
typer.Option(
help="If True, will compare HTML links.",
),
] = True,
):
ctx.obj = Config(
lang=lang,
interactive=interactive,
full_paths=full_paths,
check_code_includes=check_code_includes,
check_multiline_blocks=check_multiline_blocks,
check_headers_and_permalinks=check_headers_and_permalinks,
check_markdown_links=check_markdown_links,
check_html_links=check_html_links,
)
@cli.command()
def process_all(
ctx: typer.Context,
):
"""
Go through all documents of language and compare special blocks with the corresponding
blocks in English versions of those documents.
"""
config = cast(Config, ctx.obj)
lang_docs_root_path = get_lang_doc_root_dir(config.lang)
docs = get_all_paths(config.lang)
all_good = True
pages_with_errors: list[str] = []
for doc in docs:
res = process_one_file_with_retry(document_path=doc, config=config)
all_good = all_good and res
if not res:
pages_with_errors.append(doc)
if not all_good:
print("Some documents had errors:")
docs_path = lang_docs_root_path.relative_to(ROOT)
for page in pages_with_errors:
print(f" - {docs_path / page}")
raise typer.Exit(code=1)
@cli.command()
def process_pages(
doc_paths: Annotated[
list[str],
typer.Argument(
help="List of relative paths to the EN documents. Should be relative to docs/en/docs/",
),
],
ctx: typer.Context,
):
"""
Compare special blocks of specified EN documents with the corresponding blocks in
translated versions of those documents.
"""
config = cast(Config, ctx.obj)
lang_docs_root_path = get_lang_doc_root_dir(config.lang)
all_good = True
pages_with_errors: list[str] = []
for doc_path in doc_paths:
if config.full_paths:
path = ROOT / doc_path.lstrip("/")
doc_path = str(path.relative_to(lang_docs_root_path))
res = process_one_file_with_retry(document_path=doc_path, config=config)
all_good = all_good and res
if not res:
pages_with_errors.append(doc_path)
if not all_good:
print("Some documents had errors:")
docs_path = lang_docs_root_path.relative_to(ROOT)
for page in pages_with_errors:
print(f" - {docs_path / page}")
raise typer.Exit(code=1)
if __name__ == "__main__":
cli()

View File

@@ -19,7 +19,13 @@ from slugify import slugify as py_slugify
logging.basicConfig(level=logging.INFO)
SUPPORTED_LANGS = {"en", "de", "es", "pt", "ru"}
SUPPORTED_LANGS = {
"en",
"de",
"es",
"pt",
"ru",
}
app = typer.Typer()
@@ -232,27 +238,15 @@ def generate_readme() -> None:
"""
Generate README.md content from main index.md
"""
typer.echo("Generating README")
readme_path = Path("README.md")
old_content = readme_path.read_text()
new_content = generate_readme_content()
readme_path.write_text(new_content, encoding="utf-8")
@app.command()
def verify_readme() -> None:
"""
Verify README.md content from main index.md
"""
typer.echo("Verifying README")
readme_path = Path("README.md")
generated_content = generate_readme_content()
readme_content = readme_path.read_text("utf-8")
if generated_content != readme_content:
typer.secho(
"README.md outdated from the latest index.md", color=typer.colors.RED
)
raise typer.Abort()
typer.echo("Valid README ✅")
if new_content != old_content:
print("README.md outdated from the latest index.md")
print("Updating README.md")
readme_path.write_text(new_content, encoding="utf-8")
raise typer.Exit(1)
print("README.md is up to date ✅")
@app.command()
@@ -280,7 +274,17 @@ def update_languages() -> None:
"""
Update the mkdocs.yml file Languages section including all the available languages.
"""
update_config()
old_config = get_en_config()
updated_config = get_updated_config_content()
if old_config != updated_config:
print("docs/en/mkdocs.yml outdated")
print("Updating docs/en/mkdocs.yml")
en_config_path.write_text(
yaml.dump(updated_config, sort_keys=False, width=200, allow_unicode=True),
encoding="utf-8",
)
raise typer.Exit(1)
print("docs/en/mkdocs.yml is up to date ✅")
@app.command()
@@ -367,39 +371,12 @@ def get_updated_config_content() -> dict[str, Any]:
return config
def update_config() -> None:
config = get_updated_config_content()
en_config_path.write_text(
yaml.dump(config, sort_keys=False, width=200, allow_unicode=True),
encoding="utf-8",
)
@app.command()
def verify_config() -> None:
def ensure_non_translated() -> None:
"""
Verify main mkdocs.yml content to make sure it uses the latest language names.
Ensure there are no files in the non translatable pages.
"""
typer.echo("Verifying mkdocs.yml")
config = get_en_config()
updated_config = get_updated_config_content()
if config != updated_config:
typer.secho(
"docs/en/mkdocs.yml outdated from docs/language_names.yml, "
"update language_names.yml and run "
"python ./scripts/docs.py update-languages",
color=typer.colors.RED,
)
raise typer.Abort()
typer.echo("Valid mkdocs.yml ✅")
@app.command()
def verify_non_translated() -> None:
"""
Verify there are no files in the non translatable pages.
"""
print("Verifying non translated pages")
print("Ensuring no non translated pages")
lang_paths = get_lang_paths()
error_paths = []
for lang in lang_paths:
@@ -410,20 +387,17 @@ def verify_non_translated() -> None:
if non_translatable_path.exists():
error_paths.append(non_translatable_path)
if error_paths:
print("Non-translated pages found, remove them:")
print("Non-translated pages found, removing them:")
for error_path in error_paths:
print(error_path)
raise typer.Abort()
if error_path.is_file():
error_path.unlink()
else:
shutil.rmtree(error_path)
raise typer.Exit(1)
print("No non-translated pages found ✅")
@app.command()
def verify_docs():
verify_readme()
verify_config()
verify_non_translated()
@app.command()
def langs_json():
langs = []

View File

@@ -727,7 +727,7 @@ def translate_page(
print(f"Found existing translation: {out_path}")
old_translation = out_path.read_text(encoding="utf-8")
print(f"Translating {en_path} to {language} ({language_name})")
agent = Agent("openai:gpt-5")
agent = Agent("openai:gpt-5.2")
prompt_segments = [
general_prompt,

View File

@@ -1,5 +1,6 @@
import json
import sys
import warnings
from collections.abc import Iterator
from typing import Annotated, Any
@@ -84,96 +85,103 @@ def app(basemodel_class: type[Any]) -> FastAPI:
app = FastAPI()
@app.post("/sync/validated", response_model=ItemOut)
def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]):
return ItemOut(name=item.name, value=item.value, dep=dep)
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=DeprecationWarning,
)
@app.get("/sync/dict-no-response-model")
def sync_dict_no_response_model():
return {"name": "foo", "value": 123}
@app.post("/sync/validated", response_model=ItemOut)
def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]):
return ItemOut(name=item.name, value=item.value, dep=dep)
@app.get("/sync/dict-with-response-model", response_model=ItemOut)
def sync_dict_with_response_model(
dep: Annotated[int, Depends(dep_b)],
):
return {"name": "foo", "value": 123, "dep": dep}
@app.get("/sync/dict-no-response-model")
def sync_dict_no_response_model():
return {"name": "foo", "value": 123}
@app.get("/sync/model-no-response-model")
def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]):
return ItemOut(name="foo", value=123, dep=dep)
@app.get("/sync/dict-with-response-model", response_model=ItemOut)
def sync_dict_with_response_model(
dep: Annotated[int, Depends(dep_b)],
):
return {"name": "foo", "value": 123, "dep": dep}
@app.get("/sync/model-with-response-model", response_model=ItemOut)
def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]):
return ItemOut(name="foo", value=123, dep=dep)
@app.get("/sync/model-no-response-model")
def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]):
return ItemOut(name="foo", value=123, dep=dep)
@app.post("/async/validated", response_model=ItemOut)
async def async_validated(
item: ItemIn,
dep: Annotated[int, Depends(dep_b)],
):
return ItemOut(name=item.name, value=item.value, dep=dep)
@app.get("/sync/model-with-response-model", response_model=ItemOut)
def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]):
return ItemOut(name="foo", value=123, dep=dep)
@app.post("/sync/large-receive")
def sync_large_receive(payload: LargeIn):
return {"received": len(payload.items)}
@app.post("/async/validated", response_model=ItemOut)
async def async_validated(
item: ItemIn,
dep: Annotated[int, Depends(dep_b)],
):
return ItemOut(name=item.name, value=item.value, dep=dep)
@app.post("/async/large-receive")
async def async_large_receive(payload: LargeIn):
return {"received": len(payload.items)}
@app.post("/sync/large-receive")
def sync_large_receive(payload: LargeIn):
return {"received": len(payload.items)}
@app.get("/sync/large-dict-no-response-model")
def sync_large_dict_no_response_model():
return LARGE_PAYLOAD
@app.post("/async/large-receive")
async def async_large_receive(payload: LargeIn):
return {"received": len(payload.items)}
@app.get("/sync/large-dict-with-response-model", response_model=LargeOut)
def sync_large_dict_with_response_model():
return LARGE_PAYLOAD
@app.get("/sync/large-dict-no-response-model")
def sync_large_dict_no_response_model():
return LARGE_PAYLOAD
@app.get("/sync/large-model-no-response-model")
def sync_large_model_no_response_model():
return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)
@app.get("/sync/large-dict-with-response-model", response_model=LargeOut)
def sync_large_dict_with_response_model():
return LARGE_PAYLOAD
@app.get("/sync/large-model-with-response-model", response_model=LargeOut)
def sync_large_model_with_response_model():
return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)
@app.get("/sync/large-model-no-response-model")
def sync_large_model_no_response_model():
return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)
@app.get("/async/large-dict-no-response-model")
async def async_large_dict_no_response_model():
return LARGE_PAYLOAD
@app.get("/sync/large-model-with-response-model", response_model=LargeOut)
def sync_large_model_with_response_model():
return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)
@app.get("/async/large-dict-with-response-model", response_model=LargeOut)
async def async_large_dict_with_response_model():
return LARGE_PAYLOAD
@app.get("/async/large-dict-no-response-model")
async def async_large_dict_no_response_model():
return LARGE_PAYLOAD
@app.get("/async/large-model-no-response-model")
async def async_large_model_no_response_model():
return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)
@app.get("/async/large-dict-with-response-model", response_model=LargeOut)
async def async_large_dict_with_response_model():
return LARGE_PAYLOAD
@app.get("/async/large-model-with-response-model", response_model=LargeOut)
async def async_large_model_with_response_model():
return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)
@app.get("/async/large-model-no-response-model")
async def async_large_model_no_response_model():
return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)
@app.get("/async/dict-no-response-model")
async def async_dict_no_response_model():
return {"name": "foo", "value": 123}
@app.get("/async/large-model-with-response-model", response_model=LargeOut)
async def async_large_model_with_response_model():
return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)
@app.get("/async/dict-with-response-model", response_model=ItemOut)
async def async_dict_with_response_model(
dep: Annotated[int, Depends(dep_b)],
):
return {"name": "foo", "value": 123, "dep": dep}
@app.get("/async/dict-no-response-model")
async def async_dict_no_response_model():
return {"name": "foo", "value": 123}
@app.get("/async/model-no-response-model")
async def async_model_no_response_model(
dep: Annotated[int, Depends(dep_b)],
):
return ItemOut(name="foo", value=123, dep=dep)
@app.get("/async/dict-with-response-model", response_model=ItemOut)
async def async_dict_with_response_model(
dep: Annotated[int, Depends(dep_b)],
):
return {"name": "foo", "value": 123, "dep": dep}
@app.get("/async/model-with-response-model", response_model=ItemOut)
async def async_model_with_response_model(
dep: Annotated[int, Depends(dep_b)],
):
return ItemOut(name="foo", value=123, dep=dep)
@app.get("/async/model-no-response-model")
async def async_model_no_response_model(
dep: Annotated[int, Depends(dep_b)],
):
return ItemOut(name="foo", value=123, dep=dep)
@app.get("/async/model-with-response-model", response_model=ItemOut)
async def async_model_with_response_model(
dep: Annotated[int, Depends(dep_b)],
):
return ItemOut(name="foo", value=123, dep=dep)
return app

View File

@@ -1,4 +1,5 @@
import sys
import warnings
from typing import Optional
import pytest
@@ -33,94 +34,90 @@ class Item(BaseModel):
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/items/{item_id}")
def get_item_with_path(
item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)],
):
return {"item_id": item_id}
@app.get("/items/{item_id}")
def get_item_with_path(
item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)],
):
return {"item_id": item_id}
@app.get("/items/")
def get_items_with_query(
q: Annotated[
Optional[str],
Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$"),
] = None,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10,
):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
def get_items_with_query(
q: Annotated[
Optional[str], Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$")
] = None,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10,
):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/users/")
def get_user_with_header(
x_custom: Annotated[Optional[str], Header()] = None,
x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None,
):
return {"x_custom": x_custom, "x_token": x_token}
@app.get("/cookies/")
def get_cookies(
session_id: Annotated[Optional[str], Cookie()] = None,
tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None,
):
return {"session_id": session_id, "tracking_id": tracking_id}
@app.get("/users/")
def get_user_with_header(
x_custom: Annotated[Optional[str], Header()] = None,
x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None,
):
return {"x_custom": x_custom, "x_token": x_token}
@app.post("/items/")
def create_item(
item: Annotated[
Item,
Body(
examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]
),
],
):
return {"item": item}
@app.post("/items-embed/")
def create_item_embed(
item: Annotated[Item, Body(embed=True)],
):
return {"item": item}
@app.get("/cookies/")
def get_cookies(
session_id: Annotated[Optional[str], Cookie()] = None,
tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None,
):
return {"session_id": session_id, "tracking_id": tracking_id}
@app.put("/items/{item_id}")
def update_item(
item_id: Annotated[int, Path(ge=1)],
item: Annotated[Item, Body()],
importance: Annotated[int, Body(gt=0, le=10)],
):
return {"item": item, "importance": importance}
@app.post("/form-data/")
def submit_form(
username: Annotated[str, Form(min_length=3, max_length=50)],
password: Annotated[str, Form(min_length=8)],
email: Annotated[Optional[str], Form()] = None,
):
return {"username": username, "password": password, "email": email}
@app.post("/items/")
def create_item(
item: Annotated[
Item,
Body(examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]),
],
):
return {"item": item}
@app.post("/upload/")
def upload_file(
file: Annotated[bytes, File()],
description: Annotated[Optional[str], Form()] = None,
):
return {"file_size": len(file), "description": description}
@app.post("/items-embed/")
def create_item_embed(
item: Annotated[Item, Body(embed=True)],
):
return {"item": item}
@app.put("/items/{item_id}")
def update_item(
item_id: Annotated[int, Path(ge=1)],
item: Annotated[Item, Body()],
importance: Annotated[int, Body(gt=0, le=10)],
):
return {"item": item, "importance": importance}
@app.post("/form-data/")
def submit_form(
username: Annotated[str, Form(min_length=3, max_length=50)],
password: Annotated[str, Form(min_length=8)],
email: Annotated[Optional[str], Form()] = None,
):
return {"username": username, "password": password, "email": email}
@app.post("/upload/")
def upload_file(
file: Annotated[bytes, File()],
description: Annotated[Optional[str], Form()] = None,
):
return {"file_size": len(file), "description": description}
@app.post("/upload-multiple/")
def upload_multiple_files(
files: Annotated[list[bytes], File()],
note: Annotated[str, Form()] = "",
):
return {
"file_count": len(files),
"total_size": sum(len(f) for f in files),
"note": note,
}
@app.post("/upload-multiple/")
def upload_multiple_files(
files: Annotated[list[bytes], File()],
note: Annotated[str, Form()] = "",
):
return {
"file_count": len(files),
"total_size": sum(len(f) for f in files),
"note": note,
}
client = TestClient(app)
@@ -211,10 +208,10 @@ def test_header_params_none():
# Cookie parameter tests
def test_cookie_params():
with TestClient(app) as client:
client.cookies.set("session_id", "abc123")
client.cookies.set("tracking_id", "1234567890abcdef")
response = client.get("/cookies/")
with TestClient(app) as test_client:
test_client.cookies.set("session_id", "abc123")
test_client.cookies.set("tracking_id", "1234567890abcdef")
response = test_client.get("/cookies/")
assert response.status_code == 200
assert response.json() == {
"session_id": "abc123",
@@ -223,9 +220,9 @@ def test_cookie_params():
def test_cookie_tracking_id_too_short():
with TestClient(app) as client:
client.cookies.set("tracking_id", "short")
response = client.get("/cookies/")
with TestClient(app) as test_client:
test_client.cookies.set("tracking_id", "short")
response = test_client.get("/cookies/")
assert response.status_code == 422
assert response.json() == snapshot(
{

View File

@@ -1,3 +1,4 @@
import warnings
from datetime import datetime, timezone
from fastapi import FastAPI
@@ -48,9 +49,12 @@ def test_pydanticv1():
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
client = TestClient(app)
with client:

View File

@@ -1,3 +1,4 @@
import warnings
from typing import Optional
from fastapi import Depends, FastAPI
@@ -31,11 +32,14 @@ async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {
"name": name,
"description": "model-a-desc",
"model_b": model_c,
"tags": {"key1": "value1", "key2": "value2"},
}
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {
"name": name,
"description": "model-a-desc",
"model_b": model_c,
"tags": {"key1": "value1", "key2": "value2"},
}

View File

@@ -1,3 +1,5 @@
import warnings
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -36,12 +38,28 @@ def client_fixture(request: pytest.FixtureRequest) -> TestClient:
app = FastAPI()
@app.get("/facilities/{facility_id}")
def get_facility(facility_id: str) -> Facility:
return Facility(
id=facility_id,
address=Address(line_1="123 Main St", city="Anytown", state_province="CA"),
)
if request.param == "pydantic-v1":
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/facilities/{facility_id}")
def get_facility(facility_id: str) -> Facility:
return Facility(
id=facility_id,
address=Address(
line_1="123 Main St", city="Anytown", state_province="CA"
),
)
else:
@app.get("/facilities/{facility_id}")
def get_facility(facility_id: str) -> Facility:
return Facility(
id=facility_id,
address=Address(
line_1="123 Main St", city="Anytown", state_province="CA"
),
)
client = TestClient(app)
return client

View File

@@ -0,0 +1,98 @@
import sys
import pytest
from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14):
skip_module_if_py_gte_314()
from fastapi import FastAPI
from fastapi._compat.v1 import BaseModel
from fastapi.testclient import TestClient
def test_warns_pydantic_v1_model_in_endpoint_param() -> None:
class ParamModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.warns(
DeprecationWarning,
match=r"pydantic\.v1 is deprecated.*Please update the param data:",
):
@app.post("/param")
def endpoint(data: ParamModelV1):
return data
client = TestClient(app)
response = client.post("/param", json={"name": "test"})
assert response.status_code == 200, response.text
assert response.json() == {"name": "test"}
def test_warns_pydantic_v1_model_in_return_type() -> None:
class ReturnModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.warns(
DeprecationWarning,
match=r"pydantic\.v1 is deprecated.*Please update the response model",
):
@app.get("/return")
def endpoint() -> ReturnModelV1:
return ReturnModelV1(name="test")
client = TestClient(app)
response = client.get("/return")
assert response.status_code == 200, response.text
assert response.json() == {"name": "test"}
def test_warns_pydantic_v1_model_in_response_model() -> None:
class ResponseModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.warns(
DeprecationWarning,
match=r"pydantic\.v1 is deprecated.*Please update the response model",
):
@app.get("/response-model", response_model=ResponseModelV1)
def endpoint():
return {"name": "test"}
client = TestClient(app)
response = client.get("/response-model")
assert response.status_code == 200, response.text
assert response.json() == {"name": "test"}
def test_warns_pydantic_v1_model_in_additional_responses_model() -> None:
class ErrorModelV1(BaseModel):
detail: str
app = FastAPI()
with pytest.warns(
DeprecationWarning,
match=r"pydantic\.v1 is deprecated.*In responses=\{\}, please update",
):
@app.get(
"/responses", response_model=None, responses={400: {"model": ErrorModelV1}}
)
def endpoint():
return {"ok": True}
client = TestClient(app)
response = client.get("/responses")
assert response.status_code == 200, response.text
assert response.json() == {"ok": True}

View File

@@ -1,4 +1,5 @@
import sys
import warnings
from typing import Any, Union
from tests.utils import skip_module_if_py_gte_314
@@ -26,30 +27,29 @@ class Item(BaseModel):
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/simple-model")
def handle_simple_model(data: SubItem) -> SubItem:
return data
@app.post("/simple-model")
def handle_simple_model(data: SubItem) -> SubItem:
return data
@app.post("/simple-model-filter", response_model=SubItem)
def handle_simple_model_filter(data: SubItem) -> Any:
extended_data = data.dict()
extended_data.update({"secret_price": 42})
return extended_data
@app.post("/simple-model-filter", response_model=SubItem)
def handle_simple_model_filter(data: SubItem) -> Any:
extended_data = data.dict()
extended_data.update({"secret_price": 42})
return extended_data
@app.post("/item")
def handle_item(data: Item) -> Item:
return data
@app.post("/item")
def handle_item(data: Item) -> Item:
return data
@app.post("/item-filter", response_model=Item)
def handle_item_filter(data: Item) -> Any:
extended_data = data.dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return extended_data
@app.post("/item-filter", response_model=Item)
def handle_item_filter(data: Item) -> Any:
extended_data = data.dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return extended_data
client = TestClient(app)

View File

@@ -1,4 +1,5 @@
import sys
import warnings
from typing import Any, Union
from tests.utils import skip_module_if_py_gte_314
@@ -27,49 +28,47 @@ class Item(BaseModel):
app = FastAPI()
@app.post("/item")
def handle_item(data: Item) -> list[Item]:
return [data, data]
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/item")
def handle_item(data: Item) -> list[Item]:
return [data, data]
@app.post("/item-filter", response_model=list[Item])
def handle_item_filter(data: Item) -> Any:
extended_data = data.dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return [extended_data, extended_data]
@app.post("/item-list")
def handle_item_list(data: list[Item]) -> Item:
if data:
return data[0]
return Item(title="", size=0, sub=SubItem(name=""))
@app.post("/item-list-filter", response_model=Item)
def handle_item_list_filter(data: list[Item]) -> Any:
if data:
extended_data = data[0].dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return extended_data
return Item(title="", size=0, sub=SubItem(name=""))
@app.post("/item-list-to-list")
def handle_item_list_to_list(data: list[Item]) -> list[Item]:
return data
@app.post("/item-list-to-list-filter", response_model=list[Item])
def handle_item_list_to_list_filter(data: list[Item]) -> Any:
if data:
extended_data = data[0].dict()
@app.post("/item-filter", response_model=list[Item])
def handle_item_filter(data: Item) -> Any:
extended_data = data.dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return [extended_data, extended_data]
return []
@app.post("/item-list")
def handle_item_list(data: list[Item]) -> Item:
if data:
return data[0]
return Item(title="", size=0, sub=SubItem(name=""))
@app.post("/item-list-filter", response_model=Item)
def handle_item_list_filter(data: list[Item]) -> Any:
if data:
extended_data = data[0].dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return extended_data
return Item(title="", size=0, sub=SubItem(name=""))
@app.post("/item-list-to-list")
def handle_item_list_to_list(data: list[Item]) -> list[Item]:
return data
@app.post("/item-list-to-list-filter", response_model=list[Item])
def handle_item_list_to_list_filter(data: list[Item]) -> Any:
if data:
extended_data = data[0].dict()
extended_data.update({"secret_data": "classified", "internal_id": 12345})
extended_data["sub"].update({"internal_id": 67890})
return [extended_data, extended_data]
return []
client = TestClient(app)

View File

@@ -1,4 +1,5 @@
import sys
import warnings
from typing import Any, Union
from tests.utils import skip_module_if_py_gte_314
@@ -39,179 +40,181 @@ class NewItem(NewBaseModel):
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/v1-to-v2/item")
def handle_v1_item_to_v2(data: Item) -> NewItem:
return NewItem(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=NewSubItem(new_sub_name=data.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
)
@app.post("/v1-to-v2/item")
def handle_v1_item_to_v2(data: Item) -> NewItem:
return NewItem(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=NewSubItem(new_sub_name=data.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
)
@app.post("/v1-to-v2/item-filter", response_model=NewItem)
def handle_v1_item_to_v2_filter(data: Item) -> Any:
result = {
"new_title": data.title,
"new_size": data.size,
"new_description": data.description,
"new_sub": {
"new_sub_name": data.sub.name,
"new_sub_secret": "sub_hidden",
},
"new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
for s in data.multi
],
"secret": "hidden_v1_to_v2",
}
return result
@app.post("/v1-to-v2/item-filter", response_model=NewItem)
def handle_v1_item_to_v2_filter(data: Item) -> Any:
result = {
"new_title": data.title,
"new_size": data.size,
"new_description": data.description,
"new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"},
"new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi
],
"secret": "hidden_v1_to_v2",
}
return result
@app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: NewItem) -> Item:
return Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=SubItem(name=data.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
)
@app.post("/v2-to-v1/item-filter", response_model=Item)
def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
result = {
"title": data.new_title,
"size": data.new_size,
"description": data.new_description,
"sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
"multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"}
for s in data.new_multi
],
"secret": "hidden_v2_to_v1",
}
return result
@app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: NewItem) -> Item:
return Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=SubItem(name=data.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
)
@app.post("/v1-to-v2/item-to-list")
def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]:
converted = NewItem(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=NewSubItem(new_sub_name=data.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
)
return [converted, converted]
@app.post("/v1-to-v2/list-to-list")
def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]:
result = []
for item in data:
result.append(
NewItem(
new_title=item.title,
new_size=item.size,
new_description=item.description,
new_sub=NewSubItem(new_sub_name=item.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
)
)
return result
@app.post("/v2-to-v1/item-filter", response_model=Item)
def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
result = {
"title": data.new_title,
"size": data.new_size,
"description": data.new_description,
"sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
"multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi
],
"secret": "hidden_v2_to_v1",
}
return result
@app.post("/v1-to-v2/list-to-list-filter", response_model=list[NewItem])
def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any:
result = []
for item in data:
converted = {
"new_title": item.title,
"new_size": item.size,
"new_description": item.description,
"new_sub": {
"new_sub_name": item.sub.name,
"new_sub_secret": "sub_hidden",
},
"new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
for s in item.multi
],
"secret": "hidden_v2_to_v1",
}
result.append(converted)
return result
@app.post("/v1-to-v2/item-to-list")
def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]:
converted = NewItem(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=NewSubItem(new_sub_name=data.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
)
return [converted, converted]
@app.post("/v1-to-v2/list-to-list")
def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]:
result = []
for item in data:
result.append(
NewItem(
@app.post("/v1-to-v2/list-to-item")
def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem:
if data:
item = data[0]
return NewItem(
new_title=item.title,
new_size=item.size,
new_description=item.description,
new_sub=NewSubItem(new_sub_name=item.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
)
return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name=""))
@app.post("/v2-to-v1/item-to-list")
def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]:
converted = Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=SubItem(name=data.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
)
return result
return [converted, converted]
@app.post("/v2-to-v1/list-to-list")
def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]:
result = []
for item in data:
result.append(
Item(
title=item.new_title,
size=item.new_size,
description=item.new_description,
sub=SubItem(name=item.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
)
)
return result
@app.post("/v1-to-v2/list-to-list-filter", response_model=list[NewItem])
def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any:
result = []
for item in data:
converted = {
"new_title": item.title,
"new_size": item.size,
"new_description": item.description,
"new_sub": {"new_sub_name": item.sub.name, "new_sub_secret": "sub_hidden"},
"new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
for s in item.multi
],
"secret": "hidden_v2_to_v1",
}
result.append(converted)
return result
@app.post("/v2-to-v1/list-to-list-filter", response_model=list[Item])
def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any:
result = []
for item in data:
converted = {
"title": item.new_title,
"size": item.new_size,
"description": item.new_description,
"sub": {
"name": item.new_sub.new_sub_name,
"sub_secret": "sub_hidden",
},
"multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"}
for s in item.new_multi
],
"secret": "hidden_v2_to_v1",
}
result.append(converted)
return result
@app.post("/v1-to-v2/list-to-item")
def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem:
if data:
item = data[0]
return NewItem(
new_title=item.title,
new_size=item.size,
new_description=item.description,
new_sub=NewSubItem(new_sub_name=item.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
)
return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name=""))
@app.post("/v2-to-v1/item-to-list")
def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]:
converted = Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=SubItem(name=data.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
)
return [converted, converted]
@app.post("/v2-to-v1/list-to-list")
def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]:
result = []
for item in data:
result.append(
Item(
@app.post("/v2-to-v1/list-to-item")
def handle_v2_list_to_v1_item(data: list[NewItem]) -> Item:
if data:
item = data[0]
return Item(
title=item.new_title,
size=item.new_size,
description=item.new_description,
sub=SubItem(name=item.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
)
)
return result
@app.post("/v2-to-v1/list-to-list-filter", response_model=list[Item])
def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any:
result = []
for item in data:
converted = {
"title": item.new_title,
"size": item.new_size,
"description": item.new_description,
"sub": {"name": item.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
"multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"}
for s in item.new_multi
],
"secret": "hidden_v2_to_v1",
}
result.append(converted)
return result
@app.post("/v2-to-v1/list-to-item")
def handle_v2_list_to_v1_item(data: list[NewItem]) -> Item:
if data:
item = data[0]
return Item(
title=item.new_title,
size=item.new_size,
description=item.new_description,
sub=SubItem(name=item.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
)
return Item(title="", size=0, sub=SubItem(name=""))
return Item(title="", size=0, sub=SubItem(name=""))
client = TestClient(app)

View File

@@ -1,140 +1,137 @@
import warnings
from fastapi import FastAPI
from . import modelsv1, modelsv2, modelsv2b
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/v1-to-v2/item")
def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item:
return modelsv2.Item(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=modelsv2.SubItem(new_sub_name=data.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi],
)
@app.post("/v1-to-v2/item")
def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item:
return modelsv2.Item(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=modelsv2.SubItem(new_sub_name=data.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi],
)
@app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item:
return modelsv1.Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=modelsv1.SubItem(name=data.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi],
)
@app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item:
return modelsv1.Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=modelsv1.SubItem(name=data.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi],
)
@app.post("/v1-to-v2/item-to-list")
def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]:
converted = modelsv2.Item(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=modelsv2.SubItem(new_sub_name=data.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi],
)
return [converted, converted]
@app.post("/v1-to-v2/list-to-list")
def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]:
result = []
for item in data:
result.append(
modelsv2.Item(
new_title=item.title,
new_size=item.size,
new_description=item.description,
new_sub=modelsv2.SubItem(new_sub_name=item.sub.name),
new_multi=[
modelsv2.SubItem(new_sub_name=s.name) for s in item.multi
],
)
)
return result
@app.post("/v1-to-v2/item-to-list")
def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]:
converted = modelsv2.Item(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=modelsv2.SubItem(new_sub_name=data.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi],
)
return [converted, converted]
@app.post("/v1-to-v2/list-to-list")
def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]:
result = []
for item in data:
result.append(
modelsv2.Item(
@app.post("/v1-to-v2/list-to-item")
def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item:
if data:
item = data[0]
return modelsv2.Item(
new_title=item.title,
new_size=item.size,
new_description=item.description,
new_sub=modelsv2.SubItem(new_sub_name=item.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi],
)
)
return result
@app.post("/v1-to-v2/list-to-item")
def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item:
if data:
item = data[0]
return modelsv2.Item(
new_title=item.title,
new_size=item.size,
new_description=item.description,
new_sub=modelsv2.SubItem(new_sub_name=item.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi],
new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="")
)
return modelsv2.Item(
new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="")
)
@app.post("/v2-to-v1/item-to-list")
def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]:
converted = modelsv1.Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=modelsv1.SubItem(name=data.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi],
)
return [converted, converted]
@app.post("/v2-to-v1/item-to-list")
def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]:
converted = modelsv1.Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=modelsv1.SubItem(name=data.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi],
)
return [converted, converted]
@app.post("/v2-to-v1/list-to-list")
def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]:
result = []
for item in data:
result.append(
modelsv1.Item(
title=item.new_title,
size=item.new_size,
description=item.new_description,
sub=modelsv1.SubItem(name=item.new_sub.new_sub_name),
multi=[
modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi
],
)
)
return result
@app.post("/v2-to-v1/list-to-list")
def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]:
result = []
for item in data:
result.append(
modelsv1.Item(
@app.post("/v2-to-v1/list-to-item")
def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item:
if data:
item = data[0]
return modelsv1.Item(
title=item.new_title,
size=item.new_size,
description=item.new_description,
sub=modelsv1.SubItem(name=item.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi],
)
)
return result
return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name=""))
@app.post("/v2-to-v1/list-to-item")
def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item:
if data:
item = data[0]
@app.post("/v2-to-v1/same-name")
def handle_v2_same_name_to_v1(
item1: modelsv2.Item, item2: modelsv2b.Item
) -> modelsv1.Item:
return modelsv1.Item(
title=item.new_title,
size=item.new_size,
description=item.new_description,
sub=modelsv1.SubItem(name=item.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi],
title=item1.new_title,
size=item2.dup_size,
description=item1.new_description,
sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi],
)
return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name=""))
@app.post("/v2-to-v1/same-name")
def handle_v2_same_name_to_v1(
item1: modelsv2.Item, item2: modelsv2b.Item
) -> modelsv1.Item:
return modelsv1.Item(
title=item1.new_title,
size=item2.dup_size,
description=item1.new_description,
sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi],
)
@app.post("/v2-to-v1/list-of-items-to-list-of-items")
def handle_v2_items_in_list_to_v1_item_in_list(
data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList]
) -> list[modelsv1.ItemInList]:
result = []
item1 = data1[0]
item2 = data2[0]
result = [
modelsv1.ItemInList(name1=item1.name2),
modelsv1.ItemInList(name1=item2.dup_name2),
]
return result
@app.post("/v2-to-v1/list-of-items-to-list-of-items")
def handle_v2_items_in_list_to_v1_item_in_list(
data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList]
) -> list[modelsv1.ItemInList]:
item1 = data1[0]
item2 = data2[0]
return [
modelsv1.ItemInList(name1=item1.name2),
modelsv1.ItemInList(name1=item2.dup_name2),
]

View File

@@ -1,4 +1,5 @@
import sys
import warnings
from typing import Any, Union
from tests.utils import skip_module_if_py_gte_314
@@ -39,65 +40,69 @@ class NewItem(NewBaseModel):
app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/v1-to-v2/")
def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]:
if data.size < 0:
return None
return NewItem(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=NewSubItem(new_sub_name=data.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
)
@app.post("/v1-to-v2/")
def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]:
if data.size < 0:
return None
return NewItem(
new_title=data.title,
new_size=data.size,
new_description=data.description,
new_sub=NewSubItem(new_sub_name=data.sub.name),
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
)
@app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None])
def handle_v1_item_to_v2_filter(data: Item) -> Any:
if data.size < 0:
return None
result = {
"new_title": data.title,
"new_size": data.size,
"new_description": data.description,
"new_sub": {
"new_sub_name": data.sub.name,
"new_sub_secret": "sub_hidden",
},
"new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
for s in data.multi
],
"secret": "hidden_v1_to_v2",
}
return result
@app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None])
def handle_v1_item_to_v2_filter(data: Item) -> Any:
if data.size < 0:
return None
result = {
"new_title": data.title,
"new_size": data.size,
"new_description": data.description,
"new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"},
"new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi
],
"secret": "hidden_v1_to_v2",
}
return result
@app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]:
if data.new_size < 0:
return None
return Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=SubItem(name=data.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
)
@app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]:
if data.new_size < 0:
return None
return Item(
title=data.new_title,
size=data.new_size,
description=data.new_description,
sub=SubItem(name=data.new_sub.new_sub_name),
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
)
@app.post("/v2-to-v1/item-filter", response_model=Union[Item, None])
def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
if data.new_size < 0:
return None
result = {
"title": data.new_title,
"size": data.new_size,
"description": data.new_description,
"sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
"multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi
],
"secret": "hidden_v2_to_v1",
}
return result
@app.post("/v2-to-v1/item-filter", response_model=Union[Item, None])
def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
if data.new_size < 0:
return None
result = {
"title": data.new_title,
"size": data.new_size,
"description": data.new_description,
"sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
"multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"}
for s in data.new_multi
],
"secret": "hidden_v2_to_v1",
}
return result
client = TestClient(app)

View File

@@ -1,3 +1,4 @@
import warnings
from typing import Any
from fastapi import FastAPI
@@ -73,10 +74,13 @@ def test_read_with_orm_mode_pv1() -> None:
app = FastAPI()
@app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person)
return db_person
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person)
return db_person
client = TestClient(app)

View File

@@ -1,3 +1,4 @@
import warnings
from typing import Union
import pytest
@@ -521,11 +522,14 @@ def test_invalid_response_model_field_pv1():
class Model(v1.BaseModel):
foo: str
with pytest.raises(FastAPIError) as e:
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/")
def read_root() -> Union[Response, Model, None]:
return Response(content="Foo") # pragma: no cover
with pytest.raises(FastAPIError) as e:
@app.get("/")
def read_root() -> Union[Response, Model, None]:
return Response(content="Foo") # pragma: no cover
assert "valid Pydantic field type" in e.value.args[0]
assert "parameter response_model=None" in e.value.args[0]

View File

@@ -1,4 +1,5 @@
import sys
import warnings
import pytest
from inline_snapshot import snapshot
@@ -24,7 +25,13 @@ from ...utils import needs_py310
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=DeprecationWarning,
)
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
c = TestClient(mod.app)
return c

View File

@@ -1,4 +1,5 @@
import sys
import warnings
import pytest
from inline_snapshot import snapshot
@@ -24,7 +25,13 @@ from ...utils import needs_py310
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=DeprecationWarning,
)
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
c = TestClient(mod.app)
return c

View File

@@ -1,4 +1,5 @@
import sys
import warnings
import pytest
from inline_snapshot import snapshot
@@ -24,7 +25,13 @@ from ...utils import needs_py310
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=DeprecationWarning,
)
mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
c = TestClient(mod.app)
return c

View File

@@ -1,4 +1,5 @@
import importlib
import warnings
import pytest
from fastapi.testclient import TestClient
@@ -14,7 +15,13 @@ from ...utils import needs_pydanticv1
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.request_form_models.{request.param}")
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=DeprecationWarning,
)
mod = importlib.import_module(f"docs_src.request_form_models.{request.param}")
client = TestClient(mod.app)
return client

View File

@@ -1,4 +1,5 @@
import importlib
import warnings
import pytest
from fastapi.testclient import TestClient
@@ -15,7 +16,13 @@ from ...utils import needs_py310, needs_pydanticv1
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}")
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=DeprecationWarning,
)
mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}")
client = TestClient(mod.app)
return client