Compare commits

..

102 Commits

Author SHA1 Message Date
Sebastián Ramírez
983f1d34db 🔖 Release version 0.99.0 2023-06-30 20:55:17 +02:00
Sebastián Ramírez
efc2bcc57a 📝 Update release notes 2023-06-30 20:54:25 +02:00
github-actions
b757211299 📝 Update release notes 2023-06-30 18:25:53 +00:00
Sebastián Ramírez
7dad5a820b Add support for OpenAPI 3.1.0 (#9770)
*  Update OpenAPI models for JSON Schema 2020-12 and OpenAPI 3.1.0

*  Add support for summary and webhooks

*  Update JSON Schema for UploadFiles

* ️ Revert making paths optional, to ensure always correctness

* ️ Keep UploadFile as format: binary for compatibility with the rest of Pydantic bytes fields in v1

*  Update version of OpenAPI generated to 3.1.0

*  Update the version of Swagger UI

* 📝 Update docs about extending OpenAPI

* 📝 Update docs and links to refer to OpenAPI 3.1.0

*  Update logic for handling webhooks

* ♻️ Update parameter functions and classes, deprecate example and make examples the main field

*  Update tests for OpenAPI 3.1.0

* 📝 Update examples for OpenAPI metadata

*  Add and update tests for OpenAPI metadata

* 📝 Add source example for webhooks

* 📝 Update docs for metadata

* 📝 Update docs for Schema extra

* 📝 Add docs for webhooks

* 🔧 Add webhooks docs to MkDocs

*  Update tests for extending OpenAPI

*  Add tests for webhooks

* ♻️ Refactor generation of OpenAPI and JSON Schema with params

* 📝 Update source examples for field examples

*  Update tests for examples

*  Make sure the minimum version of typing-extensions installed has deprecated() (already a dependency of Pydantic)

* ✏️ Fix typo in Webhooks example code

* 🔥 Remove commented out code of removed nullable field

* 🗑️ Add deprecation warnings for example argument

*  Update tests to check for deprecation warnings

*  Add test for webhooks with security schemes, for coverage

* 🍱 Update image for metadata, with new summary

* 🍱 Add docs image for Webhooks

* 📝 Update docs for webhooks, add docs UI image
2023-06-30 20:25:16 +02:00
github-actions
02fc9e8a63 📝 Update release notes 2023-06-30 16:23:36 +00:00
Sebastián Ramírez
0a8423d792 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code (#9769) 2023-06-30 18:23:02 +02:00
github-actions
0f390cd4b5 📝 Update release notes 2023-06-28 16:39:44 +00:00
Carson Crane
1f21b16e03 Add support for deque objects and children in jsonable_encoder (#9433)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-28 18:39:10 +02:00
github-actions
d409c05d6f 📝 Update release notes 2023-06-27 01:14:01 +00:00
dependabot[bot]
782b1c49a9 ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0 (#9724)
Updates the requirements on [httpx](https://github.com/encode/httpx) to permit the latest version.
- [Release notes](https://github.com/encode/httpx/releases)
- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/httpx/compare/0.23.0...0.24.1)

---
updated-dependencies:
- dependency-name: httpx
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-27 03:13:10 +02:00
github-actions
706d74b6ad 📝 Update release notes 2023-06-27 01:10:40 +00:00
dependabot[bot]
9debdc97ef ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17 (#9746)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.16 to 9.1.17.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.16...9.1.17)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-27 03:08:43 +02:00
github-actions
6c143b930d 📝 Update release notes 2023-06-27 01:07:03 +00:00
github-actions
dffca555ff 📝 Update release notes 2023-06-27 01:06:48 +00:00
Sebastián Ramírez
5e7d45af16 🔥 Remove missing translation dummy pages, no longer necessary (#9751) 2023-06-27 03:06:27 +02:00
pre-commit-ci[bot]
eb312758d8 ⬆ [pre-commit.ci] pre-commit autoupdate (#9259)
updates:
- [github.com/asottile/pyupgrade: v3.3.1 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.7.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.272 → v0.0.275](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.272...v0.0.275)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-27 03:06:02 +02:00
github-actions
a95af94669 📝 Update release notes 2023-06-27 01:02:34 +00:00
mojtaba
6ba4492670 🌐 Add Persian translation for docs/fa/docs/advanced/sub-applications.md (#9692)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Amin Alaee <mohammadamin.alaee@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-27 03:02:00 +02:00
github-actions
317cef3f8a 📝 Update release notes 2023-06-27 01:00:55 +00:00
Sergei Glazkov
81772b46a8 🌐 Add Russian translation for docs/ru/docs/tutorial/response-model.md (#9675)
Co-authored-by: s.glazkov <s.glazkov@polymatica.ru>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Alexandr <alexandrhub@vk.com>
Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-27 03:00:19 +02:00
github-actions
47524eee1b 📝 Update release notes 2023-06-26 16:03:19 +00:00
Sebastián Ramírez
872af100f5 📝 Fix form for the FastAPI and friends newsletter (#9749) 2023-06-26 18:02:34 +02:00
github-actions
d1c5c5c97c 📝 Update release notes 2023-06-26 14:06:24 +00:00
Sebastián Ramírez
ed297bb2e0 Add Material for MkDocs Insiders features and cards (#9748)
*  Add dependencies for MkDocs Insiders

* 🙈 Add Insider's .cache to .gitignore

* 🔧 Update MkDocs configs for Insiders

* 💄 Add custom Insiders card layout, while the custom logo is provided from upstream

* 🔨 Update docs.py script to dynamically enable insiders if it's installed

* 👷 Add cache for MkDocs Material Insiders' cards

* 🔊 Add a small log to the docs CLI

* 🔊 Tweak logs, only after exporting languages

* 🐛 Fix accessing non existing env var

* 🔧 Invalidate deps cache

* 🔧 Tweak cache IDs

* 👷 Update cache for installing insiders

* 🔊 Log insiders

* 💚 Invalidate cache

* 👷 Tweak cache keys

* 👷 Trigger CI and test cache

* 🔥 Remove cache comment

* ️ Optimize cache usage for first runs of docs

* 👷 Tweak cache for MkDocs Material cards

* 💚 Trigger CI to test cache
2023-06-26 16:05:43 +02:00
github-actions
afc237ad53 📝 Update release notes 2023-06-25 12:57:53 +00:00
Sebastián Ramírez
b107b6a096 🔥 Remove languages without translations (#9743)
* 🔥 Remove lang directories for empty translations

* 🔥 Remove untranslated langs from main config
2023-06-25 14:57:19 +02:00
github-actions
be8e704e46 📝 Update release notes 2023-06-25 12:34:39 +00:00
Sebastián Ramírez
5656ed09ef Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages (#9742)
*  Add MkDocs hooks to re-use all config from en, and auto-generate missing docs files form en

* 🔧 Update MkDocs config for es

* 🔧 Simplify configs for all languages

*  Compute available languages from MkDocs Material for config overrides in hooks

* 🔧 Update config for MkDocs for en, to make paths compatible for other languages

* ♻️ Refactor scripts/docs.py to remove all custom logic that is now handled by the MkDocs hooks

* 🔧 Remove ta language as it's incomplete (no translations and causing errors)

* 🔥 Remove ta lang, no translations available

* 🔥 Remove dummy overrides directories, no longer needed

*  Use the same missing-translation.md file contents for hooks

* ️ Restore and refactor new-lang command

* 📝 Update docs for contributing with new simplified workflow for translations

* 🔊 Enable logs so that MkDocs can show its standard output on the docs.py script
2023-06-25 14:33:58 +02:00
github-actions
c563b5bcf1 📝 Update release notes 2023-06-24 14:47:59 +00:00
Sebastián Ramírez
51d3a8ff12 🔨 Add MkDocs hook that renames sections based on the first index file (#9737) 2023-06-24 16:47:15 +02:00
github-actions
3aea9acc68 📝 Update release notes 2023-06-24 12:31:54 +00:00
Sebastián Ramírez
dfa56f743a 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens (#9735) 2023-06-24 14:30:57 +02:00
github-actions
8cee653ad8 📝 Update release notes 2023-06-24 12:29:17 +00:00
Sebastián Ramírez
dd590f46ad 🔧 Update MkDocs for other languages (#9734) 2023-06-24 14:28:43 +02:00
github-actions
7d865c9487 📝 Update release notes 2023-06-24 00:00:47 +00:00
Sebastián Ramírez
c09e5cdfa7 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed (#9732) 2023-06-24 02:00:12 +02:00
github-actions
2848951082 📝 Update release notes 2023-06-23 23:52:34 +00:00
Sebastián Ramírez
f61217a18a 🔥 Remove old internal GitHub Action watch-previews that is no longer needed (#9730) 2023-06-24 01:51:56 +02:00
github-actions
1471bc956c 📝 Update release notes 2023-06-23 18:17:17 +00:00
Sebastián Ramírez
0c66ec7da9 ⬆️ Upgrade MkDocs and MkDocs Material (#9729) 2023-06-23 20:16:41 +02:00
github-actions
5a3bbb62de 📝 Update release notes 2023-06-23 17:55:46 +00:00
Sebastián Ramírez
42d0d6e4a5 👷 Build and deploy docs only on docs changes (#9728) 2023-06-23 19:55:09 +02:00
Sebastián Ramírez
4721405ef7 🔖 Release version 0.98.0 2023-06-22 19:58:22 +02:00
Sebastián Ramírez
8066f85b3f 📝 Update release notes 2023-06-22 19:57:25 +02:00
github-actions
2ffb08d0bc 📝 Update release notes 2023-06-22 17:52:55 +00:00
dependabot[bot]
d1805ef466 ⬆ Bump ruff from 0.0.272 to 0.0.275 (#9721)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:52:20 +02:00
github-actions
41d774ed6d 📝 Update release notes 2023-06-22 17:44:21 +00:00
dependabot[bot]
836ac56203 ⬆ Update uvicorn[standard] requirement from <0.21.0,>=0.12.0 to >=0.12.0,<0.23.0 (#9463)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:43:44 +02:00
github-actions
a01c2ca3dd 📝 Update release notes 2023-06-22 17:43:29 +00:00
dependabot[bot]
6553243dbf ⬆ Bump mypy from 1.3.0 to 1.4.0 (#9719)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:42:53 +02:00
github-actions
fdc713428e 📝 Update release notes 2023-06-22 17:26:46 +00:00
dependabot[bot]
60343161ea ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0 (#9251)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:26:01 +02:00
github-actions
586de94ca1 📝 Update release notes 2023-06-22 17:12:59 +00:00
dependabot[bot]
56bc75372f ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6 (#9482)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 19:12:24 +02:00
github-actions
4842dfadcf 📝 Update release notes 2023-06-22 17:07:05 +00:00
я котик пур-пур
cfc06a3a3d 📝 Update docs on Pydantic using ujson internally (#5804)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 17:06:25 +00:00
github-actions
c812b42293 📝 Update release notes 2023-06-22 17:04:50 +00:00
ivan-abc
68ce5b37dc ✏ Rewording in docs/en/docs/tutorial/debugging.md (#9581)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 17:04:16 +00:00
github-actions
0dc9a377dc 📝 Update release notes 2023-06-22 17:02:08 +00:00
Pankaj Kumar
d82700c96d ✏️ Fix tooltips for light/dark theme toggler in docs (#9588) 2023-06-22 19:01:28 +02:00
github-actions
fafe670db6 📝 Update release notes 2023-06-22 16:53:00 +00:00
TabarakoAkula
47342cdd18 🌐 Add Russian translation for docs/ru/docs/tutorial/metadata.md (#9681)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 18:52:24 +02:00
github-actions
e76dd3e70d 📝 Update release notes 2023-06-22 16:44:41 +00:00
Marcel Sander
e5f3d6a5eb 📝 Add german blog post (Domain-driven Design mit Python und FastAPI) (#9261) 2023-06-22 18:44:05 +02:00
github-actions
7217f167d4 📝 Update release notes 2023-06-22 16:41:05 +00:00
github-actions
762ede2bec 📝 Update release notes 2023-06-22 16:40:50 +00:00
jyothish-mohan
a3b1478221 ✏️ Tweak wording in docs/en/docs/tutorial/security/index.md (#9561) 2023-06-22 18:40:32 +02:00
Lili_DL
41ff599d4b 🌐 Fix typo in Spanish translation for docs/es/docs/tutorial/first-steps.md (#9571)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 18:40:17 +02:00
github-actions
b1f27c96c4 📝 Update release notes 2023-06-22 16:35:04 +00:00
TabarakoAkula
4c401aef0f 🌐 Add Russian translation for docs/tutorial/path-operation-configuration.md (#9696)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
Co-authored-by: Alexandr <alexandrhub@vk.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 16:33:47 +00:00
github-actions
c7dad1bb59 📝 Update release notes 2023-06-22 16:33:28 +00:00
Alexandr
0ef164e1ee 📝 Update Annotated notes in docs/en/docs/tutorial/schema-extra-example.md (#9620)
Update for docs/tutorial/schema-extra-example.md

When working on the translation, I noticed that this page is missing the annotated tips that can be found in the rest of the documentation (I checked, and it's the only page where they're missing).
2023-06-22 18:32:53 +02:00
github-actions
fd6a78cbfe 📝 Update release notes 2023-06-22 16:20:40 +00:00
lordqyxz
fa7474b2e8 🌐 Add Chinese translation for docs/zh/docs/advanced/security/index.md (#9666)
Co-authored-by: shiyz <shiyz@finchina.com>
2023-06-22 18:19:49 +02:00
github-actions
2f0541f17a 📝 Update release notes 2023-06-22 16:18:54 +00:00
雨过初晴
804a0a90cf 🌐 Add Chinese translations for docs/zh/docs/advanced/settings.md (#9652) 2023-06-22 18:18:04 +02:00
github-actions
847befdc1d 📝 Update release notes 2023-06-22 16:17:50 +00:00
雨过初晴
4a7b21483b 🌐 Add Chinese translations for docs/zh/docs/advanced/websockets.md (#9651) 2023-06-22 18:17:12 +02:00
github-actions
1182b36362 📝 Update release notes 2023-06-22 16:16:43 +00:00
吴定焕
e17cacfee4 🌐 Add Chinese translation for docs/zh/docs/tutorial/testing.md (#9641)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-22 18:16:06 +02:00
github-actions
234cecb5bf 📝 Update release notes 2023-06-22 16:14:54 +00:00
ivan-abc
612cbee165 🌐 Add Russian translation for docs/tutorial/extra-models.md (#9619)
Co-authored-by: Alexandr <alexandrhub@vk.com>
2023-06-22 18:14:16 +02:00
github-actions
223ed67682 📝 Update release notes 2023-06-22 14:30:35 +00:00
ivan-abc
a2a0119c14 🌐 Add Russian translation for docs/tutorial/cors.md (#9608)
Co-authored-by: Alexandr <alexandrhub@vk.com>
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
2023-06-22 16:29:56 +02:00
github-actions
09319d6271 📝 Update release notes 2023-06-22 14:29:41 +00:00
Michał Brotoń
a92e9c957a 🌐 Add Polish translation for docs/pl/docs/features.md (#5348)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 16:29:05 +02:00
github-actions
7505f24f2e 📝 Update release notes 2023-06-22 11:47:12 +00:00
Alexandr
57727fa4e0 🌐 Add Russian translation for docs/ru/docs/tutorial/body-nested-models.md (#9605)
Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com>
Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com>
2023-06-22 13:46:36 +02:00
github-actions
a2aede32b4 📝 Update release notes 2023-06-22 11:43:21 +00:00
Ricardo Castro
7c66ec8a8b ✏️ Fix typo Annotation -> Annotated in docs/en/docs/tutorial/query-params-str-validations.md (#9625)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 11:42:48 +00:00
github-actions
d47eea9bb6 📝 Update release notes 2023-06-22 11:35:49 +00:00
Michał Górny
74de9a7b15 🔧 Set minimal hatchling version needed to build the package (#9240)
Set minimal hatchling version needed to build the package

Set the minimal hatchling version that is needed to build fastapi to
1.13.0.  Older versions fail to build because they do not recognize
the trove classifiers used, e.g. 1.12.2 yields:

    ValueError: Unknown classifier in field `project.classifiers`: Framework :: Pydantic

Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 13:35:12 +02:00
github-actions
3279f0ba63 📝 Update release notes 2023-06-22 11:32:46 +00:00
Jacob Coffee
428376d285 📝 Add repo link to PyPI (#9559) 2023-06-22 13:32:09 +02:00
github-actions
05c5ce3689 📝 Update release notes 2023-06-22 11:26:45 +00:00
Ryan Russell
b4b39d3359 ✏️ Fix typos in data for tests (#4958)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 11:26:11 +00:00
github-actions
2f048f7199 📝 Update release notes 2023-06-22 11:20:49 +00:00
Harsha Laxman
2cef119cd7 📝 Use in memory database for testing SQL in docs (#1223)
Co-authored-by: Harsha Laxman <harsh@Harshas-MacBook-Pro.local>
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-22 11:20:12 +00:00
github-actions
dd1c2018dc 📝 Update release notes 2023-06-22 10:38:27 +00:00
cyberlis
e94c13ce74 Add allow disabling redirect_slashes at the FastAPI app level (#3432)
Co-authored-by: Denis Lisovik <ckyberlis@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2023-06-22 10:37:50 +00:00
github-actions
b7ce10079e 📝 Update release notes 2023-06-19 12:34:13 +00:00
Sebastián Ramírez
87d5870314 🔧 Update sponsors, add Flint (#9699)
* 🔧 Set up sponsor Flint

* 🔧 Add configs for Flint sponsor
2023-06-19 12:33:32 +00:00
272 changed files with 4800 additions and 16861 deletions

View File

@@ -25,12 +25,10 @@ jobs:
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
- name: Install Pydantic v2
run: pip install --pre "pydantic>=2.0.0b2,<3.0.0"
- name: Lint
run: bash scripts/lint.sh
@@ -39,7 +37,6 @@ jobs:
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
pydantic-version: ["pydantic-v1", "pydantic-v2"]
fail-fast: false
steps:
- uses: actions/checkout@v3
@@ -54,16 +51,10 @@ jobs:
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
- name: Install Pydantic v1
if: matrix.pydantic-version == 'pydantic-v1'
run: pip install "pydantic>=1.10.0,<2.0.0"
- name: Install Pydantic v2
if: matrix.pydantic-version == 'pydantic-v2'
run: pip install --pre "pydantic>=2.0.0b2,<3.0.0"
- run: mkdir coverage
- name: Test
run: bash scripts/test.sh

View File

@@ -150,20 +150,9 @@ And you could do this even if the data type in the request is not JSON.
For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON:
=== "Pydantic v2"
```Python hl_lines="17-22 24"
{!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
```
=== "Pydantic v1"
```Python hl_lines="17-22 24"
{!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!}
```
!!! info
In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_schema_json()`.
```Python hl_lines="17-22 24"
{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
```
Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML.
@@ -171,20 +160,9 @@ Then we use the request directly, and extract the body as `bytes`. This means th
And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content:
=== "Pydantic v2"
```Python hl_lines="26-33"
{!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
```
=== "Pydantic v1"
```Python hl_lines="26-33"
{!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!}
```
!!! info
In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`.
```Python hl_lines="26-33"
{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
```
!!! tip
Here we re-use the same Pydantic model.

View File

@@ -125,34 +125,7 @@ That means that any value read in Python from an environment variable will be a
## Pydantic `Settings`
Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
### Install `pydantic-settings`
First, install the `pydantic-settings` package:
<div class="termy">
```console
$ pip install pydantic-settings
---> 100%
```
</div>
It also comes included when you install the `all` extras with:
<div class="termy">
```console
$ pip install "fastapi[all]"
---> 100%
```
</div>
!!! info
In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality.
Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with <a href="https://pydantic-docs.helpmanual.io/usage/settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
### Create the `Settings` object
@@ -162,20 +135,9 @@ The same way as with Pydantic models, you declare class attributes with type ann
You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`.
=== "Pydantic v2"
```Python hl_lines="2 5-8 11"
{!> ../../../docs_src/settings/tutorial001.py!}
```
=== "Pydantic v1"
!!! info
In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`.
```Python hl_lines="2 5-8 11"
{!> ../../../docs_src/settings/tutorial001_pv1.py!}
```
```Python hl_lines="2 5-8 11"
{!../../../docs_src/settings/tutorial001.py!}
```
!!! tip
If you want something quick to copy and paste, don't use this example, use the last one below.
@@ -344,28 +306,14 @@ APP_NAME="ChimichangApp"
And then update your `config.py` with:
=== "Pydantic v2"
```Python hl_lines="9-10"
{!../../../docs_src/settings/app03/config.py!}
```
```Python hl_lines="9"
{!> ../../../docs_src/settings/app03_an/config.py!}
```
Here we create a class `Config` inside of your Pydantic `Settings` class, and set the `env_file` to the filename with the dotenv file we want to use.
!!! tip
The `model_config` attribute is used just for Pydantic configuration. You can read more at <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
=== "Pydantic v1"
```Python hl_lines="9-10"
{!> ../../../docs_src/settings/app03_an/config_pv1.py!}
```
!!! tip
The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
!!! info
In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`.
Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use.
!!! tip
The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://pydantic-docs.helpmanual.io/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>
### Creating the `Settings` only once with `lru_cache`

View File

@@ -3,16 +3,6 @@
## Latest Changes
## 0.99.1
### Fixes
* 🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. `additionalProperties: false`. PR [#9781](https://github.com/tiangolo/fastapi/pull/9781) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Update source examples to use new JSON Schema examples field. PR [#9776](https://github.com/tiangolo/fastapi/pull/9776) by [@tiangolo](https://github.com/tiangolo).
## 0.99.0
### Features

View File

@@ -277,7 +277,7 @@ You can also add a parameter `min_length`:
## Add regular expressions
You can define a <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings.">regular expression</abbr> `pattern` that the parameter should match:
You can define a <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings.">regular expression</abbr> that the parameter should match:
=== "Python 3.10+"
@@ -315,7 +315,7 @@ You can define a <abbr title="A regular expression, regex or regexp is a sequenc
{!> ../../../docs_src/query_params_str_validations/tutorial004.py!}
```
This specific regular expression pattern checks that the received parameter value:
This specific regular expression checks that the received parameter value:
* `^`: starts with the following characters, doesn't have characters before.
* `fixedquery`: has the exact value `fixedquery`.
@@ -325,20 +325,6 @@ If you feel lost with all these **"regular expression"** ideas, don't worry. The
But whenever you need them and go and learn them, know that you can already use them directly in **FastAPI**.
### Pydantic v1 `regex` instead of `pattern`
Before Pydantic version 2 and before FastAPI 0.100.0, the parameter was called `regex` instead of `pattern`, but it's now deprecated.
You could still see some code using it:
=== "Python 3.10+ Pydantic v1"
```Python hl_lines="11"
{!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310_regex.py!}
```
But know that this is deprecated and it should be updated to use the new parameter `pattern`. 🤓
## Default values
You can, of course, use default values other than `None`.

View File

@@ -4,48 +4,24 @@ You can declare examples of the data your app can receive.
Here are several ways to do it.
## Extra JSON Schema data in Pydantic models
## Pydantic `schema_extra`
You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema.
You can declare `examples` for a Pydantic model using `Config` and `schema_extra`, as described in <a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>:
=== "Python 3.10+ Pydantic v2"
=== "Python 3.10+"
```Python hl_lines="13-24"
```Python hl_lines="13-23"
{!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!}
```
=== "Python 3.10+ Pydantic v1"
=== "Python 3.6+"
```Python hl_lines="13-23"
{!> ../../../docs_src/schema_extra_example/tutorial001_py310_pv1.py!}
```
=== "Python 3.6+ Pydantic v2"
```Python hl_lines="15-26"
```Python hl_lines="15-25"
{!> ../../../docs_src/schema_extra_example/tutorial001.py!}
```
=== "Python 3.6+ Pydantic v1"
```Python hl_lines="15-25"
{!> ../../../docs_src/schema_extra_example/tutorial001_pv1.py!}
```
That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs.
=== "Pydantic v2"
In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic's docs: Model Config</a>.
You can set `"json_schema_extra"` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`.
=== "Pydantic v1"
In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>.
You can set `schema_extra` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`.
!!! tip
You could use the same technique to extend the JSON Schema and add your own custom extra info.
@@ -140,19 +116,19 @@ You can of course also pass multiple `examples`:
=== "Python 3.10+"
```Python hl_lines="23-38"
```Python hl_lines="23-49"
{!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!}
```
=== "Python 3.9+"
```Python hl_lines="23-38"
```Python hl_lines="23-49"
{!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!}
```
=== "Python 3.6+"
```Python hl_lines="24-39"
```Python hl_lines="24-50"
{!> ../../../docs_src/schema_extra_example/tutorial004_an.py!}
```
@@ -161,7 +137,7 @@ You can of course also pass multiple `examples`:
!!! tip
Prefer to use the `Annotated` version if possible.
```Python hl_lines="19-34"
```Python hl_lines="19-45"
{!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!}
```
@@ -170,7 +146,7 @@ You can of course also pass multiple `examples`:
!!! tip
Prefer to use the `Annotated` version if possible.
```Python hl_lines="21-36"
```Python hl_lines="21-47"
{!> ../../../docs_src/schema_extra_example/tutorial004.py!}
```

View File

@@ -1,5 +1,5 @@
from fastapi import FastAPI
from pydantic_settings import BaseSettings
from pydantic import BaseSettings
class Settings(BaseSettings):

View File

@@ -12,11 +12,11 @@ class BaseItem(BaseModel):
class CarItem(BaseItem):
type: str = "car"
type = "car"
class PlaneItem(BaseItem):
type: str = "plane"
type = "plane"
size: int

View File

@@ -12,11 +12,11 @@ class BaseItem(BaseModel):
class CarItem(BaseItem):
type: str = "car"
type = "car"
class PlaneItem(BaseItem):
type: str = "plane"
type = "plane"
size: int

View File

@@ -16,7 +16,7 @@ class Item(BaseModel):
"/items/",
openapi_extra={
"requestBody": {
"content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
"content": {"application/x-yaml": {"schema": Item.schema()}},
"required": True,
},
},
@@ -28,7 +28,7 @@ async def create_item(request: Request):
except yaml.YAMLError:
raise HTTPException(status_code=422, detail="Invalid YAML")
try:
item = Item.model_validate(data)
item = Item.parse_obj(data)
except ValidationError as e:
raise HTTPException(status_code=422, detail=e.errors())
return item

View File

@@ -1,34 +0,0 @@
from typing import List
import yaml
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ValidationError
app = FastAPI()
class Item(BaseModel):
name: str
tags: List[str]
@app.post(
"/items/",
openapi_extra={
"requestBody": {
"content": {"application/x-yaml": {"schema": Item.schema()}},
"required": True,
},
},
)
async def create_item(request: Request):
raw_body = await request.body()
try:
data = yaml.safe_load(raw_body)
except yaml.YAMLError:
raise HTTPException(status_code=422, detail="Invalid YAML")
try:
item = Item.parse_obj(data)
except ValidationError as e:
raise HTTPException(status_code=422, detail=e.errors())
return item

View File

@@ -8,7 +8,7 @@ app = FastAPI()
@app.get("/items/")
async def read_items(
q: Union[str, None] = Query(
default=None, min_length=3, max_length=50, pattern="^fixedquery$"
default=None, min_length=3, max_length=50, regex="^fixedquery$"
)
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}

View File

@@ -9,7 +9,7 @@ app = FastAPI()
@app.get("/items/")
async def read_items(
q: Annotated[
Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$")
Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$")
] = None
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}

View File

@@ -8,7 +8,7 @@ app = FastAPI()
@app.get("/items/")
async def read_items(
q: Annotated[
str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$")
str | None, Query(min_length=3, max_length=50, regex="^fixedquery$")
] = None
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}

View File

@@ -1,17 +0,0 @@
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Annotated[
str | None, Query(min_length=3, max_length=50, regex="^fixedquery$")
] = None
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results

View File

@@ -8,7 +8,7 @@ app = FastAPI()
@app.get("/items/")
async def read_items(
q: Annotated[
Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$")
Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$")
] = None
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}

View File

@@ -6,7 +6,7 @@ app = FastAPI()
@app.get("/items/")
async def read_items(
q: str
| None = Query(default=None, min_length=3, max_length=50, pattern="^fixedquery$")
| None = Query(default=None, min_length=3, max_length=50, regex="^fixedquery$")
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:

View File

@@ -14,7 +14,7 @@ async def read_items(
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
pattern="^fixedquery$",
regex="^fixedquery$",
deprecated=True,
)
):

View File

@@ -16,7 +16,7 @@ async def read_items(
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
pattern="^fixedquery$",
regex="^fixedquery$",
deprecated=True,
),
] = None

View File

@@ -15,7 +15,7 @@ async def read_items(
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
pattern="^fixedquery$",
regex="^fixedquery$",
deprecated=True,
),
] = None

View File

@@ -15,7 +15,7 @@ async def read_items(
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
pattern="^fixedquery$",
regex="^fixedquery$",
deprecated=True,
),
] = None

View File

@@ -13,7 +13,7 @@ async def read_items(
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
pattern="^fixedquery$",
regex="^fixedquery$",
deprecated=True,
)
):

View File

@@ -12,8 +12,8 @@ class Item(BaseModel):
price: float
tax: Union[float, None] = None
model_config = {
"json_schema_extra": {
class Config:
schema_extra = {
"examples": [
{
"name": "Foo",
@@ -23,7 +23,6 @@ class Item(BaseModel):
}
]
}
}
@app.put("/items/{item_id}")

View File

@@ -1,31 +0,0 @@
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
class Config:
schema_extra = {
"examples": [
{
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
}
]
}
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results

View File

@@ -10,8 +10,8 @@ class Item(BaseModel):
price: float
tax: float | None = None
model_config = {
"json_schema_extra": {
class Config:
schema_extra = {
"examples": [
{
"name": "Foo",
@@ -21,7 +21,6 @@ class Item(BaseModel):
}
]
}
}
@app.put("/items/{item_id}")

View File

@@ -1,29 +0,0 @@
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
class Config:
schema_extra = {
"examples": [
{
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
}
]
}
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results

View File

@@ -20,18 +20,29 @@ async def update_item(
item: Item = Body(
examples=[
{
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
"summary": "A normal example",
"description": "A **normal** item works correctly.",
"value": {
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
},
},
{
"name": "Bar",
"price": "35.4",
"summary": "An example with converted data",
"description": "FastAPI can convert price `strings` to actual `numbers` automatically",
"value": {
"name": "Bar",
"price": "35.4",
},
},
{
"name": "Baz",
"price": "thirty five point four",
"summary": "Invalid data is rejected with an error",
"value": {
"name": "Baz",
"price": "thirty five point four",
},
},
],
),

View File

@@ -23,18 +23,29 @@ async def update_item(
Body(
examples=[
{
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
"summary": "A normal example",
"description": "A **normal** item works correctly.",
"value": {
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
},
},
{
"name": "Bar",
"price": "35.4",
"summary": "An example with converted data",
"description": "FastAPI can convert price `strings` to actual `numbers` automatically",
"value": {
"name": "Bar",
"price": "35.4",
},
},
{
"name": "Baz",
"price": "thirty five point four",
"summary": "Invalid data is rejected with an error",
"value": {
"name": "Baz",
"price": "thirty five point four",
},
},
],
),

View File

@@ -22,18 +22,29 @@ async def update_item(
Body(
examples=[
{
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
"summary": "A normal example",
"description": "A **normal** item works correctly.",
"value": {
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
},
},
{
"name": "Bar",
"price": "35.4",
"summary": "An example with converted data",
"description": "FastAPI can convert price `strings` to actual `numbers` automatically",
"value": {
"name": "Bar",
"price": "35.4",
},
},
{
"name": "Baz",
"price": "thirty five point four",
"summary": "Invalid data is rejected with an error",
"value": {
"name": "Baz",
"price": "thirty five point four",
},
},
],
),

View File

@@ -22,18 +22,29 @@ async def update_item(
Body(
examples=[
{
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
"summary": "A normal example",
"description": "A **normal** item works correctly.",
"value": {
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
},
},
{
"name": "Bar",
"price": "35.4",
"summary": "An example with converted data",
"description": "FastAPI can convert price `strings` to actual `numbers` automatically",
"value": {
"name": "Bar",
"price": "35.4",
},
},
{
"name": "Baz",
"price": "thirty five point four",
"summary": "Invalid data is rejected with an error",
"value": {
"name": "Baz",
"price": "thirty five point four",
},
},
],
),

View File

@@ -18,18 +18,29 @@ async def update_item(
item: Item = Body(
examples=[
{
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
"summary": "A normal example",
"description": "A **normal** item works correctly.",
"value": {
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
},
},
{
"name": "Bar",
"price": "35.4",
"summary": "An example with converted data",
"description": "FastAPI can convert price `strings` to actual `numbers` automatically",
"value": {
"name": "Bar",
"price": "35.4",
},
},
{
"name": "Baz",
"price": "thirty five point four",
"summary": "Invalid data is rejected with an error",
"value": {
"name": "Baz",
"price": "thirty five point four",
},
},
],
),

View File

@@ -1,4 +1,4 @@
from pydantic_settings import BaseSettings
from pydantic import BaseSettings
class Settings(BaseSettings):

View File

@@ -1,4 +1,4 @@
from pydantic_settings import BaseSettings
from pydantic import BaseSettings
class Settings(BaseSettings):

View File

@@ -1,4 +1,4 @@
from pydantic_settings import BaseSettings
from pydantic import BaseSettings
class Settings(BaseSettings):

View File

@@ -1,4 +1,4 @@
from pydantic_settings import BaseSettings
from pydantic import BaseSettings
class Settings(BaseSettings):

View File

@@ -1,4 +1,4 @@
from pydantic_settings import BaseSettings
from pydantic import BaseSettings
class Settings(BaseSettings):

View File

@@ -1,4 +1,4 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import BaseSettings
class Settings(BaseSettings):
@@ -6,4 +6,5 @@ class Settings(BaseSettings):
admin_email: str
items_per_user: int = 50
model_config = SettingsConfigDict(env_file=".env")
class Config:
env_file = ".env"

View File

@@ -1,10 +0,0 @@
from pydantic import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
class Config:
env_file = ".env"

View File

@@ -1,4 +1,4 @@
from pydantic_settings import BaseSettings
from pydantic import BaseSettings
class Settings(BaseSettings):

View File

@@ -1,5 +1,5 @@
from fastapi import FastAPI
from pydantic_settings import BaseSettings
from pydantic import BaseSettings
class Settings(BaseSettings):

View File

@@ -1,21 +0,0 @@
from fastapi import FastAPI
from pydantic import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
settings = Settings()
app = FastAPI()
@app.get("/info")
async def info():
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}

View File

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

View File

@@ -1,616 +0,0 @@
from collections import deque
from copy import copy
from dataclasses import dataclass, is_dataclass
from enum import Enum
from typing import (
Any,
Callable,
Deque,
Dict,
FrozenSet,
List,
Mapping,
Sequence,
Set,
Tuple,
Type,
Union,
)
from fastapi.exceptions import RequestErrorModel
from fastapi.types import IncEx, ModelNameMap, UnionType
from pydantic import BaseModel, create_model
from pydantic.version import VERSION as PYDANTIC_VERSION
from starlette.datastructures import UploadFile
from typing_extensions import Annotated, Literal, get_args, get_origin
PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")
sequence_annotation_to_type = {
Sequence: list,
List: list,
list: list,
Tuple: tuple,
tuple: tuple,
Set: set,
set: set,
FrozenSet: frozenset,
frozenset: frozenset,
Deque: deque,
deque: deque,
}
sequence_types = tuple(sequence_annotation_to_type.keys())
if PYDANTIC_V2:
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import TypeAdapter
from pydantic import ValidationError as ValidationError
from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined]
GetJsonSchemaHandler as GetJsonSchemaHandler,
)
from pydantic._internal._typing_extra import eval_type_lenient
from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
from pydantic.fields import FieldInfo
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
from pydantic_core import CoreSchema as CoreSchema
from pydantic_core import MultiHostUrl as MultiHostUrl
from pydantic_core import PydanticUndefined, PydanticUndefinedType
from pydantic_core import Url as Url
from pydantic_core.core_schema import (
general_plain_validator_function as general_plain_validator_function,
)
Required = PydanticUndefined
Undefined = PydanticUndefined
UndefinedType = PydanticUndefinedType
evaluate_forwardref = eval_type_lenient
Validator = Any
class BaseConfig:
pass
class ErrorWrapper(Exception):
pass
@dataclass
class ModelField:
field_info: FieldInfo
name: str
mode: Literal["validation", "serialization"] = "validation"
@property
def alias(self) -> str:
a = self.field_info.alias
return a if a is not None else self.name
@property
def required(self) -> bool:
return self.field_info.is_required()
@property
def default(self) -> Any:
return self.get_default()
@property
def type_(self) -> Any:
return self.field_info.annotation
def __post_init__(self) -> None:
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[self.field_info.annotation, self.field_info]
)
def get_default(self) -> Any:
if self.field_info.is_required():
return Undefined
return self.field_info.get_default(call_default_factory=True)
def validate(
self,
value: Any,
values: Dict[str, Any] = {}, # noqa: B006
*,
loc: Tuple[Union[int, str], ...] = (),
) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
try:
return (
self._type_adapter.validate_python(value, from_attributes=True),
None,
)
except ValidationError as exc:
return None, _regenerate_error_with_loc(
errors=exc.errors(), loc_prefix=loc
)
def serialize(
self,
value: Any,
*,
mode: Literal["json", "python"] = "json",
include: Union[IncEx, None] = None,
exclude: Union[IncEx, None] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> Any:
# What calls this code passes a value that already called
# self._type_adapter.validate_python(value)
return self._type_adapter.dump_python(
value,
mode=mode,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
def __hash__(self) -> int:
# Each ModelField is unique for our purposes, to allow making a dict from
# ModelField to its JSON Schema.
return id(self)
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Any:
return annotation
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
return errors # type: ignore[return-value]
def _model_rebuild(model: Type[BaseModel]) -> None:
model.model_rebuild()
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return model.model_dump(mode=mode, **kwargs)
def _get_model_config(model: BaseModel) -> Any:
return model.model_config
def get_schema_from_model_field(
*,
field: ModelField,
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
) -> Dict[str, Any]:
# This expects that GenerateJsonSchema was already used to generate the definitions
json_schema = field_mapping[(field, field.mode)]
if "$ref" not in json_schema:
# TODO remove when deprecating Pydantic v1
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
json_schema[
"title"
] = field.field_info.title or field.alias.title().replace("_", " ")
return json_schema
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
return {}
def get_definitions(
*,
fields: List[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Tuple[
Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
Dict[str, Dict[str, Any]],
]:
inputs = [
(field, field.mode, field._type_adapter.core_schema) for field in fields
]
field_mapping, definitions = schema_generator.generate_definitions(
inputs=inputs
)
return field_mapping, definitions # type: ignore[return-value]
def is_scalar_field(field: ModelField) -> bool:
from fastapi import params
return field_annotation_is_scalar(
field.field_info.annotation
) and not isinstance(field.field_info, params.Body)
def is_sequence_field(field: ModelField) -> bool:
return field_annotation_is_sequence(field.field_info.annotation)
def is_scalar_sequence_field(field: ModelField) -> bool:
return field_annotation_is_scalar_sequence(field.field_info.annotation)
def is_bytes_field(field: ModelField) -> bool:
return is_bytes_or_nonable_bytes_annotation(field.type_)
def is_bytes_sequence_field(field: ModelField) -> bool:
return is_bytes_sequence_annotation(field.type_)
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
return type(field_info).from_annotation(annotation)
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
origin_type = (
get_origin(field.field_info.annotation) or field.field_info.annotation
)
assert issubclass(origin_type, sequence_types) # type: ignore[arg-type]
return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
error = ValidationError.from_exception_data(
"Field required", [{"type": "missing", "loc": loc, "input": {}}]
).errors()[0]
error["input"] = None
return error # type: ignore[return-value]
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> Type[BaseModel]:
field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
return BodyModel
else:
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
from pydantic import AnyUrl as Url # noqa: F401
from pydantic import ( # type: ignore[assignment]
BaseConfig as BaseConfig, # noqa: F401
)
from pydantic import ValidationError as ValidationError # noqa: F401
from pydantic.class_validators import ( # type: ignore[no-redef]
Validator as Validator, # noqa: F401
)
from pydantic.error_wrappers import ( # type: ignore[no-redef]
ErrorWrapper as ErrorWrapper, # noqa: F401
)
from pydantic.errors import MissingError
from pydantic.fields import ( # type: ignore[attr-defined]
SHAPE_FROZENSET,
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
)
from pydantic.fields import FieldInfo as FieldInfo
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
ModelField as ModelField, # noqa: F401
)
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
Required as Required, # noqa: F401
)
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
Undefined as Undefined,
)
from pydantic.fields import ( # type: ignore[no-redef, attr-defined]
UndefinedType as UndefinedType, # noqa: F401
)
from pydantic.networks import ( # type: ignore[no-redef]
MultiHostDsn as MultiHostUrl, # noqa: F401
)
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
get_model_name_map,
model_process_schema,
)
from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401
get_annotation_from_field_info as get_annotation_from_field_info,
)
from pydantic.typing import ( # type: ignore[no-redef]
evaluate_forwardref as evaluate_forwardref, # noqa: F401
)
from pydantic.utils import ( # type: ignore[no-redef]
lenient_issubclass as lenient_issubclass, # noqa: F401
)
GetJsonSchemaHandler = Any # type: ignore[assignment,misc]
JsonSchemaValue = Dict[str, Any] # type: ignore[misc]
CoreSchema = Any # type: ignore[assignment,misc]
sequence_shapes = {
SHAPE_LIST,
SHAPE_SET,
SHAPE_FROZENSET,
SHAPE_TUPLE,
SHAPE_SEQUENCE,
SHAPE_TUPLE_ELLIPSIS,
}
sequence_shape_to_type = {
SHAPE_LIST: list,
SHAPE_SET: set,
SHAPE_TUPLE: tuple,
SHAPE_SEQUENCE: list,
SHAPE_TUPLE_ELLIPSIS: list,
}
@dataclass
class GenerateJsonSchema: # type: ignore[no-redef]
ref_template: str
class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef]
pass
def general_plain_validator_function( # type: ignore[misc]
function: Callable[..., Any],
*,
ref: Union[str, None] = None,
metadata: Any = None,
serialization: Any = None,
) -> Any:
return {}
def get_model_definitions(
*,
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Dict[str, Any]:
definitions: Dict[str, Dict[str, Any]] = {}
for model in flat_models:
m_schema, m_definitions, m_nested_models = model_process_schema(
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
definitions.update(m_definitions)
model_name = model_name_map[model]
if "description" in m_schema:
m_schema["description"] = m_schema["description"].split("\f")[0]
definitions[model_name] = m_schema
return definitions
def is_pv1_scalar_field(field: ModelField) -> bool:
from fastapi import params
field_info = field.field_info
if not (
field.shape == SHAPE_SINGLETON # type: ignore[attr-defined]
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, dict)
and not field_annotation_is_sequence(field.type_)
and not is_dataclass(field.type_)
and not isinstance(field_info, params.Body)
):
return False
if field.sub_fields: # type: ignore[attr-defined]
if not all(
is_pv1_scalar_field(f)
for f in field.sub_fields # type: ignore[attr-defined]
):
return False
return True
def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined]
field.type_, BaseModel
):
if field.sub_fields is not None: # type: ignore[attr-defined]
for sub_field in field.sub_fields: # type: ignore[attr-defined]
if not is_pv1_scalar_field(sub_field):
return False
return True
if _annotation_is_sequence(field.type_):
return True
return False
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
use_errors: List[Any] = []
for error in errors:
if isinstance(error, ErrorWrapper):
new_errors = ValidationError( # type: ignore[call-arg]
errors=[error], model=RequestErrorModel
).errors()
use_errors.extend(new_errors)
elif isinstance(error, list):
use_errors.extend(_normalize_errors(error))
else:
use_errors.append(error)
return use_errors
def _model_rebuild(model: Type[BaseModel]) -> None:
model.update_forward_refs()
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return model.dict(**kwargs)
def _get_model_config(model: BaseModel) -> Any:
return model.__config__ # type: ignore[attr-defined]
def get_schema_from_model_field(
*,
field: ModelField,
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
) -> Dict[str, Any]:
# This expects that GenerateJsonSchema was already used to generate the definitions
return field_schema( # type: ignore[no-any-return]
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)[0]
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
models = get_flat_models_from_fields(fields, known_models=set())
return get_model_name_map(models) # type: ignore[no-any-return]
def get_definitions(
*,
fields: List[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
) -> Tuple[
Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
Dict[str, Dict[str, Any]],
]:
models = get_flat_models_from_fields(fields, known_models=set())
return {}, get_model_definitions(
flat_models=models, model_name_map=model_name_map
)
def is_scalar_field(field: ModelField) -> bool:
return is_pv1_scalar_field(field)
def is_sequence_field(field: ModelField) -> bool:
return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined]
def is_scalar_sequence_field(field: ModelField) -> bool:
return is_pv1_scalar_sequence_field(field)
def is_bytes_field(field: ModelField) -> bool:
return lenient_issubclass(field.type_, bytes)
def is_bytes_sequence_field(field: ModelField) -> bool:
return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined]
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
return copy(field_info)
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined]
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg]
new_error = ValidationError([missing_field_error], RequestErrorModel)
return new_error.errors()[0] # type: ignore[return-value]
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> Type[BaseModel]:
BodyModel = create_model(model_name)
for f in fields:
BodyModel.__fields__[f.name] = f # type: ignore[index]
return BodyModel
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
) -> List[Dict[str, Any]]:
updated_loc_errors: List[Any] = [
{**err, "loc": loc_prefix + err.get("loc", ())}
for err in _normalize_errors(errors)
]
return updated_loc_errors
def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
if lenient_issubclass(annotation, (str, bytes)):
return False
return lenient_issubclass(annotation, sequence_types)
def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
return _annotation_is_sequence(annotation) or _annotation_is_sequence(
get_origin(annotation)
)
def value_is_sequence(value: Any) -> bool:
return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type]
def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
return (
lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
or _annotation_is_sequence(annotation)
or is_dataclass(annotation)
)
def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
return any(field_annotation_is_complex(arg) for arg in get_args(annotation))
return (
_annotation_is_complex(annotation)
or _annotation_is_complex(origin)
or hasattr(origin, "__pydantic_core_schema__")
or hasattr(origin, "__get_pydantic_core_schema__")
)
def field_annotation_is_scalar(annotation: Any) -> bool:
# handle Ellipsis here to make tuple[int, ...] work nicely
return annotation is Ellipsis or not field_annotation_is_complex(annotation)
def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
at_least_one_scalar_sequence = False
for arg in get_args(annotation):
if field_annotation_is_scalar_sequence(arg):
at_least_one_scalar_sequence = True
continue
elif not field_annotation_is_scalar(arg):
return False
return at_least_one_scalar_sequence
return field_annotation_is_sequence(annotation) and all(
field_annotation_is_scalar(sub_annotation)
for sub_annotation in get_args(annotation)
)
def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
if lenient_issubclass(annotation, bytes):
return True
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if lenient_issubclass(arg, bytes):
return True
return False
def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool:
if lenient_issubclass(annotation, UploadFile):
return True
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if lenient_issubclass(arg, UploadFile):
return True
return False
def is_bytes_sequence_annotation(annotation: Any) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
at_least_one = False
for arg in get_args(annotation):
if is_bytes_sequence_annotation(arg):
at_least_one = True
continue
return at_least_one
return field_annotation_is_sequence(annotation) and all(
is_bytes_or_nonable_bytes_annotation(sub_annotation)
for sub_annotation in get_args(annotation)
)
def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
at_least_one = False
for arg in get_args(annotation):
if is_uploadfile_sequence_annotation(arg):
at_least_one = True
continue
return at_least_one
return field_annotation_is_sequence(annotation) and all(
is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
for sub_annotation in get_args(annotation)
)

View File

@@ -15,6 +15,7 @@ from typing import (
from fastapi import routing
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
@@ -30,7 +31,7 @@ from fastapi.openapi.docs import (
)
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from fastapi.types import DecoratedCallable, IncEx
from fastapi.types import DecoratedCallable
from fastapi.utils import generate_unique_id
from starlette.applications import Starlette
from starlette.datastructures import State
@@ -304,8 +305,8 @@ class FastAPI(Starlette):
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -362,8 +363,8 @@ class FastAPI(Starlette):
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -483,8 +484,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -538,8 +539,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -593,8 +594,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -648,8 +649,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -703,8 +704,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -758,8 +759,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -813,8 +814,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -868,8 +869,8 @@ class FastAPI(Starlette):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,

View File

@@ -1,12 +1,5 @@
from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast
from typing import Any, Callable, Dict, Iterable, Type, TypeVar
from fastapi._compat import (
PYDANTIC_V2,
CoreSchema,
GetJsonSchemaHandler,
JsonSchemaValue,
general_plain_validator_function,
)
from starlette.datastructures import URL as URL # noqa: F401
from starlette.datastructures import Address as Address # noqa: F401
from starlette.datastructures import FormData as FormData # noqa: F401
@@ -28,28 +21,8 @@ class UploadFile(StarletteUploadFile):
return v
@classmethod
def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
if not isinstance(__input_value, StarletteUploadFile):
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
return cast(UploadFile, __input_value)
if not PYDANTIC_V2:
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update({"type": "string", "format": "binary"})
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
return {"type": "string", "format": "binary"}
@classmethod
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
) -> CoreSchema:
return general_plain_validator_function(cls._validate)
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update({"type": "string", "format": "binary"})
class DefaultPlaceholder:

View File

@@ -1,7 +1,7 @@
from typing import Any, Callable, List, Optional, Sequence
from fastapi._compat import ModelField
from fastapi.security.base import SecurityBase
from pydantic.fields import ModelField
class SecurityRequirement:

View File

@@ -1,6 +1,7 @@
import dataclasses
import inspect
from contextlib import contextmanager
from copy import deepcopy
from copy import copy, deepcopy
from typing import (
Any,
Callable,
@@ -19,31 +20,6 @@ from typing import (
import anyio
from fastapi import params
from fastapi._compat import (
PYDANTIC_V2,
ErrorWrapper,
ModelField,
Required,
Undefined,
_regenerate_error_with_loc,
copy_field_info,
create_body_model,
evaluate_forwardref,
field_annotation_is_scalar,
get_annotation_from_field_info,
get_missing_field_error,
is_bytes_field,
is_bytes_sequence_field,
is_scalar_field,
is_scalar_sequence_field,
is_sequence_field,
is_uploadfile_or_nonable_uploadfile_annotation,
is_uploadfile_sequence_annotation,
lenient_issubclass,
sequence_types,
serialize_sequence_value,
value_is_sequence,
)
from fastapi.concurrency import (
AsyncExitStack,
asynccontextmanager,
@@ -55,14 +31,50 @@ from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.utils import create_response_field, get_path_param_names
from pydantic.fields import FieldInfo
from pydantic import BaseModel, create_model
from pydantic.error_wrappers import ErrorWrapper
from pydantic.errors import MissingError
from pydantic.fields import (
SHAPE_FROZENSET,
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
FieldInfo,
ModelField,
Required,
Undefined,
)
from pydantic.schema import get_annotation_from_field_info
from pydantic.typing import evaluate_forwardref, get_args, get_origin
from pydantic.utils import lenient_issubclass
from starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
from starlette.requests import HTTPConnection, Request
from starlette.responses import Response
from starlette.websockets import WebSocket
from typing_extensions import Annotated, get_args, get_origin
from typing_extensions import Annotated
sequence_shapes = {
SHAPE_LIST,
SHAPE_SET,
SHAPE_FROZENSET,
SHAPE_TUPLE,
SHAPE_SEQUENCE,
SHAPE_TUPLE_ELLIPSIS,
}
sequence_types = (list, set, tuple)
sequence_shape_to_type = {
SHAPE_LIST: list,
SHAPE_SET: set,
SHAPE_TUPLE: tuple,
SHAPE_SEQUENCE: list,
SHAPE_TUPLE_ELLIPSIS: list,
}
multipart_not_installed_error = (
'Form data requires "python-multipart" to be installed. \n'
@@ -204,6 +216,36 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
)
def is_scalar_field(field: ModelField) -> bool:
field_info = field.field_info
if not (
field.shape == SHAPE_SINGLETON
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, sequence_types + (dict,))
and not dataclasses.is_dataclass(field.type_)
and not isinstance(field_info, params.Body)
):
return False
if field.sub_fields:
if not all(is_scalar_field(f) for f in field.sub_fields):
return False
return True
def is_scalar_sequence_field(field: ModelField) -> bool:
if (field.shape in sequence_shapes) and not lenient_issubclass(
field.type_, BaseModel
):
if field.sub_fields is not None:
for sub_field in field.sub_fields:
if not is_scalar_field(sub_field):
return False
return True
if lenient_issubclass(field.type_, sequence_types):
return True
return False
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
signature = inspect.signature(call)
globalns = getattr(call, "__globals__", {})
@@ -322,11 +364,12 @@ def analyze_param(
is_path_param: bool,
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
field_info = None
used_default_field_info = False
depends = None
type_annotation: Any = Any
if (
annotation is not inspect.Signature.empty
and get_origin(annotation) is Annotated
and get_origin(annotation) is Annotated # type: ignore[comparison-overlap]
):
annotated_args = get_args(annotation)
type_annotation = annotated_args[0]
@@ -341,9 +384,7 @@ def analyze_param(
fastapi_annotation = next(iter(fastapi_annotations), None)
if isinstance(fastapi_annotation, FieldInfo):
# Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info(
field_info=fastapi_annotation, annotation=annotation
)
field_info = copy(fastapi_annotation)
assert field_info.default is Undefined or field_info.default is Required, (
f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
@@ -374,8 +415,6 @@ def analyze_param(
f" together for {param_name!r}"
)
field_info = value
if PYDANTIC_V2:
field_info.annotation = type_annotation
if depends is not None and depends.dependency is None:
depends.dependency = type_annotation
@@ -394,15 +433,10 @@ def analyze_param(
# We might check here that `default_value is Required`, but the fact is that the same
# parameter might sometimes be a path parameter and sometimes not. See
# `tests/test_infer_param_optionality.py` for an example.
field_info = params.Path(annotation=type_annotation)
elif is_uploadfile_or_nonable_uploadfile_annotation(
type_annotation
) or is_uploadfile_sequence_annotation(type_annotation):
field_info = params.File(annotation=type_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation):
field_info = params.Body(annotation=type_annotation, default=default_value)
field_info = params.Path()
else:
field_info = params.Query(annotation=type_annotation, default=default_value)
field_info = params.Query(default=default_value)
used_default_field_info = True
field = None
if field_info is not None:
@@ -416,8 +450,8 @@ def analyze_param(
and getattr(field_info, "in_", None) is None
):
field_info.in_ = params.ParamTypes.query
use_annotation = get_annotation_from_field_info(
type_annotation,
annotation = get_annotation_from_field_info(
annotation if annotation is not inspect.Signature.empty else Any,
field_info,
param_name,
)
@@ -425,15 +459,19 @@ def analyze_param(
alias = param_name.replace("_", "-")
else:
alias = field_info.alias or param_name
field_info.alias = alias
field = create_response_field(
name=param_name,
type_=use_annotation,
type_=annotation,
default=field_info.default,
alias=alias,
required=field_info.default in (Required, Undefined),
field_info=field_info,
)
if used_default_field_info:
if lenient_issubclass(field.type_, UploadFile):
field.field_info = params.File(field_info.default)
elif not is_scalar_field(field=field):
field.field_info = params.Body(field_info.default)
return type_annotation, depends, field
@@ -516,13 +554,13 @@ async def solve_dependencies(
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
) -> Tuple[
Dict[str, Any],
List[Any],
List[ErrorWrapper],
Optional[BackgroundTasks],
Response,
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
]:
values: Dict[str, Any] = {}
errors: List[Any] = []
errors: List[ErrorWrapper] = []
if response is None:
response = Response()
del response.headers["content-length"]
@@ -636,7 +674,7 @@ async def solve_dependencies(
def request_params_to_args(
required_params: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[Any]]:
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
values = {}
errors = []
for field in required_params:
@@ -650,19 +688,23 @@ def request_params_to_args(
assert isinstance(
field_info, params.Param
), "Params must be subclasses of Param"
loc = (field_info.in_.value, field.alias)
if value is None:
if field.required:
errors.append(get_missing_field_error(loc=loc))
errors.append(
ErrorWrapper(
MissingError(), loc=(field_info.in_.value, field.alias)
)
)
else:
values[field.name] = deepcopy(field.default)
continue
v_, errors_ = field.validate(value, values, loc=loc)
v_, errors_ = field.validate(
value, values, loc=(field_info.in_.value, field.alias)
)
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)
errors.extend(errors_)
else:
values[field.name] = v_
return values, errors
@@ -671,9 +713,9 @@ def request_params_to_args(
async def request_body_to_args(
required_params: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]],
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
values = {}
errors: List[Dict[str, Any]] = []
errors = []
if required_params:
field = required_params[0]
field_info = field.field_info
@@ -691,7 +733,9 @@ async def request_body_to_args(
value: Optional[Any] = None
if received_body is not None:
if (is_sequence_field(field)) and isinstance(received_body, FormData):
if (
field.shape in sequence_shapes or field.type_ in sequence_types
) and isinstance(received_body, FormData):
value = received_body.getlist(field.alias)
else:
try:
@@ -704,7 +748,7 @@ async def request_body_to_args(
or (isinstance(field_info, params.Form) and value == "")
or (
isinstance(field_info, params.Form)
and is_sequence_field(field)
and field.shape in sequence_shapes
and len(value) == 0
)
):
@@ -715,17 +759,16 @@ async def request_body_to_args(
continue
if (
isinstance(field_info, params.File)
and is_bytes_field(field)
and lenient_issubclass(field.type_, bytes)
and isinstance(value, UploadFile)
):
value = await value.read()
elif (
is_bytes_sequence_field(field)
field.shape in sequence_shapes
and isinstance(field_info, params.File)
and value_is_sequence(value)
and lenient_issubclass(field.type_, bytes)
and isinstance(value, sequence_types)
):
# For types
assert isinstance(value, sequence_types) # type: ignore[arg-type]
results: List[Union[bytes, str]] = []
async def process_fn(
@@ -737,19 +780,24 @@ async def request_body_to_args(
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)
value = sequence_shape_to_type[field.shape](results)
v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, list):
errors.extend(errors_)
elif errors_:
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
errors.extend(errors_)
else:
values[field.name] = v_
return values, errors
def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
missing_field_error = ErrorWrapper(MissingError(), loc=loc)
return missing_field_error
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
flat_dependant = get_flat_dependant(dependant)
if not flat_dependant.body_params:
@@ -767,16 +815,12 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
for param in flat_dependant.body_params:
setattr(param.field_info, "embed", True) # noqa: B010
model_name = "Body_" + name
BodyModel = create_body_model(
fields=flat_dependant.body_params, model_name=model_name
)
BodyModel: Type[BaseModel] = create_model(model_name)
for f in flat_dependant.body_params:
BodyModel.__fields__[f.name] = f
required = any(True for f in flat_dependant.body_params if f.required)
BodyFieldInfo_kwargs: Dict[str, Any] = {
"annotation": BodyModel,
"alias": "body",
}
if not required:
BodyFieldInfo_kwargs["default"] = None
BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None}
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
BodyFieldInfo: Type[params.Body] = params.File
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):

View File

@@ -1,87 +1,15 @@
import dataclasses
import datetime
from collections import defaultdict, deque
from decimal import Decimal
from enum import Enum
from ipaddress import (
IPv4Address,
IPv4Interface,
IPv4Network,
IPv6Address,
IPv6Interface,
IPv6Network,
)
from pathlib import Path, PurePath
from re import Pattern
from pathlib import PurePath
from types import GeneratorType
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
from uuid import UUID
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from fastapi.types import IncEx
from pydantic import BaseModel
from pydantic.color import Color
from pydantic.networks import NameEmail
from pydantic.types import SecretBytes, SecretStr
from pydantic.json import ENCODERS_BY_TYPE
from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump
# Taken from Pydantic v1 as is
def isoformat(o: Union[datetime.date, datetime.time]) -> str:
return o.isoformat()
# Taken from Pydantic v1 as is
# TODO: pv2 should this return strings instead?
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
"""
Encodes a Decimal as int of there's no exponent, otherwise float
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
where a integer (but not int typed) is used. Encoding this as a float
results in failed round-tripping between encode and parse.
Our Id type is a prime example of this.
>>> decimal_encoder(Decimal("1.0"))
1.0
>>> decimal_encoder(Decimal("1"))
1
"""
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
return int(dec_value)
else:
return float(dec_value)
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(),
Color: str,
datetime.date: isoformat,
datetime.datetime: isoformat,
datetime.time: isoformat,
datetime.timedelta: lambda td: td.total_seconds(),
Decimal: decimal_encoder,
Enum: lambda o: o.value,
frozenset: list,
deque: list,
GeneratorType: list,
IPv4Address: str,
IPv4Interface: str,
IPv4Network: str,
IPv6Address: str,
IPv6Interface: str,
IPv6Network: str,
NameEmail: str,
Path: str,
Pattern: lambda o: o.pattern,
SecretBytes: str,
SecretStr: str,
set: list,
UUID: str,
Url: str,
MultiHostUrl: str,
}
SetIntStr = Set[Union[int, str]]
DictIntStrAny = Dict[Union[int, str], Any]
def generate_encoders_by_class_tuples(
@@ -100,8 +28,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
def jsonable_encoder(
obj: Any,
include: Optional[IncEx] = None,
exclude: Optional[IncEx] = None,
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
@@ -122,15 +50,10 @@ def jsonable_encoder(
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)
if isinstance(obj, BaseModel):
# TODO: remove when deprecating Pydantic v1
encoders: Dict[Any, Any] = {}
if not PYDANTIC_V2:
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
if custom_encoder:
encoders.update(custom_encoder)
obj_dict = _model_dump(
obj,
mode="json",
encoder = getattr(obj.__config__, "json_encoders", {})
if custom_encoder:
encoder.update(custom_encoder)
obj_dict = obj.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
@@ -144,8 +67,7 @@ def jsonable_encoder(
obj_dict,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
# TODO: remove when deprecating Pydantic v1
custom_encoder=encoders,
custom_encoder=encoder,
sqlalchemy_safe=sqlalchemy_safe,
)
if dataclasses.is_dataclass(obj):

View File

@@ -1,6 +1,7 @@
from typing import Any, Dict, Optional, Sequence, Type
from pydantic import BaseModel, create_model
from pydantic import BaseModel, ValidationError, create_model
from pydantic.error_wrappers import ErrorList
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401
@@ -25,25 +26,12 @@ class FastAPIError(RuntimeError):
"""
class ValidationException(Exception):
def __init__(self, errors: Sequence[Any]) -> None:
self._errors = errors
def errors(self) -> Sequence[Any]:
return self._errors
class RequestValidationError(ValidationException):
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
super().__init__(errors)
class RequestValidationError(ValidationError):
def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None:
self.body = body
super().__init__(errors, RequestErrorModel)
class WebSocketRequestValidationError(ValidationException):
pass
class ResponseValidationError(ValidationException):
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
super().__init__(errors)
self.body = body
class WebSocketRequestValidationError(ValidationError):
def __init__(self, errors: Sequence[ErrorList]) -> None:
super().__init__(errors, WebSocketErrorModel)

View File

@@ -1,3 +1,2 @@
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
REF_PREFIX = "#/components/schemas/"
REF_TEMPLATE = "#/components/schemas/{model}"

View File

@@ -1,21 +1,13 @@
from enum import Enum
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union
from fastapi._compat import (
PYDANTIC_V2,
CoreSchema,
GetJsonSchemaHandler,
JsonSchemaValue,
_model_rebuild,
general_plain_validator_function,
)
from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field
from typing_extensions import Annotated, Literal
from typing_extensions import deprecated as typing_deprecated
try:
import email_validator
import email_validator # type: ignore
assert email_validator # make autoflake ignore the unused import
from pydantic import EmailStr
@@ -34,39 +26,14 @@ except ImportError: # pragma: no cover
)
return str(v)
@classmethod
def _validate(cls, __input_value: Any, _: Any) -> str:
logger.warning(
"email-validator not installed, email fields will be treated as str.\n"
"To install, run: pip install email-validator"
)
return str(__input_value)
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
return {"type": "string", "format": "email"}
@classmethod
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
) -> CoreSchema:
return general_plain_validator_function(cls._validate)
class Contact(BaseModel):
name: Optional[str] = None
url: Optional[AnyUrl] = None
email: Optional[EmailStr] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class License(BaseModel):
@@ -74,13 +41,8 @@ class License(BaseModel):
identifier: Optional[str] = None
url: Optional[AnyUrl] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Info(BaseModel):
@@ -92,27 +54,17 @@ class Info(BaseModel):
license: Optional[License] = None
version: str
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class ServerVariable(BaseModel):
enum: Annotated[Optional[List[str]], Field(min_length=1)] = None
enum: Annotated[Optional[List[str]], Field(min_items=1)] = None
default: str
description: Optional[str] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Server(BaseModel):
@@ -120,13 +72,8 @@ class Server(BaseModel):
description: Optional[str] = None
variables: Optional[Dict[str, ServerVariable]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Reference(BaseModel):
@@ -145,26 +92,16 @@ class XML(BaseModel):
attribute: Optional[bool] = None
wrapped: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class ExternalDocumentation(BaseModel):
description: Optional[str] = None
url: AnyUrl
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Schema(BaseModel):
@@ -177,30 +114,27 @@ class Schema(BaseModel):
dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor")
ref: Optional[str] = Field(default=None, alias="$ref")
dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef")
defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs")
defs: Optional[Dict[str, "Schema"]] = Field(default=None, alias="$defs")
comment: Optional[str] = Field(default=None, alias="$comment")
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s
# A Vocabulary for Applying Subschemas
allOf: Optional[List["SchemaOrBool"]] = None
anyOf: Optional[List["SchemaOrBool"]] = None
oneOf: Optional[List["SchemaOrBool"]] = None
not_: Optional["SchemaOrBool"] = Field(default=None, alias="not")
if_: Optional["SchemaOrBool"] = Field(default=None, alias="if")
then: Optional["SchemaOrBool"] = None
else_: Optional["SchemaOrBool"] = Field(default=None, alias="else")
dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None
prefixItems: Optional[List["SchemaOrBool"]] = None
# TODO: uncomment and remove below when deprecating Pydantic v1
# It generales a list of schemas for tuples, before prefixItems was available
# items: Optional["SchemaOrBool"] = None
items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None
contains: Optional["SchemaOrBool"] = None
properties: Optional[Dict[str, "SchemaOrBool"]] = None
patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None
additionalProperties: Optional["SchemaOrBool"] = None
propertyNames: Optional["SchemaOrBool"] = None
unevaluatedItems: Optional["SchemaOrBool"] = None
unevaluatedProperties: Optional["SchemaOrBool"] = None
allOf: Optional[List["Schema"]] = None
anyOf: Optional[List["Schema"]] = None
oneOf: Optional[List["Schema"]] = None
not_: Optional["Schema"] = Field(default=None, alias="not")
if_: Optional["Schema"] = Field(default=None, alias="if")
then: Optional["Schema"] = None
else_: Optional["Schema"] = Field(default=None, alias="else")
dependentSchemas: Optional[Dict[str, "Schema"]] = None
prefixItems: Optional[List["Schema"]] = None
items: Optional[Union["Schema", List["Schema"]]] = None
contains: Optional["Schema"] = None
properties: Optional[Dict[str, "Schema"]] = None
patternProperties: Optional[Dict[str, "Schema"]] = None
additionalProperties: Optional["Schema"] = None
propertyNames: Optional["Schema"] = None
unevaluatedItems: Optional["Schema"] = None
unevaluatedProperties: Optional["Schema"] = None
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural
# A Vocabulary for Structural Validation
type: Optional[str] = None
@@ -230,7 +164,7 @@ class Schema(BaseModel):
# A Vocabulary for the Contents of String-Encoded Data
contentEncoding: Optional[str] = None
contentMediaType: Optional[str] = None
contentSchema: Optional["SchemaOrBool"] = None
contentSchema: Optional["Schema"] = None
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta
# A Vocabulary for Basic Meta-Data Annotations
title: Optional[str] = None
@@ -253,18 +187,8 @@ class Schema(BaseModel):
),
] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents
# A JSON Schema MUST be an object or a boolean.
SchemaOrBool = Union[Schema, bool]
class Config:
extra: str = "allow"
class Example(BaseModel):
@@ -273,13 +197,8 @@ class Example(BaseModel):
value: Optional[Any] = None
externalValue: Optional[AnyUrl] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class ParameterInType(Enum):
@@ -296,13 +215,8 @@ class Encoding(BaseModel):
explode: Optional[bool] = None
allowReserved: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class MediaType(BaseModel):
@@ -311,13 +225,8 @@ class MediaType(BaseModel):
examples: Optional[Dict[str, Union[Example, Reference]]] = None
encoding: Optional[Dict[str, Encoding]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class ParameterBase(BaseModel):
@@ -334,13 +243,8 @@ class ParameterBase(BaseModel):
# Serialization rules for more complex scenarios
content: Optional[Dict[str, MediaType]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Parameter(ParameterBase):
@@ -357,13 +261,8 @@ class RequestBody(BaseModel):
content: Dict[str, MediaType]
required: Optional[bool] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Link(BaseModel):
@@ -374,13 +273,8 @@ class Link(BaseModel):
description: Optional[str] = None
server: Optional[Server] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Response(BaseModel):
@@ -389,13 +283,8 @@ class Response(BaseModel):
content: Optional[Dict[str, MediaType]] = None
links: Optional[Dict[str, Union[Link, Reference]]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Operation(BaseModel):
@@ -413,13 +302,8 @@ class Operation(BaseModel):
security: Optional[List[Dict[str, List[str]]]] = None
servers: Optional[List[Server]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class PathItem(BaseModel):
@@ -437,13 +321,8 @@ class PathItem(BaseModel):
servers: Optional[List[Server]] = None
parameters: Optional[List[Union[Parameter, Reference]]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class SecuritySchemeType(Enum):
@@ -457,13 +336,8 @@ class SecurityBase(BaseModel):
type_: SecuritySchemeType = Field(alias="type")
description: Optional[str] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class APIKeyIn(Enum):
@@ -492,13 +366,8 @@ class OAuthFlow(BaseModel):
refreshUrl: Optional[str] = None
scopes: Dict[str, str] = {}
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class OAuthFlowImplicit(OAuthFlow):
@@ -524,13 +393,8 @@ class OAuthFlows(BaseModel):
clientCredentials: Optional[OAuthFlowClientCredentials] = None
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class OAuth2(SecurityBase):
@@ -561,13 +425,8 @@ class Components(BaseModel):
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class Tag(BaseModel):
@@ -575,13 +434,8 @@ class Tag(BaseModel):
description: Optional[str] = None
externalDocs: Optional[ExternalDocumentation] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
class OpenAPI(BaseModel):
@@ -597,15 +451,10 @@ class OpenAPI(BaseModel):
tags: Optional[List[Tag]] = None
externalDocs: Optional[ExternalDocumentation] = None
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Config:
extra = "allow"
_model_rebuild(Schema)
_model_rebuild(Operation)
_model_rebuild(Encoding)
Schema.update_forward_refs()
Operation.update_forward_refs()
Encoding.update_forward_refs()

View File

@@ -1,37 +1,35 @@
import http.client
import inspect
import warnings
from enum import Enum
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
from fastapi import routing
from fastapi._compat import (
GenerateJsonSchema,
JsonSchemaValue,
ModelField,
Undefined,
get_compat_model_name_map,
get_definitions,
get_schema_from_model_field,
lenient_issubclass,
)
from fastapi.datastructures import DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_flat_dependant, get_flat_params
from fastapi.encoders import jsonable_encoder
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
from fastapi.openapi.models import OpenAPI
from fastapi.params import Body, Param
from fastapi.responses import Response
from fastapi.types import ModelNameMap
from fastapi.utils import (
deep_dict_update,
generate_operation_id_for_path,
get_model_definitions,
is_body_allowed_for_status_code,
)
from pydantic import BaseModel
from pydantic.fields import ModelField, Undefined
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
get_model_name_map,
)
from pydantic.utils import lenient_issubclass
from starlette.responses import JSONResponse
from starlette.routing import BaseRoute
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
from typing_extensions import Literal
validation_error_definition = {
"title": "ValidationError",
@@ -90,11 +88,7 @@ def get_openapi_security_definitions(
def get_openapi_operation_parameters(
*,
all_route_params: Sequence[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
@@ -102,17 +96,13 @@ def get_openapi_operation_parameters(
field_info = cast(Param, field_info)
if not field_info.include_in_schema:
continue
param_schema = get_schema_from_model_field(
field=param,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
)
parameter = {
"name": param.alias,
"in": field_info.in_.value,
"required": param.required,
"schema": param_schema,
"schema": field_schema(
param, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)[0],
}
if field_info.description:
parameter["description"] = field_info.description
@@ -127,20 +117,13 @@ def get_openapi_operation_parameters(
def get_openapi_operation_request_body(
*,
body_field: Optional[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Optional[Dict[str, Any]]:
if not body_field:
return None
assert isinstance(body_field, ModelField)
body_schema = get_schema_from_model_field(
field=body_field,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
body_schema, _, _ = field_schema(
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
field_info = cast(Body, body_field.field_info)
request_media_type = field_info.media_type
@@ -203,14 +186,7 @@ def get_openapi_operation_metadata(
def get_openapi_path(
*,
route: routing.APIRoute,
operation_ids: Set[str],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
*, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
path = {}
security_schemes: Dict[str, Any] = {}
@@ -238,10 +214,7 @@ def get_openapi_path(
security_schemes.update(security_definitions)
all_route_params = get_flat_params(route.dependant)
operation_parameters = get_openapi_operation_parameters(
all_route_params=all_route_params,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
all_route_params=all_route_params, model_name_map=model_name_map
)
parameters.extend(operation_parameters)
if parameters:
@@ -259,10 +232,7 @@ def get_openapi_path(
operation["parameters"] = list(all_parameters.values())
if method in METHODS_WITH_BODY:
request_body_oai = get_openapi_operation_request_body(
body_field=route.body_field,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
body_field=route.body_field, model_name_map=model_name_map
)
if request_body_oai:
operation["requestBody"] = request_body_oai
@@ -276,10 +246,8 @@ def get_openapi_path(
cb_definitions,
) = get_openapi_path(
route=callback,
operation_ids=operation_ids,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
operation_ids=operation_ids,
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
@@ -305,11 +273,10 @@ def get_openapi_path(
response_schema = {"type": "string"}
if lenient_issubclass(current_response_class, JSONResponse):
if route.response_field:
response_schema = get_schema_from_model_field(
field=route.response_field,
schema_generator=schema_generator,
response_schema, _, _ = field_schema(
route.response_field,
model_name_map=model_name_map,
field_mapping=field_mapping,
ref_prefix=REF_PREFIX,
)
else:
response_schema = {}
@@ -338,11 +305,8 @@ def get_openapi_path(
field = route.response_fields.get(additional_status_code)
additional_field_schema: Optional[Dict[str, Any]] = None
if field:
additional_field_schema = get_schema_from_model_field(
field=field,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
additional_field_schema, _, _ = field_schema(
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
media_type = route_response_media_type or "application/json"
additional_schema = (
@@ -388,13 +352,13 @@ def get_openapi_path(
return path, security_schemes, definitions
def get_fields_from_routes(
def get_flat_models_from_routes(
routes: Sequence[BaseRoute],
) -> List[ModelField]:
) -> Set[Union[Type[BaseModel], Type[Enum]]]:
body_fields_from_routes: List[ModelField] = []
responses_from_routes: List[ModelField] = []
request_fields_from_routes: List[ModelField] = []
callback_flat_models: List[ModelField] = []
callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set()
for route in routes:
if getattr(route, "include_in_schema", None) and isinstance(
route, routing.APIRoute
@@ -409,12 +373,13 @@ def get_fields_from_routes(
if route.response_fields:
responses_from_routes.extend(route.response_fields.values())
if route.callbacks:
callback_flat_models.extend(get_fields_from_routes(route.callbacks))
callback_flat_models |= get_flat_models_from_routes(route.callbacks)
params = get_flat_params(route.dependant)
request_fields_from_routes.extend(params)
flat_models = callback_flat_models + list(
body_fields_from_routes + responses_from_routes + request_fields_from_routes
flat_models = callback_flat_models | get_flat_models_from_fields(
body_fields_from_routes + responses_from_routes + request_fields_from_routes,
known_models=set(),
)
return flat_models
@@ -452,22 +417,15 @@ def get_openapi(
paths: Dict[str, Dict[str, Any]] = {}
webhook_paths: Dict[str, Dict[str, Any]] = {}
operation_ids: Set[str] = set()
all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or []))
model_name_map = get_compat_model_name_map(all_fields)
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
field_mapping, definitions = get_definitions(
fields=all_fields,
schema_generator=schema_generator,
model_name_map=model_name_map,
flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or []))
model_name_map = get_model_name_map(flat_models)
definitions = get_model_definitions(
flat_models=flat_models, model_name_map=model_name_map
)
for route in routes or []:
if isinstance(route, routing.APIRoute):
result = get_openapi_path(
route=route,
operation_ids=operation_ids,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
route=route, model_name_map=model_name_map, operation_ids=operation_ids
)
if result:
path, security_schemes, path_definitions = result
@@ -483,10 +441,8 @@ def get_openapi(
if isinstance(webhook, routing.APIRoute):
result = get_openapi_path(
route=webhook,
operation_ids=operation_ids,
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
operation_ids=operation_ids,
)
if result:
path, security_schemes, path_definitions = result

View File

@@ -1,22 +1,14 @@
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
from typing import Any, Callable, List, Optional, Sequence
from fastapi import params
from fastapi._compat import Undefined
from pydantic.fields import Undefined
from typing_extensions import Annotated, deprecated
_Unset: Any = Undefined
def Path( # noqa: N802
default: Any = ...,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -25,19 +17,7 @@ def Path( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -45,19 +25,14 @@ def Path( # noqa: N802
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
] = Undefined,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Path(
default=default,
default_factory=default_factory,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -66,19 +41,11 @@ def Path( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -86,13 +53,7 @@ def Path( # noqa: N802
def Query( # noqa: N802
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -101,19 +62,7 @@ def Query( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -121,19 +70,14 @@ def Query( # noqa: N802
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
] = Undefined,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Query(
default=default,
default_factory=default_factory,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -142,19 +86,11 @@ def Query( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -162,13 +98,7 @@ def Query( # noqa: N802
def Header( # noqa: N802
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
convert_underscores: bool = True,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -178,19 +108,7 @@ def Header( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -198,19 +116,14 @@ def Header( # noqa: N802
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
] = Undefined,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Header(
default=default,
default_factory=default_factory,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
convert_underscores=convert_underscores,
title=title,
description=description,
@@ -220,19 +133,11 @@ def Header( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -240,13 +145,7 @@ def Header( # noqa: N802
def Cookie( # noqa: N802
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -255,19 +154,7 @@ def Cookie( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -275,19 +162,14 @@ def Cookie( # noqa: N802
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
] = Undefined,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Cookie(
default=default,
default_factory=default_factory,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -296,19 +178,11 @@ def Cookie( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -316,15 +190,9 @@ def Cookie( # noqa: N802
def Body( # noqa: N802
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
embed: bool = False,
media_type: str = "application/json",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -333,19 +201,7 @@ def Body( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -353,21 +209,14 @@ def Body( # noqa: N802
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
] = Undefined,
**extra: Any,
) -> Any:
return params.Body(
default=default,
default_factory=default_factory,
embed=embed,
media_type=media_type,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -376,19 +225,9 @@ def Body( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -396,14 +235,8 @@ def Body( # noqa: N802
def Form( # noqa: N802
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
media_type: str = "application/x-www-form-urlencoded",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -412,19 +245,7 @@ def Form( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -432,20 +253,13 @@ def Form( # noqa: N802
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
] = Undefined,
**extra: Any,
) -> Any:
return params.Form(
default=default,
default_factory=default_factory,
media_type=media_type,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -454,19 +268,9 @@ def Form( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -474,14 +278,8 @@ def Form( # noqa: N802
def File( # noqa: N802
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
media_type: str = "multipart/form-data",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -490,19 +288,7 @@ def File( # noqa: N802
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -510,20 +296,13 @@ def File( # noqa: N802
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
] = Undefined,
**extra: Any,
) -> Any:
return params.File(
default=default,
default_factory=default_factory,
media_type=media_type,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -532,19 +311,9 @@ def File( # noqa: N802
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)

View File

@@ -1,14 +1,10 @@
import warnings
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
from typing import Any, Callable, List, Optional, Sequence
from pydantic.fields import FieldInfo
from pydantic.fields import FieldInfo, Undefined
from typing_extensions import Annotated, deprecated
from ._compat import PYDANTIC_V2, Undefined
_Unset: Any = Undefined
class ParamTypes(Enum):
query = "query"
@@ -24,14 +20,7 @@ class Param(FieldInfo):
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -40,19 +29,7 @@ class Param(FieldInfo):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -60,24 +37,25 @@ class Param(FieldInfo):
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
] = Undefined,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
self.deprecated = deprecated
if example is not _Unset:
if example is not Undefined:
warnings.warn(
"`example` has been depreacated, please use `examples` instead",
category=DeprecationWarning,
stacklevel=4,
stacklevel=1,
)
self.example = example
self.include_in_schema = include_in_schema
kwargs = dict(
extra_kwargs = {**extra}
if examples:
extra_kwargs["examples"] = examples
super().__init__(
default=default,
default_factory=default_factory,
alias=alias,
title=title,
description=description,
@@ -87,40 +65,9 @@ class Param(FieldInfo):
le=le,
min_length=min_length,
max_length=max_length,
discriminator=discriminator,
multiple_of=multiple_of,
allow_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
**extra,
regex=regex,
**extra_kwargs,
)
if examples is not None:
kwargs["examples"] = examples
if regex is not None:
warnings.warn(
"`regex` has been depreacated, please use `pattern` instead",
category=DeprecationWarning,
stacklevel=4,
)
current_json_schema_extra = json_schema_extra or extra
if PYDANTIC_V2:
kwargs.update(
{
"annotation": annotation,
"alias_priority": alias_priority,
"validation_alias": validation_alias,
"serialization_alias": serialization_alias,
"strict": strict,
"json_schema_extra": current_json_schema_extra,
}
)
kwargs["pattern"] = pattern or regex
else:
kwargs["regex"] = pattern or regex
kwargs.update(**current_json_schema_extra)
use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
super().__init__(**use_kwargs)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
@@ -133,14 +80,7 @@ class Path(Param):
self,
default: Any = ...,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -149,19 +89,7 @@ class Path(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -169,22 +97,16 @@ class Path(Param):
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
] = Undefined,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
assert default is ..., "Path parameters cannot have a default value"
self.in_ = self.in_
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -193,19 +115,11 @@ class Path(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -217,14 +131,7 @@ class Query(Param):
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -233,19 +140,7 @@ class Query(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -253,20 +148,14 @@ class Query(Param):
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
] = Undefined,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -275,19 +164,11 @@ class Query(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -299,14 +180,7 @@ class Header(Param):
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
convert_underscores: bool = True,
title: Optional[str] = None,
description: Optional[str] = None,
@@ -316,19 +190,7 @@ class Header(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -336,21 +198,15 @@ class Header(Param):
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
] = Undefined,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
self.convert_underscores = convert_underscores
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -359,19 +215,11 @@ class Header(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -383,14 +231,7 @@ class Cookie(Param):
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -399,19 +240,7 @@ class Cookie(Param):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -419,20 +248,14 @@ class Cookie(Param):
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
] = Undefined,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -441,19 +264,11 @@ class Cookie(Param):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -463,16 +278,9 @@ class Body(FieldInfo):
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
embed: bool = False,
media_type: str = "application/json",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -481,19 +289,7 @@ class Body(FieldInfo):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -501,26 +297,23 @@ class Body(FieldInfo):
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
] = Undefined,
**extra: Any,
):
self.embed = embed
self.media_type = media_type
self.deprecated = deprecated
if example is not _Unset:
if example is not Undefined:
warnings.warn(
"`example` has been depreacated, please use `examples` instead",
category=DeprecationWarning,
stacklevel=4,
stacklevel=1,
)
self.example = example
self.include_in_schema = include_in_schema
kwargs = dict(
extra_kwargs = {**extra}
if examples is not None:
extra_kwargs["examples"] = examples
super().__init__(
default=default,
default_factory=default_factory,
alias=alias,
title=title,
description=description,
@@ -530,41 +323,9 @@ class Body(FieldInfo):
le=le,
min_length=min_length,
max_length=max_length,
discriminator=discriminator,
multiple_of=multiple_of,
allow_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
**extra,
regex=regex,
**extra_kwargs,
)
if examples is not None:
kwargs["examples"] = examples
if regex is not None:
warnings.warn(
"`regex` has been depreacated, please use `pattern` instead",
category=DeprecationWarning,
stacklevel=4,
)
current_json_schema_extra = json_schema_extra or extra
if PYDANTIC_V2:
kwargs.update(
{
"annotation": annotation,
"alias_priority": alias_priority,
"validation_alias": validation_alias,
"serialization_alias": serialization_alias,
"strict": strict,
"json_schema_extra": current_json_schema_extra,
}
)
kwargs["pattern"] = pattern or regex
else:
kwargs["regex"] = pattern or regex
kwargs.update(**current_json_schema_extra)
use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
super().__init__(**use_kwargs)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
@@ -575,15 +336,8 @@ class Form(Body):
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
media_type: str = "application/x-www-form-urlencoded",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -592,19 +346,7 @@ class Form(Body):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -612,22 +354,14 @@ class Form(Body):
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
] = Undefined,
**extra: Any,
):
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
embed=True,
media_type=media_type,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -636,19 +370,9 @@ class Form(Body):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)
@@ -658,15 +382,8 @@ class File(Form):
self,
default: Any = Undefined,
*,
default_factory: Union[Callable[[], Any], None] = _Unset,
annotation: Optional[Any] = None,
media_type: str = "multipart/form-data",
alias: Optional[str] = None,
alias_priority: Union[int, None] = _Unset,
# TODO: update when deprecating Pydantic v1, import these types
# validation_alias: str | AliasPath | AliasChoices | None
validation_alias: Union[str, None] = None,
serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
@@ -675,19 +392,7 @@ class File(Form):
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[str] = None,
regex: Annotated[
Optional[str],
deprecated(
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
),
] = None,
discriminator: Union[str, None] = None,
strict: Union[bool, None] = _Unset,
multiple_of: Union[float, None] = _Unset,
allow_inf_nan: Union[bool, None] = _Unset,
max_digits: Union[int, None] = _Unset,
decimal_places: Union[int, None] = _Unset,
regex: Optional[str] = None,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
@@ -695,21 +400,13 @@ class File(Form):
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None,
] = Undefined,
**extra: Any,
):
super().__init__(
default=default,
default_factory=default_factory,
annotation=annotation,
media_type=media_type,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
@@ -718,19 +415,9 @@ class File(Form):
le=le,
min_length=min_length,
max_length=max_length,
pattern=pattern,
regex=regex,
discriminator=discriminator,
strict=strict,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra,
**extra,
)

View File

@@ -20,14 +20,6 @@ from typing import (
)
from fastapi import params
from fastapi._compat import (
ModelField,
Undefined,
_get_model_config,
_model_dump,
_normalize_errors,
lenient_issubclass,
)
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import (
@@ -37,14 +29,13 @@ from fastapi.dependencies.utils import (
get_typed_return_annotation,
solve_dependencies,
)
from fastapi.encoders import jsonable_encoder
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.exceptions import (
FastAPIError,
RequestValidationError,
ResponseValidationError,
WebSocketRequestValidationError,
)
from fastapi.types import DecoratedCallable, IncEx
from fastapi.types import DecoratedCallable
from fastapi.utils import (
create_cloned_field,
create_response_field,
@@ -53,6 +44,9 @@ from fastapi.utils import (
is_body_allowed_for_status_code,
)
from pydantic import BaseModel
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import ModelField, Undefined
from pydantic.utils import lenient_issubclass
from starlette import routing
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
@@ -79,15 +73,14 @@ def _prepare_response_content(
exclude_none: bool = False,
) -> Any:
if isinstance(res, BaseModel):
read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None)
read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None)
if read_with_orm_mode:
# Let from_orm extract the data from this model instead of converting
# it now to a dict.
# Otherwise there's no way to extract lazy data that requires attribute
# access instead of dict iteration, e.g. lazy relationships.
return res
return _model_dump(
res,
return res.dict(
by_alias=True,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
@@ -122,8 +115,8 @@ async def serialize_response(
*,
field: Optional[ModelField] = None,
response_content: Any,
include: Optional[IncEx] = None,
exclude: Optional[IncEx] = None,
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
@@ -132,40 +125,24 @@ async def serialize_response(
) -> Any:
if field:
errors = []
if not hasattr(field, "serialize"):
# pydantic v1
response_content = _prepare_response_content(
response_content,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
response_content = _prepare_response_content(
response_content,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
if is_coroutine:
value, errors_ = field.validate(response_content, {}, loc=("response",))
else:
value, errors_ = await run_in_threadpool(
field.validate, response_content, {}, loc=("response",)
)
if isinstance(errors_, list):
errors.extend(errors_)
elif errors_:
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
errors.extend(errors_)
if errors:
raise ResponseValidationError(
errors=_normalize_errors(errors), body=response_content
)
if hasattr(field, "serialize"):
return field.serialize(
value,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
raise ValidationError(errors, field.type_)
return jsonable_encoder(
value,
include=include,
@@ -198,8 +175,8 @@ def get_request_handler(
status_code: Optional[int] = None,
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
response_field: Optional[ModelField] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -243,16 +220,7 @@ def get_request_handler(
body = body_bytes
except json.JSONDecodeError as e:
raise RequestValidationError(
[
{
"type": "json_invalid",
"loc": ("body", e.pos),
"msg": "JSON decode error",
"input": {},
"ctx": {"error": e.msg},
}
],
body=e.doc,
[ErrorWrapper(e, ("body", e.pos))], body=e.doc
) from e
except HTTPException:
raise
@@ -268,7 +236,7 @@ def get_request_handler(
)
values, errors, background_tasks, sub_response, _ = solved_result
if errors:
raise RequestValidationError(_normalize_errors(errors), body=body)
raise RequestValidationError(errors, body=body)
else:
raw_response = await run_endpoint_function(
dependant=dependant, values=values, is_coroutine=is_coroutine
@@ -319,7 +287,7 @@ def get_websocket_app(
)
values, errors, _, _2, _3 = solved_result
if errors:
raise WebSocketRequestValidationError(_normalize_errors(errors))
raise WebSocketRequestValidationError(errors)
assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**values)
@@ -380,8 +348,8 @@ class APIRoute(routing.Route):
name: Optional[str] = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -446,11 +414,7 @@ class APIRoute(routing.Route):
), f"Status code {status_code} must not have a response body"
response_name = "Response_" + self.unique_id
self.response_field = create_response_field(
name=response_name,
type_=self.response_model,
# TODO: This should actually set mode='serialization', just, that changes the schemas
# mode="serialization",
mode="validation",
name=response_name, type_=self.response_model
)
# Create a clone of the field, so that a Pydantic submodel is not returned
# as is just because it's an instance of a subclass of a more limited class
@@ -459,7 +423,6 @@ class APIRoute(routing.Route):
# would pass the validation and be returned as is.
# By being a new field, no inheritance will be passed as is. A new model
# will be always created.
# TODO: remove when deprecating Pydantic v1
self.secure_cloned_response_field: Optional[
ModelField
] = create_cloned_field(self.response_field)
@@ -606,8 +569,8 @@ class APIRouter(routing.Router):
deprecated: Optional[bool] = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -687,8 +650,8 @@ class APIRouter(routing.Router):
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -914,8 +877,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -970,8 +933,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1026,8 +989,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1082,8 +1045,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1138,8 +1101,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1194,8 +1157,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1250,8 +1213,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
@@ -1306,8 +1269,8 @@ class APIRouter(routing.Router):
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,

View File

@@ -9,9 +9,6 @@ from fastapi.security.utils import get_authorization_scheme_param
from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
# TODO: import from typing when deprecating Python 3.9
from typing_extensions import Annotated
class OAuth2PasswordRequestForm:
"""
@@ -48,13 +45,12 @@ class OAuth2PasswordRequestForm:
def __init__(
self,
*,
grant_type: Annotated[Union[str, None], Form(pattern="password")] = None,
username: Annotated[str, Form()],
password: Annotated[str, Form()],
scope: Annotated[str, Form()] = "",
client_id: Annotated[Union[str, None], Form()] = None,
client_secret: Annotated[Union[str, None], Form()] = None,
grant_type: str = Form(default=None, regex="password"),
username: str = Form(),
password: str = Form(),
scope: str = Form(default=""),
client_id: Optional[str] = Form(default=None),
client_secret: Optional[str] = Form(default=None),
):
self.grant_type = grant_type
self.username = username
@@ -99,12 +95,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
def __init__(
self,
grant_type: Annotated[str, Form(pattern="password")],
username: Annotated[str, Form()],
password: Annotated[str, Form()],
scope: Annotated[str, Form()] = "",
client_id: Annotated[Union[str, None], Form()] = None,
client_secret: Annotated[Union[str, None], Form()] = None,
grant_type: str = Form(regex="password"),
username: str = Form(),
password: str = Form(),
scope: str = Form(default=""),
client_id: Optional[str] = Form(default=None),
client_secret: Optional[str] = Form(default=None),
):
super().__init__(
grant_type=grant_type,

View File

@@ -1,11 +1,3 @@
import types
from enum import Enum
from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
from pydantic import BaseModel
from typing import Any, Callable, TypeVar
DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
UnionType = getattr(types, "UnionType", Union)
NoneType = getattr(types, "UnionType", None)
ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]

View File

@@ -1,6 +1,7 @@
import re
import warnings
from dataclasses import is_dataclass
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
@@ -15,20 +16,13 @@ from typing import (
from weakref import WeakKeyDictionary
import fastapi
from fastapi._compat import (
PYDANTIC_V2,
BaseConfig,
ModelField,
PydanticSchemaGenerationError,
Undefined,
UndefinedType,
Validator,
lenient_issubclass,
)
from fastapi.datastructures import DefaultPlaceholder, DefaultType
from pydantic import BaseModel, create_model
from pydantic.fields import FieldInfo
from typing_extensions import Literal
from fastapi.openapi.constants import REF_PREFIX
from pydantic import BaseConfig, BaseModel, create_model
from pydantic.class_validators import Validator
from pydantic.fields import FieldInfo, ModelField, UndefinedType
from pydantic.schema import model_process_schema
from pydantic.utils import lenient_issubclass
if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute
@@ -56,6 +50,24 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
return not (current_status_code < 200 or current_status_code in {204, 304})
def get_model_definitions(
*,
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Dict[str, Any]:
definitions: Dict[str, Dict[str, Any]] = {}
for model in flat_models:
m_schema, m_definitions, m_nested_models = model_process_schema(
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
definitions.update(m_definitions)
model_name = model_name_map[model]
if "description" in m_schema:
m_schema["description"] = m_schema["description"].split("\f")[0]
definitions[model_name] = m_schema
return definitions
def get_path_param_names(path: str) -> Set[str]:
return set(re.findall("{(.*?)}", path))
@@ -64,40 +76,30 @@ def create_response_field(
name: str,
type_: Type[Any],
class_validators: Optional[Dict[str, Validator]] = None,
default: Optional[Any] = Undefined,
required: Union[bool, UndefinedType] = Undefined,
default: Optional[Any] = None,
required: Union[bool, UndefinedType] = True,
model_config: Type[BaseConfig] = BaseConfig,
field_info: Optional[FieldInfo] = None,
alias: Optional[str] = None,
mode: Literal["validation", "serialization"] = "validation",
) -> ModelField:
"""
Create a new response field. Raises if type_ is invalid.
"""
class_validators = class_validators or {}
if PYDANTIC_V2:
field_info = field_info or FieldInfo(
annotation=type_, default=default, alias=alias
)
else:
field_info = field_info or FieldInfo()
kwargs = {"name": name, "field_info": field_info}
if PYDANTIC_V2:
kwargs.update({"mode": mode})
else:
kwargs.update(
{
"type_": type_,
"class_validators": class_validators,
"default": default,
"required": required,
"model_config": model_config,
"alias": alias,
}
)
field_info = field_info or FieldInfo()
try:
return ModelField(**kwargs) # type: ignore[arg-type]
except (RuntimeError, PydanticSchemaGenerationError):
return ModelField(
name=name,
type_=type_,
class_validators=class_validators,
default=default,
required=required,
model_config=model_config,
alias=alias,
field_info=field_info,
)
except RuntimeError:
raise fastapi.exceptions.FastAPIError(
"Invalid args for response field! Hint: "
f"check that {type_} is a valid Pydantic field type. "
@@ -114,8 +116,6 @@ def create_cloned_field(
*,
cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,
) -> ModelField:
if PYDANTIC_V2:
return field
# cloned_types caches already cloned types to support recursive models and improve
# performance by avoiding unecessary cloning
if cloned_types is None:
@@ -136,30 +136,30 @@ def create_cloned_field(
f, cloned_types=cloned_types
)
new_field = create_response_field(name=field.name, type_=use_type)
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
new_field.alias = field.alias # type: ignore[misc]
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
new_field.default = field.default # type: ignore[misc]
new_field.required = field.required # type: ignore[misc]
new_field.model_config = field.model_config # type: ignore[attr-defined]
new_field.has_alias = field.has_alias
new_field.alias = field.alias
new_field.class_validators = field.class_validators
new_field.default = field.default
new_field.required = field.required
new_field.model_config = field.model_config
new_field.field_info = field.field_info
new_field.allow_none = field.allow_none # type: ignore[attr-defined]
new_field.validate_always = field.validate_always # type: ignore[attr-defined]
if field.sub_fields: # type: ignore[attr-defined]
new_field.sub_fields = [ # type: ignore[attr-defined]
new_field.allow_none = field.allow_none
new_field.validate_always = field.validate_always
if field.sub_fields:
new_field.sub_fields = [
create_cloned_field(sub_field, cloned_types=cloned_types)
for sub_field in field.sub_fields # type: ignore[attr-defined]
for sub_field in field.sub_fields
]
if field.key_field: # type: ignore[attr-defined]
new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
field.key_field, cloned_types=cloned_types # type: ignore[attr-defined]
if field.key_field:
new_field.key_field = create_cloned_field(
field.key_field, cloned_types=cloned_types
)
new_field.validators = field.validators # type: ignore[attr-defined]
new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
new_field.post_validators = field.post_validators # type: ignore[attr-defined]
new_field.parse_json = field.parse_json # type: ignore[attr-defined]
new_field.shape = field.shape # type: ignore[attr-defined]
new_field.populate_validators() # type: ignore[attr-defined]
new_field.validators = field.validators
new_field.pre_validators = field.pre_validators
new_field.post_validators = field.post_validators
new_field.parse_json = field.parse_json
new_field.shape = field.shape
new_field.populate_validators()
return new_field
@@ -220,9 +220,3 @@ def get_value_or_default(
if not isinstance(item, DefaultPlaceholder):
return item
return first_item
def match_pydantic_error_url(error_type: str) -> Any:
from dirty_equals import IsStr
return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}")

View File

@@ -42,8 +42,8 @@ classifiers = [
]
dependencies = [
"starlette>=0.27.0,<0.28.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,<3.0.0",
"typing-extensions>=4.5.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0",
"typing-extensions>=4.5.0"
]
dynamic = ["version"]
@@ -61,10 +61,8 @@ all = [
"pyyaml >=5.3.1",
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
"orjson >=3.2.1",
"email_validator >=2.0.0",
"email_validator >=1.1.1",
"uvicorn[standard] >=0.12.0",
"pydantic-settings >=2.0.0",
"pydantic-extra-types >=2.0.0",
]
[tool.hatch.version]
@@ -87,7 +85,6 @@ check_untyped_defs = true
addopts = [
"--strict-config",
"--strict-markers",
"--ignore=docs_src",
]
xfail_strict = true
junit_family = "xunit2"
@@ -145,7 +142,6 @@ ignore = [
"docs_src/custom_response/tutorial007.py" = ["B007"]
"docs_src/dataclasses/tutorial003.py" = ["I001"]
"docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"]
"docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"]
"docs_src/custom_request_and_route/tutorial002.py" = ["B904"]
"docs_src/dependencies/tutorial008_an.py" = ["F821"]
"docs_src/dependencies/tutorial008_an_py39.py" = ["F821"]

View File

@@ -1,13 +1,11 @@
-e .
pydantic-settings >=2.0.0
pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0
mypy ==1.4.0
ruff ==0.0.275
black == 23.3.0
httpx >=0.23.0,<0.25.0
email_validator >=1.1.1,<3.0.0
dirty-equals ==0.6.0
email_validator >=1.1.1,<2.0.0
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel
sqlalchemy >=1.3.18,<1.4.43

View File

@@ -1,133 +0,0 @@
from typing import Union
from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
class FooBaseModel(BaseModel):
if PYDANTIC_V2:
model_config = ConfigDict(extra="forbid")
else:
class Config:
extra = "forbid"
class Foo(FooBaseModel):
pass
app = FastAPI()
@app.post("/")
async def post(
foo: Union[Foo, None] = None,
):
return foo
client = TestClient(app)
def test_call_invalid():
response = client.post("/", json={"foo": {"bar": "baz"}})
assert response.status_code == 422
def test_call_valid():
response = client.post("/", json={})
assert response.status_code == 200
assert response.json() == {}
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": {
"/": {
"post": {
"summary": "Post",
"operationId": "post__post",
"requestBody": {
"content": {
"application/json": {
"schema": IsDict(
{
"anyOf": [
{"$ref": "#/components/schemas/Foo"},
{"type": "null"},
],
"title": "Foo",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"$ref": "#/components/schemas/Foo"}
)
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"Foo": {
"properties": {},
"additionalProperties": False,
"type": "object",
"title": "Foo",
},
"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",
},
}
},
}

View File

@@ -1,4 +1,3 @@
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, HttpUrl
@@ -43,24 +42,13 @@ def test_openapi_schema():
"parameters": [
{
"required": True,
"schema": IsDict(
{
"title": "Callback Url",
"minLength": 1,
"type": "string",
"format": "uri",
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
}
),
"schema": {
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
},
"name": "callback_url",
"in": "query",
}

View File

@@ -1,8 +1,6 @@
import pytest
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI, Query
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from typing_extensions import Annotated
app = FastAPI()
@@ -32,46 +30,21 @@ client = TestClient(app)
foo_is_missing = {
"detail": [
IsDict(
{
"loc": ["query", "foo"],
"msg": "Field required",
"type": "missing",
"input": None,
"url": match_pydantic_error_url("missing"),
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"loc": ["query", "foo"],
"msg": "field required",
"type": "value_error.missing",
}
)
{
"loc": ["query", "foo"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
foo_is_short = {
"detail": [
IsDict(
{
"ctx": {"min_length": 1},
"loc": ["query", "foo"],
"msg": "String should have at least 1 characters",
"type": "string_too_short",
"input": "",
"url": match_pydantic_error_url("string_too_short"),
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict(
{
"ctx": {"limit_value": 1},
"loc": ["query", "foo"],
"msg": "ensure this value has at least 1 characters",
"type": "value_error.any_str.min_length",
}
)
{
"ctx": {"limit_value": 1},
"loc": ["query", "foo"],
"msg": "ensure this value has at least 1 characters",
"type": "value_error.any_str.min_length",
}
]
}

View File

@@ -1,5 +1,4 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
from .main import app
@@ -267,17 +266,10 @@ def test_openapi_schema():
"operationId": "get_path_param_id_path_param__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
"required": True,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Item Id",
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict({"title": "Item Id", "type": "string"}),
}
],
}
@@ -977,17 +969,10 @@ def test_openapi_schema():
"operationId": "get_query_type_optional_query_int_optional_get",
"parameters": [
{
"required": False,
"schema": {"title": "Query", "type": "integer"},
"name": "query",
"in": "query",
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "integer"}, {"type": "null"}],
"title": "Query",
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict({"title": "Query", "type": "integer"}),
}
],
}

View File

@@ -1,93 +0,0 @@
from typing import List, Union
from fastapi import FastAPI, UploadFile
from fastapi._compat import (
ModelField,
Undefined,
_get_model_config,
is_bytes_sequence_annotation,
is_uploadfile_sequence_annotation,
)
from fastapi.testclient import TestClient
from pydantic import BaseConfig, BaseModel, ConfigDict
from pydantic.fields import FieldInfo
from .utils import needs_pydanticv1, needs_pydanticv2
@needs_pydanticv2
def test_model_field_default_required():
# For coverage
field_info = FieldInfo(annotation=str)
field = ModelField(name="foo", field_info=field_info)
assert field.default is Undefined
@needs_pydanticv1
def test_upload_file_dummy_general_plain_validator_function():
# For coverage
assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {}
@needs_pydanticv1
def test_union_scalar_list():
# For coverage
# TODO: there might not be a current valid code path that uses this, it would
# potentially enable query parameters defined as both a scalar and a list
# but that would require more refactors, also not sure it's really useful
from fastapi._compat import is_pv1_scalar_field
field_info = FieldInfo()
field = ModelField(
name="foo",
field_info=field_info,
type_=Union[str, List[int]],
class_validators={},
model_config=BaseConfig,
)
assert not is_pv1_scalar_field(field)
@needs_pydanticv2
def test_get_model_config():
# For coverage in Pydantic v2
class Foo(BaseModel):
model_config = ConfigDict(from_attributes=True)
foo = Foo()
config = _get_model_config(foo)
assert config == {"from_attributes": True}
def test_complex():
app = FastAPI()
@app.post("/")
def foo(foo: Union[str, List[int]]):
return foo
client = TestClient(app)
response = client.post("/", json="bar")
assert response.status_code == 200, response.text
assert response.json() == "bar"
response2 = client.post("/", json=[1, 2])
assert response2.status_code == 200, response2.text
assert response2.json() == [1, 2]
def test_is_bytes_sequence_annotation_union():
# For coverage
# TODO: in theory this would allow declaring types that could be lists of bytes
# to be read from files and other types, but I'm not even sure it's a good idea
# to support it as a first class "feature"
assert is_bytes_sequence_annotation(Union[List[str], List[bytes]])
def test_is_uploadfile_sequence_annotation():
# For coverage
# TODO: in theory this would allow declaring types that could be lists of UploadFile
# and other types, but I'm not even sure it's a good idea to support it as a first
# class "feature"
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])

View File

@@ -1,5 +1,4 @@
from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel
@@ -9,18 +8,10 @@ app = FastAPI()
class Item(BaseModel):
name: str
if PYDANTIC_V2:
model_config = {
"json_schema_extra": {
"x-something-internal": {"level": 4},
}
class Config:
schema_extra = {
"x-something-internal": {"level": 4},
}
else:
class Config:
schema_extra = {
"x-something-internal": {"level": 4},
}
@app.get("/foo", response_model=Item)

View File

@@ -7,17 +7,11 @@ from fastapi.datastructures import Default
from fastapi.testclient import TestClient
# TODO: remove when deprecating Pydantic v1
def test_upload_file_invalid():
with pytest.raises(ValueError):
UploadFile.validate("not a Starlette UploadFile")
def test_upload_file_invalid_pydantic_v2():
with pytest.raises(ValueError):
UploadFile._validate("not a Starlette UploadFile", {})
def test_default_placeholder_equals():
placeholder_1 = Default("a")
placeholder_2 = Default("a")

View File

@@ -4,54 +4,31 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
from .utils import needs_pydanticv1, needs_pydanticv2
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
@needs_pydanticv2
def test_pydanticv2():
from pydantic import field_serializer
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
@field_serializer("dt_field")
def serialize_datetime(self, dt_field: datetime):
return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
client = TestClient(app)
client = TestClient(app)
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_pydanticv1():
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
client = TestClient(app)
def test_dt():
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}

View File

@@ -1,9 +1,7 @@
from typing import List
from dirty_equals import IsDict
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
@@ -49,30 +47,15 @@ async def no_duplicates_sub(
def test_no_duplicates_invalid():
response = client.post("/no-duplicates", json={"item": {"data": "myitem"}})
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "item2"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "item2"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
assert response.json() == {
"detail": [
{
"loc": ["body", "item2"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
def test_no_duplicates():

View File

@@ -1,10 +1,8 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import APIRouter, Depends, FastAPI
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
app = FastAPI()
@@ -52,180 +50,99 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend
return msg
def test_main_depends():
response = client.get("/main-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_main_depends_q_foo():
response = client.get("/main-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {
"in": "main-depends",
"params": {"q": "foo", "skip": 0, "limit": 100},
}
def test_main_depends_q_foo_skip_100_limit_200():
response = client.get("/main-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {
"in": "main-depends",
"params": {"q": "foo", "skip": 100, "limit": 200},
}
def test_decorator_depends():
response = client.get("/decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_decorator_depends_q_foo():
response = client.get("/decorator-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {"in": "decorator-depends"}
def test_decorator_depends_q_foo_skip_100_limit_200():
response = client.get("/decorator-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {"in": "decorator-depends"}
def test_router_depends():
response = client.get("/router-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_router_depends_q_foo():
response = client.get("/router-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {
"in": "router-depends",
"params": {"q": "foo", "skip": 0, "limit": 100},
}
def test_router_depends_q_foo_skip_100_limit_200():
response = client.get("/router-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {
"in": "router-depends",
"params": {"q": "foo", "skip": 100, "limit": 200},
}
def test_router_decorator_depends():
response = client.get("/router-decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_router_decorator_depends_q_foo():
response = client.get("/router-decorator-depends/?q=foo")
assert response.status_code == 200
assert response.json() == {"in": "router-decorator-depends"}
def test_router_decorator_depends_q_foo_skip_100_limit_200():
response = client.get("/router-decorator-depends/?q=foo&skip=100&limit=200")
assert response.status_code == 200
assert response.json() == {"in": "router-decorator-depends"}
@pytest.mark.parametrize(
"url,status_code,expected",
[
(
"/main-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/main-depends/?q=foo",
200,
{"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
),
(
"/main-depends/?q=foo&skip=100&limit=200",
200,
{"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
),
(
"/decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}),
(
"/decorator-depends/?q=foo&skip=100&limit=200",
200,
{"in": "decorator-depends"},
),
(
"/router-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-depends/?q=foo",
200,
{"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
),
(
"/router-depends/?q=foo&skip=100&limit=200",
200,
{"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
),
(
"/router-decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "q"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}),
(
"/router-decorator-depends/?q=foo&skip=100&limit=200",
200,
{"in": "router-decorator-depends"},
),
],
)
def test_normal_app(url, status_code, expected):
response = client.get(url)
assert response.status_code == status_code
assert response.json() == expected
@pytest.mark.parametrize(
@@ -273,281 +190,126 @@ def test_override_simple(url, status_code, expected):
app.dependency_overrides = {}
def test_override_with_sub_main_depends():
@pytest.mark.parametrize(
"url,status_code,expected",
[
(
"/main-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/main-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}),
(
"/decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/decorator-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}),
(
"/router-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-depends/?k=bar",
200,
{"in": "router-depends", "params": {"k": "bar"}},
),
(
"/router-decorator-depends/",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
(
"/router-decorator-depends/?q=foo",
422,
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
),
("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}),
],
)
def test_override_with_sub(url, status_code, expected):
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/main-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub__main_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/main-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_main_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/main-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "main-depends", "params": {"k": "bar"}}
app.dependency_overrides = {}
def test_override_with_sub_decorator_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_decorator_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/decorator-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_decorator_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/decorator-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "decorator-depends"}
app.dependency_overrides = {}
def test_override_with_sub_router_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "router-depends", "params": {"k": "bar"}}
app.dependency_overrides = {}
def test_override_with_sub_router_decorator_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-decorator-depends/")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_decorator_depends_q_foo():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-decorator-depends/?q=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "k"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "k"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
app.dependency_overrides = {}
def test_override_with_sub_router_decorator_depends_k_bar():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
response = client.get("/router-decorator-depends/?k=bar")
assert response.status_code == 200
assert response.json() == {"in": "router-decorator-depends"}
response = client.get(url)
assert response.status_code == status_code
assert response.json() == expected
app.dependency_overrides = {}

View File

@@ -1,6 +1,5 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
@@ -328,14 +327,7 @@ def test_openapi_schema():
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"price": IsDict(
{
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
# TODO: remove when deprecating Pydantic v1
| IsDict({"title": "Price", "type": "number"}),
"price": {"title": "Price", "type": "number"},
},
},
"ValidationError": {

View File

@@ -1,20 +1,46 @@
from typing import Optional
import pytest
from fastapi.exceptions import ResponseValidationError
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, ValidationError, validator
from ..utils import needs_pydanticv1
app = FastAPI()
@pytest.fixture(name="client")
def get_client():
from .app_pv1 import app
client = TestClient(app)
return client
class ModelB(BaseModel):
username: str
@needs_pydanticv1
def test_filter_sub_model(client: TestClient):
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
model_b: ModelB
@validator("name")
def lower_username(cls, name: str, values):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "model_b": model_c}
client = TestClient(app)
def test_filter_sub_model():
response = client.get("/model/modelA")
assert response.status_code == 200, response.text
assert response.json() == {
@@ -24,9 +50,8 @@ def test_filter_sub_model(client: TestClient):
}
@needs_pydanticv1
def test_validator_is_cloned(client: TestClient):
with pytest.raises(ResponseValidationError) as err:
def test_validator_is_cloned():
with pytest.raises(ValidationError) as err:
client.get("/model/modelX")
assert err.value.errors() == [
{
@@ -37,8 +62,7 @@ def test_validator_is_cloned(client: TestClient):
]
@needs_pydanticv1
def test_openapi_schema(client: TestClient):
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {

View File

@@ -1,35 +0,0 @@
from typing import Optional
from fastapi import Depends, FastAPI
from pydantic import BaseModel, validator
app = FastAPI()
class ModelB(BaseModel):
username: str
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
model_b: ModelB
@validator("name")
def lower_username(cls, name: str, values):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "model_b": model_c}

View File

@@ -1,182 +0,0 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import Depends, FastAPI
from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from .utils import needs_pydanticv2
@pytest.fixture(name="client")
def get_client():
from pydantic import BaseModel, FieldValidationInfo, field_validator
app = FastAPI()
class ModelB(BaseModel):
username: str
class ModelC(ModelB):
password: str
class ModelA(BaseModel):
name: str
description: Optional[str] = None
foo: ModelB
@field_validator("name")
def lower_username(cls, name: str, info: FieldValidationInfo):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password")
@app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "foo": model_c}
client = TestClient(app)
return client
@needs_pydanticv2
def test_filter_sub_model(client: TestClient):
response = client.get("/model/modelA")
assert response.status_code == 200, response.text
assert response.json() == {
"name": "modelA",
"description": "model-a-desc",
"foo": {"username": "test-user"},
}
@needs_pydanticv2
def test_validator_is_cloned(client: TestClient):
with pytest.raises(ResponseValidationError) as err:
client.get("/model/modelX")
assert err.value.errors() == [
IsDict(
{
"type": "value_error",
"loc": ("response", "name"),
"msg": "Value error, name must end in A",
"input": "modelX",
"ctx": {"error": "name must end in A"},
"url": match_pydantic_error_url("value_error"),
}
)
| IsDict(
# TODO remove when deprecating Pydantic v1
{
"loc": ("response", "name"),
"msg": "name must end in A",
"type": "value_error",
}
)
]
@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": {
"/model/{name}": {
"get": {
"summary": "Get Model A",
"operationId": "get_model_a_model__name__get",
"parameters": [
{
"required": True,
"schema": {"title": "Name", "type": "string"},
"name": "name",
"in": "path",
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ModelA"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"ModelA": {
"title": "ModelA",
"required": ["name", "foo"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
|
# TODO remove when deprecating Pydantic v1
IsDict({"title": "Description", "type": "string"}),
"foo": {"$ref": "#/components/schemas/ModelB"},
},
},
"ModelB": {
"title": "ModelB",
"required": ["username"],
"type": "object",
"properties": {"username": {"title": "Username", "type": "string"}},
},
"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"},
},
},
}
},
}

View File

@@ -1,6 +1,5 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
@@ -105,253 +104,35 @@ def test_get_users_item():
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
def test_openapi_schema():
def test_schema_1():
"""Check that the user_id is a required path parameter under /users"""
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": {
"/users/": {
"get": {
"summary": "Get Users",
"operationId": "get_users_users__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
}
},
"/users/{user_id}": {
"get": {
"summary": "Get User",
"operationId": "get_user_users__user_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "path",
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/items/": {
"get": {
"summary": "Get Items",
"operationId": "get_items_items__get",
"parameters": [
{
"required": False,
"name": "user_id",
"in": "query",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/items/{item_id}": {
"get": {
"summary": "Get Item",
"operationId": "get_item_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
},
{
"required": False,
"name": "user_id",
"in": "query",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/users/{user_id}/items/": {
"get": {
"summary": "Get Items",
"operationId": "get_items_users__user_id__items__get",
"parameters": [
{
"required": True,
"name": "user_id",
"in": "path",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/users/{user_id}/items/{item_id}": {
"get": {
"summary": "Get Item",
"operationId": "get_item_users__user_id__items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
},
{
"required": True,
"name": "user_id",
"in": "path",
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "User Id",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "User Id", "type": "string"}
),
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"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"},
},
},
}
},
r = response.json()
d = {
"required": True,
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "path",
}
assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
def test_schema_2():
"""Check that the user_id is an optional query parameter under /items"""
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
r = response.json()
d = {
"required": False,
"schema": {"title": "User Id", "type": "string"},
"name": "user_id",
"in": "query",
}
assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
assert d in r["paths"]["/items/"]["get"]["parameters"]

View File

@@ -5,7 +5,7 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
from .utils import needs_pydanticv1, needs_pydanticv2
app = FastAPI()
class MyUuid:
@@ -26,78 +26,40 @@ class MyUuid:
raise TypeError("vars() argument must have __dict__ attribute")
@needs_pydanticv2
def test_pydanticv2():
from pydantic import field_serializer
@app.get("/fast_uuid")
def return_fast_uuid():
# I don't want to import asyncpg for this test so I made my own UUID
# Import asyncpg and uncomment the two lines below for the actual bug
app = FastAPI()
# from asyncpg.pgproto import pgproto
# asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
@app.get("/fast_uuid")
def return_fast_uuid():
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
assert isinstance(asyncpg_uuid, uuid.UUID)
assert type(asyncpg_uuid) != uuid.UUID
with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
assert isinstance(asyncpg_uuid, uuid.UUID)
assert type(asyncpg_uuid) != uuid.UUID
with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
class SomeCustomClass(BaseModel):
model_config = {"arbitrary_types_allowed": True}
a_uuid: MyUuid
class SomeCustomClass(BaseModel):
class Config:
arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str}
@field_serializer("a_uuid")
def serialize_a_uuid(self, v):
return str(v)
a_uuid: MyUuid
@app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
client = TestClient(app)
with client:
response_simple = client.get("/fast_uuid")
response_pydantic = client.get("/get_custom_class")
assert response_simple.json() == {
"fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
}
assert response_pydantic.json() == {
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_pydanticv1():
app = FastAPI()
@app.get("/fast_uuid")
def return_fast_uuid():
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
assert isinstance(asyncpg_uuid, uuid.UUID)
assert type(asyncpg_uuid) != uuid.UUID
with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
class SomeCustomClass(BaseModel):
class Config:
arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str}
a_uuid: MyUuid
@app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
client = TestClient(app)
@app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
client = TestClient(app)
def test_dt():
with client:
response_simple = client.get("/fast_uuid")
response_pydantic = client.get("/get_custom_class")

View File

@@ -1,17 +1,13 @@
from collections import deque
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional
import pytest
from fastapi._compat import PYDANTIC_V2
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, ValidationError
from .utils import needs_pydanticv1, needs_pydanticv2
from pydantic import BaseModel, Field, ValidationError, create_model
class Person:
@@ -50,6 +46,22 @@ class Unserializable:
raise NotImplementedError()
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
class Config:
pass
class RoleEnum(Enum):
admin = "admin"
normal = "normal"
@@ -58,12 +70,8 @@ class RoleEnum(Enum):
class ModelWithConfig(BaseModel):
role: Optional[RoleEnum] = None
if PYDANTIC_V2:
model_config = {"use_enum_values": True}
else:
class Config:
use_enum_values = True
class Config:
use_enum_values = True
class ModelWithAlias(BaseModel):
@@ -76,6 +84,23 @@ class ModelWithDefault(BaseModel):
bla: str = "bla"
class ModelWithRoot(BaseModel):
__root__: str
@pytest.fixture(
name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath]
)
def fixture_model_with_path(request):
class Config:
arbitrary_types_allowed = True
ModelWithPath = create_model(
"ModelWithPath", path=(request.param, ...), __config__=Config # type: ignore
)
return ModelWithPath(path=request.param("/foo", "bar"))
def test_encode_dict():
pet = {"name": "Firulais", "owner": {"name": "Foo"}}
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
@@ -129,47 +154,14 @@ def test_encode_unsupported():
jsonable_encoder(unserializable)
@needs_pydanticv2
def test_encode_custom_json_encoders_model_pydanticv2():
from pydantic import field_serializer
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
@field_serializer("dt_field")
def serialize_dt_field(self, dt):
return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
pass
def test_encode_custom_json_encoders_model():
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_encode_custom_json_encoders_model_pydanticv1():
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
class Config:
pass
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
def test_encode_custom_json_encoders_model_subclass():
model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
def test_encode_model_with_config():
@@ -205,7 +197,6 @@ def test_encode_model_with_default():
}
@needs_pydanticv1
def test_custom_encoders():
class safe_datetime(datetime):
pass
@@ -236,72 +227,19 @@ def test_custom_enum_encoders():
assert encoded_instance == custom_enum_encoder(instance)
def test_encode_model_with_pure_path():
class ModelWithPath(BaseModel):
path: PurePath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PurePath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
def test_encode_model_with_path(model_with_path):
if isinstance(model_with_path.path, PureWindowsPath):
expected = "\\foo\\bar"
else:
expected = "/foo/bar"
assert jsonable_encoder(model_with_path) == {"path": expected}
def test_encode_model_with_pure_posix_path():
class ModelWithPath(BaseModel):
path: PurePosixPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PurePosixPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
def test_encode_model_with_pure_windows_path():
class ModelWithPath(BaseModel):
path: PureWindowsPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PureWindowsPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "\\foo\\bar"}
@needs_pydanticv1
def test_encode_root():
class ModelWithRoot(BaseModel):
__root__: str
model = ModelWithRoot(__root__="Foo")
assert jsonable_encoder(model) == "Foo"
@needs_pydanticv2
def test_decimal_encoder_float():
data = {"value": Decimal(1.23)}
assert jsonable_encoder(data) == {"value": 1.23}
@needs_pydanticv2
def test_decimal_encoder_int():
data = {"value": Decimal(2)}
assert jsonable_encoder(data) == {"value": 2}
def test_encode_deque_encodes_child_models():
class Model(BaseModel):
test: str

View File

@@ -1,10 +1,8 @@
from decimal import Decimal
from typing import List
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel, condecimal
app = FastAPI()
@@ -23,115 +21,59 @@ def save_item_no_body(item: List[Item]):
client = TestClient(app)
single_error = {
"detail": [
{
"ctx": {"limit_value": 0.0},
"loc": ["body", 0, "age"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
multiple_errors = {
"detail": [
{
"loc": ["body", 0, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 0, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
{
"loc": ["body", 1, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 1, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
]
}
def test_put_correct_body():
response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
assert response.status_code == 200, response.text
assert response.json() == {
"item": [
{
"name": "Foo",
"age": IsOneOf(
5,
# TODO: remove when deprecating Pydantic v1
"5",
),
}
]
}
assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
def test_jsonable_encoder_requiring_error():
response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}])
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "greater_than",
"loc": ["body", 0, "age"],
"msg": "Input should be greater than 0",
"input": -1.0,
"ctx": {"gt": 0.0},
"url": match_pydantic_error_url("greater_than"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"ctx": {"limit_value": 0.0},
"loc": ["body", 0, "age"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
}
]
}
)
assert response.json() == single_error
def test_put_incorrect_body_multiple():
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", 0, "name"],
"msg": "Field required",
"input": {"age": "five"},
"url": match_pydantic_error_url("missing"),
},
{
"type": "decimal_parsing",
"loc": ["body", 0, "age"],
"msg": "Input should be a valid decimal",
"input": "five",
},
{
"type": "missing",
"loc": ["body", 1, "name"],
"msg": "Field required",
"input": {"age": "six"},
"url": match_pydantic_error_url("missing"),
},
{
"type": "decimal_parsing",
"loc": ["body", 1, "age"],
"msg": "Input should be a valid decimal",
"input": "six",
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", 0, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 0, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
{
"loc": ["body", 1, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", 1, "age"],
"msg": "value is not a valid decimal",
"type": "type_error.decimal",
},
]
}
)
assert response.json() == multiple_errors
def test_openapi_schema():
@@ -184,23 +126,11 @@ def test_openapi_schema():
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"age": IsDict(
{
"title": "Age",
"anyOf": [
{"exclusiveMinimum": 0.0, "type": "number"},
{"type": "string"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Age",
"exclusiveMinimum": 0.0,
"type": "number",
}
),
"age": {
"title": "Age",
"exclusiveMinimum": 0.0,
"type": "number",
},
},
},
"ValidationError": {

View File

@@ -1,9 +1,7 @@
from typing import List
from dirty_equals import IsDict
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
app = FastAPI()
@@ -16,6 +14,22 @@ def read_items(q: List[int] = Query(default=None)):
client = TestClient(app)
multiple_errors = {
"detail": [
{
"loc": ["query", "q", 0],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
{
"loc": ["query", "q", 1],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
]
}
def test_multi_query():
response = client.get("/items/?q=5&q=6")
assert response.status_code == 200, response.text
@@ -25,42 +39,7 @@ def test_multi_query():
def test_multi_query_incorrect():
response = client.get("/items/?q=five&q=six")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "q", 0],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "five",
"url": match_pydantic_error_url("int_parsing"),
},
{
"type": "int_parsing",
"loc": ["query", "q", 1],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "six",
"url": match_pydantic_error_url("int_parsing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "q", 0],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
{
"loc": ["query", "q", 1],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
]
}
)
assert response.json() == multiple_errors
def test_openapi_schema():

View File

@@ -1,6 +1,5 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -53,21 +52,11 @@ def test_openapi():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "integer"}, {"type": "null"}],
"default": 50,
"title": "Standard Query Param",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Standard Query Param",
"type": "integer",
"default": 50,
}
),
"schema": {
"title": "Standard Query Param",
"type": "integer",
"default": 50,
},
"name": "standard_query_param",
"in": "query",
},

View File

@@ -1,4 +1,3 @@
from dirty_equals import IsOneOf
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -36,20 +35,10 @@ def test_openapi_schema():
"servers": [
{"url": "/", "description": "Default, relative server"},
{
"url": IsOneOf(
"http://staging.localhost.tiangolo.com:8000/",
# TODO: remove when deprecating Pydantic v1
"http://staging.localhost.tiangolo.com:8000",
),
"url": "http://staging.localhost.tiangolo.com:8000",
"description": "Staging but actually localhost still",
},
{
"url": IsOneOf(
"https://prod.example.com/",
# TODO: remove when deprecating Pydantic v1
"https://prod.example.com",
)
},
{"url": "https://prod.example.com"},
],
"paths": {
"/foo": {

View File

@@ -1,6 +1,6 @@
from typing import Any, List
from dirty_equals import IsOneOf
import pytest
from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query
test_data: List[Any] = ["teststr", None, ..., 1, []]
@@ -10,137 +10,34 @@ def get_user():
return {} # pragma: no cover
def test_param_repr_str():
assert repr(Param("teststr")) == "Param(teststr)"
@pytest.fixture(scope="function", params=test_data)
def params(request):
return request.param
def test_param_repr_none():
assert repr(Param(None)) == "Param(None)"
def test_param_repr_ellipsis():
assert repr(Param(...)) == IsOneOf(
"Param(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Param(Ellipsis)",
)
def test_param_repr_number():
assert repr(Param(1)) == "Param(1)"
def test_param_repr_list():
assert repr(Param([])) == "Param([])"
def test_param_repr(params):
assert repr(Param(params)) == "Param(" + str(params) + ")"
def test_path_repr():
assert repr(Path()) == IsOneOf(
"Path(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Path(Ellipsis)",
)
assert repr(Path(...)) == IsOneOf(
"Path(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Path(Ellipsis)",
)
assert repr(Path()) == "Path(Ellipsis)"
assert repr(Path(...)) == "Path(Ellipsis)"
def test_query_repr_str():
assert repr(Query("teststr")) == "Query(teststr)"
def test_query_repr(params):
assert repr(Query(params)) == "Query(" + str(params) + ")"
def test_query_repr_none():
assert repr(Query(None)) == "Query(None)"
def test_header_repr(params):
assert repr(Header(params)) == "Header(" + str(params) + ")"
def test_query_repr_ellipsis():
assert repr(Query(...)) == IsOneOf(
"Query(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Query(Ellipsis)",
)
def test_cookie_repr(params):
assert repr(Cookie(params)) == "Cookie(" + str(params) + ")"
def test_query_repr_number():
assert repr(Query(1)) == "Query(1)"
def test_query_repr_list():
assert repr(Query([])) == "Query([])"
def test_header_repr_str():
assert repr(Header("teststr")) == "Header(teststr)"
def test_header_repr_none():
assert repr(Header(None)) == "Header(None)"
def test_header_repr_ellipsis():
assert repr(Header(...)) == IsOneOf(
"Header(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Header(Ellipsis)",
)
def test_header_repr_number():
assert repr(Header(1)) == "Header(1)"
def test_header_repr_list():
assert repr(Header([])) == "Header([])"
def test_cookie_repr_str():
assert repr(Cookie("teststr")) == "Cookie(teststr)"
def test_cookie_repr_none():
assert repr(Cookie(None)) == "Cookie(None)"
def test_cookie_repr_ellipsis():
assert repr(Cookie(...)) == IsOneOf(
"Cookie(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Cookie(Ellipsis)",
)
def test_cookie_repr_number():
assert repr(Cookie(1)) == "Cookie(1)"
def test_cookie_repr_list():
assert repr(Cookie([])) == "Cookie([])"
def test_body_repr_str():
assert repr(Body("teststr")) == "Body(teststr)"
def test_body_repr_none():
assert repr(Body(None)) == "Body(None)"
def test_body_repr_ellipsis():
assert repr(Body(...)) == IsOneOf(
"Body(PydanticUndefined)",
# TODO: remove when deprecating Pydantic v1
"Body(Ellipsis)",
)
def test_body_repr_number():
assert repr(Body(1)) == "Body(1)"
def test_body_repr_list():
assert repr(Body([])) == "Body([])"
def test_body_repr(params):
assert repr(Body(params)) == "Body(" + str(params) + ")"
def test_depends_repr():

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,410 +1,62 @@
from dirty_equals import IsDict
import pytest
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from .main import app
client = TestClient(app)
def test_query():
response = client.get("/query")
assert response.status_code == 422
assert response.json() == IsDict(
response_missing = {
"detail": [
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
]
}
response_not_valid_int = {
"detail": [
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
)
]
}
def test_query_query_baz():
response = client.get("/query?query=baz")
assert response.status_code == 200
assert response.json() == "foo bar baz"
def test_query_not_declared_baz():
response = client.get("/query?not_declared=baz")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_optional():
response = client.get("/query/optional")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_optional_query_baz():
response = client.get("/query/optional?query=baz")
assert response.status_code == 200
assert response.json() == "foo bar baz"
def test_query_optional_not_declared_baz():
response = client.get("/query/optional?not_declared=baz")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_int():
response = client.get("/query/int")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_int_query_42():
response = client.get("/query/int?query=42")
assert response.status_code == 200
assert response.json() == "foo bar 42"
def test_query_int_query_42_5():
response = client.get("/query/int?query=42.5")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "42.5",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_int_query_baz():
response = client.get("/query/int?query=baz")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "baz",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_int_not_declared_baz():
response = client.get("/query/int?not_declared=baz")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_int_optional():
response = client.get("/query/int/optional")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_int_optional_query_50():
response = client.get("/query/int/optional?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_int_optional_query_foo():
response = client.get("/query/int/optional?query=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_int_default():
response = client.get("/query/int/default")
assert response.status_code == 200
assert response.json() == "foo bar 10"
def test_query_int_default_query_50():
response = client.get("/query/int/default?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_int_default_query_foo():
response = client.get("/query/int/default?query=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_param():
response = client.get("/query/param")
assert response.status_code == 200
assert response.json() == "foo bar"
def test_query_param_query_50():
response = client.get("/query/param?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_param_required():
response = client.get("/query/param-required")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_param_required_query_50():
response = client.get("/query/param-required?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_param_required_int():
response = client.get("/query/param-required/int")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "query"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_query_param_required_int_query_50():
response = client.get("/query/param-required/int?query=50")
assert response.status_code == 200
assert response.json() == "foo bar 50"
def test_query_param_required_int_query_foo():
response = client.get("/query/param-required/int?query=foo")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "int_parsing",
"loc": ["query", "query"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "foo",
"url": match_pydantic_error_url("int_parsing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "query"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
}
)
def test_query_frozenset_query_1_query_1_query_2():
response = client.get("/query/frozenset/?query=1&query=1&query=2")
assert response.status_code == 200
assert response.json() == "1,2"
@pytest.mark.parametrize(
"path,expected_status,expected_response",
[
("/query", 422, response_missing),
("/query?query=baz", 200, "foo bar baz"),
("/query?not_declared=baz", 422, response_missing),
("/query/optional", 200, "foo bar"),
("/query/optional?query=baz", 200, "foo bar baz"),
("/query/optional?not_declared=baz", 200, "foo bar"),
("/query/int", 422, response_missing),
("/query/int?query=42", 200, "foo bar 42"),
("/query/int?query=42.5", 422, response_not_valid_int),
("/query/int?query=baz", 422, response_not_valid_int),
("/query/int?not_declared=baz", 422, response_missing),
("/query/int/optional", 200, "foo bar"),
("/query/int/optional?query=50", 200, "foo bar 50"),
("/query/int/optional?query=foo", 422, response_not_valid_int),
("/query/int/default", 200, "foo bar 10"),
("/query/int/default?query=50", 200, "foo bar 50"),
("/query/int/default?query=foo", 422, response_not_valid_int),
("/query/param", 200, "foo bar"),
("/query/param?query=50", 200, "foo bar 50"),
("/query/param-required", 422, response_missing),
("/query/param-required?query=50", 200, "foo bar 50"),
("/query/param-required/int", 422, response_missing),
("/query/param-required/int?query=50", 200, "foo bar 50"),
("/query/param-required/int?query=foo", 422, response_not_valid_int),
("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"),
],
)
def test_get_path(path, expected_status, expected_response):
response = client.get(path)
assert response.status_code == expected_status
assert response.json() == expected_response

View File

@@ -2,83 +2,48 @@ from typing import Any
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
from .utils import needs_pydanticv1, needs_pydanticv2
from pydantic import BaseModel
class PersonBase(BaseModel):
name: str
lastname: str
class Person(PersonBase):
@property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
class Config:
orm_mode = True
read_with_orm_mode = True
class PersonCreate(PersonBase):
pass
class PersonRead(PersonBase):
full_name: str
class Config:
orm_mode = True
app = FastAPI()
@app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person)
return db_person
client = TestClient(app)
@needs_pydanticv2
def test_read_with_orm_mode() -> None:
class PersonBase(BaseModel):
name: str
lastname: str
class Person(PersonBase):
@property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
model_config = ConfigDict(from_attributes=True)
class PersonCreate(PersonBase):
pass
class PersonRead(PersonBase):
full_name: str
model_config = {"from_attributes": True}
app = FastAPI()
@app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.model_validate(person)
return db_person
client = TestClient(app)
person_data = {"name": "Dive", "lastname": "Wilson"}
response = client.post("/people/", json=person_data)
data = response.json()
assert response.status_code == 200, response.text
assert data["name"] == person_data["name"]
assert data["lastname"] == person_data["lastname"]
assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]
@needs_pydanticv1
def test_read_with_orm_mode_pv1() -> None:
class PersonBase(BaseModel):
name: str
lastname: str
class Person(PersonBase):
@property
def full_name(self) -> str:
return f"{self.name} {self.lastname}"
class Config:
orm_mode = True
read_with_orm_mode = True
class PersonCreate(PersonBase):
pass
class PersonRead(PersonBase):
full_name: str
class Config:
orm_mode = True
app = FastAPI()
@app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person)
return db_person
client = TestClient(app)
person_data = {"name": "Dive", "lastname": "Wilson"}
response = client.post("/people/", json=person_data)
data = response.json()

View File

@@ -1,182 +0,0 @@
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from typing_extensions import Annotated
from .utils import needs_py310
def get_client():
app = FastAPI()
with pytest.warns(DeprecationWarning):
@app.post("/items/")
async def read_items(
q: Annotated[str | None, Form(regex="^fixedquery$")] = None
):
if q:
return f"Hello {q}"
else:
return "Hello World"
client = TestClient(app)
return client
@needs_py310
def test_no_query():
client = get_client()
response = client.post("/items/")
assert response.status_code == 200
assert response.json() == "Hello World"
@needs_py310
def test_q_fixedquery():
client = get_client()
response = client.post("/items/", data={"q": "fixedquery"})
assert response.status_code == 200
assert response.json() == "Hello fixedquery"
@needs_py310
def test_query_nonregexquery():
client = get_client()
response = client.post("/items/", data={"q": "nonregexquery"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "string_pattern_mismatch",
"loc": ["body", "q"],
"msg": "String should match pattern '^fixedquery$'",
"input": "nonregexquery",
"ctx": {"pattern": "^fixedquery$"},
"url": match_pydantic_error_url("string_pattern_mismatch"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"ctx": {"pattern": "^fixedquery$"},
"loc": ["body", "q"],
"msg": 'string does not match regex "^fixedquery$"',
"type": "value_error.str.regex",
}
]
}
)
@needs_py310
def test_openapi_schema():
client = get_client()
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
# insert_assert(response.json())
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"post": {
"summary": "Read Items",
"operationId": "read_items_items__post",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": IsDict(
{
"allOf": [
{
"$ref": "#/components/schemas/Body_read_items_items__post"
}
],
"title": "Body",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"$ref": "#/components/schemas/Body_read_items_items__post"
}
)
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"Body_read_items_items__post": {
"properties": {
"q": IsDict(
{
"anyOf": [
{"type": "string", "pattern": "^fixedquery$"},
{"type": "null"},
],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"type": "string", "pattern": "^fixedquery$", "title": "Q"}
)
},
"type": "object",
"title": "Body_read_items_items__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",
},
}
},
}

View File

@@ -1,165 +0,0 @@
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from typing_extensions import Annotated
from .utils import needs_py310
def get_client():
app = FastAPI()
with pytest.warns(DeprecationWarning):
@app.get("/items/")
async def read_items(
q: Annotated[str | None, Query(regex="^fixedquery$")] = None
):
if q:
return f"Hello {q}"
else:
return "Hello World"
client = TestClient(app)
return client
@needs_py310
def test_query_params_str_validations_no_query():
client = get_client()
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == "Hello World"
@needs_py310
def test_query_params_str_validations_q_fixedquery():
client = get_client()
response = client.get("/items/", params={"q": "fixedquery"})
assert response.status_code == 200
assert response.json() == "Hello fixedquery"
@needs_py310
def test_query_params_str_validations_item_query_nonregexquery():
client = get_client()
response = client.get("/items/", params={"q": "nonregexquery"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "string_pattern_mismatch",
"loc": ["query", "q"],
"msg": "String should match pattern '^fixedquery$'",
"input": "nonregexquery",
"ctx": {"pattern": "^fixedquery$"},
"url": match_pydantic_error_url("string_pattern_mismatch"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"ctx": {"pattern": "^fixedquery$"},
"loc": ["query", "q"],
"msg": 'string does not match regex "^fixedquery$"',
"type": "value_error.str.regex",
}
]
}
)
@needs_py310
def test_openapi_schema():
client = get_client()
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
# insert_assert(response.json())
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"name": "q",
"in": "query",
"required": False,
"schema": IsDict(
{
"anyOf": [
{"type": "string", "pattern": "^fixedquery$"},
{"type": "null"},
],
"title": "Q",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"type": "string",
"pattern": "^fixedquery$",
"title": "Q",
}
),
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"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",
},
}
},
}

View File

@@ -39,6 +39,7 @@ client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
# insert_assert(response.json())
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},

View File

@@ -1,9 +1,8 @@
from typing import List
from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field
app = FastAPI()
@@ -15,24 +14,13 @@ class Model(BaseModel):
class ModelNoAlias(BaseModel):
name: str
if PYDANTIC_V2:
model_config = ConfigDict(
json_schema_extra={
"description": (
"response_model_by_alias=False is basically a quick hack, to support "
"proper OpenAPI use another model with the correct field names"
)
}
)
else:
class Config:
schema_extra = {
"description": (
"response_model_by_alias=False is basically a quick hack, to support "
"proper OpenAPI use another model with the correct field names"
)
}
class Config:
schema_extra = {
"description": (
"response_model_by_alias=False is basically a quick hack, to support "
"proper OpenAPI use another model with the correct field names"
)
}
@app.get("/dict", response_model=Model, response_model_by_alias=False)

View File

@@ -2,10 +2,10 @@ from typing import List, Union
import pytest
from fastapi import FastAPI
from fastapi.exceptions import FastAPIError, ResponseValidationError
from fastapi.exceptions import FastAPIError
from fastapi.responses import JSONResponse, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError
class BaseUser(BaseModel):
@@ -277,12 +277,12 @@ def test_response_model_no_annotation_return_exact_dict():
def test_response_model_no_annotation_return_invalid_dict():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/response_model-no_annotation-return_invalid_dict")
def test_response_model_no_annotation_return_invalid_model():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/response_model-no_annotation-return_invalid_model")
@@ -313,12 +313,12 @@ def test_no_response_model_annotation_return_exact_dict():
def test_no_response_model_annotation_return_invalid_dict():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/no_response_model-annotation-return_invalid_dict")
def test_no_response_model_annotation_return_invalid_model():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/no_response_model-annotation-return_invalid_model")
@@ -395,12 +395,12 @@ def test_response_model_model1_annotation_model2_return_exact_dict():
def test_response_model_model1_annotation_model2_return_invalid_dict():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/response_model_model1-annotation_model2-return_invalid_dict")
def test_response_model_model1_annotation_model2_return_invalid_model():
with pytest.raises(ResponseValidationError):
with pytest.raises(ValidationError):
client.get("/response_model_model1-annotation_model2-return_invalid_model")

View File

@@ -1,81 +0,0 @@
from typing import List
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class UserBase(BaseModel):
email: str
class UserCreate(UserBase):
password: str
class UserDB(UserBase):
hashed_password: str
class PetDB(BaseModel):
name: str
owner: UserDB
class PetOut(BaseModel):
name: str
owner: UserBase
@app.post("/users/", response_model=UserBase)
async def create_user(user: UserCreate):
return user
@app.get("/pets/{pet_id}", response_model=PetOut)
async def read_pet(pet_id: int):
user = UserDB(
email="johndoe@example.com",
hashed_password="secrethashed",
)
pet = PetDB(name="Nibbler", owner=user)
return pet
@app.get("/pets/", response_model=List[PetOut])
async def read_pets():
user = UserDB(
email="johndoe@example.com",
hashed_password="secrethashed",
)
pet1 = PetDB(name="Nibbler", owner=user)
pet2 = PetDB(name="Zoidberg", owner=user)
return [pet1, pet2]
client = TestClient(app)
def test_filter_top_level_model():
response = client.post(
"/users", json={"email": "johndoe@example.com", "password": "secret"}
)
assert response.json() == {"email": "johndoe@example.com"}
def test_filter_second_level_model():
response = client.get("/pets/1")
assert response.json() == {
"name": "Nibbler",
"owner": {"email": "johndoe@example.com"},
}
def test_list_of_models():
response = client.get("/pets/")
assert response.json() == [
{"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
{"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
]

View File

@@ -1,83 +0,0 @@
from typing import List
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
email: str
password: str
class UserDB(BaseModel):
email: str
hashed_password: str
class User(BaseModel):
email: str
class PetDB(BaseModel):
name: str
owner: UserDB
class PetOut(BaseModel):
name: str
owner: User
@app.post("/users/", response_model=User)
async def create_user(user: UserCreate):
return user
@app.get("/pets/{pet_id}", response_model=PetOut)
async def read_pet(pet_id: int):
user = UserDB(
email="johndoe@example.com",
hashed_password="secrethashed",
)
pet = PetDB(name="Nibbler", owner=user)
return pet
@app.get("/pets/", response_model=List[PetOut])
async def read_pets():
user = UserDB(
email="johndoe@example.com",
hashed_password="secrethashed",
)
pet1 = PetDB(name="Nibbler", owner=user)
pet2 = PetDB(name="Zoidberg", owner=user)
return [pet1, pet2]
client = TestClient(app)
def test_filter_top_level_model():
response = client.post(
"/users", json={"email": "johndoe@example.com", "password": "secret"}
)
assert response.json() == {"email": "johndoe@example.com"}
def test_filter_second_level_model():
response = client.get("/pets/1")
assert response.json() == {
"name": "Nibbler",
"owner": {"email": "johndoe@example.com"},
}
def test_list_of_models():
response = client.get("/pets/")
assert response.json() == [
{"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
{"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
]

View File

@@ -1,11 +1,9 @@
from typing import Union
import pytest
from dirty_equals import IsDict
from fastapi import Body, Cookie, FastAPI, Header, Path, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel
def create_app():
@@ -14,14 +12,8 @@ def create_app():
class Item(BaseModel):
data: str
if PYDANTIC_V2:
model_config = ConfigDict(
json_schema_extra={"example": {"data": "Data in schema_extra"}}
)
else:
class Config:
schema_extra = {"example": {"data": "Data in schema_extra"}}
class Config:
schema_extra = {"example": {"data": "Data in schema_extra"}}
@app.post("/schema_extra/")
def schema_extra(item: Item):
@@ -341,28 +333,14 @@ def test_openapi_schema():
"requestBody": {
"content": {
"application/json": {
"schema": IsDict(
{
"$ref": "#/components/schemas/Item",
"examples": [
{"data": "Data in Body examples, example1"},
{"data": "Data in Body examples, example2"},
],
}
)
| IsDict(
# TODO: remove this when deprecating Pydantic v1
{
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Item",
"examples": [
{"data": "Data in Body examples, example1"},
{"data": "Data in Body examples, example2"},
],
}
)
"schema": {
"allOf": [{"$ref": "#/components/schemas/Item"}],
"title": "Item",
"examples": [
{"data": "Data in Body examples, example1"},
{"data": "Data in Body examples, example2"},
],
}
}
},
"required": True,
@@ -392,28 +370,14 @@ def test_openapi_schema():
"requestBody": {
"content": {
"application/json": {
"schema": IsDict(
{
"$ref": "#/components/schemas/Item",
"examples": [
{"data": "examples example_examples 1"},
{"data": "examples example_examples 2"},
],
}
)
| IsDict(
# TODO: remove this when deprecating Pydantic v1
{
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Item",
"examples": [
{"data": "examples example_examples 1"},
{"data": "examples example_examples 2"},
],
},
),
"schema": {
"allOf": [{"$ref": "#/components/schemas/Item"}],
"title": "Item",
"examples": [
{"data": "examples example_examples 1"},
{"data": "examples example_examples 2"},
],
},
"example": {"data": "Overridden example"},
}
},
@@ -544,16 +508,7 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"schema": {"title": "Data", "type": "string"},
"example": "query1",
"name": "data",
"in": "query",
@@ -584,21 +539,11 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
"examples": ["query1", "query2"],
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{
"type": "string",
"title": "Data",
"examples": ["query1", "query2"],
}
),
"schema": {
"type": "string",
"title": "Data",
"examples": ["query1", "query2"],
},
"name": "data",
"in": "query",
}
@@ -628,21 +573,11 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
"examples": ["query1", "query2"],
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{
"type": "string",
"title": "Data",
"examples": ["query1", "query2"],
}
),
"schema": {
"type": "string",
"title": "Data",
"examples": ["query1", "query2"],
},
"example": "query_overridden",
"name": "data",
"in": "query",
@@ -673,16 +608,7 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"schema": {"type": "string", "title": "Data"},
"example": "header1",
"name": "data",
"in": "header",
@@ -713,21 +639,11 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
"examples": ["header1", "header2"],
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{
"type": "string",
"title": "Data",
"examples": ["header1", "header2"],
}
),
"schema": {
"type": "string",
"title": "Data",
"examples": ["header1", "header2"],
},
"name": "data",
"in": "header",
}
@@ -757,21 +673,11 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
"examples": ["header1", "header2"],
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{
"title": "Data",
"type": "string",
"examples": ["header1", "header2"],
}
),
"schema": {
"type": "string",
"title": "Data",
"examples": ["header1", "header2"],
},
"example": "header_overridden",
"name": "data",
"in": "header",
@@ -802,16 +708,7 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{"title": "Data", "type": "string"}
),
"schema": {"type": "string", "title": "Data"},
"example": "cookie1",
"name": "data",
"in": "cookie",
@@ -842,21 +739,11 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
"examples": ["cookie1", "cookie2"],
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{
"title": "Data",
"type": "string",
"examples": ["cookie1", "cookie2"],
}
),
"schema": {
"type": "string",
"title": "Data",
"examples": ["cookie1", "cookie2"],
},
"name": "data",
"in": "cookie",
}
@@ -886,21 +773,11 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Data",
"examples": ["cookie1", "cookie2"],
}
)
| IsDict(
# TODO: Remove this when deprecating Pydantic v1
{
"title": "Data",
"type": "string",
"examples": ["cookie1", "cookie2"],
}
),
"schema": {
"type": "string",
"title": "Data",
"examples": ["cookie1", "cookie2"],
},
"example": "cookie_overridden",
"name": "data",
"in": "cookie",

View File

@@ -1,8 +1,7 @@
from dirty_equals import IsDict
import pytest
from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
@@ -60,136 +59,76 @@ def test_security_oauth2_password_bearer_no_header():
assert response.json() == {"detail": "Not authenticated"}
def test_strict_login_no_data():
response = client.post("/login")
assert response.status_code == 422
assert response.json() == IsDict(
required_params = {
"detail": [
{
"detail": [
{
"type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "username"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "password"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
},
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "password"],
"msg": "field required",
"type": "value_error.missing",
},
]
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "password"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
grant_type_required = {
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
}
)
]
}
grant_type_incorrect = {
"detail": [
{
"loc": ["body", "grant_type"],
"msg": 'string does not match regex "password"',
"type": "value_error.str.regex",
"ctx": {"pattern": "password"},
}
]
}
def test_strict_login_no_grant_type():
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_strict_login_incorrect_grant_type():
response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "string_pattern_mismatch",
"loc": ["body", "grant_type"],
"msg": "String should match pattern 'password'",
"input": "incorrect",
"ctx": {"pattern": "password"},
"url": match_pydantic_error_url("string_pattern_mismatch"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": 'string does not match regex "password"',
"type": "value_error.str.regex",
"ctx": {"pattern": "password"},
}
]
}
)
def test_strict_login_correct_grant_type():
response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
)
assert response.status_code == 200
assert response.json() == {
"grant_type": "password",
"username": "johndoe",
"password": "secret",
"scopes": [],
"client_id": None,
"client_secret": None,
}
@pytest.mark.parametrize(
"data,expected_status,expected_response",
[
(None, 422, required_params),
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
(
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
422,
grant_type_incorrect,
),
(
{"username": "johndoe", "password": "secret", "grant_type": "password"},
200,
{
"grant_type": "password",
"username": "johndoe",
"password": "secret",
"scopes": [],
"client_id": None,
"client_secret": None,
},
),
],
)
def test_strict_login(data, expected_status, expected_response):
response = client.post("/login", data=data)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema():
@@ -260,26 +199,8 @@ def test_openapi_schema():
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": IsDict(
{
"title": "Client Id",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Id", "type": "string"}
),
"client_secret": IsDict(
{
"title": "Client Secret",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Secret", "type": "string"}
),
"client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client Secret", "type": "string"},
},
},
"ValidationError": {

View File

@@ -1,10 +1,9 @@
from typing import Optional
from dirty_equals import IsDict
import pytest
from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
@@ -64,136 +63,76 @@ def test_security_oauth2_password_bearer_no_header():
assert response.json() == {"msg": "Create an account first"}
def test_strict_login_no_data():
response = client.post("/login")
assert response.status_code == 422
assert response.json() == IsDict(
required_params = {
"detail": [
{
"detail": [
{
"type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "username"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "password"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
},
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "password"],
"msg": "field required",
"type": "value_error.missing",
},
]
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "password"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
grant_type_required = {
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
}
)
]
}
grant_type_incorrect = {
"detail": [
{
"loc": ["body", "grant_type"],
"msg": 'string does not match regex "password"',
"type": "value_error.str.regex",
"ctx": {"pattern": "password"},
}
]
}
def test_strict_login_no_grant_type():
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_strict_login_incorrect_grant_type():
response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "string_pattern_mismatch",
"loc": ["body", "grant_type"],
"msg": "String should match pattern 'password'",
"input": "incorrect",
"ctx": {"pattern": "password"},
"url": match_pydantic_error_url("string_pattern_mismatch"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": 'string does not match regex "password"',
"type": "value_error.str.regex",
"ctx": {"pattern": "password"},
}
]
}
)
def test_strict_login_correct_data():
response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
)
assert response.status_code == 200
assert response.json() == {
"grant_type": "password",
"username": "johndoe",
"password": "secret",
"scopes": [],
"client_id": None,
"client_secret": None,
}
@pytest.mark.parametrize(
"data,expected_status,expected_response",
[
(None, 422, required_params),
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
(
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
422,
grant_type_incorrect,
),
(
{"username": "johndoe", "password": "secret", "grant_type": "password"},
200,
{
"grant_type": "password",
"username": "johndoe",
"password": "secret",
"scopes": [],
"client_id": None,
"client_secret": None,
},
),
],
)
def test_strict_login(data, expected_status, expected_response):
response = client.post("/login", data=data)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema():
@@ -264,26 +203,8 @@ def test_openapi_schema():
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": IsDict(
{
"title": "Client Id",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Id", "type": "string"}
),
"client_secret": IsDict(
{
"title": "Client Secret",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Secret", "type": "string"}
),
"client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client Secret", "type": "string"},
},
},
"ValidationError": {

View File

@@ -1,10 +1,9 @@
from typing import Optional
from dirty_equals import IsDict
import pytest
from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient
from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
@@ -65,136 +64,76 @@ def test_security_oauth2_password_bearer_no_header():
assert response.json() == {"msg": "Create an account first"}
def test_strict_login_None():
response = client.post("/login", data=None)
assert response.status_code == 422
assert response.json() == IsDict(
required_params = {
"detail": [
{
"detail": [
{
"type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "username"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
{
"type": "missing",
"loc": ["body", "password"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
},
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
},
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "password"],
"msg": "field required",
"type": "value_error.missing",
},
]
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "password"],
"msg": "field required",
"type": "value_error.missing",
},
]
}
grant_type_required = {
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
}
)
]
}
grant_type_incorrect = {
"detail": [
{
"loc": ["body", "grant_type"],
"msg": 'string does not match regex "password"',
"type": "value_error.str.regex",
"ctx": {"pattern": "password"},
}
]
}
def test_strict_login_no_grant_type():
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "grant_type"],
"msg": "Field required",
"input": None,
"url": match_pydantic_error_url("missing"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
def test_strict_login_incorrect_grant_type():
response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "string_pattern_mismatch",
"loc": ["body", "grant_type"],
"msg": "String should match pattern 'password'",
"input": "incorrect",
"ctx": {"pattern": "password"},
"url": match_pydantic_error_url("string_pattern_mismatch"),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "grant_type"],
"msg": 'string does not match regex "password"',
"type": "value_error.str.regex",
"ctx": {"pattern": "password"},
}
]
}
)
def test_strict_login_correct_correct_grant_type():
response = client.post(
"/login",
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
)
assert response.status_code == 200, response.text
assert response.json() == {
"grant_type": "password",
"username": "johndoe",
"password": "secret",
"scopes": [],
"client_id": None,
"client_secret": None,
}
@pytest.mark.parametrize(
"data,expected_status,expected_response",
[
(None, 422, required_params),
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
(
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
422,
grant_type_incorrect,
),
(
{"username": "johndoe", "password": "secret", "grant_type": "password"},
200,
{
"grant_type": "password",
"username": "johndoe",
"password": "secret",
"scopes": [],
"client_id": None,
"client_secret": None,
},
),
],
)
def test_strict_login(data, expected_status, expected_response):
response = client.post("/login", data=data)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_openapi_schema():
@@ -265,26 +204,8 @@ def test_openapi_schema():
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": IsDict(
{
"title": "Client Id",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Id", "type": "string"}
),
"client_secret": IsDict(
{
"title": "Client Secret",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Client Secret", "type": "string"}
),
"client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client Secret", "type": "string"},
},
},
"ValidationError": {

View File

@@ -12,7 +12,7 @@ class SubModel(BaseModel):
class Model(BaseModel):
x: Optional[int] = None
x: Optional[int]
sub: SubModel

View File

@@ -1,6 +1,5 @@
from typing import Optional
from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, HttpUrl
@@ -99,30 +98,13 @@ def test_openapi_schema():
"parameters": [
{
"required": False,
"schema": IsDict(
{
"title": "Callback Url",
"anyOf": [
{
"type": "string",
"format": "uri",
"minLength": 1,
"maxLength": 2083,
},
{"type": "null"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
}
),
"schema": {
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
},
"name": "callback_url",
"in": "query",
}
@@ -262,16 +244,7 @@ def test_openapi_schema():
"type": "object",
"properties": {
"id": {"title": "Id", "type": "string"},
"title": IsDict(
{
"title": "Title",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Title", "type": "string"}
),
"title": {"title": "Title", "type": "string"},
"customer": {"title": "Customer", "type": "string"},
"total": {"title": "Total", "type": "number"},
},

View File

@@ -1,6 +1,5 @@
from typing import List, Tuple
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel
@@ -127,31 +126,16 @@ def test_openapi_schema():
"requestBody": {
"content": {
"application/json": {
"schema": IsDict(
{
"title": "Square",
"maxItems": 2,
"minItems": 2,
"type": "array",
"prefixItems": [
{"$ref": "#/components/schemas/Coordinate"},
{"$ref": "#/components/schemas/Coordinate"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Square",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [
{"$ref": "#/components/schemas/Coordinate"},
{"$ref": "#/components/schemas/Coordinate"},
],
}
)
"schema": {
"title": "Square",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [
{"$ref": "#/components/schemas/Coordinate"},
{"$ref": "#/components/schemas/Coordinate"},
],
}
}
},
"required": True,
@@ -214,28 +198,13 @@ def test_openapi_schema():
"required": ["values"],
"type": "object",
"properties": {
"values": IsDict(
{
"title": "Values",
"maxItems": 2,
"minItems": 2,
"type": "array",
"prefixItems": [
{"type": "integer"},
{"type": "integer"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "Values",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "integer"}, {"type": "integer"}],
}
)
"values": {
"title": "Values",
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "integer"}, {"type": "integer"}],
}
},
},
"Coordinate": {
@@ -266,26 +235,12 @@ def test_openapi_schema():
"items": {
"title": "Items",
"type": "array",
"items": IsDict(
{
"maxItems": 2,
"minItems": 2,
"type": "array",
"prefixItems": [
{"type": "string"},
{"type": "string"},
],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "string"}, {"type": "string"}],
}
),
"items": {
"maxItems": 2,
"minItems": 2,
"type": "array",
"items": [{"type": "string"}, {"type": "string"}],
},
}
},
},

Some files were not shown because too many files have changed in this diff Show More