diff --git a/.github/labeler.yml b/.github/labeler.yml
index 1d49a24116570..c5b1f84f3d942 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -7,6 +7,8 @@ docs:
- all-globs-to-all-files:
- '!fastapi/**'
- '!pyproject.toml'
+ - '!docs/en/data/sponsors.yml'
+ - '!docs/en/overrides/main.html'
lang-all:
- all:
@@ -28,6 +30,8 @@ internal:
- .pre-commit-config.yaml
- pdm_build.py
- requirements*.txt
+ - docs/en/data/sponsors.yml
+ - docs/en/overrides/main.html
- all-globs-to-all-files:
- '!docs/*/docs/**'
- '!fastapi/**'
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index e46629e9b40d2..52c34a49ea6ba 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -113,6 +113,7 @@ jobs:
with:
name: docs-site-${{ matrix.lang }}
path: ./site/**
+ include-hidden-files: true
# https://github.com/marketplace/actions/alls-green#why
docs-all-green: # This job does nothing and is only used for the branch protection
diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml
index d5b947a9c52a2..439084434dd0b 100644
--- a/.github/workflows/issue-manager.yml
+++ b/.github/workflows/issue-manager.yml
@@ -2,7 +2,7 @@ name: Issue Manager
on:
schedule:
- - cron: "10 3 * * *"
+ - cron: "13 22 * * *"
issue_comment:
types:
- created
@@ -16,6 +16,7 @@ on:
permissions:
issues: write
+ pull-requests: write
jobs:
issue-manager:
@@ -26,7 +27,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: tiangolo/issue-manager@0.5.0
+ - uses: tiangolo/issue-manager@0.5.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
config: >
@@ -35,8 +36,8 @@ jobs:
"delay": 864000,
"message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs."
},
- "changes-requested": {
+ "waiting": {
"delay": 2628000,
- "message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR."
+ "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR."
}
}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 591df634b201b..5004b94ddb5a6 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -35,7 +35,7 @@ jobs:
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }}
run: python -m build
- name: Publish
- uses: pypa/gh-action-pypi-publish@v1.9.0
+ uses: pypa/gh-action-pypi-publish@v1.10.1
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0458f83ffe208..fb4b083c4e0c1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -37,7 +37,7 @@ jobs:
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
- name: Install Pydantic v2
- run: pip install "pydantic>=2.0.2,<3.0.0"
+ run: pip install --upgrade "pydantic>=2.0.2,<3.0.0"
- name: Lint
run: bash scripts/lint.sh
@@ -79,7 +79,7 @@ jobs:
run: pip install "pydantic>=1.10.0,<2.0.0"
- name: Install Pydantic v2
if: matrix.pydantic-version == 'pydantic-v2'
- run: pip install "pydantic>=2.0.2,<3.0.0"
+ run: pip install --upgrade "pydantic>=2.0.2,<3.0.0"
- run: mkdir coverage
- name: Test
run: bash scripts/test.sh
@@ -91,6 +91,7 @@ jobs:
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
path: coverage
+ include-hidden-files: true
coverage-combine:
needs: [test]
@@ -123,6 +124,7 @@ jobs:
with:
name: coverage-html
path: htmlcov
+ include-hidden-files: true
# https://github.com/marketplace/actions/alls-green#why
check: # This job does nothing and is only used for the branch protection
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3175140622488..f74816f12352c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,7 +14,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.6.2
+ rev: v0.6.4
hooks:
- id: ruff
args:
diff --git a/README.md b/README.md
index 5554f71d48da7..3b01b713a2f4c 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ The key features are:
-
+
diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml
index 15f6169eef815..63fd3d0cf8618 100644
--- a/docs/en/data/external_links.yml
+++ b/docs/en/data/external_links.yml
@@ -264,6 +264,14 @@ Articles:
author_link: https://devonray.com
link: https://devonray.com/blog/deploying-a-fastapi-project-using-aws-lambda-aurora-cdk
title: Deployment using Docker, Lambda, Aurora, CDK & GH Actions
+ - author: Shubhendra Kushwaha
+ author_link: https://www.linkedin.com/in/theshubhendra/
+ link: https://theshubhendra.medium.com/mastering-soft-delete-advanced-sqlalchemy-techniques-4678f4738947
+ title: 'Mastering Soft Delete: Advanced SQLAlchemy Techniques'
+ - author: Shubhendra Kushwaha
+ author_link: https://www.linkedin.com/in/theshubhendra/
+ link: https://theshubhendra.medium.com/role-based-row-filtering-advanced-sqlalchemy-techniques-733e6b1328f6
+ title: 'Role based row filtering: Advanced SQLAlchemy Techniques'
German:
- author: Marcel Sander (actidoo)
author_link: https://www.actidoo.com
diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml
index 3a767b6b18c4c..d96646fb39c6e 100644
--- a/docs/en/data/sponsors.yml
+++ b/docs/en/data/sponsors.yml
@@ -17,7 +17,7 @@ gold:
- url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge
title: Auth, user management and more for your B2B product
img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png
- - url: https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs
+ - url: https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website
title: Coherence
img: https://fastapi.tiangolo.com/img/sponsors/coherence.png
- url: https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral
diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md
index 3ea5087f8c967..41ada859d4fc2 100644
--- a/docs/en/docs/deployment/cloud.md
+++ b/docs/en/docs/deployment/cloud.md
@@ -14,4 +14,4 @@ You might want to try their services and follow their guides:
* Platform.sh
* Porter
-* Coherence
+* Coherence
diff --git a/docs/en/docs/environment-variables.md b/docs/en/docs/environment-variables.md
index 78e82d5af65a4..43dd06add3d56 100644
--- a/docs/en/docs/environment-variables.md
+++ b/docs/en/docs/environment-variables.md
@@ -243,8 +243,6 @@ This way, when you type `python` in the terminal, the system will find the Pytho
////
-This way, when you type `python` in the terminal, the system will find the Python program in `/opt/custompython/bin` (the last directory) and use that one.
-
So, if you type:
-
+
diff --git a/docs/ko/docs/project-generation.md b/docs/ko/docs/project-generation.md
new file mode 100644
index 0000000000000..019919f776c87
--- /dev/null
+++ b/docs/ko/docs/project-generation.md
@@ -0,0 +1,28 @@
+# Full Stack FastAPI 템플릿
+
+템플릿은 일반적으로 특정 설정과 함께 제공되지만, 유연하고 커스터마이징이 가능하게 디자인 되었습니다. 이 특성들은 여러분이 프로젝트의 요구사항에 맞춰 수정, 적용을 할 수 있게 해주고, 템플릿이 완벽한 시작점이 되게 해줍니다. 🏁
+
+많은 초기 설정, 보안, 데이터베이스 및 일부 API 엔드포인트가 이미 준비되어 있으므로, 여러분은 이 템플릿을 (프로젝트를) 시작하는 데 사용할 수 있습니다.
+
+GitHub 저장소:
Full Stack FastAPI 템플릿
+
+## Full Stack FastAPI 템플릿 - 기술 스택과 기능들
+
+- ⚡ [**FastAPI**](https://fastapi.tiangolo.com): Python 백엔드 API.
+ - 🧰 [SQLModel](https://sqlmodel.tiangolo.com): Python SQL 데이터 상호작용을 위한 (ORM).
+ - 🔍 [Pydantic](https://docs.pydantic.dev): FastAPI에 의해 사용되는, 데이터 검증과 설정관리.
+ - 💾 [PostgreSQL](https://www.postgresql.org): SQL 데이터베이스.
+- 🚀 [React](https://react.dev): 프론트엔드.
+ - 💃 TypeScript, hooks, Vite 및 기타 현대적인 프론트엔드 스택을 사용.
+ - 🎨 [Chakra UI](https://chakra-ui.com): 프론트엔드 컴포넌트.
+ - 🤖 자동으로 생성된 프론트엔드 클라이언트.
+ - 🧪 E2E 테스트를 위한 Playwright.
+ - 🦇 다크 모드 지원.
+- 🐋 [Docker Compose](https://www.docker.com): 개발 환경과 프로덕션(운영).
+- 🔒 기본으로 지원되는 안전한 비밀번호 해싱.
+- 🔑 JWT 토큰 인증.
+- 📫 이메일 기반 비밀번호 복구.
+- ✅ [Pytest]를 이용한 테스트(https://pytest.org).
+- 📞 [Traefik](https://traefik.io): 리버스 프록시 / 로드 밸런서.
+- 🚢 Docker Compose를 이용한 배포 지침: 자동 HTTPS 인증서를 처리하기 위한 프론트엔드 Traefik 프록시 설정 방법을 포함.
+- 🏭 GitHub Actions를 기반으로 CI (지속적인 통합) 및 CD (지속적인 배포).
diff --git a/docs/nl/docs/features.md b/docs/nl/docs/features.md
new file mode 100644
index 0000000000000..848b155eced6a
--- /dev/null
+++ b/docs/nl/docs/features.md
@@ -0,0 +1,201 @@
+# Functionaliteit
+
+## FastAPI functionaliteit
+
+**FastAPI** biedt je het volgende:
+
+### Gebaseerd op open standaarden
+
+*
OpenAPI voor het maken van API's, inclusief declaraties van
padbewerkingen, parameters, request bodies, beveiliging, enz.
+* Automatische datamodel documentatie met
JSON Schema (aangezien OpenAPI zelf is gebaseerd op JSON Schema).
+* Ontworpen op basis van deze standaarden, na zorgvuldig onderzoek. In plaats van achteraf deze laag er bovenop te bouwen.
+* Dit maakt het ook mogelijk om automatisch **clientcode te genereren** in verschillende programmeertalen.
+
+### Automatische documentatie
+
+Interactieve API-documentatie en verkenning van webgebruikersinterfaces. Aangezien dit framework is gebaseerd op OpenAPI, zijn er meerdere documentatie opties mogelijk, waarvan er standaard 2 zijn inbegrepen.
+
+*
Swagger UI, met interactieve interface, maakt het mogelijk je API rechtstreeks vanuit de browser aan te roepen en te testen.
+
+
+
+* Alternatieve API-documentatie met
ReDoc.
+
+
+
+### Gewoon Moderne Python
+
+Het is allemaal gebaseerd op standaard **Python type** declaraties (dankzij Pydantic). Je hoeft dus geen nieuwe syntax te leren. Het is gewoon standaard moderne Python.
+
+Als je een opfriscursus van 2 minuten nodig hebt over het gebruik van Python types (zelfs als je FastAPI niet gebruikt), bekijk dan deze korte tutorial: [Python Types](python-types.md){.internal-link target=_blank}.
+
+Je schrijft gewoon standaard Python met types:
+
+```Python
+from datetime import date
+
+from pydantic import BaseModel
+
+# Declareer een variabele als een str
+# en krijg editorondersteuning in de functie
+def main(user_id: str):
+ return user_id
+
+
+# Een Pydantic model
+class User(BaseModel):
+ id: int
+ name: str
+ joined: date
+```
+
+Vervolgens kan je het op deze manier gebruiken:
+
+```Python
+my_user: User = User(id=3, name="John Doe", joined="2018-07-19")
+
+second_user_data = {
+ "id": 4,
+ "name": "Mary",
+ "joined": "2018-11-30",
+}
+
+my_second_user: User = User(**second_user_data)
+```
+
+/// info
+
+`**second_user_data` betekent:
+
+Geef de sleutels (keys) en waarden (values) van de `second_user_data` dict direct door als sleutel-waarden argumenten, gelijk aan: `User(id=4, name=“Mary”, joined=“2018-11-30”)`
+
+///
+
+### Editor-ondersteuning
+
+Het gehele framework is ontworpen om eenvoudig en intuïtief te zijn in gebruik. Alle beslissingen zijn getest op meerdere code-editors nog voordat het daadwerkelijke ontwikkelen begon, om zo de beste ontwikkelervaring te garanderen.
+
+Uit enquêtes onder Python ontwikkelaars blijkt maar al te duidelijk dat "(automatische) code aanvulling"
een van de meest gebruikte functionaliteiten is.
+
+Het hele **FastAPI** framework is daarop gebaseerd. Automatische code aanvulling werkt overal.
+
+Je hoeft zelden terug te vallen op de documentatie.
+
+Zo kan je editor je helpen:
+
+* in
Visual Studio Code:
+
+
+
+* in
PyCharm:
+
+
+
+Je krijgt autocomletion die je voorheen misschien zelfs voor onmogelijk had gehouden. Zoals bijvoorbeeld de `price` key in een JSON body (die genest had kunnen zijn) die afkomstig is van een request.
+
+Je hoeft niet langer de verkeerde keys in te typen, op en neer te gaan tussen de documentatie, of heen en weer te scrollen om te checken of je `username` of toch `user_name` had gebruikt.
+
+### Kort
+
+Dit framework heeft voor alles verstandige **standaardinstellingen**, met overal optionele configuraties. Alle parameters kunnen worden verfijnd zodat het past bij wat je nodig hebt, om zo de API te kunnen definiëren die jij nodig hebt.
+
+Maar standaard werkt alles **“gewoon”**.
+
+### Validatie
+
+* Validatie voor de meeste (of misschien wel alle?) Python **datatypes**, inclusief:
+ * JSON objecten (`dict`).
+ * JSON array (`list`) die itemtypes definiëren.
+ * String (`str`) velden, die min en max lengtes hebben.
+ * Getallen (`int`, `float`) met min en max waarden, enz.
+
+* Validatie voor meer exotische typen, zoals:
+ * URL.
+ * E-mail.
+ * UUID.
+ * ...en anderen.
+
+Alle validatie wordt uitgevoerd door het beproefde en robuuste **Pydantic**.
+
+### Beveiliging en authenticatie
+
+Beveiliging en authenticatie is geïntegreerd. Zonder compromissen te doen naar databases of datamodellen.
+
+Alle beveiligingsschema's gedefinieerd in OpenAPI, inclusief:
+
+* HTTP Basic.
+* **OAuth2** (ook met **JWT tokens**). Bekijk de tutorial over [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}.
+* API keys in:
+ * Headers.
+ * Query parameters.
+ * Cookies, enz.
+
+Plus alle beveiligingsfuncties van Starlette (inclusief **sessiecookies**).
+
+Gebouwd als een herbruikbare tool met componenten die makkelijk te integreren zijn in en met je systemen, datastores, relationele en NoSQL databases, enz.
+
+### Dependency Injection
+
+FastAPI bevat een uiterst eenvoudig, maar uiterst krachtig
Dependency Injection systeem.
+
+* Zelfs dependencies kunnen dependencies hebben, waardoor een hiërarchie of **“graph” van dependencies** ontstaat.
+* Allemaal **automatisch afgehandeld** door het framework.
+* Alle dependencies kunnen data nodig hebben van request, de vereiste **padoperaties veranderen** en automatische documentatie verstrekken.
+* **Automatische validatie** zelfs voor *padoperatie* parameters gedefinieerd in dependencies.
+* Ondersteuning voor complexe gebruikersauthenticatiesystemen, **databaseverbindingen**, enz.
+* **Geen compromisen** met databases, gebruikersinterfaces, enz. Maar eenvoudige integratie met ze allemaal.
+
+### Ongelimiteerde "plug-ins"
+
+Of anders gezegd, je hebt ze niet nodig, importeer en gebruik de code die je nodig hebt.
+
+Elke integratie is ontworpen om eenvoudig te gebruiken (met afhankelijkheden), zodat je een “plug-in" kunt maken in 2 regels code, met dezelfde structuur en syntax die wordt gebruikt voor je *padbewerkingen*.
+
+### Getest
+
+* 100%
van de code is getest.
+* 100%
type geannoteerde codebase.
+* Wordt gebruikt in productietoepassingen.
+
+## Starlette functies
+
+**FastAPI** is volledig verenigbaar met (en gebaseerd op)
Starlette.
+
+`FastAPI` is eigenlijk een subklasse van `Starlette`. Dus als je Starlette al kent of gebruikt, zal de meeste functionaliteit op dezelfde manier werken.
+
+Met **FastAPI** krijg je alle functies van **Starlette** (FastAPI is gewoon Starlette op steroïden):
+
+* Zeer indrukwekkende prestaties. Het is
een van de snelste Python frameworks, vergelijkbaar met **NodeJS** en **Go**.
+* **WebSocket** ondersteuning.
+* Taken in de achtergrond tijdens het proces.
+* Opstart- en afsluit events.
+* Test client gebouwd op HTTPX.
+* **CORS**, GZip, Statische bestanden, Streaming reacties.
+* **Sessie en Cookie** ondersteuning.
+* 100% van de code is getest.
+* 100% type geannoteerde codebase.
+
+## Pydantic functionaliteit
+
+**FastAPI** is volledig verenigbaar met (en gebaseerd op) Pydantic. Dus alle extra
Pydantic code die je nog hebt werkt ook.
+
+Inclusief externe pakketten die ook gebaseerd zijn op Pydantic, zoals
ORMs,
ODMs voor databases.
+
+Dit betekent ook dat je in veel gevallen het object dat je van een request krijgt **direct naar je database** kunt sturen, omdat alles automatisch wordt gevalideerd.
+
+Hetzelfde geldt ook andersom, in veel gevallen kun je dus het object dat je krijgt van de database **direct doorgeven aan de client**.
+
+Met **FastAPI** krijg je alle functionaliteit van **Pydantic** (omdat FastAPI is gebaseerd op Pydantic voor alle dataverwerking):
+
+* **Geen brainfucks**:
+ * Je hoeft geen nieuwe microtaal voor schemadefinities te leren.
+ * Als je bekend bent Python types, weet je hoe je Pydantic moet gebruiken.
+* Werkt goed samen met je **
IDE/
linter/hersenen**:
+ * Doordat pydantic's datastructuren enkel instanties zijn van klassen, die je definieert, werkt automatische aanvulling, linting, mypy en je intuïtie allemaal goed met je gevalideerde data.
+* Valideer **complexe structuren**:
+ * Gebruik van hiërarchische Pydantic modellen, Python `typing`'s `List` en `Dict`, enz.
+ * Met validators kunnen complexe dataschema's duidelijk en eenvoudig worden gedefinieerd, gecontroleerd en gedocumenteerd als JSON Schema.
+ * Je kunt diep **geneste JSON** objecten laten valideren en annoteren.
+* **Uitbreidbaar**:
+ * Met Pydantic kunnen op maat gemaakte datatypen worden gedefinieerd of je kunt validatie uitbreiden met methoden op een model dat is ingericht met de decorator validator.
+* 100% van de code is getest.
diff --git a/docs/nl/docs/python-types.md b/docs/nl/docs/python-types.md
new file mode 100644
index 0000000000000..a5562b7950a04
--- /dev/null
+++ b/docs/nl/docs/python-types.md
@@ -0,0 +1,597 @@
+# Introductie tot Python Types
+
+Python biedt ondersteuning voor optionele "type hints" (ook wel "type annotaties" genoemd).
+
+Deze **"type hints"** of annotaties zijn een speciale syntax waarmee het
type van een variabele kan worden gedeclareerd.
+
+Door types voor je variabelen te declareren, kunnen editors en hulpmiddelen je beter ondersteunen.
+
+Dit is slechts een **korte tutorial/opfrisser** over Python type hints. Het behandelt enkel het minimum dat nodig is om ze te gebruiken met **FastAPI**... en dat is relatief weinig.
+
+**FastAPI** is helemaal gebaseerd op deze type hints, ze geven veel voordelen.
+
+Maar zelfs als je **FastAPI** nooit gebruikt, heb je er baat bij om er iets over te leren.
+
+/// note
+
+Als je een Python expert bent en alles al weet over type hints, sla dan dit hoofdstuk over.
+
+///
+
+## Motivatie
+
+Laten we beginnen met een eenvoudig voorbeeld:
+
+```Python
+{!../../../docs_src/python_types/tutorial001.py!}
+```
+
+Het aanroepen van dit programma leidt tot het volgende resultaat:
+
+```
+John Doe
+```
+
+De functie voert het volgende uit:
+
+* Neem een `first_name` en een `last_name`
+* Converteer de eerste letter van elk naar een hoofdletter met `title()`.
+``
+*
Voeg samen met een spatie in het midden.
+
+```Python hl_lines="2"
+{!../../../docs_src/python_types/tutorial001.py!}
+```
+
+### Bewerk het
+
+Dit is een heel eenvoudig programma.
+
+Maar stel je nu voor dat je het vanaf nul zou moeten maken.
+
+Op een gegeven moment zou je aan de definitie van de functie zijn begonnen, je had de parameters klaar...
+
+Maar dan moet je “die methode die de eerste letter naar hoofdletters converteert” aanroepen.
+
+Was het `upper`? Was het `uppercase`? `first_uppercase`? `capitalize`?
+
+Dan roep je de hulp in van je oude programmeursvriend, (automatische) code aanvulling in je editor.
+
+Je typt de eerste parameter van de functie, `first_name`, dan een punt (`.`) en drukt dan op `Ctrl+Spatie` om de aanvulling te activeren.
+
+Maar helaas krijg je niets bruikbaars:
+
+

+
+### Types toevoegen
+
+Laten we een enkele regel uit de vorige versie aanpassen.
+
+We zullen precies dit fragment, de parameters van de functie, wijzigen van:
+
+```Python
+ first_name, last_name
+```
+
+naar:
+
+```Python
+ first_name: str, last_name: str
+```
+
+Dat is alles.
+
+Dat zijn de "type hints":
+
+```Python hl_lines="1"
+{!../../../docs_src/python_types/tutorial002.py!}
+```
+
+Dit is niet hetzelfde als het declareren van standaardwaarden zoals bij:
+
+```Python
+ first_name="john", last_name="doe"
+```
+
+Het is iets anders.
+
+We gebruiken dubbele punten (`:`), geen gelijkheidstekens (`=`).
+
+Het toevoegen van type hints verandert normaal gesproken niet wat er gebeurt in je programma t.o.v. wat er zonder type hints zou gebeuren.
+
+Maar stel je voor dat je weer bezig bent met het maken van een functie, maar deze keer met type hints.
+
+Op hetzelfde moment probeer je de automatische aanvulling te activeren met `Ctrl+Spatie` en je ziet:
+
+

+
+Nu kun je de opties bekijken en er doorheen scrollen totdat je de optie vindt die “een belletje doet rinkelen”:
+
+

+
+### Meer motivatie
+
+Bekijk deze functie, deze heeft al type hints:
+
+```Python hl_lines="1"
+{!../../../docs_src/python_types/tutorial003.py!}
+```
+
+Omdat de editor de types van de variabelen kent, krijgt u niet alleen aanvulling, maar ook controles op fouten:
+
+

+
+Nu weet je hoe je het moet oplossen, converteer `age` naar een string met `str(age)`:
+
+```Python hl_lines="2"
+{!../../../docs_src/python_types/tutorial004.py!}
+```
+
+## Types declareren
+
+Je hebt net de belangrijkste plek om type hints te declareren gezien. Namelijk als functieparameters.
+
+Dit is ook de belangrijkste plek waar je ze gebruikt met **FastAPI**.
+
+### Eenvoudige types
+
+Je kunt alle standaard Python types declareren, niet alleen `str`.
+
+Je kunt bijvoorbeeld het volgende gebruiken:
+
+* `int`
+* `float`
+* `bool`
+* `bytes`
+
+```Python hl_lines="1"
+{!../../../docs_src/python_types/tutorial005.py!}
+```
+
+### Generieke types met typeparameters
+
+Er zijn enkele datastructuren die andere waarden kunnen bevatten, zoals `dict`, `list`, `set` en `tuple` en waar ook de interne waarden hun eigen type kunnen hebben.
+
+Deze types die interne types hebben worden “**generieke**” types genoemd. Het is mogelijk om ze te declareren, zelfs met hun interne types.
+
+Om deze types en de interne types te declareren, kun je de standaard Python module `typing` gebruiken. Deze module is speciaal gemaakt om deze type hints te ondersteunen.
+
+#### Nieuwere versies van Python
+
+De syntax met `typing` is **verenigbaar** met alle versies, van Python 3.6 tot aan de nieuwste, inclusief Python 3.9, Python 3.10, enz.
+
+Naarmate Python zich ontwikkelt, worden **nieuwere versies**, met verbeterde ondersteuning voor deze type annotaties, beschikbaar. In veel gevallen hoef je niet eens de `typing` module te importeren en te gebruiken om de type annotaties te declareren.
+
+Als je een recentere versie van Python kunt kiezen voor je project, kun je profiteren van die extra eenvoud.
+
+In alle documentatie staan voorbeelden die compatibel zijn met elke versie van Python (als er een verschil is).
+
+Bijvoorbeeld “**Python 3.6+**” betekent dat het compatibel is met Python 3.6 of hoger (inclusief 3.7, 3.8, 3.9, 3.10, etc). En “**Python 3.9+**” betekent dat het compatibel is met Python 3.9 of hoger (inclusief 3.10, etc).
+
+Als je de **laatste versies van Python** kunt gebruiken, gebruik dan de voorbeelden voor de laatste versie, die hebben de **beste en eenvoudigste syntax**, bijvoorbeeld “**Python 3.10+**”.
+
+#### List
+
+Laten we bijvoorbeeld een variabele definiëren als een `list` van `str`.
+
+//// tab | Python 3.9+
+
+Declareer de variabele met dezelfde dubbele punt (`:`) syntax.
+
+Als type, vul `list` in.
+
+Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes:
+
+```Python hl_lines="1"
+{!> ../../../docs_src/python_types/tutorial006_py39.py!}
+```
+
+////
+
+//// tab | Python 3.8+
+
+Van `typing`, importeer `List` (met een hoofdletter `L`):
+
+```Python hl_lines="1"
+{!> ../../../docs_src/python_types/tutorial006.py!}
+```
+
+Declareer de variabele met dezelfde dubbele punt (`:`) syntax.
+
+Zet als type de `List` die je hebt geïmporteerd uit `typing`.
+
+Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes:
+
+```Python hl_lines="4"
+{!> ../../../docs_src/python_types/tutorial006.py!}
+```
+
+////
+
+/// info
+
+De interne types tussen vierkante haakjes worden “typeparameters” genoemd.
+
+In dit geval is `str` de typeparameter die wordt doorgegeven aan `List` (of `list` in Python 3.9 en hoger).
+
+///
+
+Dat betekent: “de variabele `items` is een `list`, en elk van de items in deze list is een `str`”.
+
+/// tip
+
+Als je Python 3.9 of hoger gebruikt, hoef je `List` niet te importeren uit `typing`, je kunt in plaats daarvan hetzelfde reguliere `list` type gebruiken.
+
+///
+
+Door dat te doen, kan je editor ondersteuning bieden, zelfs tijdens het verwerken van items uit de list:
+
+

+
+Zonder types is dat bijna onmogelijk om te bereiken.
+
+Merk op dat de variabele `item` een van de elementen is in de lijst `items`.
+
+Toch weet de editor dat het een `str` is, en biedt daar vervolgens ondersteuning voor aan.
+
+#### Tuple en Set
+
+Je kunt hetzelfde doen om `tuple`s en `set`s te declareren:
+
+//// tab | Python 3.9+
+
+```Python hl_lines="1"
+{!> ../../../docs_src/python_types/tutorial007_py39.py!}
+```
+
+////
+
+//// tab | Python 3.8+
+
+```Python hl_lines="1 4"
+{!> ../../../docs_src/python_types/tutorial007.py!}
+```
+
+////
+
+Dit betekent:
+
+* De variabele `items_t` is een `tuple` met 3 items, een `int`, nog een `int`, en een `str`.
+* De variabele `items_s` is een `set`, en elk van de items is van het type `bytes`.
+
+#### Dict
+
+Om een `dict` te definiëren, geef je 2 typeparameters door, gescheiden door komma's.
+
+De eerste typeparameter is voor de sleutels (keys) van de `dict`.
+
+De tweede typeparameter is voor de waarden (values) van het `dict`:
+
+//// tab | Python 3.9+
+
+```Python hl_lines="1"
+{!> ../../../docs_src/python_types/tutorial008_py39.py!}
+```
+
+////
+
+//// tab | Python 3.8+
+
+```Python hl_lines="1 4"
+{!> ../../../docs_src/python_types/tutorial008.py!}
+```
+
+////
+
+Dit betekent:
+
+* De variabele `prices` is een `dict`:
+ * De sleutels van dit `dict` zijn van het type `str` (bijvoorbeeld de naam van elk item).
+ * De waarden van dit `dict` zijn van het type `float` (bijvoorbeeld de prijs van elk item).
+
+#### Union
+
+Je kunt een variable declareren die van **verschillende types** kan zijn, bijvoorbeeld een `int` of een `str`.
+
+In Python 3.6 en hoger (inclusief Python 3.10) kun je het `Union`-type van `typing` gebruiken en de mogelijke types die je wilt accepteren, tussen de vierkante haakjes zetten.
+
+In Python 3.10 is er ook een **nieuwe syntax** waarin je de mogelijke types kunt scheiden door een
verticale balk (`|`).
+
+//// tab | Python 3.10+
+
+```Python hl_lines="1"
+{!> ../../../docs_src/python_types/tutorial008b_py310.py!}
+```
+
+////
+
+//// tab | Python 3.8+
+
+```Python hl_lines="1 4"
+{!> ../../../docs_src/python_types/tutorial008b.py!}
+```
+
+////
+
+In beide gevallen betekent dit dat `item` een `int` of een `str` kan zijn.
+
+#### Mogelijk `None`
+
+Je kunt declareren dat een waarde een type kan hebben, zoals `str`, maar dat het ook `None` kan zijn.
+
+In Python 3.6 en hoger (inclusief Python 3.10) kun je het declareren door `Optional` te importeren en te gebruiken vanuit de `typing`-module.
+
+```Python hl_lines="1 4"
+{!../../../docs_src/python_types/tutorial009.py!}
+```
+
+Door `Optional[str]` te gebruiken in plaats van alleen `str`, kan de editor je helpen fouten te detecteren waarbij je ervan uit zou kunnen gaan dat een waarde altijd een `str` is, terwijl het in werkelijkheid ook `None` zou kunnen zijn.
+
+`Optional[EenType]` is eigenlijk een snelkoppeling voor `Union[EenType, None]`, ze zijn equivalent.
+
+Dit betekent ook dat je in Python 3.10 `EenType | None` kunt gebruiken:
+
+//// tab | Python 3.10+
+
+```Python hl_lines="1"
+{!> ../../../docs_src/python_types/tutorial009_py310.py!}
+```
+
+////
+
+//// tab | Python 3.8+
+
+```Python hl_lines="1 4"
+{!> ../../../docs_src/python_types/tutorial009.py!}
+```
+
+////
+
+//// tab | Python 3.8+ alternative
+
+```Python hl_lines="1 4"
+{!> ../../../docs_src/python_types/tutorial009b.py!}
+```
+
+////
+
+#### Gebruik van `Union` of `Optional`
+
+Als je een Python versie lager dan 3.10 gebruikt, is dit een tip vanuit mijn **subjectieve** standpunt:
+
+* 🚨 Vermijd het gebruik van `Optional[EenType]`.
+* Gebruik in plaats daarvan **`Union[EenType, None]`** ✨.
+
+Beide zijn gelijkwaardig en onderliggend zijn ze hetzelfde, maar ik zou `Union` aanraden in plaats van `Optional` omdat het woord “**optional**” lijkt te impliceren dat de waarde optioneel is, en het eigenlijk betekent “het kan `None` zijn”, zelfs als het niet optioneel is en nog steeds vereist is.
+
+Ik denk dat `Union[SomeType, None]` explicieter is over wat het betekent.
+
+Het gaat alleen om de woorden en naamgeving. Maar die naamgeving kan invloed hebben op hoe jij en je teamgenoten over de code denken.
+
+Laten we als voorbeeld deze functie nemen:
+
+```Python hl_lines="1 4"
+{!../../../docs_src/python_types/tutorial009c.py!}
+```
+
+De parameter `name` is gedefinieerd als `Optional[str]`, maar is **niet optioneel**, je kunt de functie niet aanroepen zonder de parameter:
+
+```Python
+say_hi() # Oh, nee, dit geeft een foutmelding! 😱
+```
+
+De `name` parameter is **nog steeds vereist** (niet *optioneel*) omdat het geen standaardwaarde heeft. Toch accepteert `name` `None` als waarde:
+
+```Python
+say_hi(name=None) # Dit werkt, None is geldig 🎉
+```
+
+Het goede nieuws is dat als je eenmaal Python 3.10 gebruikt, je je daar geen zorgen meer over hoeft te maken, omdat je dan gewoon `|` kunt gebruiken om unions van types te definiëren:
+
+```Python hl_lines="1 4"
+{!../../../docs_src/python_types/tutorial009c_py310.py!}
+```
+
+Dan hoef je je geen zorgen te maken over namen als `Optional` en `Union`. 😎
+
+#### Generieke typen
+
+De types die typeparameters in vierkante haakjes gebruiken, worden **Generieke types** of **Generics** genoemd, bijvoorbeeld:
+
+//// tab | Python 3.10+
+
+Je kunt dezelfde ingebouwde types gebruiken als generics (met vierkante haakjes en types erin):
+
+* `list`
+* `tuple`
+* `set`
+* `dict`
+
+Hetzelfde als bij Python 3.8, uit de `typing`-module:
+
+* `Union`
+* `Optional` (hetzelfde als bij Python 3.8)
+* ...en anderen.
+
+In Python 3.10 kun je , als alternatief voor de generieke `Union` en `Optional`, de
verticale lijn (`|`) gebruiken om unions van typen te voorzien, dat is veel beter en eenvoudiger.
+
+////
+
+//// tab | Python 3.9+
+
+Je kunt dezelfde ingebouwde types gebruiken als generieke types (met vierkante haakjes en types erin):
+
+* `list`
+* `tuple`
+* `set`
+* `dict`
+
+En hetzelfde als met Python 3.8, vanuit de `typing`-module:
+
+* `Union`
+* `Optional`
+* ...en anderen.
+
+////
+
+//// tab | Python 3.8+
+
+* `List`
+* `Tuple`
+* `Set`
+* `Dict`
+* `Union`
+* `Optional`
+* ...en anderen.
+
+////
+
+### Klassen als types
+
+Je kunt een klasse ook declareren als het type van een variabele.
+
+Stel dat je een klasse `Person` hebt, met een naam:
+
+```Python hl_lines="1-3"
+{!../../../docs_src/python_types/tutorial010.py!}
+```
+
+Vervolgens kun je een variabele van het type `Persoon` declareren:
+
+```Python hl_lines="6"
+{!../../../docs_src/python_types/tutorial010.py!}
+```
+
+Dan krijg je ook nog eens volledige editorondersteuning:
+
+

+
+Merk op dat dit betekent dat "`one_person` een **instantie** is van de klasse `Person`".
+
+Dit betekent niet dat `one_person` de **klasse** is met de naam `Person`.
+
+## Pydantic modellen
+
+
Pydantic is een Python-pakket voor het uitvoeren van datavalidatie.
+
+Je declareert de "vorm" van de data als klassen met attributen.
+
+Elk attribuut heeft een type.
+
+Vervolgens maak je een instantie van die klasse met een aantal waarden en het valideert de waarden, converteert ze naar het juiste type (als dat het geval is) en geeft je een object met alle data terug.
+
+Daarnaast krijg je volledige editorondersteuning met dat resulterende object.
+
+Een voorbeeld uit de officiële Pydantic-documentatie:
+
+//// tab | Python 3.10+
+
+```Python
+{!> ../../../docs_src/python_types/tutorial011_py310.py!}
+```
+
+////
+
+//// tab | Python 3.9+
+
+```Python
+{!> ../../../docs_src/python_types/tutorial011_py39.py!}
+```
+
+////
+
+//// tab | Python 3.8+
+
+```Python
+{!> ../../../docs_src/python_types/tutorial011.py!}
+```
+
+////
+
+/// info
+
+Om meer te leren over
Pydantic, bekijk de documentatie.
+
+///
+
+**FastAPI** is volledig gebaseerd op Pydantic.
+
+Je zult veel meer van dit alles in de praktijk zien in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}.
+
+/// tip
+
+Pydantic heeft een speciaal gedrag wanneer je `Optional` of `Union[EenType, None]` gebruikt zonder een standaardwaarde, je kunt er meer over lezen in de Pydantic-documentatie over
Verplichte optionele velden.
+
+///
+
+## Type Hints met Metadata Annotaties
+
+Python heeft ook een functie waarmee je **extra
metadata** in deze type hints kunt toevoegen met behulp van `Annotated`.
+
+//// tab | Python 3.9+
+
+In Python 3.9 is `Annotated` onderdeel van de standaardpakket, dus je kunt het importeren vanuit `typing`.
+
+```Python hl_lines="1 4"
+{!> ../../../docs_src/python_types/tutorial013_py39.py!}
+```
+
+////
+
+//// tab | Python 3.8+
+
+In versies lager dan Python 3.9 importeer je `Annotated` vanuit `typing_extensions`.
+
+Het wordt al geïnstalleerd met **FastAPI**.
+
+```Python hl_lines="1 4"
+{!> ../../../docs_src/python_types/tutorial013.py!}
+```
+
+////
+
+Python zelf doet niets met deze `Annotated` en voor editors en andere hulpmiddelen is het type nog steeds een `str`.
+
+Maar je kunt deze ruimte in `Annotated` gebruiken om **FastAPI** te voorzien van extra metadata over hoe je wilt dat je applicatie zich gedraagt.
+
+Het belangrijkste om te onthouden is dat **de eerste *typeparameter*** die je doorgeeft aan `Annotated` het **werkelijke type** is. De rest is gewoon metadata voor andere hulpmiddelen.
+
+Voor nu hoef je alleen te weten dat `Annotated` bestaat en dat het standaard Python is. 😎
+
+Later zul je zien hoe **krachtig** het kan zijn.
+
+/// tip
+
+Het feit dat dit **standaard Python** is, betekent dat je nog steeds de **best mogelijke ontwikkelaarservaring** krijgt in je editor, met de hulpmiddelen die je gebruikt om je code te analyseren en te refactoren, enz. ✨
+
+Daarnaast betekent het ook dat je code zeer verenigbaar zal zijn met veel andere Python-hulpmiddelen en -pakketten. 🚀
+
+///
+
+## Type hints in **FastAPI**
+
+**FastAPI** maakt gebruik van type hints om verschillende dingen te doen.
+
+Met **FastAPI** declareer je parameters met type hints en krijg je:
+
+* **Editor ondersteuning**.
+* **Type checks**.
+
+...en **FastAPI** gebruikt dezelfde declaraties om:
+
+* **Vereisten te definïeren **: van request pad parameters, query parameters, headers, bodies, dependencies, enz.
+* **Data te converteren**: van de request naar het vereiste type.
+* **Data te valideren**: afkomstig van elke request:
+ * **Automatische foutmeldingen** te genereren die naar de client worden geretourneerd wanneer de data ongeldig is.
+* De API met OpenAPI te **documenteren**:
+ * die vervolgens wordt gebruikt door de automatische interactieve documentatie gebruikersinterfaces.
+
+Dit klinkt misschien allemaal abstract. Maak je geen zorgen. Je ziet dit allemaal in actie in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}.
+
+Het belangrijkste is dat door standaard Python types te gebruiken, op één plek (in plaats van meer klassen, decorators, enz. toe te voegen), **FastAPI** een groot deel van het werk voor je doet.
+
+/// info
+
+Als je de hele tutorial al hebt doorgenomen en terug bent gekomen om meer te weten te komen over types, is een goede bron
het "cheat sheet" van `mypy`.
+
+///
diff --git a/docs/pt/docs/advanced/security/index.md b/docs/pt/docs/advanced/security/index.md
new file mode 100644
index 0000000000000..ae63f1c96d12e
--- /dev/null
+++ b/docs/pt/docs/advanced/security/index.md
@@ -0,0 +1,19 @@
+# Segurança Avançada
+
+## Funcionalidades Adicionais
+
+Existem algumas funcionalidades adicionais para lidar com segurança além das cobertas em [Tutorial - Guia de Usuário: Segurança](../../tutorial/security/index.md){.internal-link target=_blank}.
+
+/// tip | "Dica"
+
+As próximas seções **não são necessariamente "avançadas"**.
+
+E é possível que para o seu caso de uso, a solução está em uma delas.
+
+///
+
+## Leia o Tutorial primeiro
+
+As próximas seções pressupõem que você já leu o principal [Tutorial - Guia de Usuário: Segurança](../../tutorial/security/index.md){.internal-link target=_blank}.
+
+Todas elas são baseadas nos mesmos conceitos, mas permitem algumas funcionalidades extras.
diff --git a/docs/pt/docs/advanced/testing-events.md b/docs/pt/docs/advanced/testing-events.md
new file mode 100644
index 0000000000000..392fb741cac08
--- /dev/null
+++ b/docs/pt/docs/advanced/testing-events.md
@@ -0,0 +1,7 @@
+# Testando Eventos: inicialização - encerramento
+
+Quando você precisa que os seus manipuladores de eventos (`startup` e `shutdown`) sejam executados em seus testes, você pode utilizar o `TestClient` usando a instrução `with`:
+
+```Python hl_lines="9-12 20-24"
+{!../../../docs_src/app_testing/tutorial003.py!}
+```
diff --git a/docs/pt/docs/environment-variables.md b/docs/pt/docs/environment-variables.md
new file mode 100644
index 0000000000000..360d1c496bd49
--- /dev/null
+++ b/docs/pt/docs/environment-variables.md
@@ -0,0 +1,298 @@
+# Variáveis de Ambiente
+
+/// tip | "Dica"
+
+Se você já sabe o que são "variáveis de ambiente" e como usá-las, pode pular esta seção.
+
+///
+
+Uma variável de ambiente (também conhecida como "**env var**") é uma variável que existe **fora** do código Python, no **sistema operacional**, e pode ser lida pelo seu código Python (ou por outros programas também).
+
+Variáveis de ambiente podem ser úteis para lidar com **configurações** do aplicativo, como parte da **instalação** do Python, etc.
+
+## Criar e Usar Variáveis de Ambiente
+
+Você pode **criar** e usar variáveis de ambiente no **shell (terminal)**, sem precisar do Python:
+
+//// tab | Linux, macOS, Windows Bash
+
+
+
+```console
+// Você pode criar uma variável de ambiente MY_NAME com
+$ export MY_NAME="Wade Wilson"
+
+// Então você pode usá-la com outros programas, como
+$ echo "Hello $MY_NAME"
+
+Hello Wade Wilson
+```
+
+
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+// Criar uma variável de ambiente MY_NAME
+$ $Env:MY_NAME = "Wade Wilson"
+
+// Usá-la com outros programas, como
+$ echo "Hello $Env:MY_NAME"
+
+Hello Wade Wilson
+```
+
+
+
+////
+
+## Ler Variáveis de Ambiente no Python
+
+Você também pode criar variáveis de ambiente **fora** do Python, no terminal (ou com qualquer outro método) e depois **lê-las no Python**.
+
+Por exemplo, você poderia ter um arquivo `main.py` com:
+
+```Python hl_lines="3"
+import os
+
+name = os.getenv("MY_NAME", "World")
+print(f"Hello {name} from Python")
+```
+
+/// tip | "Dica"
+
+O segundo argumento para
`os.getenv()` é o valor padrão a ser retornado.
+
+Se não for fornecido, é `None` por padrão, Aqui fornecemos `"World"` como o valor padrão a ser usado.
+
+///
+
+Então você poderia chamar esse programa Python:
+
+//// tab | Linux, macOS, Windows Bash
+
+
+
+```console
+// Aqui ainda não definimos a variável de ambiente
+$ python main.py
+
+// Como não definimos a variável de ambiente, obtemos o valor padrão
+
+Hello World from Python
+
+// Mas se criarmos uma variável de ambiente primeiro
+$ export MY_NAME="Wade Wilson"
+
+// E então chamar o programa novamente
+$ python main.py
+
+// Agora ele pode ler a variável de ambiente
+
+Hello Wade Wilson from Python
+```
+
+
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+// Aqui ainda não definimos a variável de ambiente
+$ python main.py
+
+// Como não definimos a variável de ambiente, obtemos o valor padrão
+
+Hello World from Python
+
+// Mas se criarmos uma variável de ambiente primeiro
+$ $Env:MY_NAME = "Wade Wilson"
+
+// E então chamar o programa novamente
+$ python main.py
+
+// Agora ele pode ler a variável de ambiente
+
+Hello Wade Wilson from Python
+```
+
+
+
+////
+
+Como as variáveis de ambiente podem ser definidas fora do código, mas podem ser lidas pelo código e não precisam ser armazenadas (com versão no `git`) com o restante dos arquivos, é comum usá-las para configurações ou **definições**.
+
+Você também pode criar uma variável de ambiente apenas para uma **invocação específica do programa**, que só está disponível para aquele programa e apenas pela duração dele.
+
+Para fazer isso, crie-a na mesma linha, antes do próprio programa:
+
+
+
+```console
+// Criar uma variável de ambiente MY_NAME para esta chamada de programa
+$ MY_NAME="Wade Wilson" python main.py
+
+// Agora ele pode ler a variável de ambiente
+
+Hello Wade Wilson from Python
+
+// A variável de ambiente não existe mais depois
+$ python main.py
+
+Hello World from Python
+```
+
+
+
+/// tip | "Dica"
+
+Você pode ler mais sobre isso em
The Twelve-Factor App: Config.
+
+///
+
+## Tipos e Validação
+
+Essas variáveis de ambiente só podem lidar com **strings de texto**, pois são externas ao Python e precisam ser compatíveis com outros programas e com o resto do sistema (e até mesmo com diferentes sistemas operacionais, como Linux, Windows, macOS).
+
+Isso significa que **qualquer valor** lido em Python de uma variável de ambiente **será uma `str`**, e qualquer conversão para um tipo diferente ou qualquer validação precisa ser feita no código.
+
+Você aprenderá mais sobre como usar variáveis de ambiente para lidar com **configurações do aplicativo** no [Guia do Usuário Avançado - Configurações e Variáveis de Ambiente](./advanced/settings.md){.internal-link target=_blank}.
+
+## Variável de Ambiente `PATH`
+
+Existe uma variável de ambiente **especial** chamada **`PATH`** que é usada pelos sistemas operacionais (Linux, macOS, Windows) para encontrar programas para executar.
+
+O valor da variável `PATH` é uma longa string composta por diretórios separados por dois pontos `:` no Linux e macOS, e por ponto e vírgula `;` no Windows.
+
+Por exemplo, a variável de ambiente `PATH` poderia ter esta aparência:
+
+//// tab | Linux, macOS
+
+```plaintext
+/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+```
+
+Isso significa que o sistema deve procurar programas nos diretórios:
+
+* `/usr/local/bin`
+* `/usr/bin`
+* `/bin`
+* `/usr/sbin`
+* `/sbin`
+
+////
+
+//// tab | Windows
+
+```plaintext
+C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32
+```
+
+Isso significa que o sistema deve procurar programas nos diretórios:
+
+* `C:\Program Files\Python312\Scripts`
+* `C:\Program Files\Python312`
+* `C:\Windows\System32`
+
+////
+
+Quando você digita um **comando** no terminal, o sistema operacional **procura** o programa em **cada um dos diretórios** listados na variável de ambiente `PATH`.
+
+Por exemplo, quando você digita `python` no terminal, o sistema operacional procura um programa chamado `python` no **primeiro diretório** dessa lista.
+
+Se ele o encontrar, então ele o **usará**. Caso contrário, ele continua procurando nos **outros diretórios**.
+
+### Instalando o Python e Atualizando o `PATH`
+
+Durante a instalação do Python, você pode ser questionado sobre a atualização da variável de ambiente `PATH`.
+
+//// tab | Linux, macOS
+
+Vamos supor que você instale o Python e ele fique em um diretório `/opt/custompython/bin`.
+
+Se você concordar em atualizar a variável de ambiente `PATH`, o instalador adicionará `/opt/custompython/bin` para a variável de ambiente `PATH`.
+
+Poderia parecer assim:
+
+```plaintext
+/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin
+```
+
+Dessa forma, ao digitar `python` no terminal, o sistema encontrará o programa Python em `/opt/custompython/bin` (último diretório) e o utilizará.
+
+////
+
+//// tab | Windows
+
+Digamos que você instala o Python e ele acaba em um diretório `C:\opt\custompython\bin`.
+
+Se você disser sim para atualizar a variável de ambiente `PATH`, o instalador adicionará `C:\opt\custompython\bin` à variável de ambiente `PATH`.
+
+```plaintext
+C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin
+```
+
+Dessa forma, quando você digitar `python` no terminal, o sistema encontrará o programa Python em `C:\opt\custompython\bin` (o último diretório) e o utilizará.
+
+////
+
+Então, se você digitar:
+
+
+
+```console
+$ python
+```
+
+
+
+//// tab | Linux, macOS
+
+O sistema **encontrará** o programa `python` em `/opt/custompython/bin` e o executará.
+
+Seria aproximadamente equivalente a digitar:
+
+
+
+```console
+$ /opt/custompython/bin/python
+```
+
+
+
+////
+
+//// tab | Windows
+
+O sistema **encontrará** o programa `python` em `C:\opt\custompython\bin\python` e o executará.
+
+Seria aproximadamente equivalente a digitar:
+
+
+
+```console
+$ C:\opt\custompython\bin\python
+```
+
+
+
+////
+
+Essas informações serão úteis ao aprender sobre [Ambientes Virtuais](virtual-environments.md){.internal-link target=_blank}.
+
+## Conclusão
+
+Com isso, você deve ter uma compreensão básica do que são **variáveis de ambiente** e como usá-las em Python.
+
+Você também pode ler mais sobre elas na
Wikipedia para Variáveis de Ambiente.
+
+Em muitos casos, não é muito óbvio como as variáveis de ambiente seriam úteis e aplicáveis imediatamente. Mas elas continuam aparecendo em muitos cenários diferentes quando você está desenvolvendo, então é bom saber sobre elas.
+
+Por exemplo, você precisará dessas informações na próxima seção, sobre [Ambientes Virtuais](virtual-environments.md).
diff --git a/docs/pt/docs/tutorial/debugging.md b/docs/pt/docs/tutorial/debugging.md
new file mode 100644
index 0000000000000..54582fcbcb073
--- /dev/null
+++ b/docs/pt/docs/tutorial/debugging.md
@@ -0,0 +1,115 @@
+# Depuração
+
+Você pode conectar o depurador no seu editor, por exemplo, com o Visual Studio Code ou PyCharm.
+
+## Chamar `uvicorn`
+
+Em seu aplicativo FastAPI, importe e execute `uvicorn` diretamente:
+
+```Python hl_lines="1 15"
+{!../../../docs_src/debugging/tutorial001.py!}
+```
+
+### Sobre `__name__ == "__main__"`
+
+O objetivo principal de `__name__ == "__main__"` é ter algum código que seja executado quando seu arquivo for chamado com:
+
+
+
+```console
+$ python myapp.py
+```
+
+
+
+mas não é chamado quando outro arquivo o importa, como em:
+
+```Python
+from myapp import app
+```
+
+#### Mais detalhes
+
+Digamos que seu arquivo se chama `myapp.py`.
+
+Se você executá-lo com:
+
+
+
+```console
+$ python myapp.py
+```
+
+
+
+então a variável interna `__name__` no seu arquivo, criada automaticamente pelo Python, terá como valor a string `"__main__"`.
+
+Então, a seção:
+
+```Python
+ uvicorn.run(app, host="0.0.0.0", port=8000)
+```
+
+vai executar.
+
+---
+
+Isso não acontecerá se você importar esse módulo (arquivo).
+
+Então, se você tiver outro arquivo `importer.py` com:
+
+```Python
+from myapp import app
+
+# Mais um pouco de código
+```
+
+nesse caso, a variável criada automaticamente dentro de `myapp.py` não terá a variável `__name__` com o valor `"__main__"`.
+
+Então, a linha:
+
+```Python
+ uvicorn.run(app, host="0.0.0.0", port=8000)
+```
+
+não será executada.
+
+/// info | "Informação"
+
+Para mais informações, consulte
a documentação oficial do Python.
+
+///
+
+## Execute seu código com seu depurador
+
+Como você está executando o servidor Uvicorn diretamente do seu código, você pode chamar seu programa Python (seu aplicativo FastAPI) diretamente do depurador.
+
+---
+
+Por exemplo, no Visual Studio Code, você pode:
+
+* Ir para o painel "Debug".
+* "Add configuration...".
+* Selecionar "Python"
+* Executar o depurador com a opção "`Python: Current File (Integrated Terminal)`".
+
+Em seguida, ele iniciará o servidor com seu código **FastAPI**, parará em seus pontos de interrupção, etc.
+
+Veja como pode parecer:
+
+

+
+---
+
+Se você usar o Pycharm, você pode:
+
+* Abrir o menu "Executar".
+* Selecionar a opção "Depurar...".
+* Então um menu de contexto aparece.
+* Selecionar o arquivo para depurar (neste caso, `main.py`).
+
+Em seguida, ele iniciará o servidor com seu código **FastAPI**, parará em seus pontos de interrupção, etc.
+
+Veja como pode parecer:
+
+

diff --git a/docs/pt/docs/tutorial/testing.md b/docs/pt/docs/tutorial/testing.md
new file mode 100644
index 0000000000000..f734a7d9a5d22
--- /dev/null
+++ b/docs/pt/docs/tutorial/testing.md
@@ -0,0 +1,249 @@
+# Testando
+
+Graças ao
Starlette, testar aplicativos **FastAPI** é fácil e agradável.
+
+Ele é baseado no
HTTPX, que por sua vez é projetado com base em Requests, por isso é muito familiar e intuitivo.
+
+Com ele, você pode usar o
pytest diretamente com **FastAPI**.
+
+## Usando `TestClient`
+
+/// info | "Informação"
+
+Para usar o `TestClient`, primeiro instale o
`httpx`.
+
+Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalá-lo, por exemplo:
+
+```console
+$ pip install httpx
+```
+
+///
+
+Importe `TestClient`.
+
+Crie um `TestClient` passando seu aplicativo **FastAPI** para ele.
+
+Crie funções com um nome que comece com `test_` (essa é a convenção padrão do `pytest`).
+
+Use o objeto `TestClient` da mesma forma que você faz com `httpx`.
+
+Escreva instruções `assert` simples com as expressões Python padrão que você precisa verificar (novamente, `pytest` padrão).
+
+```Python hl_lines="2 12 15-18"
+{!../../../docs_src/app_testing/tutorial001.py!}
+```
+
+/// tip | "Dica"
+
+Observe que as funções de teste são `def` normais, não `async def`.
+
+E as chamadas para o cliente também são chamadas normais, não usando `await`.
+
+Isso permite que você use `pytest` diretamente sem complicações.
+
+///
+
+/// note | "Detalhes técnicos"
+
+Você também pode usar `from starlette.testclient import TestClient`.
+
+**FastAPI** fornece o mesmo `starlette.testclient` que `fastapi.testclient` apenas como uma conveniência para você, o desenvolvedor. Mas ele vem diretamente da Starlette.
+
+///
+
+/// tip | "Dica"
+
+Se você quiser chamar funções `async` em seus testes além de enviar solicitações ao seu aplicativo FastAPI (por exemplo, funções de banco de dados assíncronas), dê uma olhada em [Testes assíncronos](../advanced/async-tests.md){.internal-link target=_blank} no tutorial avançado.
+
+///
+
+## Separando testes
+
+Em uma aplicação real, você provavelmente teria seus testes em um arquivo diferente.
+
+E seu aplicativo **FastAPI** também pode ser composto de vários arquivos/módulos, etc.
+
+### Arquivo do aplicativo **FastAPI**
+
+Digamos que você tenha uma estrutura de arquivo conforme descrito em [Aplicativos maiores](bigger-applications.md){.internal-link target=_blank}:
+
+```
+.
+├── app
+│ ├── __init__.py
+│ └── main.py
+```
+
+No arquivo `main.py` você tem seu aplicativo **FastAPI**:
+
+
+```Python
+{!../../../docs_src/app_testing/main.py!}
+```
+
+### Arquivo de teste
+
+Então você poderia ter um arquivo `test_main.py` com seus testes. Ele poderia estar no mesmo pacote Python (o mesmo diretório com um arquivo `__init__.py`):
+
+``` hl_lines="5"
+.
+├── app
+│ ├── __init__.py
+│ ├── main.py
+│ └── test_main.py
+```
+
+Como esse arquivo está no mesmo pacote, você pode usar importações relativas para importar o objeto `app` do módulo `main` (`main.py`):
+
+```Python hl_lines="3"
+{!../../../docs_src/app_testing/test_main.py!}
+```
+
+...e ter o código para os testes como antes.
+
+## Testando: exemplo estendido
+
+Agora vamos estender este exemplo e adicionar mais detalhes para ver como testar diferentes partes.
+
+### Arquivo de aplicativo **FastAPI** estendido
+
+Vamos continuar com a mesma estrutura de arquivo de antes:
+
+```
+.
+├── app
+│ ├── __init__.py
+│ ├── main.py
+│ └── test_main.py
+```
+
+Digamos que agora o arquivo `main.py` com seu aplicativo **FastAPI** tenha algumas outras **operações de rotas**.
+
+Ele tem uma operação `GET` que pode retornar um erro.
+
+Ele tem uma operação `POST` que pode retornar vários erros.
+
+Ambas as *operações de rotas* requerem um cabeçalho `X-Token`.
+
+//// tab | Python 3.10+
+
+```Python
+{!> ../../../docs_src/app_testing/app_b_an_py310/main.py!}
+```
+
+////
+
+//// tab | Python 3.9+
+
+```Python
+{!> ../../../docs_src/app_testing/app_b_an_py39/main.py!}
+```
+
+////
+
+//// tab | Python 3.8+
+
+```Python
+{!> ../../../docs_src/app_testing/app_b_an/main.py!}
+```
+
+////
+
+//// tab | Python 3.10+ non-Annotated
+
+/// tip | "Dica"
+
+Prefira usar a versão `Annotated` se possível.
+
+///
+
+```Python
+{!> ../../../docs_src/app_testing/app_b_py310/main.py!}
+```
+
+////
+
+//// tab | Python 3.8+ non-Annotated
+
+/// tip | "Dica"
+
+Prefira usar a versão `Annotated` se possível.
+
+///
+
+```Python
+{!> ../../../docs_src/app_testing/app_b/main.py!}
+```
+
+////
+
+### Arquivo de teste estendido
+
+Você pode então atualizar `test_main.py` com os testes estendidos:
+
+```Python
+{!> ../../../docs_src/app_testing/app_b/test_main.py!}
+```
+
+Sempre que você precisar que o cliente passe informações na requisição e não souber como, você pode pesquisar (no Google) como fazer isso no `httpx`, ou até mesmo como fazer isso com `requests`, já que o design do HTTPX é baseado no design do Requests.
+
+Depois é só fazer o mesmo nos seus testes.
+
+Por exemplo:
+
+* Para passar um parâmetro *path* ou *query*, adicione-o à própria URL.
+* Para passar um corpo JSON, passe um objeto Python (por exemplo, um `dict`) para o parâmetro `json`.
+* Se você precisar enviar *Dados de Formulário* em vez de JSON, use o parâmetro `data`.
+* Para passar *headers*, use um `dict` no parâmetro `headers`.
+* Para *cookies*, um `dict` no parâmetro `cookies`.
+
+Para mais informações sobre como passar dados para o backend (usando `httpx` ou `TestClient`), consulte a
documentação do HTTPX.
+
+/// info | "Informação"
+
+Observe que o `TestClient` recebe dados que podem ser convertidos para JSON, não para modelos Pydantic.
+
+Se você tiver um modelo Pydantic em seu teste e quiser enviar seus dados para o aplicativo durante o teste, poderá usar o `jsonable_encoder` descrito em [Codificador compatível com JSON](encoder.md){.internal-link target=_blank}.
+
+///
+
+## Execute-o
+
+Depois disso, você só precisa instalar o `pytest`.
+
+Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalá-lo, por exemplo:
+
+
+
+```console
+$ pip install pytest
+
+---> 100%
+```
+
+
+
+Ele detectará os arquivos e os testes automaticamente, os executará e informará os resultados para você.
+
+Execute os testes com:
+
+
+
+```console
+$ pytest
+
+================ test session starts ================
+platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
+rootdir: /home/user/code/superawesome-cli/app
+plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
+collected 6 items
+
+---> 100%
+
+test_main.py ...... [100%]
+
+================= 1 passed in 0.03s =================
+```
+
+
diff --git a/docs/pt/docs/virtual-environments.md b/docs/pt/docs/virtual-environments.md
new file mode 100644
index 0000000000000..863c8d65e6714
--- /dev/null
+++ b/docs/pt/docs/virtual-environments.md
@@ -0,0 +1,844 @@
+# Ambientes Virtuais
+
+Ao trabalhar em projetos Python, você provavelmente deve usar um **ambiente virtual** (ou um mecanismo similar) para isolar os pacotes que você instala para cada projeto.
+
+/// info | "Informação"
+
+Se você já sabe sobre ambientes virtuais, como criá-los e usá-los, talvez seja melhor pular esta seção. 🤓
+
+///
+
+/// tip | "Dica"
+
+Um **ambiente virtual** é diferente de uma **variável de ambiente**.
+
+Uma **variável de ambiente** é uma variável no sistema que pode ser usada por programas.
+
+Um **ambiente virtual** é um diretório com alguns arquivos.
+
+///
+
+/// info | "Informação"
+
+Esta página lhe ensinará como usar **ambientes virtuais** e como eles funcionam.
+
+Se você estiver pronto para adotar uma **ferramenta que gerencia tudo** para você (incluindo a instalação do Python), experimente
uv.
+
+///
+
+## Criar um Projeto
+
+Primeiro, crie um diretório para seu projeto.
+
+O que normalmente faço é criar um diretório chamado `code` dentro do meu diretório home/user.
+
+E dentro disso eu crio um diretório por projeto.
+
+
+
+```console
+// Vá para o diretório inicial
+$ cd
+// Crie um diretório para todos os seus projetos de código
+$ mkdir code
+// Entre nesse diretório de código
+$ cd code
+// Crie um diretório para este projeto
+$ mkdir awesome-project
+// Entre no diretório do projeto
+$ cd awesome-project
+```
+
+
+
+## Crie um ambiente virtual
+
+Ao começar a trabalhar em um projeto Python **pela primeira vez**, crie um ambiente virtual **
dentro do seu projeto**.
+
+/// tip | "Dica"
+
+Você só precisa fazer isso **uma vez por projeto**, não toda vez que trabalhar.
+
+///
+
+//// tab | `venv`
+
+Para criar um ambiente virtual, você pode usar o módulo `venv` que vem com o Python.
+
+
+
+```console
+$ python -m venv .venv
+```
+
+
+
+/// details | O que esse comando significa
+
+* `python`: usa o programa chamado `python`
+* `-m`: chama um módulo como um script, nós diremos a ele qual módulo vem em seguida
+* `venv`: usa o módulo chamado `venv` que normalmente vem instalado com o Python
+* `.venv`: cria o ambiente virtual no novo diretório `.venv`
+
+///
+
+////
+
+//// tab | `uv`
+
+Se você tiver o
`uv` instalado, poderá usá-lo para criar um ambiente virtual.
+
+
+
+```console
+$ uv venv
+```
+
+
+
+/// tip | "Dica"
+
+Por padrão, `uv` criará um ambiente virtual em um diretório chamado `.venv`.
+
+Mas você pode personalizá-lo passando um argumento adicional com o nome do diretório.
+
+///
+
+////
+
+Esse comando cria um novo ambiente virtual em um diretório chamado `.venv`.
+
+/// details | `.venv` ou outro nome
+
+Você pode criar o ambiente virtual em um diretório diferente, mas há uma convenção para chamá-lo de `.venv`.
+
+///
+
+## Ative o ambiente virtual
+
+Ative o novo ambiente virtual para que qualquer comando Python que você executar ou pacote que você instalar o utilize.
+
+/// tip | "Dica"
+
+Faça isso **toda vez** que iniciar uma **nova sessão de terminal** para trabalhar no projeto.
+
+///
+
+//// tab | Linux, macOS
+
+
+
+```console
+$ source .venv/bin/activate
+```
+
+
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+$ .venv\Scripts\Activate.ps1
+```
+
+
+
+////
+
+//// tab | Windows Bash
+
+Ou se você usa o Bash para Windows (por exemplo,
Git Bash):
+
+
+
+```console
+$ source .venv/Scripts/activate
+```
+
+
+
+////
+
+/// tip | "Dica"
+
+Toda vez que você instalar um **novo pacote** naquele ambiente, **ative** o ambiente novamente.
+
+Isso garante que, se você usar um **programa de terminal (
CLI)** instalado por esse pacote, você usará aquele do seu ambiente virtual e não qualquer outro que possa ser instalado globalmente, provavelmente com uma versão diferente do que você precisa.
+
+///
+
+## Verifique se o ambiente virtual está ativo
+
+Verifique se o ambiente virtual está ativo (o comando anterior funcionou).
+
+/// tip | "Dica"
+
+Isso é **opcional**, mas é uma boa maneira de **verificar** se tudo está funcionando conforme o esperado e se você está usando o ambiente virtual pretendido.
+
+///
+
+//// tab | Linux, macOS, Windows Bash
+
+
+
+```console
+$ which python
+
+/home/user/code/awesome-project/.venv/bin/python
+```
+
+
+
+Se ele mostrar o binário `python` em `.venv/bin/python`, dentro do seu projeto (neste caso `awesome-project`), então funcionou. 🎉
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+$ Get-Command python
+
+C:\Users\user\code\awesome-project\.venv\Scripts\python
+```
+
+
+
+Se ele mostrar o binário `python` em `.venv\Scripts\python`, dentro do seu projeto (neste caso `awesome-project`), então funcionou. 🎉
+
+////
+
+## Atualizar `pip`
+
+/// tip | "Dica"
+
+Se você usar
`uv`, você o usará para instalar coisas em vez do `pip`, então não precisará atualizar o `pip`. 😎
+
+///
+
+Se você estiver usando `pip` para instalar pacotes (ele vem por padrão com o Python), você deve **atualizá-lo** para a versão mais recente.
+
+Muitos erros exóticos durante a instalação de um pacote são resolvidos apenas atualizando o `pip` primeiro.
+
+/// tip | "Dica"
+
+Normalmente, você faria isso **uma vez**, logo após criar o ambiente virtual.
+
+///
+
+Certifique-se de que o ambiente virtual esteja ativo (com o comando acima) e execute:
+
+
+
+```console
+$ python -m pip install --upgrade pip
+
+---> 100%
+```
+
+
+
+## Adicionar `.gitignore`
+
+Se você estiver usando **Git** (você deveria), adicione um arquivo `.gitignore` para excluir tudo em seu `.venv` do Git.
+
+/// tip | "Dica"
+
+Se você usou
`uv` para criar o ambiente virtual, ele já fez isso para você, você pode pular esta etapa. 😎
+
+///
+
+/// tip | "Dica"
+
+Faça isso **uma vez**, logo após criar o ambiente virtual.
+
+///
+
+
+
+```console
+$ echo "*" > .venv/.gitignore
+```
+
+
+
+/// details | O que esse comando significa
+
+* `echo "*"`: irá "imprimir" o texto `*` no terminal (a próxima parte muda isso um pouco)
+* `>`: qualquer coisa impressa no terminal pelo comando à esquerda de `>` não deve ser impressa, mas sim escrita no arquivo que vai à direita de `>`
+* `.gitignore`: o nome do arquivo onde o texto deve ser escrito
+
+E `*` para Git significa "tudo". Então, ele ignorará tudo no diretório `.venv`.
+
+Esse comando criará um arquivo `.gitignore` com o conteúdo:
+
+```gitignore
+*
+```
+
+///
+
+## Instalar Pacotes
+
+Após ativar o ambiente, você pode instalar pacotes nele.
+
+/// tip | "Dica"
+
+Faça isso **uma vez** ao instalar ou atualizar os pacotes que seu projeto precisa.
+
+Se precisar atualizar uma versão ou adicionar um novo pacote, você **fará isso novamente**.
+
+///
+
+### Instalar pacotes diretamente
+
+Se estiver com pressa e não quiser usar um arquivo para declarar os requisitos de pacote do seu projeto, você pode instalá-los diretamente.
+
+/// tip | "Dica"
+
+É uma (muito) boa ideia colocar os pacotes e versões que seu programa precisa em um arquivo (por exemplo `requirements.txt` ou `pyproject.toml`).
+
+///
+
+//// tab | `pip`
+
+
+
+```console
+$ pip install "fastapi[standard]"
+
+---> 100%
+```
+
+
+
+////
+
+//// tab | `uv`
+
+Se você tem o
`uv`:
+
+
+
+```console
+$ uv pip install "fastapi[standard]"
+---> 100%
+```
+
+
+
+////
+
+### Instalar a partir de `requirements.txt`
+
+Se você tiver um `requirements.txt`, agora poderá usá-lo para instalar seus pacotes.
+
+//// tab | `pip`
+
+
+
+```console
+$ pip install -r requirements.txt
+---> 100%
+```
+
+
+
+////
+
+//// tab | `uv`
+
+Se você tem o
`uv`:
+
+
+
+```console
+$ uv pip install -r requirements.txt
+---> 100%
+```
+
+
+
+////
+
+/// details | `requirements.txt`
+
+Um `requirements.txt` com alguns pacotes poderia se parecer com:
+
+```requirements.txt
+fastapi[standard]==0.113.0
+pydantic==2.8.0
+```
+
+///
+
+## Execute seu programa
+
+Depois de ativar o ambiente virtual, você pode executar seu programa, e ele usará o Python dentro do seu ambiente virtual com os pacotes que você instalou lá.
+
+
+
+```console
+$ python main.py
+
+Hello World
+```
+
+
+
+## Configure seu editor
+
+Você provavelmente usaria um editor. Certifique-se de configurá-lo para usar o mesmo ambiente virtual que você criou (ele provavelmente o detectará automaticamente) para que você possa obter erros de preenchimento automático e em linha.
+
+Por exemplo:
+
+*
VS Code
+*
PyCharm
+
+/// tip | "Dica"
+
+Normalmente, você só precisa fazer isso **uma vez**, ao criar o ambiente virtual.
+
+///
+
+## Desativar o ambiente virtual
+
+Quando terminar de trabalhar no seu projeto, você pode **desativar** o ambiente virtual.
+
+
+
+```console
+$ deactivate
+```
+
+
+
+Dessa forma, quando você executar `python`, ele não tentará executá-lo naquele ambiente virtual com os pacotes instalados nele.
+
+## Pronto para trabalhar
+
+Agora você está pronto para começar a trabalhar no seu projeto.
+
+
+
+/// tip | "Dica"
+
+Você quer entender o que é tudo isso acima?
+
+Continue lendo. 👇🤓
+
+///
+
+## Por que ambientes virtuais
+
+Para trabalhar com o FastAPI, você precisa instalar o
Python.
+
+Depois disso, você precisará **instalar** o FastAPI e quaisquer outros **pacotes** que queira usar.
+
+Para instalar pacotes, você normalmente usaria o comando `pip` que vem com o Python (ou alternativas semelhantes).
+
+No entanto, se você usar `pip` diretamente, os pacotes serão instalados no seu **ambiente Python global** (a instalação global do Python).
+
+### O Problema
+
+Então, qual é o problema em instalar pacotes no ambiente global do Python?
+
+Em algum momento, você provavelmente acabará escrevendo muitos programas diferentes que dependem de **pacotes diferentes**. E alguns desses projetos em que você trabalha dependerão de **versões diferentes** do mesmo pacote. 😱
+
+Por exemplo, você pode criar um projeto chamado `philosophers-stone`, este programa depende de outro pacote chamado **`harry`, usando a versão `1`**. Então, você precisa instalar `harry`.
+
+```mermaid
+flowchart LR
+ stone(philosophers-stone) -->|requires| harry-1[harry v1]
+```
+
+Então, em algum momento depois, você cria outro projeto chamado `prisoner-of-azkaban`, e esse projeto também depende de `harry`, mas esse projeto precisa do **`harry` versão `3`**.
+
+```mermaid
+flowchart LR
+ azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3]
+```
+
+Mas agora o problema é que, se você instalar os pacotes globalmente (no ambiente global) em vez de em um **ambiente virtual** local, você terá que escolher qual versão do `harry` instalar.
+
+Se você quiser executar `philosophers-stone`, precisará primeiro instalar `harry` versão `1`, por exemplo com:
+
+
+
+```console
+$ pip install "harry==1"
+```
+
+
+
+E então você acabaria com `harry` versão `1` instalado em seu ambiente Python global.
+
+```mermaid
+flowchart LR
+ subgraph global[global env]
+ harry-1[harry v1]
+ end
+ subgraph stone-project[philosophers-stone project]
+ stone(philosophers-stone) -->|requires| harry-1
+ end
+```
+
+Mas se você quiser executar `prisoner-of-azkaban`, você precisará desinstalar `harry` versão `1` e instalar `harry` versão `3` (ou apenas instalar a versão `3` desinstalaria automaticamente a versão `1`).
+
+
+
+```console
+$ pip install "harry==3"
+```
+
+
+
+E então você acabaria com `harry` versão `3` instalado em seu ambiente Python global.
+
+E se você tentar executar `philosophers-stone` novamente, há uma chance de que **não funcione** porque ele precisa de `harry` versão `1`.
+
+```mermaid
+flowchart LR
+ subgraph global[global env]
+ harry-1[
harry v1]
+ style harry-1 fill:#ccc,stroke-dasharray: 5 5
+ harry-3[harry v3]
+ end
+ subgraph stone-project[philosophers-stone project]
+ stone(philosophers-stone) -.-x|⛔️| harry-1
+ end
+ subgraph azkaban-project[prisoner-of-azkaban project]
+ azkaban(prisoner-of-azkaban) --> |requires| harry-3
+ end
+```
+
+/// tip | "Dica"
+
+É muito comum em pacotes Python tentar ao máximo **evitar alterações drásticas** em **novas versões**, mas é melhor prevenir do que remediar e instalar versões mais recentes intencionalmente e, quando possível, executar os testes para verificar se tudo está funcionando corretamente.
+
+///
+
+Agora, imagine isso com **muitos** outros **pacotes** dos quais todos os seus **projetos dependem**. Isso é muito difícil de gerenciar. E você provavelmente acabaria executando alguns projetos com algumas **versões incompatíveis** dos pacotes, e não saberia por que algo não está funcionando.
+
+Além disso, dependendo do seu sistema operacional (por exemplo, Linux, Windows, macOS), ele pode ter vindo com o Python já instalado. E, nesse caso, provavelmente tinha alguns pacotes pré-instalados com algumas versões específicas **necessárias para o seu sistema**. Se você instalar pacotes no ambiente global do Python, poderá acabar **quebrando** alguns dos programas que vieram com seu sistema operacional.
+
+## Onde os pacotes são instalados
+
+Quando você instala o Python, ele cria alguns diretórios com alguns arquivos no seu computador.
+
+Alguns desses diretórios são os responsáveis por ter todos os pacotes que você instala.
+
+Quando você executa:
+
+
+
+```console
+// Não execute isso agora, é apenas um exemplo 🤓
+$ pip install "fastapi[standard]"
+---> 100%
+```
+
+
+
+Isso fará o download de um arquivo compactado com o código FastAPI, normalmente do
PyPI.
+
+Ele também fará o **download** de arquivos para outros pacotes dos quais o FastAPI depende.
+
+Em seguida, ele **extrairá** todos esses arquivos e os colocará em um diretório no seu computador.
+
+Por padrão, ele colocará os arquivos baixados e extraídos no diretório que vem com a instalação do Python, que é o **ambiente global**.
+
+## O que são ambientes virtuais
+
+A solução para os problemas de ter todos os pacotes no ambiente global é usar um **ambiente virtual para cada projeto** em que você trabalha.
+
+Um ambiente virtual é um **diretório**, muito semelhante ao global, onde você pode instalar os pacotes para um projeto.
+
+Dessa forma, cada projeto terá seu próprio ambiente virtual (diretório `.venv`) com seus próprios pacotes.
+
+```mermaid
+flowchart TB
+ subgraph stone-project[philosophers-stone project]
+ stone(philosophers-stone) --->|requires| harry-1
+ subgraph venv1[.venv]
+ harry-1[harry v1]
+ end
+ end
+ subgraph azkaban-project[prisoner-of-azkaban project]
+ azkaban(prisoner-of-azkaban) --->|requires| harry-3
+ subgraph venv2[.venv]
+ harry-3[harry v3]
+ end
+ end
+ stone-project ~~~ azkaban-project
+```
+
+## O que significa ativar um ambiente virtual
+
+Quando você ativa um ambiente virtual, por exemplo com:
+
+//// tab | Linux, macOS
+
+
+
+```console
+$ source .venv/bin/activate
+```
+
+
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+$ .venv\Scripts\Activate.ps1
+```
+
+
+
+////
+
+//// tab | Windows Bash
+
+Ou se você usa o Bash para Windows (por exemplo,
Git Bash):
+
+
+
+```console
+$ source .venv/Scripts/activate
+```
+
+
+
+////
+
+Esse comando criará ou modificará algumas [variáveis de ambiente](environment-variables.md){.internal-link target=_blank} que estarão disponíveis para os próximos comandos.
+
+Uma dessas variáveis é a variável `PATH`.
+
+/// tip | "Dica"
+
+Você pode aprender mais sobre a variável de ambiente `PATH` na seção [Variáveis de ambiente](environment-variables.md#path-environment-variable){.internal-link target=_blank}.
+
+///
+
+A ativação de um ambiente virtual adiciona seu caminho `.venv/bin` (no Linux e macOS) ou `.venv\Scripts` (no Windows) à variável de ambiente `PATH`.
+
+Digamos que antes de ativar o ambiente, a variável `PATH` estava assim:
+
+//// tab | Linux, macOS
+
+```plaintext
+/usr/bin:/bin:/usr/sbin:/sbin
+```
+
+Isso significa que o sistema procuraria programas em:
+
+* `/usr/bin`
+* `/bin`
+* `/usr/sbin`
+* `/sbin`
+
+////
+
+//// tab | Windows
+
+```plaintext
+C:\Windows\System32
+```
+
+Isso significa que o sistema procuraria programas em:
+
+* `C:\Windows\System32`
+
+////
+
+Após ativar o ambiente virtual, a variável `PATH` ficaria mais ou menos assim:
+
+//// tab | Linux, macOS
+
+```plaintext
+/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin
+```
+
+Isso significa que o sistema agora começará a procurar primeiro por programas em:
+
+```plaintext
+/home/user/code/awesome-project/.venv/bin
+```
+
+antes de procurar nos outros diretórios.
+
+Então, quando você digita `python` no terminal, o sistema encontrará o programa Python em
+
+```plaintext
+/home/user/code/awesome-project/.venv/bin/python
+```
+
+e usa esse.
+
+////
+
+//// tab | Windows
+
+```plaintext
+C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32
+```
+
+Isso significa que o sistema agora começará a procurar primeiro por programas em:
+
+```plaintext
+C:\Users\user\code\awesome-project\.venv\Scripts
+```
+
+antes de procurar nos outros diretórios.
+
+Então, quando você digita `python` no terminal, o sistema encontrará o programa Python em
+
+```plaintext
+C:\Users\user\code\awesome-project\.venv\Scripts\python
+```
+
+e usa esse.
+
+////
+
+Um detalhe importante é que ele colocará o caminho do ambiente virtual no **início** da variável `PATH`. O sistema o encontrará **antes** de encontrar qualquer outro Python disponível. Dessa forma, quando você executar `python`, ele usará o Python **do ambiente virtual** em vez de qualquer outro `python` (por exemplo, um `python` de um ambiente global).
+
+Ativar um ambiente virtual também muda algumas outras coisas, mas esta é uma das mais importantes.
+
+## Verificando um ambiente virtual
+
+Ao verificar se um ambiente virtual está ativo, por exemplo com:
+
+//// tab | Linux, macOS, Windows Bash
+
+
+
+```console
+$ which python
+
+/home/user/code/awesome-project/.venv/bin/python
+```
+
+
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+$ Get-Command python
+
+C:\Users\user\code\awesome-project\.venv\Scripts\python
+```
+
+
+
+////
+
+Isso significa que o programa `python` que será usado é aquele **no ambiente virtual**.
+
+você usa `which` no Linux e macOS e `Get-Command` no Windows PowerShell.
+
+A maneira como esse comando funciona é que ele vai e verifica na variável de ambiente `PATH`, passando por **cada caminho em ordem**, procurando pelo programa chamado `python`. Uma vez que ele o encontre, ele **mostrará o caminho** para esse programa.
+
+A parte mais importante é que quando você chama ``python`, esse é exatamente o "`python`" que será executado.
+
+Assim, você pode confirmar se está no ambiente virtual correto.
+
+/// tip | "Dica"
+
+É fácil ativar um ambiente virtual, obter um Python e então **ir para outro projeto**.
+
+E o segundo projeto **não funcionaria** porque você está usando o **Python incorreto**, de um ambiente virtual para outro projeto.
+
+É útil poder verificar qual `python` está sendo usado. 🤓
+
+///
+
+## Por que desativar um ambiente virtual
+
+Por exemplo, você pode estar trabalhando em um projeto `philosophers-stone`, **ativar esse ambiente virtual**, instalar pacotes e trabalhar com esse ambiente.
+
+E então você quer trabalhar em **outro projeto** `prisoner-of-azkaban`.
+
+Você vai para aquele projeto:
+
+
+
+```console
+$ cd ~/code/prisoner-of-azkaban
+```
+
+
+
+Se você não desativar o ambiente virtual para `philosophers-stone`, quando você executar `python` no terminal, ele tentará usar o Python de `philosophers-stone`.
+
+
+
+```console
+$ cd ~/code/prisoner-of-azkaban
+
+$ python main.py
+
+// Erro ao importar o Sirius, ele não está instalado 😱
+Traceback (most recent call last):
+ File "main.py", line 1, in
+ import sirius
+```
+
+
+
+Mas se você desativar o ambiente virtual e ativar o novo para `prisoner-of-askaban`, quando você executar `python`, ele usará o Python do ambiente virtual em `prisoner-of-azkaban`.
+
+
+
+```console
+$ cd ~/code/prisoner-of-azkaban
+
+// Você não precisa estar no diretório antigo para desativar, você pode fazer isso de onde estiver, mesmo depois de ir para o outro projeto 😎
+$ deactivate
+
+// Ative o ambiente virtual em prisoner-of-azkaban/.venv 🚀
+$ source .venv/bin/activate
+
+// Agora, quando você executar o python, ele encontrará o pacote sirius instalado neste ambiente virtual ✨
+$ python main.py
+
+Eu juro solenemente 🐺
+```
+
+
+
+## Alternativas
+
+Este é um guia simples para você começar e lhe ensinar como tudo funciona **por baixo**.
+
+Existem muitas **alternativas** para gerenciar ambientes virtuais, dependências de pacotes (requisitos) e projetos.
+
+Quando estiver pronto e quiser usar uma ferramenta para **gerenciar todo o projeto**, dependências de pacotes, ambientes virtuais, etc., sugiro que você experimente o
uv.
+
+`uv` pode fazer muitas coisas, ele pode:
+
+* **Instalar o Python** para você, incluindo versões diferentes
+* Gerenciar o **ambiente virtual** para seus projetos
+* Instalar **pacotes**
+* Gerenciar **dependências e versões** de pacotes para seu projeto
+* Certifique-se de ter um conjunto **exato** de pacotes e versões para instalar, incluindo suas dependências, para que você possa ter certeza de que pode executar seu projeto em produção exatamente da mesma forma que em seu computador durante o desenvolvimento, isso é chamado de **bloqueio**
+* E muitas outras coisas
+
+## Conclusão
+
+Se você leu e entendeu tudo isso, agora **você sabe muito mais** sobre ambientes virtuais do que muitos desenvolvedores por aí. 🤓
+
+Saber esses detalhes provavelmente será útil no futuro, quando você estiver depurando algo que parece complexo, mas você saberá **como tudo funciona**. 😎
diff --git a/docs/zh/docs/project-generation.md b/docs/zh/docs/project-generation.md
index 0655cb0a9cf57..9b37355398de7 100644
--- a/docs/zh/docs/project-generation.md
+++ b/docs/zh/docs/project-generation.md
@@ -1,84 +1,28 @@
-# 项目生成 - 模板
-
-项目生成器一般都会提供很多初始设置、安全措施、数据库,甚至还准备好了第一个 API 端点,能帮助您快速上手。
-
-项目生成器的设置通常都很主观,您可以按需更新或修改,但对于您的项目来说,它是非常好的起点。
-
-## 全栈 FastAPI + PostgreSQL
-
-GitHub:
https://github.com/tiangolo/full-stack-fastapi-postgresql
-
-### 全栈 FastAPI + PostgreSQL - 功能
-
-* 完整的 **Docker** 集成(基于 Docker)
-* Docker Swarm 开发模式
-* **Docker Compose** 本地开发集成与优化
-* **生产可用**的 Python 网络服务器,使用 Uvicorn 或 Gunicorn
-* Python
**FastAPI** 后端:
-* * **速度快**:可与 **NodeJS** 和 **Go** 比肩的极高性能(归功于 Starlette 和 Pydantic)
- * **直观**:强大的编辑器支持,处处皆可
自动补全,减少调试时间
- * **简单**:易学、易用,阅读文档所需时间更短
- * **简短**:代码重复最小化,每次参数声明都可以实现多个功能
- * **健壮**: 生产级别的代码,还有自动交互文档
- * **基于标准**:完全兼容并基于 API 开放标准:
OpenAPI 和
JSON Schema
- *
**更多功能**包括自动验证、序列化、交互文档、OAuth2 JWT 令牌身份验证等
-* **安全密码**,默认使用密码哈希
-* **JWT 令牌**身份验证
-* **SQLAlchemy** 模型(独立于 Flask 扩展,可直接用于 Celery Worker)
-* 基础的用户模型(可按需修改或删除)
-* **Alembic** 迁移
-* **CORS**(跨域资源共享)
-* **Celery** Worker 可从后端其它部分有选择地导入并使用模型和代码
-* REST 后端测试基于 Pytest,并与 Docker 集成,可独立于数据库实现完整的 API 交互测试。因为是在 Docker 中运行,每次都可从头构建新的数据存储(使用 ElasticSearch、MongoDB、CouchDB 等数据库,仅测试 API 运行)
-* Python 与 **Jupyter Kernels** 集成,用于远程或 Docker 容器内部开发,使用 Atom Hydrogen 或 Visual Studio Code 的 Jupyter 插件
-* **Vue** 前端:
- * 由 Vue CLI 生成
- * **JWT 身份验证**处理
- * 登录视图
- * 登录后显示主仪表盘视图
- * 主仪表盘支持用户创建与编辑
- * 用户信息编辑
- * **Vuex**
- * **Vue-router**
- * **Vuetify** 美化组件
- * **TypeScript**
- * 基于 **Nginx** 的 Docker 服务器(优化了 Vue-router 配置)
- * Docker 多阶段构建,无需保存或提交编译的代码
- * 在构建时运行前端测试(可禁用)
- * 尽量模块化,开箱即用,但仍可使用 Vue CLI 重新生成或创建所需项目,或复用所需内容
-* 使用 **PGAdmin** 管理 PostgreSQL 数据库,可轻松替换为 PHPMyAdmin 或 MySQL
-* 使用 **Flower** 监控 Celery 任务
-* 使用 **Traefik** 处理前后端负载平衡,可把前后端放在同一个域下,按路径分隔,但在不同容器中提供服务
-* Traefik 集成,包括自动生成 Let's Encrypt **HTTPS** 凭证
-* GitLab **CI**(持续集成),包括前后端测试
-
-## 全栈 FastAPI + Couchbase
-
-GitHub:
https://github.com/tiangolo/full-stack-fastapi-couchbase
-
-⚠️ **警告** ⚠️
-
-如果您想从头开始创建新项目,建议使用以下备选方案。
-
-例如,项目生成器
全栈 FastAPI + PostgreSQL 会更适用,这个项目的维护积极,用的人也多,还包括了所有新功能和改进内容。
-
-当然,您也可以放心使用这个基于 Couchbase 的生成器,它也能正常使用。就算用它生成项目也没有任何问题(为了更好地满足需求,您可以自行更新这个项目)。
-
-详见资源仓库中的文档。
-
-## 全栈 FastAPI + MongoDB
-
-……敬请期待,得看我有没有时间做这个项目。😅 🎉
-
-## FastAPI + spaCy 机器学习模型
-
-GitHub:
https://github.com/microsoft/cookiecutter-spacy-fastapi
-
-### FastAPI + spaCy 机器学习模型 - 功能
-
-* 集成 **spaCy** NER 模型
-* 内置 **Azure 认知搜索**请求格式
-* **生产可用**的 Python 网络服务器,使用 Uvicorn 与 Gunicorn
-* 内置 **Azure DevOps** Kubernetes (AKS) CI/CD 开发
-* **多语**支持,可在项目设置时选择 spaCy 内置的语言
-* 不仅局限于 spaCy,可**轻松扩展**至其它模型框架(Pytorch、TensorFlow)
+# FastAPI全栈模板
+
+模板通常带有特定的设置,而且被设计为灵活和可定制的。这允许您根据项目的需求修改和调整它们,使它们成为一个很好的起点。🏁
+
+您可以使用此模板开始,因为它包含了许多已经为您完成的初始设置、安全性、数据库和一些API端点。
+
+代码仓:
Full Stack FastAPI Template
+
+## FastAPI全栈模板 - 技术栈和特性
+
+- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) 用于Python后端API.
+ - 🧰 [SQLModel](https://sqlmodel.tiangolo.com) 用于Python和SQL数据库的集成(ORM)。
+ - 🔍 [Pydantic](https://docs.pydantic.dev) FastAPI的依赖项之一,用于数据验证和配置管理。
+ - 💾 [PostgreSQL](https://www.postgresql.org) 作为SQL数据库。
+- 🚀 [React](https://react.dev) 用于前端。
+ - 💃 使用了TypeScript、hooks、Vite和其他一些现代化的前端技术栈。
+ - 🎨 [Chakra UI](https://chakra-ui.com) 用于前端组件。
+ - 🤖 一个自动化生成的前端客户端。
+ - 🧪 Playwright用于端到端测试。
+ - 🦇 支持暗黑主题(Dark mode)。
+- 🐋 [Docker Compose](https://www.docker.com) 用于开发环境和生产环境。
+- 🔒 默认使用密码哈希来保证安全。
+- 🔑 JWT令牌用于权限验证。
+- 📫 使用邮箱来进行密码恢复。
+- ✅ 单元测试用了[Pytest](https://pytest.org).
+- 📞 [Traefik](https://traefik.io) 用于反向代理和负载均衡。
+- 🚢 部署指南(Docker Compose)包含了如何起一个Traefik前端代理来自动化HTTPS认证。
+- 🏭 CI(持续集成)和 CD(持续部署)基于GitHub Actions。
diff --git a/docs_src/middleware/tutorial001.py b/docs_src/middleware/tutorial001.py
index 6bab3410a6548..e65a7dade1791 100644
--- a/docs_src/middleware/tutorial001.py
+++ b/docs_src/middleware/tutorial001.py
@@ -7,8 +7,8 @@
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
- start_time = time.time()
+ start_time = time.perf_counter()
response = await call_next(request)
- process_time = time.time() - start_time
+ process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
diff --git a/docs_src/request_form_models/tutorial001.py b/docs_src/request_form_models/tutorial001.py
new file mode 100644
index 0000000000000..98feff0b9f4e8
--- /dev/null
+++ b/docs_src/request_form_models/tutorial001.py
@@ -0,0 +1,14 @@
+from fastapi import FastAPI, Form
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class FormData(BaseModel):
+ username: str
+ password: str
+
+
+@app.post("/login/")
+async def login(data: FormData = Form()):
+ return data
diff --git a/docs_src/request_form_models/tutorial001_an.py b/docs_src/request_form_models/tutorial001_an.py
new file mode 100644
index 0000000000000..30483d44555f8
--- /dev/null
+++ b/docs_src/request_form_models/tutorial001_an.py
@@ -0,0 +1,15 @@
+from fastapi import FastAPI, Form
+from pydantic import BaseModel
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+class FormData(BaseModel):
+ username: str
+ password: str
+
+
+@app.post("/login/")
+async def login(data: Annotated[FormData, Form()]):
+ return data
diff --git a/docs_src/request_form_models/tutorial001_an_py39.py b/docs_src/request_form_models/tutorial001_an_py39.py
new file mode 100644
index 0000000000000..7cc81aae9586d
--- /dev/null
+++ b/docs_src/request_form_models/tutorial001_an_py39.py
@@ -0,0 +1,16 @@
+from typing import Annotated
+
+from fastapi import FastAPI, Form
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class FormData(BaseModel):
+ username: str
+ password: str
+
+
+@app.post("/login/")
+async def login(data: Annotated[FormData, Form()]):
+ return data
diff --git a/docs_src/request_form_models/tutorial002.py b/docs_src/request_form_models/tutorial002.py
new file mode 100644
index 0000000000000..59b329e8d8955
--- /dev/null
+++ b/docs_src/request_form_models/tutorial002.py
@@ -0,0 +1,15 @@
+from fastapi import FastAPI, Form
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class FormData(BaseModel):
+ username: str
+ password: str
+ model_config = {"extra": "forbid"}
+
+
+@app.post("/login/")
+async def login(data: FormData = Form()):
+ return data
diff --git a/docs_src/request_form_models/tutorial002_an.py b/docs_src/request_form_models/tutorial002_an.py
new file mode 100644
index 0000000000000..bcb0227959b47
--- /dev/null
+++ b/docs_src/request_form_models/tutorial002_an.py
@@ -0,0 +1,16 @@
+from fastapi import FastAPI, Form
+from pydantic import BaseModel
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+class FormData(BaseModel):
+ username: str
+ password: str
+ model_config = {"extra": "forbid"}
+
+
+@app.post("/login/")
+async def login(data: Annotated[FormData, Form()]):
+ return data
diff --git a/docs_src/request_form_models/tutorial002_an_py39.py b/docs_src/request_form_models/tutorial002_an_py39.py
new file mode 100644
index 0000000000000..3004e085243c2
--- /dev/null
+++ b/docs_src/request_form_models/tutorial002_an_py39.py
@@ -0,0 +1,17 @@
+from typing import Annotated
+
+from fastapi import FastAPI, Form
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class FormData(BaseModel):
+ username: str
+ password: str
+ model_config = {"extra": "forbid"}
+
+
+@app.post("/login/")
+async def login(data: Annotated[FormData, Form()]):
+ return data
diff --git a/docs_src/request_form_models/tutorial002_pv1.py b/docs_src/request_form_models/tutorial002_pv1.py
new file mode 100644
index 0000000000000..d5f7db2a67ba4
--- /dev/null
+++ b/docs_src/request_form_models/tutorial002_pv1.py
@@ -0,0 +1,17 @@
+from fastapi import FastAPI, Form
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class FormData(BaseModel):
+ username: str
+ password: str
+
+ class Config:
+ extra = "forbid"
+
+
+@app.post("/login/")
+async def login(data: FormData = Form()):
+ return data
diff --git a/docs_src/request_form_models/tutorial002_pv1_an.py b/docs_src/request_form_models/tutorial002_pv1_an.py
new file mode 100644
index 0000000000000..fe9dbc344ba95
--- /dev/null
+++ b/docs_src/request_form_models/tutorial002_pv1_an.py
@@ -0,0 +1,18 @@
+from fastapi import FastAPI, Form
+from pydantic import BaseModel
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+class FormData(BaseModel):
+ username: str
+ password: str
+
+ class Config:
+ extra = "forbid"
+
+
+@app.post("/login/")
+async def login(data: Annotated[FormData, Form()]):
+ return data
diff --git a/docs_src/request_form_models/tutorial002_pv1_an_py39.py b/docs_src/request_form_models/tutorial002_pv1_an_py39.py
new file mode 100644
index 0000000000000..942d5d4118af9
--- /dev/null
+++ b/docs_src/request_form_models/tutorial002_pv1_an_py39.py
@@ -0,0 +1,19 @@
+from typing import Annotated
+
+from fastapi import FastAPI, Form
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class FormData(BaseModel):
+ username: str
+ password: str
+
+ class Config:
+ extra = "forbid"
+
+
+@app.post("/login/")
+async def login(data: Annotated[FormData, Form()]):
+ return data
diff --git a/fastapi/__init__.py b/fastapi/__init__.py
index ac2508d89d919..c2ed4859a77bb 100644
--- a/fastapi/__init__.py
+++ b/fastapi/__init__.py
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
-__version__ = "0.112.2"
+__version__ = "0.114.1"
from starlette import status as status
diff --git a/fastapi/_compat.py b/fastapi/_compat.py
index 06b847b4f3818..4b07b44fa5829 100644
--- a/fastapi/_compat.py
+++ b/fastapi/_compat.py
@@ -2,6 +2,7 @@
from copy import copy
from dataclasses import dataclass, is_dataclass
from enum import Enum
+from functools import lru_cache
from typing import (
Any,
Callable,
@@ -279,6 +280,12 @@ def create_body_model(
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
return BodyModel
+ def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
+ return [
+ ModelField(field_info=field_info, name=name)
+ for name, field_info in model.model_fields.items()
+ ]
+
else:
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
from pydantic import AnyUrl as Url # noqa: F401
@@ -513,6 +520,9 @@ def create_body_model(
BodyModel.__fields__[f.name] = f # type: ignore[index]
return BodyModel
+ def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
+ return list(model.__fields__.values()) # type: ignore[attr-defined]
+
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
@@ -532,6 +542,12 @@ def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
+ origin = get_origin(annotation)
+ if origin is Union or origin is UnionType:
+ for arg in get_args(annotation):
+ if field_annotation_is_sequence(arg):
+ return True
+ return False
return _annotation_is_sequence(annotation) or _annotation_is_sequence(
get_origin(annotation)
)
@@ -634,3 +650,8 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
for sub_annotation in get_args(annotation)
)
+
+
+@lru_cache
+def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
+ return get_model_fields(model)
diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index 61ef006387781..418c117259aa3 100644
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -1,58 +1,37 @@
-from typing import Any, Callable, List, Optional, Sequence
+from dataclasses import dataclass, field
+from typing import Any, Callable, List, Optional, Sequence, Tuple
from fastapi._compat import ModelField
from fastapi.security.base import SecurityBase
+@dataclass
class SecurityRequirement:
- def __init__(
- self, security_scheme: SecurityBase, scopes: Optional[Sequence[str]] = None
- ):
- self.security_scheme = security_scheme
- self.scopes = scopes
+ security_scheme: SecurityBase
+ scopes: Optional[Sequence[str]] = None
+@dataclass
class Dependant:
- def __init__(
- self,
- *,
- path_params: Optional[List[ModelField]] = None,
- query_params: Optional[List[ModelField]] = None,
- header_params: Optional[List[ModelField]] = None,
- cookie_params: Optional[List[ModelField]] = None,
- body_params: Optional[List[ModelField]] = None,
- dependencies: Optional[List["Dependant"]] = None,
- security_schemes: Optional[List[SecurityRequirement]] = None,
- name: Optional[str] = None,
- call: Optional[Callable[..., Any]] = None,
- request_param_name: Optional[str] = None,
- websocket_param_name: Optional[str] = None,
- http_connection_param_name: Optional[str] = None,
- response_param_name: Optional[str] = None,
- background_tasks_param_name: Optional[str] = None,
- security_scopes_param_name: Optional[str] = None,
- security_scopes: Optional[List[str]] = None,
- use_cache: bool = True,
- path: Optional[str] = None,
- ) -> None:
- self.path_params = path_params or []
- self.query_params = query_params or []
- self.header_params = header_params or []
- self.cookie_params = cookie_params or []
- self.body_params = body_params or []
- self.dependencies = dependencies or []
- self.security_requirements = security_schemes or []
- self.request_param_name = request_param_name
- self.websocket_param_name = websocket_param_name
- self.http_connection_param_name = http_connection_param_name
- self.response_param_name = response_param_name
- self.background_tasks_param_name = background_tasks_param_name
- self.security_scopes = security_scopes
- self.security_scopes_param_name = security_scopes_param_name
- self.name = name
- self.call = call
- self.use_cache = use_cache
- # Store the path to be able to re-generate a dependable from it in overrides
- self.path = path
- # Save the cache key at creation to optimize performance
+ path_params: List[ModelField] = field(default_factory=list)
+ query_params: List[ModelField] = field(default_factory=list)
+ header_params: List[ModelField] = field(default_factory=list)
+ cookie_params: List[ModelField] = field(default_factory=list)
+ body_params: List[ModelField] = field(default_factory=list)
+ dependencies: List["Dependant"] = field(default_factory=list)
+ security_requirements: List[SecurityRequirement] = field(default_factory=list)
+ name: Optional[str] = None
+ call: Optional[Callable[..., Any]] = None
+ request_param_name: Optional[str] = None
+ websocket_param_name: Optional[str] = None
+ http_connection_param_name: Optional[str] = None
+ response_param_name: Optional[str] = None
+ background_tasks_param_name: Optional[str] = None
+ security_scopes_param_name: Optional[str] = None
+ security_scopes: Optional[List[str]] = None
+ use_cache: bool = True
+ path: Optional[str] = None
+ cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False)
+
+ def __post_init__(self) -> None:
self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or []))))
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 3e8e7b4101e0f..f18eace9d430e 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -1,6 +1,7 @@
import inspect
from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy
+from dataclasses import dataclass
from typing import (
Any,
Callable,
@@ -31,6 +32,7 @@
evaluate_forwardref,
field_annotation_is_scalar,
get_annotation_from_field_info,
+ get_cached_model_fields,
get_missing_field_error,
is_bytes_field,
is_bytes_sequence_field,
@@ -54,11 +56,18 @@
from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect
-from fastapi.utils import create_response_field, get_path_param_names
+from fastapi.utils import create_model_field, get_path_param_names
+from pydantic import BaseModel
from pydantic.fields import FieldInfo
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
from starlette.concurrency import run_in_threadpool
-from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
+from starlette.datastructures import (
+ FormData,
+ Headers,
+ ImmutableMultiDict,
+ QueryParams,
+ UploadFile,
+)
from starlette.requests import HTTPConnection, Request
from starlette.responses import Response
from starlette.websockets import WebSocket
@@ -79,25 +88,23 @@
)
-def check_file_field(field: ModelField) -> None:
- field_info = field.field_info
- if isinstance(field_info, params.Form):
- try:
- # __version__ is available in both multiparts, and can be mocked
- from multipart import __version__ # type: ignore
+def ensure_multipart_is_installed() -> None:
+ try:
+ # __version__ is available in both multiparts, and can be mocked
+ from multipart import __version__ # type: ignore
- assert __version__
- try:
- # parse_options_header is only available in the right multipart
- from multipart.multipart import parse_options_header # type: ignore
+ assert __version__
+ try:
+ # parse_options_header is only available in the right multipart
+ from multipart.multipart import parse_options_header # type: ignore
- assert parse_options_header
- except ImportError:
- logger.error(multipart_incorrect_install_error)
- raise RuntimeError(multipart_incorrect_install_error) from None
+ assert parse_options_header
except ImportError:
- logger.error(multipart_not_installed_error)
- raise RuntimeError(multipart_not_installed_error) from None
+ logger.error(multipart_incorrect_install_error)
+ raise RuntimeError(multipart_incorrect_install_error) from None
+ except ImportError:
+ logger.error(multipart_not_installed_error)
+ raise RuntimeError(multipart_not_installed_error) from None
def get_param_sub_dependant(
@@ -175,7 +182,7 @@ def get_flat_dependant(
header_params=dependant.header_params.copy(),
cookie_params=dependant.cookie_params.copy(),
body_params=dependant.body_params.copy(),
- security_schemes=dependant.security_requirements.copy(),
+ security_requirements=dependant.security_requirements.copy(),
use_cache=dependant.use_cache,
path=dependant.path,
)
@@ -258,16 +265,16 @@ def get_dependant(
)
for param_name, param in signature_params.items():
is_path_param = param_name in path_param_names
- type_annotation, depends, param_field = analyze_param(
+ param_details = analyze_param(
param_name=param_name,
annotation=param.annotation,
value=param.default,
is_path_param=is_path_param,
)
- if depends is not None:
+ if param_details.depends is not None:
sub_dependant = get_param_sub_dependant(
param_name=param_name,
- depends=depends,
+ depends=param_details.depends,
path=path,
security_scopes=security_scopes,
)
@@ -275,18 +282,18 @@ def get_dependant(
continue
if add_non_field_param_to_dependency(
param_name=param_name,
- type_annotation=type_annotation,
+ type_annotation=param_details.type_annotation,
dependant=dependant,
):
assert (
- param_field is None
+ param_details.field is None
), f"Cannot specify multiple FastAPI annotations for {param_name!r}"
continue
- assert param_field is not None
- if is_body_param(param_field=param_field, is_path_param=is_path_param):
- dependant.body_params.append(param_field)
+ assert param_details.field is not None
+ if isinstance(param_details.field.field_info, params.Body):
+ dependant.body_params.append(param_details.field)
else:
- add_param_to_fields(field=param_field, dependant=dependant)
+ add_param_to_fields(field=param_details.field, dependant=dependant)
return dependant
@@ -314,13 +321,20 @@ def add_non_field_param_to_dependency(
return None
+@dataclass
+class ParamDetails:
+ type_annotation: Any
+ depends: Optional[params.Depends]
+ field: Optional[ModelField]
+
+
def analyze_param(
*,
param_name: str,
annotation: Any,
value: Any,
is_path_param: bool,
-) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
+) -> ParamDetails:
field_info = None
depends = None
type_annotation: Any = Any
@@ -328,6 +342,7 @@ def analyze_param(
if annotation is not inspect.Signature.empty:
use_annotation = annotation
type_annotation = annotation
+ # Extract Annotated info
if get_origin(use_annotation) is Annotated:
annotated_args = get_args(annotation)
type_annotation = annotated_args[0]
@@ -347,6 +362,7 @@ def analyze_param(
)
else:
fastapi_annotation = None
+ # Set default for Annotated FieldInfo
if isinstance(fastapi_annotation, FieldInfo):
# Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info(
@@ -361,9 +377,10 @@ def analyze_param(
field_info.default = value
else:
field_info.default = Required
+ # Get Annotated Depends
elif isinstance(fastapi_annotation, params.Depends):
depends = fastapi_annotation
-
+ # Get Depends from default value
if isinstance(value, params.Depends):
assert depends is None, (
"Cannot specify `Depends` in `Annotated` and default value"
@@ -374,6 +391,7 @@ def analyze_param(
f" default value together for {param_name!r}"
)
depends = value
+ # Get FieldInfo from default value
elif isinstance(value, FieldInfo):
assert field_info is None, (
"Cannot specify FastAPI annotations in `Annotated` and default value"
@@ -383,11 +401,13 @@ def analyze_param(
if PYDANTIC_V2:
field_info.annotation = type_annotation
+ # Get Depends from type annotation
if depends is not None and depends.dependency is None:
# Copy `depends` before mutating it
depends = copy(depends)
depends.dependency = type_annotation
+ # Handle non-param type annotations like Request
if lenient_issubclass(
type_annotation,
(
@@ -403,6 +423,7 @@ def analyze_param(
assert (
field_info is None
), f"Cannot specify FastAPI annotation for type {type_annotation!r}"
+ # Handle default assignations, neither field_info nor depends was not found in Annotated nor default value
elif field_info is None and depends is None:
default_value = value if value is not inspect.Signature.empty else Required
if is_path_param:
@@ -420,7 +441,9 @@ def analyze_param(
field_info = params.Query(annotation=use_annotation, default=default_value)
field = None
+ # It's a field_info, not a dependency
if field_info is not None:
+ # Handle field_info.in_
if is_path_param:
assert isinstance(field_info, params.Path), (
f"Cannot use `{field_info.__class__.__name__}` for path param"
@@ -436,12 +459,14 @@ def analyze_param(
field_info,
param_name,
)
+ if isinstance(field_info, params.Form):
+ ensure_multipart_is_installed()
if not field_info.alias and getattr(field_info, "convert_underscores", None):
alias = param_name.replace("_", "-")
else:
alias = field_info.alias or param_name
field_info.alias = alias
- field = create_response_field(
+ field = create_model_field(
name=param_name,
type_=use_annotation_from_field_info,
default=field_info.default,
@@ -449,27 +474,14 @@ def analyze_param(
required=field_info.default in (Required, Undefined),
field_info=field_info,
)
+ if is_path_param:
+ assert is_scalar_field(
+ field=field
+ ), "Path params must be of one of the supported types"
+ elif isinstance(field_info, params.Query):
+ assert is_scalar_field(field) or is_scalar_sequence_field(field)
- return type_annotation, depends, field
-
-
-def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool:
- if is_path_param:
- assert is_scalar_field(
- field=param_field
- ), "Path params must be of one of the supported types"
- return False
- elif is_scalar_field(field=param_field):
- return False
- elif isinstance(
- param_field.field_info, (params.Query, params.Header)
- ) and is_scalar_sequence_field(param_field):
- return False
- else:
- assert isinstance(
- param_field.field_info, params.Body
- ), f"Param: {param_field.name} can only be a request body, using Body()"
- return True
+ return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
@@ -521,6 +533,15 @@ async def solve_generator(
return await stack.enter_async_context(cm)
+@dataclass
+class SolvedDependency:
+ values: Dict[str, Any]
+ errors: List[Any]
+ background_tasks: Optional[StarletteBackgroundTasks]
+ response: Response
+ dependency_cache: Dict[Tuple[Callable[..., Any], Tuple[str]], Any]
+
+
async def solve_dependencies(
*,
request: Union[Request, WebSocket],
@@ -531,13 +552,8 @@ async def solve_dependencies(
dependency_overrides_provider: Optional[Any] = None,
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
async_exit_stack: AsyncExitStack,
-) -> Tuple[
- Dict[str, Any],
- List[Any],
- Optional[StarletteBackgroundTasks],
- Response,
- Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
-]:
+ embed_body_fields: bool,
+) -> SolvedDependency:
values: Dict[str, Any] = {}
errors: List[Any] = []
if response is None:
@@ -578,28 +594,23 @@ async def solve_dependencies(
dependency_overrides_provider=dependency_overrides_provider,
dependency_cache=dependency_cache,
async_exit_stack=async_exit_stack,
+ embed_body_fields=embed_body_fields,
)
- (
- sub_values,
- sub_errors,
- background_tasks,
- _, # the subdependency returns the same response we have
- sub_dependency_cache,
- ) = solved_result
- dependency_cache.update(sub_dependency_cache)
- if sub_errors:
- errors.extend(sub_errors)
+ background_tasks = solved_result.background_tasks
+ dependency_cache.update(solved_result.dependency_cache)
+ if solved_result.errors:
+ errors.extend(solved_result.errors)
continue
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
solved = dependency_cache[sub_dependant.cache_key]
elif is_gen_callable(call) or is_async_gen_callable(call):
solved = await solve_generator(
- call=call, stack=async_exit_stack, sub_values=sub_values
+ call=call, stack=async_exit_stack, sub_values=solved_result.values
)
elif is_coroutine_callable(call):
- solved = await call(**sub_values)
+ solved = await call(**solved_result.values)
else:
- solved = await run_in_threadpool(call, **sub_values)
+ solved = await run_in_threadpool(call, **solved_result.values)
if sub_dependant.name is not None:
values[sub_dependant.name] = solved
if sub_dependant.cache_key not in dependency_cache:
@@ -626,7 +637,9 @@ async def solve_dependencies(
body_values,
body_errors,
) = await request_body_to_args( # body_params checked above
- required_params=dependant.body_params, received_body=body
+ body_fields=dependant.body_params,
+ received_body=body,
+ embed_body_fields=embed_body_fields,
)
values.update(body_values)
errors.extend(body_errors)
@@ -646,142 +659,206 @@ async def solve_dependencies(
values[dependant.security_scopes_param_name] = SecurityScopes(
scopes=dependant.security_scopes
)
- return values, errors, background_tasks, response, dependency_cache
+ return SolvedDependency(
+ values=values,
+ errors=errors,
+ background_tasks=background_tasks,
+ response=response,
+ dependency_cache=dependency_cache,
+ )
+
+
+def _validate_value_with_model_field(
+ *, field: ModelField, value: Any, values: Dict[str, Any], loc: Tuple[str, ...]
+) -> Tuple[Any, List[Any]]:
+ if value is None:
+ if field.required:
+ return None, [get_missing_field_error(loc=loc)]
+ else:
+ return deepcopy(field.default), []
+ v_, errors_ = field.validate(value, values, loc=loc)
+ if isinstance(errors_, ErrorWrapper):
+ return None, [errors_]
+ elif isinstance(errors_, list):
+ new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
+ return None, new_errors
+ else:
+ return v_, []
+
+
+def _get_multidict_value(field: ModelField, values: Mapping[str, Any]) -> Any:
+ if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
+ value = values.getlist(field.alias)
+ else:
+ value = values.get(field.alias, None)
+ if (
+ value is None
+ or (
+ isinstance(field.field_info, params.Form)
+ and isinstance(value, str) # For type checks
+ and value == ""
+ )
+ or (is_sequence_field(field) and len(value) == 0)
+ ):
+ if field.required:
+ return
+ else:
+ return deepcopy(field.default)
+ return value
def request_params_to_args(
- required_params: Sequence[ModelField],
+ fields: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[Any]]:
- values = {}
+ values: Dict[str, Any] = {}
errors = []
- for field in required_params:
- if is_scalar_sequence_field(field) and isinstance(
- received_params, (QueryParams, Headers)
- ):
- value = received_params.getlist(field.alias) or field.default
- else:
- value = received_params.get(field.alias)
+ for field in fields:
+ value = _get_multidict_value(field, received_params)
field_info = field.field_info
assert isinstance(
field_info, params.Param
), "Params must be subclasses of Param"
loc = (field_info.in_.value, field.alias)
- if value is None:
- if field.required:
- errors.append(get_missing_field_error(loc=loc))
- else:
- values[field.name] = deepcopy(field.default)
- continue
- v_, errors_ = field.validate(value, values, loc=loc)
- if isinstance(errors_, ErrorWrapper):
- errors.append(errors_)
- elif isinstance(errors_, list):
- new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
- errors.extend(new_errors)
+ v_, errors_ = _validate_value_with_model_field(
+ field=field, value=value, values=values, loc=loc
+ )
+ if errors_:
+ errors.extend(errors_)
else:
values[field.name] = v_
return values, errors
+def _should_embed_body_fields(fields: List[ModelField]) -> bool:
+ if not fields:
+ return False
+ # More than one dependency could have the same field, it would show up as multiple
+ # fields but it's the same one, so count them by name
+ body_param_names_set = {field.name for field in fields}
+ # A top level field has to be a single field, not multiple
+ if len(body_param_names_set) > 1:
+ return True
+ first_field = fields[0]
+ # If it explicitly specifies it is embedded, it has to be embedded
+ if getattr(first_field.field_info, "embed", None):
+ return True
+ # If it's a Form (or File) field, it has to be a BaseModel to be top level
+ # otherwise it has to be embedded, so that the key value pair can be extracted
+ if isinstance(first_field.field_info, params.Form) and not lenient_issubclass(
+ first_field.type_, BaseModel
+ ):
+ return True
+ return False
+
+
+async def _extract_form_body(
+ body_fields: List[ModelField],
+ received_body: FormData,
+) -> Dict[str, Any]:
+ values = {}
+ first_field = body_fields[0]
+ first_field_info = first_field.field_info
+
+ for field in body_fields:
+ value = _get_multidict_value(field, received_body)
+ if (
+ isinstance(first_field_info, params.File)
+ and is_bytes_field(field)
+ and isinstance(value, UploadFile)
+ ):
+ value = await value.read()
+ elif (
+ is_bytes_sequence_field(field)
+ and isinstance(first_field_info, params.File)
+ and value_is_sequence(value)
+ ):
+ # For types
+ assert isinstance(value, sequence_types) # type: ignore[arg-type]
+ results: List[Union[bytes, str]] = []
+
+ async def process_fn(
+ fn: Callable[[], Coroutine[Any, Any, Any]],
+ ) -> None:
+ result = await fn()
+ results.append(result) # noqa: B023
+
+ async with anyio.create_task_group() as tg:
+ for sub_value in value:
+ tg.start_soon(process_fn, sub_value.read)
+ value = serialize_sequence_value(field=field, value=results)
+ if value is not None:
+ values[field.name] = value
+ for key, value in received_body.items():
+ if key not in values:
+ values[key] = value
+ return values
+
+
async def request_body_to_args(
- required_params: List[ModelField],
+ body_fields: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]],
+ embed_body_fields: bool,
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
- values = {}
+ values: Dict[str, Any] = {}
errors: List[Dict[str, Any]] = []
- if required_params:
- field = required_params[0]
- field_info = field.field_info
- embed = getattr(field_info, "embed", None)
- field_alias_omitted = len(required_params) == 1 and not embed
- if field_alias_omitted:
- received_body = {field.alias: received_body}
-
- for field in required_params:
- loc: Tuple[str, ...]
- if field_alias_omitted:
- loc = ("body",)
- else:
- loc = ("body", field.alias)
-
- value: Optional[Any] = None
- if received_body is not None:
- if (is_sequence_field(field)) and isinstance(received_body, FormData):
- value = received_body.getlist(field.alias)
- else:
- try:
- value = received_body.get(field.alias)
- except AttributeError:
- errors.append(get_missing_field_error(loc))
- continue
- if (
- value is None
- or (isinstance(field_info, params.Form) and value == "")
- or (
- isinstance(field_info, params.Form)
- and is_sequence_field(field)
- and len(value) == 0
- )
- ):
- if field.required:
- errors.append(get_missing_field_error(loc))
- else:
- values[field.name] = deepcopy(field.default)
+ assert body_fields, "request_body_to_args() should be called with fields"
+ single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields
+ first_field = body_fields[0]
+ body_to_process = received_body
+
+ fields_to_extract: List[ModelField] = body_fields
+
+ if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel):
+ fields_to_extract = get_cached_model_fields(first_field.type_)
+
+ if isinstance(received_body, FormData):
+ body_to_process = await _extract_form_body(fields_to_extract, received_body)
+
+ if single_not_embedded_field:
+ loc: Tuple[str, ...] = ("body",)
+ v_, errors_ = _validate_value_with_model_field(
+ field=first_field, value=body_to_process, values=values, loc=loc
+ )
+ return {first_field.name: v_}, errors_
+ for field in body_fields:
+ loc = ("body", field.alias)
+ value: Optional[Any] = None
+ if body_to_process is not None:
+ try:
+ value = body_to_process.get(field.alias)
+ # If the received body is a list, not a dict
+ except AttributeError:
+ errors.append(get_missing_field_error(loc))
continue
- if (
- isinstance(field_info, params.File)
- and is_bytes_field(field)
- and isinstance(value, UploadFile)
- ):
- value = await value.read()
- elif (
- is_bytes_sequence_field(field)
- and isinstance(field_info, params.File)
- and value_is_sequence(value)
- ):
- # For types
- assert isinstance(value, sequence_types) # type: ignore[arg-type]
- results: List[Union[bytes, str]] = []
-
- async def process_fn(
- fn: Callable[[], Coroutine[Any, Any, Any]],
- ) -> None:
- result = await fn()
- results.append(result) # noqa: B023
-
- async with anyio.create_task_group() as tg:
- for sub_value in value:
- tg.start_soon(process_fn, sub_value.read)
- value = serialize_sequence_value(field=field, value=results)
-
- v_, errors_ = field.validate(value, values, loc=loc)
-
- if isinstance(errors_, list):
- errors.extend(errors_)
- elif errors_:
- errors.append(errors_)
- else:
- values[field.name] = v_
+ v_, errors_ = _validate_value_with_model_field(
+ field=field, value=value, values=values, loc=loc
+ )
+ if errors_:
+ errors.extend(errors_)
+ else:
+ values[field.name] = v_
return values, errors
-def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
- flat_dependant = get_flat_dependant(dependant)
+def get_body_field(
+ *, flat_dependant: Dependant, name: str, embed_body_fields: bool
+) -> Optional[ModelField]:
+ """
+ Get a ModelField representing the request body for a path operation, combining
+ all body parameters into a single field if necessary.
+
+ Used to check if it's form data (with `isinstance(body_field, params.Form)`)
+ or JSON and to generate the JSON Schema for a request body.
+
+ This is **not** used to validate/parse the request body, that's done with each
+ individual body parameter.
+ """
if not flat_dependant.body_params:
return None
first_param = flat_dependant.body_params[0]
- field_info = first_param.field_info
- embed = getattr(field_info, "embed", None)
- body_param_names_set = {param.name for param in flat_dependant.body_params}
- if len(body_param_names_set) == 1 and not embed:
- check_file_field(first_param)
+ if not embed_body_fields:
return first_param
- # If one field requires to embed, all have to be embedded
- # in case a sub-dependency is evaluated with a single unique body field
- # That is combined (embedded) with other body fields
- for param in flat_dependant.body_params:
- setattr(param.field_info, "embed", True) # noqa: B010
model_name = "Body_" + name
BodyModel = create_body_model(
fields=flat_dependant.body_params, model_name=model_name
@@ -807,12 +884,11 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
]
if len(set(body_param_media_types)) == 1:
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]
- final_field = create_response_field(
+ final_field = create_model_field(
name="body",
type_=BodyModel,
required=required,
alias="body",
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
)
- check_file_field(final_field)
return final_field
diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py
index 0d5f27af48a74..7ddaace25a936 100644
--- a/fastapi/param_functions.py
+++ b/fastapi/param_functions.py
@@ -1282,7 +1282,7 @@ def Body( # noqa: N802
),
] = _Unset,
embed: Annotated[
- bool,
+ Union[bool, None],
Doc(
"""
When `embed` is `True`, the parameter will be expected in a JSON body as a
@@ -1294,7 +1294,7 @@ def Body( # noqa: N802
[FastAPI docs for Body - Multiple Parameters](https://fastapi.tiangolo.com/tutorial/body-multiple-params/#embed-a-single-body-parameter).
"""
),
- ] = False,
+ ] = None,
media_type: Annotated[
str,
Doc(
diff --git a/fastapi/params.py b/fastapi/params.py
index cc2a5c13c0261..90ca7cb010e6c 100644
--- a/fastapi/params.py
+++ b/fastapi/params.py
@@ -479,7 +479,7 @@ def __init__(
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
- embed: bool = False,
+ embed: Union[bool, None] = None,
media_type: str = "application/json",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
@@ -556,7 +556,7 @@ def __init__(
kwargs["examples"] = examples
if regex is not None:
warnings.warn(
- "`regex` has been depreacated, please use `pattern` instead",
+ "`regex` has been deprecated, please use `pattern` instead",
category=DeprecationWarning,
stacklevel=4,
)
@@ -642,7 +642,6 @@ def __init__(
default=default,
default_factory=default_factory,
annotation=annotation,
- embed=True,
media_type=media_type,
alias=alias,
alias_priority=alias_priority,
diff --git a/fastapi/routing.py b/fastapi/routing.py
index 49f1b60138f3d..86e30360216cb 100644
--- a/fastapi/routing.py
+++ b/fastapi/routing.py
@@ -33,8 +33,10 @@
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import (
+ _should_embed_body_fields,
get_body_field,
get_dependant,
+ get_flat_dependant,
get_parameterless_sub_dependant,
get_typed_return_annotation,
solve_dependencies,
@@ -49,7 +51,7 @@
from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import (
create_cloned_field,
- create_response_field,
+ create_model_field,
generate_unique_id,
get_value_or_default,
is_body_allowed_for_status_code,
@@ -225,6 +227,7 @@ def get_request_handler(
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
dependency_overrides_provider: Optional[Any] = None,
+ embed_body_fields: bool = False,
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = asyncio.iscoroutinefunction(dependant.call)
@@ -291,27 +294,36 @@ async def app(request: Request) -> Response:
body=body,
dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack,
+ embed_body_fields=embed_body_fields,
)
- values, errors, background_tasks, sub_response, _ = solved_result
+ errors = solved_result.errors
if not errors:
raw_response = await run_endpoint_function(
- dependant=dependant, values=values, is_coroutine=is_coroutine
+ dependant=dependant,
+ values=solved_result.values,
+ is_coroutine=is_coroutine,
)
if isinstance(raw_response, Response):
if raw_response.background is None:
- raw_response.background = background_tasks
+ raw_response.background = solved_result.background_tasks
response = raw_response
else:
- response_args: Dict[str, Any] = {"background": background_tasks}
+ response_args: Dict[str, Any] = {
+ "background": solved_result.background_tasks
+ }
# If status_code was set, use it, otherwise use the default from the
# response class, in the case of redirect it's 307
current_status_code = (
- status_code if status_code else sub_response.status_code
+ status_code
+ if status_code
+ else solved_result.response.status_code
)
if current_status_code is not None:
response_args["status_code"] = current_status_code
- if sub_response.status_code:
- response_args["status_code"] = sub_response.status_code
+ if solved_result.response.status_code:
+ response_args["status_code"] = (
+ solved_result.response.status_code
+ )
content = await serialize_response(
field=response_field,
response_content=raw_response,
@@ -326,7 +338,7 @@ async def app(request: Request) -> Response:
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(sub_response.headers.raw)
+ response.headers.raw.extend(solved_result.response.headers.raw)
if errors:
validation_error = RequestValidationError(
_normalize_errors(errors), body=body
@@ -346,7 +358,9 @@ async def app(request: Request) -> Response:
def get_websocket_app(
- dependant: Dependant, dependency_overrides_provider: Optional[Any] = None
+ dependant: Dependant,
+ dependency_overrides_provider: Optional[Any] = None,
+ embed_body_fields: bool = False,
) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:
async def app(websocket: WebSocket) -> None:
async with AsyncExitStack() as async_exit_stack:
@@ -359,12 +373,14 @@ async def app(websocket: WebSocket) -> None:
dependant=dependant,
dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack,
+ embed_body_fields=embed_body_fields,
)
- values, errors, _, _2, _3 = solved_result
- if errors:
- raise WebSocketRequestValidationError(_normalize_errors(errors))
+ if solved_result.errors:
+ raise WebSocketRequestValidationError(
+ _normalize_errors(solved_result.errors)
+ )
assert dependant.call is not None, "dependant.call must be a function"
- await dependant.call(**values)
+ await dependant.call(**solved_result.values)
return app
@@ -390,11 +406,15 @@ def __init__(
0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
)
-
+ self._flat_dependant = get_flat_dependant(self.dependant)
+ self._embed_body_fields = _should_embed_body_fields(
+ self._flat_dependant.body_params
+ )
self.app = websocket_session(
get_websocket_app(
dependant=self.dependant,
dependency_overrides_provider=dependency_overrides_provider,
+ embed_body_fields=self._embed_body_fields,
)
)
@@ -488,7 +508,7 @@ def __init__(
status_code
), f"Status code {status_code} must not have a response body"
response_name = "Response_" + self.unique_id
- self.response_field = create_response_field(
+ self.response_field = create_model_field(
name=response_name,
type_=self.response_model,
mode="serialization",
@@ -521,7 +541,7 @@ def __init__(
additional_status_code
), f"Status code {additional_status_code} must not have a response body"
response_name = f"Response_{additional_status_code}_{self.unique_id}"
- response_field = create_response_field(name=response_name, type_=model)
+ response_field = create_model_field(name=response_name, type_=model)
response_fields[additional_status_code] = response_field
if response_fields:
self.response_fields: Dict[Union[int, str], ModelField] = response_fields
@@ -535,7 +555,15 @@ def __init__(
0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
)
- self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
+ self._flat_dependant = get_flat_dependant(self.dependant)
+ self._embed_body_fields = _should_embed_body_fields(
+ self._flat_dependant.body_params
+ )
+ self.body_field = get_body_field(
+ flat_dependant=self._flat_dependant,
+ name=self.unique_id,
+ embed_body_fields=self._embed_body_fields,
+ )
self.app = request_response(self.get_route_handler())
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
@@ -552,6 +580,7 @@ def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]
response_model_exclude_defaults=self.response_model_exclude_defaults,
response_model_exclude_none=self.response_model_exclude_none,
dependency_overrides_provider=self.dependency_overrides_provider,
+ embed_body_fields=self._embed_body_fields,
)
def matches(self, scope: Scope) -> Tuple[Match, Scope]:
diff --git a/fastapi/utils.py b/fastapi/utils.py
index 5c2538facaa93..4c7350fea9e5e 100644
--- a/fastapi/utils.py
+++ b/fastapi/utils.py
@@ -60,9 +60,9 @@ def get_path_param_names(path: str) -> Set[str]:
return set(re.findall("{(.*?)}", path))
-def create_response_field(
+def create_model_field(
name: str,
- type_: Type[Any],
+ type_: Any,
class_validators: Optional[Dict[str, Validator]] = None,
default: Optional[Any] = Undefined,
required: Union[bool, UndefinedType] = Undefined,
@@ -71,9 +71,6 @@ def create_response_field(
alias: Optional[str] = None,
mode: Literal["validation", "serialization"] = "validation",
) -> ModelField:
- """
- Create a new response field. Raises if type_ is invalid.
- """
class_validators = class_validators or {}
if PYDANTIC_V2:
field_info = field_info or FieldInfo(
@@ -135,7 +132,7 @@ def create_cloned_field(
use_type.__fields__[f.name] = create_cloned_field(
f, cloned_types=cloned_types
)
- new_field = create_response_field(name=field.name, type_=use_type)
+ new_field = create_model_field(name=field.name, type_=use_type)
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
new_field.alias = field.alias # type: ignore[misc]
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
diff --git a/pyproject.toml b/pyproject.toml
index bb87be470e375..1be2817a1f32f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -241,3 +241,7 @@ known-third-party = ["fastapi", "pydantic", "starlette"]
[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true
+
+[tool.inline-snapshot]
+# default-flags=["fix"]
+# default-flags=["create"]
diff --git a/requirements-docs.txt b/requirements-docs.txt
index ab2b0165be0b8..332fd1857eddb 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -8,7 +8,7 @@ pyyaml >=5.3.1,<7.0.0
# For Material for MkDocs, Chinese search
jieba==0.42.1
# For image processing by Material for MkDocs
-pillow==10.3.0
+pillow==10.4.0
# For image processing by Material for MkDocs
cairosvg==2.7.1
mkdocstrings[python]==0.25.1
diff --git a/requirements-tests.txt b/requirements-tests.txt
index 08561d23a9ac0..2f2576dd503e1 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -3,7 +3,7 @@
pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0
mypy ==1.8.0
-ruff ==0.6.1
+ruff ==0.6.4
dirty-equals ==0.6.0
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel
@@ -14,7 +14,7 @@ anyio[trio] >=3.2.1,<4.0.0
PyJWT==2.8.0
pyyaml >=5.3.1,<7.0.0
passlib[bcrypt] >=1.7.2,<2.0.0
-
+inline-snapshot==0.13.0
# types
types-ujson ==5.7.0.1
types-orjson ==3.6.2
diff --git a/scripts/playwright/request_form_models/image01.py b/scripts/playwright/request_form_models/image01.py
new file mode 100644
index 0000000000000..15bd3858c52c8
--- /dev/null
+++ b/scripts/playwright/request_form_models/image01.py
@@ -0,0 +1,36 @@
+import subprocess
+import time
+
+import httpx
+from playwright.sync_api import Playwright, sync_playwright
+
+
+# Run playwright codegen to generate the code below, copy paste the sections in run()
+def run(playwright: Playwright) -> None:
+ browser = playwright.chromium.launch(headless=False)
+ context = browser.new_context()
+ page = context.new_page()
+ page.goto("http://localhost:8000/docs")
+ page.get_by_role("button", name="POST /login/ Login").click()
+ page.get_by_role("button", name="Try it out").click()
+ page.screenshot(path="docs/en/docs/img/tutorial/request-form-models/image01.png")
+
+ # ---------------------
+ context.close()
+ browser.close()
+
+
+process = subprocess.Popen(
+ ["fastapi", "run", "docs_src/request_form_models/tutorial001.py"]
+)
+try:
+ for _ in range(3):
+ try:
+ response = httpx.get("http://localhost:8000/docs")
+ except httpx.ConnectError:
+ time.sleep(1)
+ break
+ with sync_playwright() as playwright:
+ run(playwright)
+finally:
+ process.terminate()
diff --git a/tests/test_compat.py b/tests/test_compat.py
index bf268b860b1dc..f4a3093c5ee4f 100644
--- a/tests/test_compat.py
+++ b/tests/test_compat.py
@@ -1,11 +1,14 @@
-from typing import List, Union
+from typing import Any, Dict, List, Union
from fastapi import FastAPI, UploadFile
from fastapi._compat import (
ModelField,
Undefined,
_get_model_config,
+ get_cached_model_fields,
+ get_model_fields,
is_bytes_sequence_annotation,
+ is_scalar_field,
is_uploadfile_sequence_annotation,
)
from fastapi.testclient import TestClient
@@ -91,3 +94,27 @@ def test_is_uploadfile_sequence_annotation():
# and other types, but I'm not even sure it's a good idea to support it as a first
# class "feature"
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
+
+
+def test_is_pv1_scalar_field():
+ # For coverage
+ class Model(BaseModel):
+ foo: Union[str, Dict[str, Any]]
+
+ fields = get_model_fields(Model)
+ assert not is_scalar_field(fields[0])
+
+
+def test_get_model_fields_cached():
+ class Model(BaseModel):
+ foo: str
+
+ non_cached_fields = get_model_fields(Model)
+ non_cached_fields2 = get_model_fields(Model)
+ cached_fields = get_cached_model_fields(Model)
+ cached_fields2 = get_cached_model_fields(Model)
+ for f1, f2 in zip(cached_fields, cached_fields2):
+ assert f1 is f2
+
+ assert non_cached_fields is not non_cached_fields2
+ assert cached_fields is cached_fields2
diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py
new file mode 100644
index 0000000000000..7ed3ba3a2df55
--- /dev/null
+++ b/tests/test_forms_single_model.py
@@ -0,0 +1,129 @@
+from typing import List, Optional
+
+from dirty_equals import IsDict
+from fastapi import FastAPI, Form
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+class FormModel(BaseModel):
+ username: str
+ lastname: str
+ age: Optional[int] = None
+ tags: List[str] = ["foo", "bar"]
+
+
+@app.post("/form/")
+def post_form(user: Annotated[FormModel, Form()]):
+ return user
+
+
+client = TestClient(app)
+
+
+def test_send_all_data():
+ response = client.post(
+ "/form/",
+ data={
+ "username": "Rick",
+ "lastname": "Sanchez",
+ "age": "70",
+ "tags": ["plumbus", "citadel"],
+ },
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "username": "Rick",
+ "lastname": "Sanchez",
+ "age": 70,
+ "tags": ["plumbus", "citadel"],
+ }
+
+
+def test_defaults():
+ response = client.post("/form/", data={"username": "Rick", "lastname": "Sanchez"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "username": "Rick",
+ "lastname": "Sanchez",
+ "age": None,
+ "tags": ["foo", "bar"],
+ }
+
+
+def test_invalid_data():
+ response = client.post(
+ "/form/",
+ data={
+ "username": "Rick",
+ "lastname": "Sanchez",
+ "age": "seventy",
+ "tags": ["plumbus", "citadel"],
+ },
+ )
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["body", "age"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "seventy",
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "age"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_no_data():
+ response = client.post("/form/")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {"tags": ["foo", "bar"]},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "lastname"],
+ "msg": "Field required",
+ "input": {"tags": ["foo", "bar"]},
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "lastname"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
diff --git a/tests/test_forms_single_param.py b/tests/test_forms_single_param.py
new file mode 100644
index 0000000000000..3bb951441f52c
--- /dev/null
+++ b/tests/test_forms_single_param.py
@@ -0,0 +1,99 @@
+from fastapi import FastAPI, Form
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+@app.post("/form/")
+def post_form(username: Annotated[str, Form()]):
+ return username
+
+
+client = TestClient(app)
+
+
+def test_single_form_field():
+ response = client.post("/form/", data={"username": "Rick"})
+ assert response.status_code == 200, response.text
+ assert response.json() == "Rick"
+
+
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/form/": {
+ "post": {
+ "summary": "Post Form",
+ "operationId": "post_form_form__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_post_form_form__post"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Body_post_form_form__post": {
+ "properties": {"username": {"type": "string", "title": "Username"}},
+ "type": "object",
+ "required": ["username"],
+ "title": "Body_post_form_form__post",
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py
index 6597e5058bfed..b3f83ae237ff8 100644
--- a/tests/test_openapi_examples.py
+++ b/tests/test_openapi_examples.py
@@ -155,13 +155,26 @@ def test_openapi_schema():
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- "examples": [
- {"data": "Data in Body examples, example1"}
- ],
- },
+ "schema": IsDict(
+ {
+ "$ref": "#/components/schemas/Item",
+ "examples": [
+ {"data": "Data in Body examples, example1"}
+ ],
+ }
+ )
+ | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "allOf": [
+ {"$ref": "#/components/schemas/Item"}
+ ],
+ "title": "Item",
+ "examples": [
+ {"data": "Data in Body examples, example1"}
+ ],
+ }
+ ),
"examples": {
"Example One": {
"summary": "Example One Summary",
diff --git a/tests/test_tutorial/test_request_form_models/__init__.py b/tests/test_tutorial/test_request_form_models/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py
new file mode 100644
index 0000000000000..46c130ee8c167
--- /dev/null
+++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py
@@ -0,0 +1,232 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_form_models.tutorial001 import app
+
+ client = TestClient(app)
+ return client
+
+
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo", "password": "secret"}
+
+
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {"username": "Foo"},
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {"password": "secret"},
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_post_body_json(client: TestClient):
+ response = client.post("/login/", json={"username": "Foo", "password": "secret"})
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/login/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Login",
+ "operationId": "login_login__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {"$ref": "#/components/schemas/FormData"}
+ }
+ },
+ "required": True,
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FormData": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "password": {"type": "string", "title": "Password"},
+ },
+ "type": "object",
+ "required": ["username", "password"],
+ "title": "FormData",
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py
new file mode 100644
index 0000000000000..4e14d89c84f26
--- /dev/null
+++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py
@@ -0,0 +1,232 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_form_models.tutorial001_an import app
+
+ client = TestClient(app)
+ return client
+
+
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo", "password": "secret"}
+
+
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {"username": "Foo"},
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {"password": "secret"},
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_post_body_json(client: TestClient):
+ response = client.post("/login/", json={"username": "Foo", "password": "secret"})
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/login/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Login",
+ "operationId": "login_login__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {"$ref": "#/components/schemas/FormData"}
+ }
+ },
+ "required": True,
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FormData": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "password": {"type": "string", "title": "Password"},
+ },
+ "type": "object",
+ "required": ["username", "password"],
+ "title": "FormData",
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py
new file mode 100644
index 0000000000000..2e6426aa78165
--- /dev/null
+++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py
@@ -0,0 +1,240 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+
+from tests.utils import needs_py39
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_form_models.tutorial001_an_py39 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py39
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo", "password": "secret"}
+
+
+@needs_py39
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {"username": "Foo"},
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {"password": "secret"},
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_post_body_json(client: TestClient):
+ response = client.post("/login/", json={"username": "Foo", "password": "secret"})
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/login/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Login",
+ "operationId": "login_login__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {"$ref": "#/components/schemas/FormData"}
+ }
+ },
+ "required": True,
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FormData": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "password": {"type": "string", "title": "Password"},
+ },
+ "type": "object",
+ "required": ["username", "password"],
+ "title": "FormData",
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002.py b/tests/test_tutorial/test_request_form_models/test_tutorial002.py
new file mode 100644
index 0000000000000..76f4800016fb8
--- /dev/null
+++ b/tests/test_tutorial/test_request_form_models/test_tutorial002.py
@@ -0,0 +1,196 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from tests.utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_form_models.tutorial002 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv2
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo", "password": "secret"}
+
+
+@needs_pydanticv2
+def test_post_body_extra_form(client: TestClient):
+ response = client.post(
+ "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"}
+ )
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "extra_forbidden",
+ "loc": ["body", "extra"],
+ "msg": "Extra inputs are not permitted",
+ "input": "extra",
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {"username": "Foo"},
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {"password": "secret"},
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_post_body_json(client: TestClient):
+ response = client.post("/login/", json={"username": "Foo", "password": "secret"})
+ assert response.status_code == 422, response.text
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/login/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Login",
+ "operationId": "login_login__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {"$ref": "#/components/schemas/FormData"}
+ }
+ },
+ "required": True,
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FormData": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "password": {"type": "string", "title": "Password"},
+ },
+ "additionalProperties": False,
+ "type": "object",
+ "required": ["username", "password"],
+ "title": "FormData",
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py
new file mode 100644
index 0000000000000..179b2977d6695
--- /dev/null
+++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py
@@ -0,0 +1,196 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from tests.utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_form_models.tutorial002_an import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv2
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo", "password": "secret"}
+
+
+@needs_pydanticv2
+def test_post_body_extra_form(client: TestClient):
+ response = client.post(
+ "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"}
+ )
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "extra_forbidden",
+ "loc": ["body", "extra"],
+ "msg": "Extra inputs are not permitted",
+ "input": "extra",
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {"username": "Foo"},
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {"password": "secret"},
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_post_body_json(client: TestClient):
+ response = client.post("/login/", json={"username": "Foo", "password": "secret"})
+ assert response.status_code == 422, response.text
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/login/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Login",
+ "operationId": "login_login__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {"$ref": "#/components/schemas/FormData"}
+ }
+ },
+ "required": True,
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FormData": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "password": {"type": "string", "title": "Password"},
+ },
+ "additionalProperties": False,
+ "type": "object",
+ "required": ["username", "password"],
+ "title": "FormData",
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py
new file mode 100644
index 0000000000000..510ad9d7c022a
--- /dev/null
+++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py
@@ -0,0 +1,203 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from tests.utils import needs_py39, needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_form_models.tutorial002_an_py39 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv2
+@needs_py39
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo", "password": "secret"}
+
+
+@needs_pydanticv2
+@needs_py39
+def test_post_body_extra_form(client: TestClient):
+ response = client.post(
+ "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"}
+ )
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "extra_forbidden",
+ "loc": ["body", "extra"],
+ "msg": "Extra inputs are not permitted",
+ "input": "extra",
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@needs_py39
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {"username": "Foo"},
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@needs_py39
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {"password": "secret"},
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@needs_py39
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+
+
+@needs_pydanticv2
+@needs_py39
+def test_post_body_json(client: TestClient):
+ response = client.post("/login/", json={"username": "Foo", "password": "secret"})
+ assert response.status_code == 422, response.text
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": {},
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": {},
+ },
+ ]
+ }
+
+
+@needs_pydanticv2
+@needs_py39
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/login/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Login",
+ "operationId": "login_login__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {"$ref": "#/components/schemas/FormData"}
+ }
+ },
+ "required": True,
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FormData": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "password": {"type": "string", "title": "Password"},
+ },
+ "additionalProperties": False,
+ "type": "object",
+ "required": ["username", "password"],
+ "title": "FormData",
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py
new file mode 100644
index 0000000000000..249b9379df726
--- /dev/null
+++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py
@@ -0,0 +1,189 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from tests.utils import needs_pydanticv1
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_form_models.tutorial002_pv1 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv1
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo", "password": "secret"}
+
+
+@needs_pydanticv1
+def test_post_body_extra_form(client: TestClient):
+ response = client.post(
+ "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"}
+ )
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.extra",
+ "loc": ["body", "extra"],
+ "msg": "extra fields not permitted",
+ }
+ ]
+ }
+
+
+@needs_pydanticv1
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "password"],
+ "msg": "field required",
+ }
+ ]
+ }
+
+
+@needs_pydanticv1
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "username"],
+ "msg": "field required",
+ }
+ ]
+ }
+
+
+@needs_pydanticv1
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "username"],
+ "msg": "field required",
+ },
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "password"],
+ "msg": "field required",
+ },
+ ]
+ }
+
+
+@needs_pydanticv1
+def test_post_body_json(client: TestClient):
+ response = client.post("/login/", json={"username": "Foo", "password": "secret"})
+ assert response.status_code == 422, response.text
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "username"],
+ "msg": "field required",
+ },
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "password"],
+ "msg": "field required",
+ },
+ ]
+ }
+
+
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/login/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Login",
+ "operationId": "login_login__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {"$ref": "#/components/schemas/FormData"}
+ }
+ },
+ "required": True,
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FormData": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "password": {"type": "string", "title": "Password"},
+ },
+ "additionalProperties": False,
+ "type": "object",
+ "required": ["username", "password"],
+ "title": "FormData",
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py
new file mode 100644
index 0000000000000..44cb3c32b63f8
--- /dev/null
+++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py
@@ -0,0 +1,196 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from tests.utils import needs_pydanticv1
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_form_models.tutorial002_pv1_an import app
+
+ client = TestClient(app)
+ return client
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo", "password": "secret"}
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_post_body_extra_form(client: TestClient):
+ response = client.post(
+ "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"}
+ )
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.extra",
+ "loc": ["body", "extra"],
+ "msg": "extra fields not permitted",
+ }
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "password"],
+ "msg": "field required",
+ }
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "username"],
+ "msg": "field required",
+ }
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "username"],
+ "msg": "field required",
+ },
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "password"],
+ "msg": "field required",
+ },
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_post_body_json(client: TestClient):
+ response = client.post("/login/", json={"username": "Foo", "password": "secret"})
+ assert response.status_code == 422, response.text
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "username"],
+ "msg": "field required",
+ },
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "password"],
+ "msg": "field required",
+ },
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/login/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Login",
+ "operationId": "login_login__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {"$ref": "#/components/schemas/FormData"}
+ }
+ },
+ "required": True,
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FormData": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "password": {"type": "string", "title": "Password"},
+ },
+ "additionalProperties": False,
+ "type": "object",
+ "required": ["username", "password"],
+ "title": "FormData",
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py
new file mode 100644
index 0000000000000..899549e406647
--- /dev/null
+++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py
@@ -0,0 +1,203 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from tests.utils import needs_py39, needs_pydanticv1
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_form_models.tutorial002_pv1_an_py39 import app
+
+ client = TestClient(app)
+ return client
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+@needs_py39
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo", "password": "secret"}
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+@needs_py39
+def test_post_body_extra_form(client: TestClient):
+ response = client.post(
+ "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"}
+ )
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.extra",
+ "loc": ["body", "extra"],
+ "msg": "extra fields not permitted",
+ }
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+@needs_py39
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "password"],
+ "msg": "field required",
+ }
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+@needs_py39
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "username"],
+ "msg": "field required",
+ }
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+@needs_py39
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "username"],
+ "msg": "field required",
+ },
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "password"],
+ "msg": "field required",
+ },
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+@needs_py39
+def test_post_body_json(client: TestClient):
+ response = client.post("/login/", json={"username": "Foo", "password": "secret"})
+ assert response.status_code == 422, response.text
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "username"],
+ "msg": "field required",
+ },
+ {
+ "type": "value_error.missing",
+ "loc": ["body", "password"],
+ "msg": "field required",
+ },
+ ]
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+@needs_py39
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/login/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Login",
+ "operationId": "login_login__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {"$ref": "#/components/schemas/FormData"}
+ }
+ },
+ "required": True,
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FormData": {
+ "properties": {
+ "username": {"type": "string", "title": "Username"},
+ "password": {"type": "string", "title": "Password"},
+ },
+ "additionalProperties": False,
+ "type": "object",
+ "required": ["username", "password"],
+ "title": "FormData",
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+ }