Browse Source

Merge branch 'master' into ka-GE_translation-init

pull/11897/head
David Kadaria 7 months ago
committed by GitHub
parent
commit
9927896176
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      .github/labeler.yml
  2. 1
      .github/workflows/build-docs.yml
  3. 7
      .github/workflows/issue-manager.yml
  4. 2
      .github/workflows/publish.yml
  5. 6
      .github/workflows/test.yml
  6. 2
      .pre-commit-config.yaml
  7. 2
      README.md
  8. 8
      docs/en/data/external_links.yml
  9. 2
      docs/en/data/sponsors.yml
  10. 2
      docs/en/docs/deployment/cloud.md
  11. BIN
      docs/en/docs/img/tutorial/request-form-models/image01.png
  12. 120
      docs/en/docs/release-notes.md
  13. 6
      docs/en/docs/tutorial/middleware.md
  14. 134
      docs/en/docs/tutorial/request-form-models.md
  15. 1
      docs/en/mkdocs.yml
  16. 2
      docs/en/overrides/main.html
  17. 28
      docs/ko/docs/project-generation.md
  18. 201
      docs/nl/docs/features.md
  19. 19
      docs/pt/docs/advanced/security/index.md
  20. 7
      docs/pt/docs/advanced/testing-events.md
  21. 4
      docs_src/middleware/tutorial001.py
  22. 14
      docs_src/request_form_models/tutorial001.py
  23. 15
      docs_src/request_form_models/tutorial001_an.py
  24. 16
      docs_src/request_form_models/tutorial001_an_py39.py
  25. 15
      docs_src/request_form_models/tutorial002.py
  26. 16
      docs_src/request_form_models/tutorial002_an.py
  27. 17
      docs_src/request_form_models/tutorial002_an_py39.py
  28. 17
      docs_src/request_form_models/tutorial002_pv1.py
  29. 18
      docs_src/request_form_models/tutorial002_pv1_an.py
  30. 19
      docs_src/request_form_models/tutorial002_pv1_an_py39.py
  31. 2
      fastapi/__init__.py
  32. 15
      fastapi/_compat.py
  33. 75
      fastapi/dependencies/models.py
  34. 440
      fastapi/dependencies/utils.py
  35. 4
      fastapi/param_functions.py
  36. 5
      fastapi/params.py
  37. 65
      fastapi/routing.py
  38. 9
      fastapi/utils.py
  39. 2
      requirements-docs.txt
  40. 2
      requirements-tests.txt
  41. 36
      scripts/playwright/request_form_models/image01.py
  42. 13
      tests/test_compat.py
  43. 129
      tests/test_forms_single_model.py
  44. 99
      tests/test_forms_single_param.py
  45. 27
      tests/test_openapi_examples.py
  46. 0
      tests/test_tutorial/test_request_form_models/__init__.py
  47. 232
      tests/test_tutorial/test_request_form_models/test_tutorial001.py
  48. 232
      tests/test_tutorial/test_request_form_models/test_tutorial001_an.py
  49. 240
      tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py
  50. 196
      tests/test_tutorial/test_request_form_models/test_tutorial002.py
  51. 196
      tests/test_tutorial/test_request_form_models/test_tutorial002_an.py
  52. 203
      tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py
  53. 189
      tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py
  54. 196
      tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py
  55. 203
      tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py

4
.github/labeler.yml

@ -7,6 +7,8 @@ docs:
- all-globs-to-all-files: - all-globs-to-all-files:
- '!fastapi/**' - '!fastapi/**'
- '!pyproject.toml' - '!pyproject.toml'
- '!docs/en/data/sponsors.yml'
- '!docs/en/overrides/main.html'
lang-all: lang-all:
- all: - all:
@ -28,6 +30,8 @@ internal:
- .pre-commit-config.yaml - .pre-commit-config.yaml
- pdm_build.py - pdm_build.py
- requirements*.txt - requirements*.txt
- docs/en/data/sponsors.yml
- docs/en/overrides/main.html
- all-globs-to-all-files: - all-globs-to-all-files:
- '!docs/*/docs/**' - '!docs/*/docs/**'
- '!fastapi/**' - '!fastapi/**'

1
.github/workflows/build-docs.yml

@ -113,6 +113,7 @@ jobs:
with: with:
name: docs-site-${{ matrix.lang }} name: docs-site-${{ matrix.lang }}
path: ./site/** path: ./site/**
include-hidden-files: true
# https://github.com/marketplace/actions/alls-green#why # https://github.com/marketplace/actions/alls-green#why
docs-all-green: # This job does nothing and is only used for the branch protection docs-all-green: # This job does nothing and is only used for the branch protection

7
.github/workflows/issue-manager.yml

@ -2,7 +2,7 @@ name: Issue Manager
on: on:
schedule: schedule:
- cron: "10 3 * * *" - cron: "13 22 * * *"
issue_comment: issue_comment:
types: types:
- created - created
@ -16,6 +16,7 @@ on:
permissions: permissions:
issues: write issues: write
pull-requests: write
jobs: jobs:
issue-manager: issue-manager:
@ -35,8 +36,8 @@ jobs:
"delay": 864000, "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." "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, "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."
} }
} }

2
.github/workflows/publish.yml

@ -35,7 +35,7 @@ jobs:
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }}
run: python -m build run: python -m build
- name: Publish - name: Publish
uses: pypa/gh-action-pypi-publish@v1.9.0 uses: pypa/gh-action-pypi-publish@v1.10.1
- name: Dump GitHub context - name: Dump GitHub context
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}

6
.github/workflows/test.yml

@ -37,7 +37,7 @@ jobs:
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt run: pip install -r requirements-tests.txt
- name: Install Pydantic v2 - 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 - name: Lint
run: bash scripts/lint.sh run: bash scripts/lint.sh
@ -79,7 +79,7 @@ jobs:
run: pip install "pydantic>=1.10.0,<2.0.0" run: pip install "pydantic>=1.10.0,<2.0.0"
- name: Install Pydantic v2 - name: Install Pydantic v2
if: matrix.pydantic-version == '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 - run: mkdir coverage
- name: Test - name: Test
run: bash scripts/test.sh run: bash scripts/test.sh
@ -91,6 +91,7 @@ jobs:
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
path: coverage path: coverage
include-hidden-files: true
coverage-combine: coverage-combine:
needs: [test] needs: [test]
@ -123,6 +124,7 @@ jobs:
with: with:
name: coverage-html name: coverage-html
path: htmlcov path: htmlcov
include-hidden-files: true
# https://github.com/marketplace/actions/alls-green#why # https://github.com/marketplace/actions/alls-green#why
check: # This job does nothing and is only used for the branch protection check: # This job does nothing and is only used for the branch protection

2
.pre-commit-config.yaml

@ -14,7 +14,7 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.2 rev: v0.6.3
hooks: hooks:
- id: ruff - id: ruff
args: args:

2
README.md

@ -52,7 +52,7 @@ The key features are:
<a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a> <a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a>
<a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a> <a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a>
<a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a> <a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a>
<a href="https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs" target="_blank" title="Coherence"><img src="https://fastapi.tiangolo.com/img/sponsors/coherence.png"></a> <a href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" target="_blank" title="Coherence"><img src="https://fastapi.tiangolo.com/img/sponsors/coherence.png"></a>
<a href="https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral" target="_blank" title="Simplify Full Stack Development with FastAPI & MongoDB"><img src="https://fastapi.tiangolo.com/img/sponsors/mongodb.png"></a> <a href="https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral" target="_blank" title="Simplify Full Stack Development with FastAPI & MongoDB"><img src="https://fastapi.tiangolo.com/img/sponsors/mongodb.png"></a>
<a href="https://zuplo.link/fastapi-gh" target="_blank" title="Zuplo: Scale, Protect, Document, and Monetize your FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/zuplo.png"></a> <a href="https://zuplo.link/fastapi-gh" target="_blank" title="Zuplo: Scale, Protect, Document, and Monetize your FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/zuplo.png"></a>
<a href="https://fine.dev?ref=fastapibadge" target="_blank" title="Fine's AI FastAPI Workflow: Effortlessly Deploy and Integrate FastAPI into Your Project"><img src="https://fastapi.tiangolo.com/img/sponsors/fine.png"></a> <a href="https://fine.dev?ref=fastapibadge" target="_blank" title="Fine's AI FastAPI Workflow: Effortlessly Deploy and Integrate FastAPI into Your Project"><img src="https://fastapi.tiangolo.com/img/sponsors/fine.png"></a>

8
docs/en/data/external_links.yml

@ -264,6 +264,14 @@ Articles:
author_link: https://devonray.com author_link: https://devonray.com
link: https://devonray.com/blog/deploying-a-fastapi-project-using-aws-lambda-aurora-cdk link: https://devonray.com/blog/deploying-a-fastapi-project-using-aws-lambda-aurora-cdk
title: Deployment using Docker, Lambda, Aurora, CDK & GH Actions 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: German:
- author: Marcel Sander (actidoo) - author: Marcel Sander (actidoo)
author_link: https://www.actidoo.com author_link: https://www.actidoo.com

2
docs/en/data/sponsors.yml

@ -17,7 +17,7 @@ gold:
- url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge - url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge
title: Auth, user management and more for your B2B product title: Auth, user management and more for your B2B product
img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png 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 title: Coherence
img: https://fastapi.tiangolo.com/img/sponsors/coherence.png 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 - url: https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral

2
docs/en/docs/deployment/cloud.md

@ -14,4 +14,4 @@ You might want to try their services and follow their guides:
* <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank">Platform.sh</a> * <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank">Platform.sh</a>
* <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a> * <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a>
* <a href="https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs" class="external-link" target="_blank">Coherence</a> * <a href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" class="external-link" target="_blank">Coherence</a>

BIN
docs/en/docs/img/tutorial/request-form-models/image01.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

120
docs/en/docs/release-notes.md

@ -7,8 +7,118 @@ hide:
## Latest Changes ## Latest Changes
### Translations
* 🌐 Add Korean translation for `docs/ko/docs/project-generation.md`. PR [#12157](https://github.com/fastapi/fastapi/pull/12157) by [@BORA040126](https://github.com/BORA040126).
### Internal
* 👷 Update `issue-manager.yml`. PR [#12159](https://github.com/fastapi/fastapi/pull/12159) by [@tiangolo](https://github.com/tiangolo).
* ✏️ Fix typo in `fastapi/params.py`. PR [#12143](https://github.com/fastapi/fastapi/pull/12143) by [@surreal30](https://github.com/surreal30).
## 0.114.0
You can restrict form fields to only include those declared in a Pydantic model and forbid any extra field sent in the request using Pydantic's `model_config = {"extra": "forbid"}`:
```python
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
```
Read the new docs: [Form Models - Forbid Extra Form Fields](https://fastapi.tiangolo.com/tutorial/request-form-models/#forbid-extra-form-fields).
### Features
* ✨ Add support for forbidding extra form fields with Pydantic models. PR [#12134](https://github.com/fastapi/fastapi/pull/12134) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Update docs, Form Models section title, to match config name. PR [#12152](https://github.com/fastapi/fastapi/pull/12152) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ✅ Update internal tests for latest Pydantic, including CI tweaks to install the latest Pydantic. PR [#12147](https://github.com/fastapi/fastapi/pull/12147) by [@tiangolo](https://github.com/tiangolo).
## 0.113.0
Now you can declare form fields with Pydantic models:
```python
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
```
Read the new docs: [Form Models](https://fastapi.tiangolo.com/tutorial/request-form-models/).
### Features
* ✨ Add support for Pydantic models in `Form` parameters. PR [#12129](https://github.com/fastapi/fastapi/pull/12129) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 🔧 Update sponsors: Coherence link. PR [#12130](https://github.com/fastapi/fastapi/pull/12130) by [@tiangolo](https://github.com/tiangolo).
## 0.112.4
This release is mainly a big internal refactor to enable adding support for Pydantic models for `Form` fields, but that feature comes in the next release.
This release shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. It's just a checkpoint. 🤓
### Refactors
* ♻️ Refactor deciding if `embed` body fields, do not overwrite fields, compute once per router, refactor internals in preparation for Pydantic models in `Form`, `Query` and others. PR [#12117](https://github.com/fastapi/fastapi/pull/12117) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` parameters" to make a checkpoint release. PR [#12128](https://github.com/fastapi/fastapi/pull/12128) by [@tiangolo](https://github.com/tiangolo). Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129).
* ✨ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). Reverted by PR [#12128](https://github.com/fastapi/fastapi/pull/12128) to make a checkpoint release with only refactors. Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129).
## 0.112.3
This release is mainly internal refactors, it shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. There are a few bigger releases coming right after. 🚀
### Refactors
* ♻️ Refactor internal `check_file_field()`, rename to `ensure_multipart_is_installed()` to clarify its purpose. PR [#12106](https://github.com/fastapi/fastapi/pull/12106) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Rename internal `create_response_field()` to `create_model_field()` as it's used for more than response models. PR [#12103](https://github.com/fastapi/fastapi/pull/12103) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Refactor and simplify internal data from `solve_dependencies()` using dataclasses. PR [#12100](https://github.com/fastapi/fastapi/pull/12100) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Refactor and simplify internal `analyze_param()` to structure data with dataclasses instead of tuple. PR [#12099](https://github.com/fastapi/fastapi/pull/12099) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Refactor and simplify dependencies data structures with dataclasses. PR [#12098](https://github.com/fastapi/fastapi/pull/12098) by [@tiangolo](https://github.com/tiangolo).
### Docs ### Docs
* 📝 Add External Link: Techniques and applications of SQLAlchemy global filters in FastAPI. PR [#12109](https://github.com/fastapi/fastapi/pull/12109) by [@TheShubhendra](https://github.com/TheShubhendra).
* 📝 Add note about `time.perf_counter()` in middlewares. PR [#12095](https://github.com/fastapi/fastapi/pull/12095) by [@tiangolo](https://github.com/tiangolo).
* 📝 Tweak middleware code sample `time.time()` to `time.perf_counter()`. PR [#11957](https://github.com/fastapi/fastapi/pull/11957) by [@domdent](https://github.com/domdent).
* 🔧 Update sponsors: Coherence. PR [#12093](https://github.com/fastapi/fastapi/pull/12093) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: Coherence. PR [#12093](https://github.com/fastapi/fastapi/pull/12093) by [@tiangolo](https://github.com/tiangolo).
* 📝 Fix async test example not to trigger DeprecationWarning. PR [#12084](https://github.com/fastapi/fastapi/pull/12084) by [@marcinsulikowski](https://github.com/marcinsulikowski). * 📝 Fix async test example not to trigger DeprecationWarning. PR [#12084](https://github.com/fastapi/fastapi/pull/12084) by [@marcinsulikowski](https://github.com/marcinsulikowski).
* 📝 Update `docs_src/path_params_numeric_validations/tutorial006.py`. PR [#11478](https://github.com/fastapi/fastapi/pull/11478) by [@MuhammadAshiqAmeer](https://github.com/MuhammadAshiqAmeer). * 📝 Update `docs_src/path_params_numeric_validations/tutorial006.py`. PR [#11478](https://github.com/fastapi/fastapi/pull/11478) by [@MuhammadAshiqAmeer](https://github.com/MuhammadAshiqAmeer).
@ -20,11 +130,21 @@ hide:
### Translations ### Translations
* 🌐 Add Dutch translation for `docs/nl/docs/features.md`. PR [#12101](https://github.com/fastapi/fastapi/pull/12101) by [@maxscheijen](https://github.com/maxscheijen).
* 🌐 Add Portuguese translation for `docs/pt/docs/advanced/testing-events.md`. PR [#12108](https://github.com/fastapi/fastapi/pull/12108) by [@ceb10n](https://github.com/ceb10n).
* 🌐 Add Portuguese translation for `docs/pt/docs/advanced/security/index.md`. PR [#12114](https://github.com/fastapi/fastapi/pull/12114) by [@ceb10n](https://github.com/ceb10n).
* 🌐 Add Dutch translation for `docs/nl/docs/index.md`. PR [#12042](https://github.com/fastapi/fastapi/pull/12042) by [@svlandeg](https://github.com/svlandeg). * 🌐 Add Dutch translation for `docs/nl/docs/index.md`. PR [#12042](https://github.com/fastapi/fastapi/pull/12042) by [@svlandeg](https://github.com/svlandeg).
* 🌐 Update Chinese translation for `docs/zh/docs/how-to/index.md`. PR [#12070](https://github.com/fastapi/fastapi/pull/12070) by [@synthpop123](https://github.com/synthpop123). * 🌐 Update Chinese translation for `docs/zh/docs/how-to/index.md`. PR [#12070](https://github.com/fastapi/fastapi/pull/12070) by [@synthpop123](https://github.com/synthpop123).
### Internal ### Internal
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12115](https://github.com/fastapi/fastapi/pull/12115) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump pypa/gh-action-pypi-publish from 1.10.0 to 1.10.1. PR [#12120](https://github.com/fastapi/fastapi/pull/12120) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pillow from 10.3.0 to 10.4.0. PR [#12105](https://github.com/fastapi/fastapi/pull/12105) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#12118](https://github.com/fastapi/fastapi/pull/12118) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.0. PR [#12112](https://github.com/fastapi/fastapi/pull/12112) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 🔧 Update sponsors link: Coherence. PR [#12097](https://github.com/fastapi/fastapi/pull/12097) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update labeler config to handle sponsorships data. PR [#12096](https://github.com/fastapi/fastapi/pull/12096) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update sponsors, remove Kong. PR [#12085](https://github.com/fastapi/fastapi/pull/12085) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Kong. PR [#12085](https://github.com/fastapi/fastapi/pull/12085) by [@tiangolo](https://github.com/tiangolo).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12076](https://github.com/fastapi/fastapi/pull/12076) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12076](https://github.com/fastapi/fastapi/pull/12076) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 👷 Update `latest-changes` GitHub Action. PR [#12073](https://github.com/fastapi/fastapi/pull/12073) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `latest-changes` GitHub Action. PR [#12073](https://github.com/fastapi/fastapi/pull/12073) by [@tiangolo](https://github.com/tiangolo).

6
docs/en/docs/tutorial/middleware.md

@ -63,6 +63,12 @@ For example, you could add a custom header `X-Process-Time` containing the time
{!../../../docs_src/middleware/tutorial001.py!} {!../../../docs_src/middleware/tutorial001.py!}
``` ```
/// tip
Here we use <a href="https://docs.python.org/3/library/time.html#time.perf_counter" class="external-link" target="_blank">`time.perf_counter()`</a> instead of `time.time()` because it can be more precise for these use cases. 🤓
///
## Other middlewares ## Other middlewares
You can later read more about other middlewares in the [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank}. You can later read more about other middlewares in the [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank}.

134
docs/en/docs/tutorial/request-form-models.md

@ -0,0 +1,134 @@
# Form Models
You can use **Pydantic models** to declare **form fields** in FastAPI.
/// info
To use forms, first install <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>.
Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example:
```console
$ pip install python-multipart
```
///
/// note
This is supported since FastAPI version `0.113.0`. 🤓
///
## Pydantic Models for Forms
You just need to declare a **Pydantic model** with the fields you want to receive as **form fields**, and then declare the parameter as `Form`:
//// tab | Python 3.9+
```Python hl_lines="9-11 15"
{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="8-10 14"
{!> ../../../docs_src/request_form_models/tutorial001_an.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="7-9 13"
{!> ../../../docs_src/request_form_models/tutorial001.py!}
```
////
**FastAPI** will **extract** the data for **each field** from the **form data** in the request and give you the Pydantic model you defined.
## Check the Docs
You can verify it in the docs UI at `/docs`:
<div class="screenshot">
<img src="/img/tutorial/request-form-models/image01.png">
</div>
## Forbid Extra Form Fields
In some special use cases (probably not very common), you might want to **restrict** the form fields to only those declared in the Pydantic model. And **forbid** any **extra** fields.
/// note
This is supported since FastAPI version `0.114.0`. 🤓
///
You can use Pydantic's model configuration to `forbid` any `extra` fields:
//// tab | Python 3.9+
```Python hl_lines="12"
{!> ../../../docs_src/request_form_models/tutorial002_an_py39.py!}
```
////
//// tab | Python 3.8+
```Python hl_lines="11"
{!> ../../../docs_src/request_form_models/tutorial002_an.py!}
```
////
//// tab | Python 3.8+ non-Annotated
/// tip
Prefer to use the `Annotated` version if possible.
///
```Python hl_lines="10"
{!> ../../../docs_src/request_form_models/tutorial002.py!}
```
////
If a client tries to send some extra data, they will receive an **error** response.
For example, if the client tries to send the form fields:
* `username`: `Rick`
* `password`: `Portal Gun`
* `extra`: `Mr. Poopybutthole`
They will receive an error response telling them that the field `extra` is not allowed:
```json
{
"detail": [
{
"type": "extra_forbidden",
"loc": ["body", "extra"],
"msg": "Extra inputs are not permitted",
"input": "Mr. Poopybutthole"
}
]
}
```
## Summary
You can use Pydantic models to declare form fields in FastAPI. 😎

1
docs/en/mkdocs.yml

@ -129,6 +129,7 @@ nav:
- tutorial/extra-models.md - tutorial/extra-models.md
- tutorial/response-status-code.md - tutorial/response-status-code.md
- tutorial/request-forms.md - tutorial/request-forms.md
- tutorial/request-form-models.md
- tutorial/request-files.md - tutorial/request-files.md
- tutorial/request-forms-and-files.md - tutorial/request-forms-and-files.md
- tutorial/handling-errors.md - tutorial/handling-errors.md

2
docs/en/overrides/main.html

@ -59,7 +59,7 @@
</a> </a>
</div> </div>
<div class="item"> <div class="item">
<a title="Coherence" style="display: block; position: relative;" href="https://docs.withcoherence.com/configuration/frameworks/?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs#fastapi-example" target="_blank"> <a title="Coherence" style="display: block; position: relative;" href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" target="_blank">
<span class="sponsor-badge">sponsor</span> <span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/coherence-banner.png" /> <img class="sponsor-image" src="/img/sponsors/coherence-banner.png" />
</a> </a>

28
docs/ko/docs/project-generation.md

@ -0,0 +1,28 @@
# Full Stack FastAPI 템플릿
템플릿은 일반적으로 특정 설정과 함께 제공되지만, 유연하고 커스터마이징이 가능하게 디자인 되었습니다. 이 특성들은 여러분이 프로젝트의 요구사항에 맞춰 수정, 적용을 할 수 있게 해주고, 템플릿이 완벽한 시작점이 되게 해줍니다. 🏁
많은 초기 설정, 보안, 데이터베이스 및 일부 API 엔드포인트가 이미 준비되어 있으므로, 여러분은 이 템플릿을 (프로젝트를) 시작하는 데 사용할 수 있습니다.
GitHub 저장소: <a href="https://github.com/tiangolo/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI 템플릿</a>
## 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 (지속적인 배포).

201
docs/nl/docs/features.md

@ -0,0 +1,201 @@
# Functionaliteit
## FastAPI functionaliteit
**FastAPI** biedt je het volgende:
### Gebaseerd op open standaarden
* <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> voor het maken van API's, inclusief declaraties van <abbr title="ook bekend als: endpoints, routess">pad</abbr><abbr title="ook bekend als HTTP-methoden, zoals POST, GET, PUT, DELETE">bewerkingen</abbr>, parameters, request bodies, beveiliging, enz.
* Automatische datamodel documentatie met <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a> (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.
* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a>, met interactieve interface, maakt het mogelijk je API rechtstreeks vanuit de browser aan te roepen en te testen.
![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png)
* Alternatieve API-documentatie met <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a>.
![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png)
### 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" <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">een van de meest gebruikte functionaliteiten is</a>.
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 <a href="https://code.visualstudio.com/" class="external-link" target="_blank">Visual Studio Code</a>:
![editor ondersteuning](https://fastapi.tiangolo.com/img/vscode-completion.png)
* in <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>:
![editor ondersteuning](https://fastapi.tiangolo.com/img/pycharm-completion.png)
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 <abbr title='ook bekend als "componenten", "bronnen", "diensten", "aanbieders"'><strong>Dependency Injection</strong></abbr> 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% <abbr title="De hoeveelheid code die automatisch wordt getest">van de code is getest</abbr>.
* 100% <abbr title="Python type annotaties, hiermee kunnen je editor en externe tools je beter ondersteunen">type geannoteerde</abbr> codebase.
* Wordt gebruikt in productietoepassingen.
## Starlette functies
**FastAPI** is volledig verenigbaar met (en gebaseerd op) <a href="https://www.starlette.io/" class="external-link" target="_blank"><strong>Starlette</strong></a>.
`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 <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">een van de snelste Python frameworks, vergelijkbaar met **NodeJS** en **Go**</a>.
* **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 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank"><strong>Pydantic</strong></a> code die je nog hebt werkt ook.
Inclusief externe pakketten die ook gebaseerd zijn op Pydantic, zoals <abbr title="Object-Relational Mapper">ORM</abbr>s, <abbr title="Object-Document Mapper">ODM</abbr>s 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 **<abbr title=“Integrated Development Environment, vergelijkbaar met een code editor>IDE</abbr>/<abbr title=“Een programma dat controleert op fouten in de code>linter</abbr>/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.

19
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.

7
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!}
```

4
docs_src/middleware/tutorial001.py

@ -7,8 +7,8 @@ app = FastAPI()
@app.middleware("http") @app.middleware("http")
async def add_process_time_header(request: Request, call_next): async def add_process_time_header(request: Request, call_next):
start_time = time.time() start_time = time.perf_counter()
response = await call_next(request) 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) response.headers["X-Process-Time"] = str(process_time)
return response return response

14
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

15
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

16
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

15
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

16
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

17
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

17
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

18
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

19
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

2
fastapi/__init__.py

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

15
fastapi/_compat.py

@ -279,6 +279,12 @@ if PYDANTIC_V2:
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
return BodyModel 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: else:
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
from pydantic import AnyUrl as Url # noqa: F401 from pydantic import AnyUrl as Url # noqa: F401
@ -513,6 +519,9 @@ else:
BodyModel.__fields__[f.name] = f # type: ignore[index] BodyModel.__fields__[f.name] = f # type: ignore[index]
return BodyModel 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( def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
@ -532,6 +541,12 @@ def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
def field_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( return _annotation_is_sequence(annotation) or _annotation_is_sequence(
get_origin(annotation) get_origin(annotation)
) )

75
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._compat import ModelField
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
@dataclass
class SecurityRequirement: class SecurityRequirement:
def __init__( security_scheme: SecurityBase
self, security_scheme: SecurityBase, scopes: Optional[Sequence[str]] = None scopes: Optional[Sequence[str]] = None
):
self.security_scheme = security_scheme
self.scopes = scopes
@dataclass
class Dependant: class Dependant:
def __init__( path_params: List[ModelField] = field(default_factory=list)
self, query_params: List[ModelField] = field(default_factory=list)
*, header_params: List[ModelField] = field(default_factory=list)
path_params: Optional[List[ModelField]] = None, cookie_params: List[ModelField] = field(default_factory=list)
query_params: Optional[List[ModelField]] = None, body_params: List[ModelField] = field(default_factory=list)
header_params: Optional[List[ModelField]] = None, dependencies: List["Dependant"] = field(default_factory=list)
cookie_params: Optional[List[ModelField]] = None, security_requirements: List[SecurityRequirement] = field(default_factory=list)
body_params: Optional[List[ModelField]] = None, name: Optional[str] = None
dependencies: Optional[List["Dependant"]] = None, call: Optional[Callable[..., Any]] = None
security_schemes: Optional[List[SecurityRequirement]] = None, request_param_name: Optional[str] = None
name: Optional[str] = None, websocket_param_name: Optional[str] = None
call: Optional[Callable[..., Any]] = None, http_connection_param_name: Optional[str] = None
request_param_name: Optional[str] = None, response_param_name: Optional[str] = None
websocket_param_name: Optional[str] = None, background_tasks_param_name: Optional[str] = None
http_connection_param_name: Optional[str] = None, security_scopes_param_name: Optional[str] = None
response_param_name: Optional[str] = None, security_scopes: Optional[List[str]] = None
background_tasks_param_name: Optional[str] = None, use_cache: bool = True
security_scopes_param_name: Optional[str] = None, path: Optional[str] = None
security_scopes: Optional[List[str]] = None, cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False)
use_cache: bool = True,
path: Optional[str] = None, def __post_init__(self) -> 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
self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or [])))) self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or []))))

440
fastapi/dependencies/utils.py

@ -1,6 +1,7 @@
import inspect import inspect
from contextlib import AsyncExitStack, contextmanager from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy from copy import copy, deepcopy
from dataclasses import dataclass
from typing import ( from typing import (
Any, Any,
Callable, Callable,
@ -32,6 +33,7 @@ from fastapi._compat import (
field_annotation_is_scalar, field_annotation_is_scalar,
get_annotation_from_field_info, get_annotation_from_field_info,
get_missing_field_error, get_missing_field_error,
get_model_fields,
is_bytes_field, is_bytes_field,
is_bytes_sequence_field, is_bytes_sequence_field,
is_scalar_field, is_scalar_field,
@ -54,11 +56,18 @@ from fastapi.logger import logger
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect 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 pydantic.fields import FieldInfo
from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.background import BackgroundTasks as StarletteBackgroundTasks
from starlette.concurrency import run_in_threadpool 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.requests import HTTPConnection, Request
from starlette.responses import Response from starlette.responses import Response
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
@ -79,25 +88,23 @@ multipart_incorrect_install_error = (
) )
def check_file_field(field: ModelField) -> None: def ensure_multipart_is_installed() -> None:
field_info = field.field_info try:
if isinstance(field_info, params.Form): # __version__ is available in both multiparts, and can be mocked
try: from multipart import __version__ # type: ignore
# __version__ is available in both multiparts, and can be mocked
from multipart import __version__ # type: ignore
assert __version__ assert __version__
try: try:
# parse_options_header is only available in the right multipart # parse_options_header is only available in the right multipart
from multipart.multipart import parse_options_header # type: ignore from multipart.multipart import parse_options_header # type: ignore
assert parse_options_header assert parse_options_header
except ImportError:
logger.error(multipart_incorrect_install_error)
raise RuntimeError(multipart_incorrect_install_error) from None
except ImportError: except ImportError:
logger.error(multipart_not_installed_error) logger.error(multipart_incorrect_install_error)
raise RuntimeError(multipart_not_installed_error) from None 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( def get_param_sub_dependant(
@ -175,7 +182,7 @@ def get_flat_dependant(
header_params=dependant.header_params.copy(), header_params=dependant.header_params.copy(),
cookie_params=dependant.cookie_params.copy(), cookie_params=dependant.cookie_params.copy(),
body_params=dependant.body_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, use_cache=dependant.use_cache,
path=dependant.path, path=dependant.path,
) )
@ -258,16 +265,16 @@ def get_dependant(
) )
for param_name, param in signature_params.items(): for param_name, param in signature_params.items():
is_path_param = param_name in path_param_names is_path_param = param_name in path_param_names
type_annotation, depends, param_field = analyze_param( param_details = analyze_param(
param_name=param_name, param_name=param_name,
annotation=param.annotation, annotation=param.annotation,
value=param.default, value=param.default,
is_path_param=is_path_param, is_path_param=is_path_param,
) )
if depends is not None: if param_details.depends is not None:
sub_dependant = get_param_sub_dependant( sub_dependant = get_param_sub_dependant(
param_name=param_name, param_name=param_name,
depends=depends, depends=param_details.depends,
path=path, path=path,
security_scopes=security_scopes, security_scopes=security_scopes,
) )
@ -275,18 +282,18 @@ def get_dependant(
continue continue
if add_non_field_param_to_dependency( if add_non_field_param_to_dependency(
param_name=param_name, param_name=param_name,
type_annotation=type_annotation, type_annotation=param_details.type_annotation,
dependant=dependant, dependant=dependant,
): ):
assert ( assert (
param_field is None param_details.field is None
), f"Cannot specify multiple FastAPI annotations for {param_name!r}" ), f"Cannot specify multiple FastAPI annotations for {param_name!r}"
continue continue
assert param_field is not None assert param_details.field is not None
if is_body_param(param_field=param_field, is_path_param=is_path_param): if isinstance(param_details.field.field_info, params.Body):
dependant.body_params.append(param_field) dependant.body_params.append(param_details.field)
else: else:
add_param_to_fields(field=param_field, dependant=dependant) add_param_to_fields(field=param_details.field, dependant=dependant)
return dependant return dependant
@ -314,13 +321,20 @@ def add_non_field_param_to_dependency(
return None return None
@dataclass
class ParamDetails:
type_annotation: Any
depends: Optional[params.Depends]
field: Optional[ModelField]
def analyze_param( def analyze_param(
*, *,
param_name: str, param_name: str,
annotation: Any, annotation: Any,
value: Any, value: Any,
is_path_param: bool, is_path_param: bool,
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]: ) -> ParamDetails:
field_info = None field_info = None
depends = None depends = None
type_annotation: Any = Any type_annotation: Any = Any
@ -328,6 +342,7 @@ def analyze_param(
if annotation is not inspect.Signature.empty: if annotation is not inspect.Signature.empty:
use_annotation = annotation use_annotation = annotation
type_annotation = annotation type_annotation = annotation
# Extract Annotated info
if get_origin(use_annotation) is Annotated: if get_origin(use_annotation) is Annotated:
annotated_args = get_args(annotation) annotated_args = get_args(annotation)
type_annotation = annotated_args[0] type_annotation = annotated_args[0]
@ -347,6 +362,7 @@ def analyze_param(
) )
else: else:
fastapi_annotation = None fastapi_annotation = None
# Set default for Annotated FieldInfo
if isinstance(fastapi_annotation, FieldInfo): if isinstance(fastapi_annotation, FieldInfo):
# Copy `field_info` because we mutate `field_info.default` below. # Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info( field_info = copy_field_info(
@ -361,9 +377,10 @@ def analyze_param(
field_info.default = value field_info.default = value
else: else:
field_info.default = Required field_info.default = Required
# Get Annotated Depends
elif isinstance(fastapi_annotation, params.Depends): elif isinstance(fastapi_annotation, params.Depends):
depends = fastapi_annotation depends = fastapi_annotation
# Get Depends from default value
if isinstance(value, params.Depends): if isinstance(value, params.Depends):
assert depends is None, ( assert depends is None, (
"Cannot specify `Depends` in `Annotated` and default value" "Cannot specify `Depends` in `Annotated` and default value"
@ -374,6 +391,7 @@ def analyze_param(
f" default value together for {param_name!r}" f" default value together for {param_name!r}"
) )
depends = value depends = value
# Get FieldInfo from default value
elif isinstance(value, FieldInfo): elif isinstance(value, FieldInfo):
assert field_info is None, ( assert field_info is None, (
"Cannot specify FastAPI annotations in `Annotated` and default value" "Cannot specify FastAPI annotations in `Annotated` and default value"
@ -383,11 +401,13 @@ def analyze_param(
if PYDANTIC_V2: if PYDANTIC_V2:
field_info.annotation = type_annotation field_info.annotation = type_annotation
# Get Depends from type annotation
if depends is not None and depends.dependency is None: if depends is not None and depends.dependency is None:
# Copy `depends` before mutating it # Copy `depends` before mutating it
depends = copy(depends) depends = copy(depends)
depends.dependency = type_annotation depends.dependency = type_annotation
# Handle non-param type annotations like Request
if lenient_issubclass( if lenient_issubclass(
type_annotation, type_annotation,
( (
@ -403,6 +423,7 @@ def analyze_param(
assert ( assert (
field_info is None field_info is None
), f"Cannot specify FastAPI annotation for type {type_annotation!r}" ), 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: elif field_info is None and depends is None:
default_value = value if value is not inspect.Signature.empty else Required default_value = value if value is not inspect.Signature.empty else Required
if is_path_param: if is_path_param:
@ -420,7 +441,9 @@ def analyze_param(
field_info = params.Query(annotation=use_annotation, default=default_value) field_info = params.Query(annotation=use_annotation, default=default_value)
field = None field = None
# It's a field_info, not a dependency
if field_info is not None: if field_info is not None:
# Handle field_info.in_
if is_path_param: if is_path_param:
assert isinstance(field_info, params.Path), ( assert isinstance(field_info, params.Path), (
f"Cannot use `{field_info.__class__.__name__}` for path param" f"Cannot use `{field_info.__class__.__name__}` for path param"
@ -436,12 +459,14 @@ def analyze_param(
field_info, field_info,
param_name, param_name,
) )
if isinstance(field_info, params.Form):
ensure_multipart_is_installed()
if not field_info.alias and getattr(field_info, "convert_underscores", None): if not field_info.alias and getattr(field_info, "convert_underscores", None):
alias = param_name.replace("_", "-") alias = param_name.replace("_", "-")
else: else:
alias = field_info.alias or param_name alias = field_info.alias or param_name
field_info.alias = alias field_info.alias = alias
field = create_response_field( field = create_model_field(
name=param_name, name=param_name,
type_=use_annotation_from_field_info, type_=use_annotation_from_field_info,
default=field_info.default, default=field_info.default,
@ -449,27 +474,14 @@ def analyze_param(
required=field_info.default in (Required, Undefined), required=field_info.default in (Required, Undefined),
field_info=field_info, 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 return ParamDetails(type_annotation=type_annotation, depends=depends, field=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
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: 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) 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( async def solve_dependencies(
*, *,
request: Union[Request, WebSocket], request: Union[Request, WebSocket],
@ -531,13 +552,8 @@ async def solve_dependencies(
dependency_overrides_provider: Optional[Any] = None, dependency_overrides_provider: Optional[Any] = None,
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
async_exit_stack: AsyncExitStack, async_exit_stack: AsyncExitStack,
) -> Tuple[ embed_body_fields: bool,
Dict[str, Any], ) -> SolvedDependency:
List[Any],
Optional[StarletteBackgroundTasks],
Response,
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
]:
values: Dict[str, Any] = {} values: Dict[str, Any] = {}
errors: List[Any] = [] errors: List[Any] = []
if response is None: if response is None:
@ -578,28 +594,23 @@ async def solve_dependencies(
dependency_overrides_provider=dependency_overrides_provider, dependency_overrides_provider=dependency_overrides_provider,
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
async_exit_stack=async_exit_stack, async_exit_stack=async_exit_stack,
embed_body_fields=embed_body_fields,
) )
( background_tasks = solved_result.background_tasks
sub_values, dependency_cache.update(solved_result.dependency_cache)
sub_errors, if solved_result.errors:
background_tasks, errors.extend(solved_result.errors)
_, # 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)
continue continue
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache: if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
solved = dependency_cache[sub_dependant.cache_key] solved = dependency_cache[sub_dependant.cache_key]
elif is_gen_callable(call) or is_async_gen_callable(call): elif is_gen_callable(call) or is_async_gen_callable(call):
solved = await solve_generator( 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): elif is_coroutine_callable(call):
solved = await call(**sub_values) solved = await call(**solved_result.values)
else: 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: if sub_dependant.name is not None:
values[sub_dependant.name] = solved values[sub_dependant.name] = solved
if sub_dependant.cache_key not in dependency_cache: if sub_dependant.cache_key not in dependency_cache:
@ -626,7 +637,9 @@ async def solve_dependencies(
body_values, body_values,
body_errors, body_errors,
) = await request_body_to_args( # body_params checked above ) = 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) values.update(body_values)
errors.extend(body_errors) errors.extend(body_errors)
@ -646,142 +659,206 @@ async def solve_dependencies(
values[dependant.security_scopes_param_name] = SecurityScopes( values[dependant.security_scopes_param_name] = SecurityScopes(
scopes=dependant.security_scopes 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( def request_params_to_args(
required_params: Sequence[ModelField], fields: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers], received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[Any]]: ) -> Tuple[Dict[str, Any], List[Any]]:
values = {} values: Dict[str, Any] = {}
errors = [] errors = []
for field in required_params: for field in fields:
if is_scalar_sequence_field(field) and isinstance( value = _get_multidict_value(field, received_params)
received_params, (QueryParams, Headers)
):
value = received_params.getlist(field.alias) or field.default
else:
value = received_params.get(field.alias)
field_info = field.field_info field_info = field.field_info
assert isinstance( assert isinstance(
field_info, params.Param field_info, params.Param
), "Params must be subclasses of Param" ), "Params must be subclasses of Param"
loc = (field_info.in_.value, field.alias) loc = (field_info.in_.value, field.alias)
if value is None: v_, errors_ = _validate_value_with_model_field(
if field.required: field=field, value=value, values=values, loc=loc
errors.append(get_missing_field_error(loc=loc)) )
else: if errors_:
values[field.name] = deepcopy(field.default) errors.extend(errors_)
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)
else: else:
values[field.name] = v_ values[field.name] = v_
return values, errors 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( async def request_body_to_args(
required_params: List[ModelField], body_fields: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]], received_body: Optional[Union[Dict[str, Any], FormData]],
embed_body_fields: bool,
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
values = {} values: Dict[str, Any] = {}
errors: List[Dict[str, Any]] = [] errors: List[Dict[str, Any]] = []
if required_params: assert body_fields, "request_body_to_args() should be called with fields"
field = required_params[0] single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields
field_info = field.field_info first_field = body_fields[0]
embed = getattr(field_info, "embed", None) body_to_process = received_body
field_alias_omitted = len(required_params) == 1 and not embed
if field_alias_omitted: fields_to_extract: List[ModelField] = body_fields
received_body = {field.alias: received_body}
if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel):
for field in required_params: fields_to_extract = get_model_fields(first_field.type_)
loc: Tuple[str, ...]
if field_alias_omitted: if isinstance(received_body, FormData):
loc = ("body",) body_to_process = await _extract_form_body(fields_to_extract, received_body)
else:
loc = ("body", field.alias) if single_not_embedded_field:
loc: Tuple[str, ...] = ("body",)
value: Optional[Any] = None v_, errors_ = _validate_value_with_model_field(
if received_body is not None: field=first_field, value=body_to_process, values=values, loc=loc
if (is_sequence_field(field)) and isinstance(received_body, FormData): )
value = received_body.getlist(field.alias) return {first_field.name: v_}, errors_
else: for field in body_fields:
try: loc = ("body", field.alias)
value = received_body.get(field.alias) value: Optional[Any] = None
except AttributeError: if body_to_process is not None:
errors.append(get_missing_field_error(loc)) try:
continue value = body_to_process.get(field.alias)
if ( # If the received body is a list, not a dict
value is None except AttributeError:
or (isinstance(field_info, params.Form) and value == "") errors.append(get_missing_field_error(loc))
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)
continue continue
if ( v_, errors_ = _validate_value_with_model_field(
isinstance(field_info, params.File) field=field, value=value, values=values, loc=loc
and is_bytes_field(field) )
and isinstance(value, UploadFile) if errors_:
): errors.extend(errors_)
value = await value.read() else:
elif ( values[field.name] = v_
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_
return values, errors return values, errors
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: def get_body_field(
flat_dependant = get_flat_dependant(dependant) *, 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: if not flat_dependant.body_params:
return None return None
first_param = flat_dependant.body_params[0] first_param = flat_dependant.body_params[0]
field_info = first_param.field_info if not embed_body_fields:
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)
return first_param 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 model_name = "Body_" + name
BodyModel = create_body_model( BodyModel = create_body_model(
fields=flat_dependant.body_params, model_name=model_name 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: if len(set(body_param_media_types)) == 1:
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]
final_field = create_response_field( final_field = create_model_field(
name="body", name="body",
type_=BodyModel, type_=BodyModel,
required=required, required=required,
alias="body", alias="body",
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs), field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
) )
check_file_field(final_field)
return final_field return final_field

4
fastapi/param_functions.py

@ -1282,7 +1282,7 @@ def Body( # noqa: N802
), ),
] = _Unset, ] = _Unset,
embed: Annotated[ embed: Annotated[
bool, Union[bool, None],
Doc( Doc(
""" """
When `embed` is `True`, the parameter will be expected in a JSON body as a 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). [FastAPI docs for Body - Multiple Parameters](https://fastapi.tiangolo.com/tutorial/body-multiple-params/#embed-a-single-body-parameter).
""" """
), ),
] = False, ] = None,
media_type: Annotated[ media_type: Annotated[
str, str,
Doc( Doc(

5
fastapi/params.py

@ -479,7 +479,7 @@ class Body(FieldInfo):
*, *,
default_factory: Union[Callable[[], Any], None] = _Unset, default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None, annotation: Optional[Any] = None,
embed: bool = False, embed: Union[bool, None] = None,
media_type: str = "application/json", media_type: str = "application/json",
alias: Optional[str] = None, alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset, alias_priority: Union[int, None] = _Unset,
@ -556,7 +556,7 @@ class Body(FieldInfo):
kwargs["examples"] = examples kwargs["examples"] = examples
if regex is not None: if regex is not None:
warnings.warn( warnings.warn(
"`regex` has been depreacated, please use `pattern` instead", "`regex` has been deprecated, please use `pattern` instead",
category=DeprecationWarning, category=DeprecationWarning,
stacklevel=4, stacklevel=4,
) )
@ -642,7 +642,6 @@ class Form(Body):
default=default, default=default,
default_factory=default_factory, default_factory=default_factory,
annotation=annotation, annotation=annotation,
embed=True,
media_type=media_type, media_type=media_type,
alias=alias, alias=alias,
alias_priority=alias_priority, alias_priority=alias_priority,

65
fastapi/routing.py

@ -33,8 +33,10 @@ from fastapi._compat import (
from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import ( from fastapi.dependencies.utils import (
_should_embed_body_fields,
get_body_field, get_body_field,
get_dependant, get_dependant,
get_flat_dependant,
get_parameterless_sub_dependant, get_parameterless_sub_dependant,
get_typed_return_annotation, get_typed_return_annotation,
solve_dependencies, solve_dependencies,
@ -49,7 +51,7 @@ from fastapi.exceptions import (
from fastapi.types import DecoratedCallable, IncEx from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import ( from fastapi.utils import (
create_cloned_field, create_cloned_field,
create_response_field, create_model_field,
generate_unique_id, generate_unique_id,
get_value_or_default, get_value_or_default,
is_body_allowed_for_status_code, is_body_allowed_for_status_code,
@ -225,6 +227,7 @@ def get_request_handler(
response_model_exclude_defaults: bool = False, response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False, response_model_exclude_none: bool = False,
dependency_overrides_provider: Optional[Any] = None, dependency_overrides_provider: Optional[Any] = None,
embed_body_fields: bool = False,
) -> Callable[[Request], Coroutine[Any, Any, Response]]: ) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function" assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = asyncio.iscoroutinefunction(dependant.call) is_coroutine = asyncio.iscoroutinefunction(dependant.call)
@ -291,27 +294,36 @@ def get_request_handler(
body=body, body=body,
dependency_overrides_provider=dependency_overrides_provider, dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack, 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: if not errors:
raw_response = await run_endpoint_function( 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 isinstance(raw_response, Response):
if raw_response.background is None: if raw_response.background is None:
raw_response.background = background_tasks raw_response.background = solved_result.background_tasks
response = raw_response response = raw_response
else: 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 # If status_code was set, use it, otherwise use the default from the
# response class, in the case of redirect it's 307 # response class, in the case of redirect it's 307
current_status_code = ( 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: if current_status_code is not None:
response_args["status_code"] = current_status_code response_args["status_code"] = current_status_code
if sub_response.status_code: if solved_result.response.status_code:
response_args["status_code"] = sub_response.status_code response_args["status_code"] = (
solved_result.response.status_code
)
content = await serialize_response( content = await serialize_response(
field=response_field, field=response_field,
response_content=raw_response, response_content=raw_response,
@ -326,7 +338,7 @@ def get_request_handler(
response = actual_response_class(content, **response_args) response = actual_response_class(content, **response_args)
if not is_body_allowed_for_status_code(response.status_code): if not is_body_allowed_for_status_code(response.status_code):
response.body = b"" response.body = b""
response.headers.raw.extend(sub_response.headers.raw) response.headers.raw.extend(solved_result.response.headers.raw)
if errors: if errors:
validation_error = RequestValidationError( validation_error = RequestValidationError(
_normalize_errors(errors), body=body _normalize_errors(errors), body=body
@ -346,7 +358,9 @@ def get_request_handler(
def get_websocket_app( 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]]: ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:
async def app(websocket: WebSocket) -> None: async def app(websocket: WebSocket) -> None:
async with AsyncExitStack() as async_exit_stack: async with AsyncExitStack() as async_exit_stack:
@ -359,12 +373,14 @@ def get_websocket_app(
dependant=dependant, dependant=dependant,
dependency_overrides_provider=dependency_overrides_provider, dependency_overrides_provider=dependency_overrides_provider,
async_exit_stack=async_exit_stack, async_exit_stack=async_exit_stack,
embed_body_fields=embed_body_fields,
) )
values, errors, _, _2, _3 = solved_result if solved_result.errors:
if errors: raise WebSocketRequestValidationError(
raise WebSocketRequestValidationError(_normalize_errors(errors)) _normalize_errors(solved_result.errors)
)
assert dependant.call is not None, "dependant.call must be a function" assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**values) await dependant.call(**solved_result.values)
return app return app
@ -390,11 +406,15 @@ class APIWebSocketRoute(routing.WebSocketRoute):
0, 0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format), 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( self.app = websocket_session(
get_websocket_app( get_websocket_app(
dependant=self.dependant, dependant=self.dependant,
dependency_overrides_provider=dependency_overrides_provider, dependency_overrides_provider=dependency_overrides_provider,
embed_body_fields=self._embed_body_fields,
) )
) )
@ -488,7 +508,7 @@ class APIRoute(routing.Route):
status_code status_code
), f"Status code {status_code} must not have a response body" ), f"Status code {status_code} must not have a response body"
response_name = "Response_" + self.unique_id response_name = "Response_" + self.unique_id
self.response_field = create_response_field( self.response_field = create_model_field(
name=response_name, name=response_name,
type_=self.response_model, type_=self.response_model,
mode="serialization", mode="serialization",
@ -521,7 +541,7 @@ class APIRoute(routing.Route):
additional_status_code additional_status_code
), f"Status code {additional_status_code} must not have a response body" ), f"Status code {additional_status_code} must not have a response body"
response_name = f"Response_{additional_status_code}_{self.unique_id}" 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 response_fields[additional_status_code] = response_field
if response_fields: if response_fields:
self.response_fields: Dict[Union[int, str], ModelField] = response_fields self.response_fields: Dict[Union[int, str], ModelField] = response_fields
@ -535,7 +555,15 @@ class APIRoute(routing.Route):
0, 0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format), 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()) self.app = request_response(self.get_route_handler())
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]: def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
@ -552,6 +580,7 @@ class APIRoute(routing.Route):
response_model_exclude_defaults=self.response_model_exclude_defaults, response_model_exclude_defaults=self.response_model_exclude_defaults,
response_model_exclude_none=self.response_model_exclude_none, response_model_exclude_none=self.response_model_exclude_none,
dependency_overrides_provider=self.dependency_overrides_provider, dependency_overrides_provider=self.dependency_overrides_provider,
embed_body_fields=self._embed_body_fields,
) )
def matches(self, scope: Scope) -> Tuple[Match, Scope]: def matches(self, scope: Scope) -> Tuple[Match, Scope]:

9
fastapi/utils.py

@ -60,9 +60,9 @@ def get_path_param_names(path: str) -> Set[str]:
return set(re.findall("{(.*?)}", path)) return set(re.findall("{(.*?)}", path))
def create_response_field( def create_model_field(
name: str, name: str,
type_: Type[Any], type_: Any,
class_validators: Optional[Dict[str, Validator]] = None, class_validators: Optional[Dict[str, Validator]] = None,
default: Optional[Any] = Undefined, default: Optional[Any] = Undefined,
required: Union[bool, UndefinedType] = Undefined, required: Union[bool, UndefinedType] = Undefined,
@ -71,9 +71,6 @@ def create_response_field(
alias: Optional[str] = None, alias: Optional[str] = None,
mode: Literal["validation", "serialization"] = "validation", mode: Literal["validation", "serialization"] = "validation",
) -> ModelField: ) -> ModelField:
"""
Create a new response field. Raises if type_ is invalid.
"""
class_validators = class_validators or {} class_validators = class_validators or {}
if PYDANTIC_V2: if PYDANTIC_V2:
field_info = field_info or FieldInfo( field_info = field_info or FieldInfo(
@ -135,7 +132,7 @@ def create_cloned_field(
use_type.__fields__[f.name] = create_cloned_field( use_type.__fields__[f.name] = create_cloned_field(
f, cloned_types=cloned_types 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.has_alias = field.has_alias # type: ignore[attr-defined]
new_field.alias = field.alias # type: ignore[misc] new_field.alias = field.alias # type: ignore[misc]
new_field.class_validators = field.class_validators # type: ignore[attr-defined] new_field.class_validators = field.class_validators # type: ignore[attr-defined]

2
requirements-docs.txt

@ -8,7 +8,7 @@ pyyaml >=5.3.1,<7.0.0
# For Material for MkDocs, Chinese search # For Material for MkDocs, Chinese search
jieba==0.42.1 jieba==0.42.1
# For image processing by Material for MkDocs # For image processing by Material for MkDocs
pillow==10.3.0 pillow==10.4.0
# For image processing by Material for MkDocs # For image processing by Material for MkDocs
cairosvg==2.7.1 cairosvg==2.7.1
mkdocstrings[python]==0.25.1 mkdocstrings[python]==0.25.1

2
requirements-tests.txt

@ -3,7 +3,7 @@
pytest >=7.1.3,<8.0.0 pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0 coverage[toml] >= 6.5.0,< 8.0
mypy ==1.8.0 mypy ==1.8.0
ruff ==0.6.1 ruff ==0.6.3
dirty-equals ==0.6.0 dirty-equals ==0.6.0
# TODO: once removing databases from tutorial, upgrade SQLAlchemy # TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel # probably when including SQLModel

36
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()

13
tests/test_compat.py

@ -1,11 +1,13 @@
from typing import List, Union from typing import Any, Dict, List, Union
from fastapi import FastAPI, UploadFile from fastapi import FastAPI, UploadFile
from fastapi._compat import ( from fastapi._compat import (
ModelField, ModelField,
Undefined, Undefined,
_get_model_config, _get_model_config,
get_model_fields,
is_bytes_sequence_annotation, is_bytes_sequence_annotation,
is_scalar_field,
is_uploadfile_sequence_annotation, is_uploadfile_sequence_annotation,
) )
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -91,3 +93,12 @@ 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 # and other types, but I'm not even sure it's a good idea to support it as a first
# class "feature" # class "feature"
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) 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])

129
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",
},
]
}
)

99
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",
},
}
},
}

27
tests/test_openapi_examples.py

@ -155,13 +155,26 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": IsDict(
"allOf": [{"$ref": "#/components/schemas/Item"}], {
"title": "Item", "$ref": "#/components/schemas/Item",
"examples": [ "examples": [
{"data": "Data in Body examples, example1"} {"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": { "examples": {
"Example One": { "Example One": {
"summary": "Example One Summary", "summary": "Example One Summary",

0
tests/test_tutorial/test_request_form_models/__init__.py

232
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"},
}
},
},
}
},
}

232
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"},
}
},
},
}
},
}

240
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"},
}
},
},
}
},
}

196
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"},
}
},
},
}
},
}

196
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"},
}
},
},
}
},
}

203
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"},
}
},
},
}
},
}

189
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"},
}
},
},
}
},
}

196
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"},
}
},
},
}
},
}

203
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"},
}
},
},
}
},
}
Loading…
Cancel
Save