From c264467efec71548d4feeecbfb29da78cceb1be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 25 Dec 2025 03:01:37 -0800 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=93=9D=20Add=20documentary=20to=20w?= =?UTF-8?q?ebsite=20(#14600)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++++ docs/en/docs/img/fastapi-documentary.jpg | Bin 0 -> 191709 bytes docs/en/docs/index.md | 6 ++++++ 3 files changed, 12 insertions(+) create mode 100644 docs/en/docs/img/fastapi-documentary.jpg diff --git a/README.md b/README.md index a42cedae6..1057b8694 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,12 @@ The key features are: --- +## FastAPI mini documentary + +There's a FastAPI mini documentary released at the end of 2025, you can watch it online: + +FastAPI Mini Documentary + ## **Typer**, the FastAPI of CLIs diff --git a/docs/en/docs/img/fastapi-documentary.jpg b/docs/en/docs/img/fastapi-documentary.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3ddbfdb389794fd3b2d87e086bb6e73145ed3d1e GIT binary patch literal 191709 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 z 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�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

F^9*`A$pM;f~n&LF{A$<{T5%ER3WlFsS?xM*8 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 mini documentary released at the end of 2025, you can watch it online: + +FastAPI Mini Documentary + ## **Typer**, the FastAPI of CLIs { #typer-the-fastapi-of-clis } From 23caa2709be0d7dfb18b388299235a83258a19c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 25 Dec 2025 11:02:01 +0000 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 30d5c4403..dbaa2db87 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Add documentary to website. PR [#14600](https://github.com/fastapi/fastapi/pull/14600) by [@tiangolo](https://github.com/tiangolo). + ### Translations * 🌐 Update translations for de (update-outdated). PR [#14581](https://github.com/fastapi/fastapi/pull/14581) by [@nilslindemann](https://github.com/nilslindemann). From a4d04c9b7e97ee5aff0c27e5ea92225f77c7a348 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 26 Dec 2025 09:53:59 +0100 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=91=B7=20Remove=20`lint`=20job=20fr?= =?UTF-8?q?om=20`test`=20CI=20workflow=20(#14593)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a12d69c8..3ad630d94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,29 +16,6 @@ env: UV_SYSTEM_PYTHON: 1 jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.11" - - name: Setup uv - uses: astral-sh/setup-uv@v7 - with: - cache-dependency-glob: | - requirements**.txt - pyproject.toml - - name: Install Dependencies - run: uv pip install -r requirements-tests.txt - - name: Lint - run: bash scripts/lint.sh - test: strategy: matrix: From 55b556a7d1cc71cca9f557aa82b144183e2da055 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 08:54:23 +0000 Subject: [PATCH 04/13] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index dbaa2db87..d54193ffe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Internal +* 👷 Remove `lint` job from `test` CI workflow. PR [#14593](https://github.com/fastapi/fastapi/pull/14593) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). * 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update scripts and pre-commit to autofix files. PR [#14585](https://github.com/fastapi/fastapi/pull/14585) by [@tiangolo](https://github.com/tiangolo). From 7c751a2e1cea212a6a39032a9aac0fab937a2b20 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Fri, 26 Dec 2025 10:39:53 +0100 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20fo?= =?UTF-8?q?r=20de=20(update-outdated)=20(#14602)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync with #14600 * A few changes The LLM suggested a few changes when retranslating the document, these are the good ones. I also added a term to the llm prompt, the LLM instead used just "Abdeckung", which is too broad in this context. --- docs/de/docs/index.md | 18 ++++++++++++------ docs/de/llm-prompt.md | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 1920df8ff..11fb6c983 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -117,6 +117,12 @@ Seine Schlüssel-Merkmale sind: --- +## FastAPI Mini-Dokumentarfilm { #fastapi-mini-documentary } + +Es gibt einen FastAPI-Mini-Dokumentarfilm, veröffentlicht Ende 2025, Sie können ihn online ansehen: + +FastAPI Mini-Dokumentarfilm + ## **Typer**, das FastAPI der CLIs { #typer-the-fastapi-of-clis } @@ -233,7 +239,7 @@ INFO: Application startup complete.

-Was der Befehl fastapi dev main.py macht ... +Über den Befehl fastapi dev main.py ... Der Befehl `fastapi dev` liest Ihre `main.py`-Datei, erkennt die **FastAPI**-App darin und startet einen Server mit Uvicorn. @@ -276,7 +282,7 @@ Sie sehen die alternative automatische Dokumentation (bereitgestellt von Body eines `PUT`-Requests zu empfangen. @@ -326,7 +332,7 @@ Gehen Sie jetzt auf Dependency Injection**. +* Ein sehr leistungsfähiges und einfach zu bedienendes System für **Dependency Injection**. * Sicherheit und Authentifizierung, einschließlich Unterstützung für **OAuth2** mit **JWT-Tokens** und **HTTP Basic** Authentifizierung. * Fortgeschrittenere (aber ebenso einfache) Techniken zur Deklaration **tief verschachtelter JSON-Modelle** (dank Pydantic). * **GraphQL**-Integration mit Strawberry und anderen Bibliotheken. @@ -452,7 +458,7 @@ Für ein vollständigeres Beispiel, mit weiteren Funktionen, siehe das FastAPI Cloud deployen, treten Sie der Warteliste bei, falls noch nicht geschehen. 🚀 +Optional können Sie Ihre FastAPI-App in die FastAPI Cloud deployen, gehen Sie und treten Sie der Warteliste bei, falls noch nicht geschehen. 🚀 Wenn Sie bereits ein **FastAPI Cloud**-Konto haben (wir haben Sie von der Warteliste eingeladen 😉), können Sie Ihre Anwendung mit einem einzigen Befehl deployen. @@ -494,7 +500,7 @@ Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Zugreifens** Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉 -FastAPI Cloud ist der Hauptsponsor und Finanzierer der „FastAPI and friends“ Open-Source-Projekte. ✨ +FastAPI Cloud ist der Hauptsponsor und Finanzierer der *FastAPI and friends* Open-Source-Projekte. ✨ #### Bei anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers } diff --git a/docs/de/llm-prompt.md b/docs/de/llm-prompt.md index 5df904ac7..35ca9f069 100644 --- a/docs/de/llm-prompt.md +++ b/docs/de/llm-prompt.md @@ -251,6 +251,7 @@ Below is a list of English terms and their preferred German translations, separa * «the button»: «der Button» * «the cloud provider»: «der Cloudanbieter» * «the CLI»: «Das CLI» +* «the coverage»: «Die Testabdeckung» * «the command line interface»: «Das Kommandozeileninterface» * «the default value»: «der Defaultwert» * «the default value»: NOT «der Standardwert» From 5eb8d6ed8ac4e638f4d5883c954a8031ef3f3c42 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 09:40:18 +0000 Subject: [PATCH 06/13] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d54193ffe..bc2441f7f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Update translations for de (update-outdated). PR [#14602](https://github.com/fastapi/fastapi/pull/14602) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Update translations for de (update-outdated). PR [#14581](https://github.com/fastapi/fastapi/pull/14581) by [@nilslindemann](https://github.com/nilslindemann). ### Internal From 3063ada72f4cd493393e1d8f13bb6498f8d5cb93 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:43:02 +0100 Subject: [PATCH 07/13] =?UTF-8?q?=E2=9C=85=20Add=20missing=20tests=20for?= =?UTF-8?q?=20code=20examples=20(#14569)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: github-actions[bot] Co-authored-by: Nils-Hero Lindemann --- docs/de/docs/advanced/dataclasses.md | 6 +- docs/de/docs/how-to/graphql.md | 2 +- docs/en/docs/advanced/dataclasses.md | 6 +- docs/en/docs/how-to/graphql.md | 2 +- docs/es/docs/advanced/dataclasses.md | 6 +- docs/es/docs/how-to/graphql.md | 2 +- docs/pt/docs/advanced/dataclasses.md | 6 +- docs/pt/docs/how-to/graphql.md | 2 +- docs/ru/docs/advanced/dataclasses.md | 6 +- docs/ru/docs/how-to/graphql.md | 2 +- docs/zh/docs/advanced/dataclasses.md | 6 +- docs_src/additional_responses/__init__.py | 0 docs_src/additional_status_codes/__init__.py | 0 docs_src/advanced_middleware/__init__.py | 0 .../__init__.py | 0 docs_src/background_tasks/__init__.py | 0 docs_src/behind_a_proxy/__init__.py | 0 docs_src/body/__init__.py | 0 docs_src/body_fields/__init__.py | 0 docs_src/body_multiple_params/__init__.py | 0 docs_src/body_nested_models/__init__.py | 0 docs_src/body_updates/__init__.py | 0 docs_src/conditional_openapi/__init__.py | 0 docs_src/configure_swagger_ui/__init__.py | 0 docs_src/cookie_param_models/__init__.py | 0 docs_src/cookie_params/__init__.py | 0 docs_src/cors/__init__.py | 0 docs_src/custom_docs_ui/__init__.py | 0 docs_src/custom_request_and_route/__init__.py | 0 docs_src/custom_response/__init__.py | 0 docs_src/dataclasses_/__init__.py | 0 .../tutorial001_py310.py | 0 .../tutorial001_py39.py | 0 .../tutorial002_py310.py | 0 .../tutorial002_py39.py | 0 .../tutorial003_py310.py | 0 .../tutorial003_py39.py | 0 docs_src/debugging/__init__.py | 0 docs_src/dependencies/__init__.py | 0 docs_src/dependency_testing/__init__.py | 0 docs_src/encoder/__init__.py | 0 docs_src/events/__init__.py | 0 docs_src/extending_openapi/__init__.py | 0 docs_src/extra_data_types/__init__.py | 0 docs_src/extra_models/__init__.py | 0 docs_src/first_steps/__init__.py | 0 docs_src/generate_clients/__init__.py | 0 docs_src/graphql_/__init__.py | 0 .../{graphql => graphql_}/tutorial001_py39.py | 0 docs_src/handling_errors/__init__.py | 0 docs_src/header_param_models/__init__.py | 0 docs_src/header_params/__init__.py | 0 docs_src/metadata/__init__.py | 0 docs_src/middleware/__init__.py | 0 docs_src/openapi_callbacks/__init__.py | 0 docs_src/openapi_webhooks/__init__.py | 0 .../__init__.py | 0 .../path_operation_configuration/__init__.py | 0 docs_src/path_params/__init__.py | 0 .../__init__.py | 0 docs_src/pydantic_v1_in_v2/__init__.py | 0 docs_src/python_types/__init__.py | 0 docs_src/query_param_models/__init__.py | 0 docs_src/query_params/__init__.py | 0 .../query_params_str_validations/__init__.py | 0 docs_src/request_files/__init__.py | 0 docs_src/request_form_models/__init__.py | 0 docs_src/request_forms/__init__.py | 0 docs_src/request_forms_and_files/__init__.py | 0 .../response_change_status_code/__init__.py | 0 docs_src/response_cookies/__init__.py | 0 docs_src/response_directly/__init__.py | 0 docs_src/response_headers/__init__.py | 0 docs_src/response_model/__init__.py | 0 docs_src/response_status_code/__init__.py | 0 docs_src/schema_extra_example/__init__.py | 0 docs_src/security/__init__.py | 0 docs_src/separate_openapi_schemas/__init__.py | 0 docs_src/settings/__init__.py | 0 docs_src/static_files/__init__.py | 0 docs_src/sub_applications/__init__.py | 0 docs_src/templates/__init__.py | 0 docs_src/templates/static/__init__.py | 0 docs_src/templates/templates/__init__.py | 0 docs_src/using_request_directly/__init__.py | 0 docs_src/wsgi/__init__.py | 0 pyproject.toml | 11 + requirements-tests.txt | 1 + .../test_body/test_tutorial002.py | 161 ++++++++ .../test_body/test_tutorial003.py | 171 +++++++++ .../test_body/test_tutorial004.py | 182 +++++++++ .../test_tutorial002.py | 361 +++++++++++++++++ .../test_tutorial004.py | 290 ++++++++++++++ .../test_tutorial005.py | 272 +++++++++++++ ...est_tutorial001_tutorial002_tutorial003.py | 251 ++++++++++++ .../test_tutorial004.py | 275 +++++++++++++ .../test_tutorial005.py | 301 +++++++++++++++ .../test_tutorial006.py | 269 +++++++++++++ .../test_tutorial007.py | 344 +++++++++++++++++ .../test_tutorial008.py | 157 ++++++++ .../test_body_updates/test_tutorial002.py | 207 ++++++++++ .../test_custom_response/test_tutorial001.py | 20 +- ...est_tutorial002_tutorial003_tutorial004.py | 68 ++++ .../test_dataclasses/test_tutorial001.py | 2 +- .../test_dataclasses/test_tutorial002.py | 2 +- .../test_dataclasses/test_tutorial003.py | 2 +- .../test_tutorial/test_debugging/__init__.py | 0 .../test_debugging/test_tutorial001.py | 64 +++ ....py => test_tutorial001_tutorial001_02.py} | 31 +- ...st_tutorial002_tutorial003_tutorial004.py} | 23 +- .../test_dependencies/test_tutorial005.py | 139 +++++++ .../test_dependencies/test_tutorial007.py | 24 ++ .../test_dependencies/test_tutorial008.py | 58 +++ .../test_dependencies/test_tutorial010.py | 29 ++ .../test_dependencies/test_tutorial011.py | 120 ++++++ tests/test_tutorial/test_encoder/__init__.py | 0 .../test_encoder/test_tutorial001.py | 208 ++++++++++ .../test_tutorial001_tutorial002.py | 156 ++++++++ ...st_tutorial001_tutorial002_tutorial003.py} | 19 +- .../test_generate_clients/test_tutorial001.py | 142 +++++++ .../test_generate_clients/test_tutorial002.py | 187 +++++++++ .../test_generate_clients/test_tutorial004.py | 230 +++++++++++ tests/test_tutorial/test_graphql/__init__.py | 0 .../test_graphql/test_tutorial001.py | 70 ++++ .../test_tutorial002.py} | 36 +- .../test_metadata/test_tutorial003.py | 53 +++ .../test_tutorial/test_middleware/__init__.py | 0 .../test_middleware/test_tutorial001.py | 24 ++ .../test_tutorial001.py | 186 +++++++++ .../test_tutorial002.py | 223 +++++++++++ .../test_tutorial003_tutorial004.py | 208 ++++++++++ .../test_path_params/test_tutorial001.py | 116 ++++++ .../test_path_params/test_tutorial002.py | 124 ++++++ .../test_path_params/test_tutorial003.py | 133 +++++++ .../test_path_params/test_tutorial003b.py | 44 +++ .../__init__.py | 0 .../test_tutorial001.py | 164 ++++++++ .../test_tutorial002_tutorial003.py | 170 ++++++++ .../test_tutorial004.py | 185 +++++++++ .../test_tutorial005.py | 202 ++++++++++ .../test_tutorial006.py | 221 +++++++++++ .../test_python_types/__init__.py | 0 .../test_tutorial001_tutorial002.py | 18 + .../test_python_types/test_tutorial003.py | 12 + .../test_python_types/test_tutorial004.py | 5 + .../test_python_types/test_tutorial005.py | 12 + .../test_python_types/test_tutorial006.py | 16 + .../test_python_types/test_tutorial007.py | 8 + .../test_python_types/test_tutorial008.py | 17 + .../test_python_types/test_tutorial008b.py | 27 ++ .../test_tutorial009_tutorial009b.py | 33 ++ .../test_python_types/test_tutorial009c.py | 33 ++ .../test_python_types/test_tutorial010.py | 5 + .../test_python_types/test_tutorial011.py | 25 ++ .../test_python_types/test_tutorial012.py | 7 + .../test_python_types/test_tutorial013.py | 5 + .../test_query_params/test_tutorial001.py | 126 ++++++ .../test_query_params/test_tutorial002.py | 127 ++++++ .../test_query_params/test_tutorial003.py | 148 +++++++ .../test_query_params/test_tutorial004.py | 156 ++++++++ .../test_tutorial001.py | 121 ++++++ .../test_tutorial002.py | 142 +++++++ .../test_tutorial003.py | 153 ++++++++ .../test_tutorial004.py | 147 +++++++ .../test_tutorial005.py | 131 +++++++ .../test_tutorial006.py | 136 +++++++ .../test_tutorial006c.py | 148 +++++++ .../test_tutorial007.py | 136 +++++++ .../test_tutorial008.py | 138 +++++++ .../test_tutorial009.py | 123 ++++++ .../test_tutorial002.py | 65 ++++ .../test_tutorial001_tutorial001_01.py | 193 ++++++++++ .../test_response_model/test_tutorial002.py | 129 +++++++ .../test_response_status_code/__init__.py | 0 .../test_tutorial001_tutorial002.py | 96 +++++ .../test_tutorial002.py | 141 +++++++ .../test_tutorial003.py | 143 +++++++ .../test_security/test_tutorial002.py | 71 ++++ .../test_security/test_tutorial004.py | 363 ++++++++++++++++++ .../test_security/test_tutorial007.py | 89 +++++ .../test_tutorial/test_settings/test_app01.py | 78 ++++ .../test_static_files/__init__.py | 0 .../test_static_files/test_tutorial001.py | 40 ++ 183 files changed, 10459 insertions(+), 86 deletions(-) create mode 100644 docs_src/additional_responses/__init__.py create mode 100644 docs_src/additional_status_codes/__init__.py create mode 100644 docs_src/advanced_middleware/__init__.py create mode 100644 docs_src/authentication_error_status_code/__init__.py create mode 100644 docs_src/background_tasks/__init__.py create mode 100644 docs_src/behind_a_proxy/__init__.py create mode 100644 docs_src/body/__init__.py create mode 100644 docs_src/body_fields/__init__.py create mode 100644 docs_src/body_multiple_params/__init__.py create mode 100644 docs_src/body_nested_models/__init__.py create mode 100644 docs_src/body_updates/__init__.py create mode 100644 docs_src/conditional_openapi/__init__.py create mode 100644 docs_src/configure_swagger_ui/__init__.py create mode 100644 docs_src/cookie_param_models/__init__.py create mode 100644 docs_src/cookie_params/__init__.py create mode 100644 docs_src/cors/__init__.py create mode 100644 docs_src/custom_docs_ui/__init__.py create mode 100644 docs_src/custom_request_and_route/__init__.py create mode 100644 docs_src/custom_response/__init__.py create mode 100644 docs_src/dataclasses_/__init__.py rename docs_src/{dataclasses => dataclasses_}/tutorial001_py310.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial001_py39.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial002_py310.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial002_py39.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial003_py310.py (100%) rename docs_src/{dataclasses => dataclasses_}/tutorial003_py39.py (100%) create mode 100644 docs_src/debugging/__init__.py create mode 100644 docs_src/dependencies/__init__.py create mode 100644 docs_src/dependency_testing/__init__.py create mode 100644 docs_src/encoder/__init__.py create mode 100644 docs_src/events/__init__.py create mode 100644 docs_src/extending_openapi/__init__.py create mode 100644 docs_src/extra_data_types/__init__.py create mode 100644 docs_src/extra_models/__init__.py create mode 100644 docs_src/first_steps/__init__.py create mode 100644 docs_src/generate_clients/__init__.py create mode 100644 docs_src/graphql_/__init__.py rename docs_src/{graphql => graphql_}/tutorial001_py39.py (100%) create mode 100644 docs_src/handling_errors/__init__.py create mode 100644 docs_src/header_param_models/__init__.py create mode 100644 docs_src/header_params/__init__.py create mode 100644 docs_src/metadata/__init__.py create mode 100644 docs_src/middleware/__init__.py create mode 100644 docs_src/openapi_callbacks/__init__.py create mode 100644 docs_src/openapi_webhooks/__init__.py create mode 100644 docs_src/path_operation_advanced_configuration/__init__.py create mode 100644 docs_src/path_operation_configuration/__init__.py create mode 100644 docs_src/path_params/__init__.py create mode 100644 docs_src/path_params_numeric_validations/__init__.py create mode 100644 docs_src/pydantic_v1_in_v2/__init__.py create mode 100644 docs_src/python_types/__init__.py create mode 100644 docs_src/query_param_models/__init__.py create mode 100644 docs_src/query_params/__init__.py create mode 100644 docs_src/query_params_str_validations/__init__.py create mode 100644 docs_src/request_files/__init__.py create mode 100644 docs_src/request_form_models/__init__.py create mode 100644 docs_src/request_forms/__init__.py create mode 100644 docs_src/request_forms_and_files/__init__.py create mode 100644 docs_src/response_change_status_code/__init__.py create mode 100644 docs_src/response_cookies/__init__.py create mode 100644 docs_src/response_directly/__init__.py create mode 100644 docs_src/response_headers/__init__.py create mode 100644 docs_src/response_model/__init__.py create mode 100644 docs_src/response_status_code/__init__.py create mode 100644 docs_src/schema_extra_example/__init__.py create mode 100644 docs_src/security/__init__.py create mode 100644 docs_src/separate_openapi_schemas/__init__.py create mode 100644 docs_src/settings/__init__.py create mode 100644 docs_src/static_files/__init__.py create mode 100644 docs_src/sub_applications/__init__.py create mode 100644 docs_src/templates/__init__.py create mode 100644 docs_src/templates/static/__init__.py create mode 100644 docs_src/templates/templates/__init__.py create mode 100644 docs_src/using_request_directly/__init__.py create mode 100644 docs_src/wsgi/__init__.py create mode 100644 tests/test_tutorial/test_body/test_tutorial002.py create mode 100644 tests/test_tutorial/test_body/test_tutorial003.py create mode 100644 tests/test_tutorial/test_body/test_tutorial004.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial002.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial004.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial005.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial004.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial005.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial006.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial007.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial008.py create mode 100644 tests/test_tutorial/test_body_updates/test_tutorial002.py create mode 100644 tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py create mode 100644 tests/test_tutorial/test_debugging/__init__.py create mode 100644 tests/test_tutorial/test_debugging/test_tutorial001.py rename tests/test_tutorial/test_dependencies/{test_tutorial001.py => test_tutorial001_tutorial001_02.py} (86%) rename tests/test_tutorial/test_dependencies/{test_tutorial004.py => test_tutorial002_tutorial003_tutorial004.py} (89%) create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial005.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial007.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial010.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial011.py create mode 100644 tests/test_tutorial/test_encoder/__init__.py create mode 100644 tests/test_tutorial/test_encoder/test_tutorial001.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py rename tests/test_tutorial/test_first_steps/{test_tutorial001.py => test_tutorial001_tutorial002_tutorial003.py} (70%) create mode 100644 tests/test_tutorial/test_generate_clients/test_tutorial001.py create mode 100644 tests/test_tutorial/test_generate_clients/test_tutorial002.py create mode 100644 tests/test_tutorial/test_generate_clients/test_tutorial004.py create mode 100644 tests/test_tutorial/test_graphql/__init__.py create mode 100644 tests/test_tutorial/test_graphql/test_tutorial001.py rename tests/test_tutorial/{test_custom_response/test_tutorial004.py => test_metadata/test_tutorial002.py} (61%) create mode 100644 tests/test_tutorial/test_metadata/test_tutorial003.py create mode 100644 tests/test_tutorial/test_middleware/__init__.py create mode 100644 tests/test_tutorial/test_middleware/test_tutorial001.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py create mode 100644 tests/test_tutorial/test_path_params/test_tutorial001.py create mode 100644 tests/test_tutorial/test_path_params/test_tutorial002.py create mode 100644 tests/test_tutorial/test_path_params/test_tutorial003.py create mode 100644 tests/test_tutorial/test_path_params/test_tutorial003b.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/__init__.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py create mode 100644 tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py create mode 100644 tests/test_tutorial/test_python_types/__init__.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial003.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial004.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial005.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial006.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial007.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial008.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial008b.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial009c.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial010.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial011.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial012.py create mode 100644 tests/test_tutorial/test_python_types/test_tutorial013.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial001.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial002.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial003.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial004.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py create mode 100644 tests/test_tutorial/test_response_directly/test_tutorial002.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial002.py create mode 100644 tests/test_tutorial/test_response_status_code/__init__.py create mode 100644 tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial002.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial003.py create mode 100644 tests/test_tutorial/test_security/test_tutorial002.py create mode 100644 tests/test_tutorial/test_security/test_tutorial004.py create mode 100644 tests/test_tutorial/test_security/test_tutorial007.py create mode 100644 tests/test_tutorial/test_settings/test_app01.py create mode 100644 tests/test_tutorial/test_static_files/__init__.py create mode 100644 tests/test_tutorial/test_static_files/test_tutorial001.py diff --git a/docs/de/docs/advanced/dataclasses.md b/docs/de/docs/advanced/dataclasses.md index e2d59c776..52b9634ae 100644 --- a/docs/de/docs/advanced/dataclasses.md +++ b/docs/de/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI basiert auf **Pydantic**, und ich habe Ihnen gezeigt, wie Sie Pydantic-M Aber FastAPI unterstützt auf die gleiche Weise auch die Verwendung von `dataclasses`: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Das ist dank **Pydantic** ebenfalls möglich, da es `dataclasses` intern unterstützt. @@ -32,7 +32,7 @@ Wenn Sie jedoch eine Menge Datenklassen herumliegen haben, ist dies ein guter Tr Sie können `dataclasses` auch im Parameter `response_model` verwenden: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} Die Datenklasse wird automatisch in eine Pydantic-Datenklasse konvertiert. @@ -48,7 +48,7 @@ In einigen Fällen müssen Sie möglicherweise immer noch Pydantics Version von In diesem Fall können Sie einfach die Standard-`dataclasses` durch `pydantic.dataclasses` ersetzen, was einen direkten Ersatz darstellt: -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Wir importieren `field` weiterhin von Standard-`dataclasses`. diff --git a/docs/de/docs/how-to/graphql.md b/docs/de/docs/how-to/graphql.md index 0583faf4a..5c908cec4 100644 --- a/docs/de/docs/how-to/graphql.md +++ b/docs/de/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Abhängig von Ihrem Anwendungsfall könnten Sie eine andere Bibliothek vorziehen Hier ist eine kleine Vorschau, wie Sie Strawberry mit FastAPI integrieren können: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Weitere Informationen zu Strawberry finden Sie in der Strawberry-Dokumentation. diff --git a/docs/en/docs/advanced/dataclasses.md b/docs/en/docs/advanced/dataclasses.md index 574beb65f..dbc91409a 100644 --- a/docs/en/docs/advanced/dataclasses.md +++ b/docs/en/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI is built on top of **Pydantic**, and I have been showing you how to use But FastAPI also supports using `dataclasses` the same way: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} This is still supported thanks to **Pydantic**, as it has internal support for `dataclasses`. @@ -32,7 +32,7 @@ But if you have a bunch of dataclasses laying around, this is a nice trick to us You can also use `dataclasses` in the `response_model` parameter: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} The dataclass will be automatically converted to a Pydantic dataclass. @@ -48,7 +48,7 @@ In some cases, you might still have to use Pydantic's version of `dataclasses`. In that case, you can simply swap the standard `dataclasses` with `pydantic.dataclasses`, which is a drop-in replacement: -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. We still import `field` from standard `dataclasses`. diff --git a/docs/en/docs/how-to/graphql.md b/docs/en/docs/how-to/graphql.md index a002c08ca..666f819b0 100644 --- a/docs/en/docs/how-to/graphql.md +++ b/docs/en/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Depending on your use case, you might prefer to use a different library, but if Here's a small preview of how you could integrate Strawberry with FastAPI: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} You can learn more about Strawberry in the Strawberry documentation. diff --git a/docs/es/docs/advanced/dataclasses.md b/docs/es/docs/advanced/dataclasses.md index 8d96171c7..3a07482ad 100644 --- a/docs/es/docs/advanced/dataclasses.md +++ b/docs/es/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI está construido sobre **Pydantic**, y te he estado mostrando cómo usar Pero FastAPI también soporta el uso de `dataclasses` de la misma manera: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Esto sigue siendo soportado gracias a **Pydantic**, ya que tiene soporte interno para `dataclasses`. @@ -32,7 +32,7 @@ Pero si tienes un montón de dataclasses por ahí, este es un buen truco para us También puedes usar `dataclasses` en el parámetro `response_model`: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} El dataclass será automáticamente convertido a un dataclass de Pydantic. @@ -48,7 +48,7 @@ En algunos casos, todavía podrías tener que usar la versión de `dataclasses` En ese caso, simplemente puedes intercambiar los `dataclasses` estándar con `pydantic.dataclasses`, que es un reemplazo directo: -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Todavía importamos `field` de los `dataclasses` estándar. diff --git a/docs/es/docs/how-to/graphql.md b/docs/es/docs/how-to/graphql.md index 2ebfb3dd0..e50c1ae0a 100644 --- a/docs/es/docs/how-to/graphql.md +++ b/docs/es/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Dependiendo de tu caso de uso, podrías preferir usar un paquete diferente, pero Aquí tienes una pequeña vista previa de cómo podrías integrar Strawberry con FastAPI: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Puedes aprender más sobre Strawberry en la documentación de Strawberry. diff --git a/docs/pt/docs/advanced/dataclasses.md b/docs/pt/docs/advanced/dataclasses.md index 646737696..6dc9feb29 100644 --- a/docs/pt/docs/advanced/dataclasses.md +++ b/docs/pt/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI é construído em cima do **Pydantic**, e eu tenho mostrado como usar mo Mas o FastAPI também suporta o uso de `dataclasses` da mesma forma: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Isso ainda é suportado graças ao **Pydantic**, pois ele tem suporte interno para `dataclasses`. @@ -32,7 +32,7 @@ Mas se você tem um monte de dataclasses por aí, este é um truque legal para u Você também pode usar `dataclasses` no parâmetro `response_model`: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} A dataclass será automaticamente convertida para uma dataclass Pydantic. @@ -48,7 +48,7 @@ Em alguns casos, você ainda pode ter que usar a versão do Pydantic das `datacl Nesse caso, você pode simplesmente trocar as `dataclasses` padrão por `pydantic.dataclasses`, que é um substituto direto: -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Ainda importamos `field` das `dataclasses` padrão. diff --git a/docs/pt/docs/how-to/graphql.md b/docs/pt/docs/how-to/graphql.md index 7af4c6b75..98266cc28 100644 --- a/docs/pt/docs/how-to/graphql.md +++ b/docs/pt/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Dependendo do seu caso de uso, você pode preferir usar uma biblioteca diferente Aqui está uma pequena prévia de como você poderia integrar Strawberry com FastAPI: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Você pode aprender mais sobre Strawberry na documentação do Strawberry. diff --git a/docs/ru/docs/advanced/dataclasses.md b/docs/ru/docs/advanced/dataclasses.md index c37ce3023..b3ced37c1 100644 --- a/docs/ru/docs/advanced/dataclasses.md +++ b/docs/ru/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в Но FastAPI также поддерживает использование `dataclasses` тем же способом: -{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *} +{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} Это по-прежнему поддерживается благодаря **Pydantic**, так как в нём есть встроенная поддержка `dataclasses`. @@ -32,7 +32,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в Вы также можете использовать `dataclasses` в параметре `response_model`: -{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *} +{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} Этот dataclass будет автоматически преобразован в Pydantic dataclass. @@ -48,7 +48,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в В таком случае вы можете просто заменить стандартные `dataclasses` на `pydantic.dataclasses`, которая является полностью совместимой заменой (drop-in replacement): -{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} +{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} 1. Мы по-прежнему импортируем `field` из стандартных `dataclasses`. diff --git a/docs/ru/docs/how-to/graphql.md b/docs/ru/docs/how-to/graphql.md index 97278069a..50c321e7d 100644 --- a/docs/ru/docs/how-to/graphql.md +++ b/docs/ru/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Вот небольшой пример того, как можно интегрировать Strawberry с FastAPI: -{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *} +{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *} Подробнее о Strawberry можно узнать в документации Strawberry. diff --git a/docs/zh/docs/advanced/dataclasses.md b/docs/zh/docs/advanced/dataclasses.md index c74ce65c3..4e8e77d2a 100644 --- a/docs/zh/docs/advanced/dataclasses.md +++ b/docs/zh/docs/advanced/dataclasses.md @@ -4,7 +4,7 @@ FastAPI 基于 **Pydantic** 构建,前文已经介绍过如何使用 Pydantic 但 FastAPI 还可以使用数据类(`dataclasses`): -{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} +{* ../../docs_src/dataclasses_/tutorial001.py hl[1,7:12,19:20] *} 这还是借助于 **Pydantic** 及其内置的 `dataclasses`。 @@ -32,7 +32,7 @@ FastAPI 基于 **Pydantic** 构建,前文已经介绍过如何使用 Pydantic 在 `response_model` 参数中使用 `dataclasses`: -{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *} +{* ../../docs_src/dataclasses_/tutorial002.py hl[1,7:13,19] *} 本例把数据类自动转换为 Pydantic 数据类。 @@ -49,7 +49,7 @@ API 文档中也会显示相关概图: 本例把标准的 `dataclasses` 直接替换为 `pydantic.dataclasses`: ```{ .python .annotate hl_lines="1 5 8-11 14-17 23-25 28" } -{!../../docs_src/dataclasses/tutorial003.py!} +{!../../docs_src/dataclasses_/tutorial003.py!} ``` 1. 本例依然要从标准的 `dataclasses` 中导入 `field`; diff --git a/docs_src/additional_responses/__init__.py b/docs_src/additional_responses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/additional_status_codes/__init__.py b/docs_src/additional_status_codes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/advanced_middleware/__init__.py b/docs_src/advanced_middleware/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/authentication_error_status_code/__init__.py b/docs_src/authentication_error_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/background_tasks/__init__.py b/docs_src/background_tasks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/behind_a_proxy/__init__.py b/docs_src/behind_a_proxy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body/__init__.py b/docs_src/body/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body_fields/__init__.py b/docs_src/body_fields/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body_multiple_params/__init__.py b/docs_src/body_multiple_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body_nested_models/__init__.py b/docs_src/body_nested_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/body_updates/__init__.py b/docs_src/body_updates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/conditional_openapi/__init__.py b/docs_src/conditional_openapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/configure_swagger_ui/__init__.py b/docs_src/configure_swagger_ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/cookie_param_models/__init__.py b/docs_src/cookie_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/cookie_params/__init__.py b/docs_src/cookie_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/cors/__init__.py b/docs_src/cors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/custom_docs_ui/__init__.py b/docs_src/custom_docs_ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/custom_request_and_route/__init__.py b/docs_src/custom_request_and_route/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/custom_response/__init__.py b/docs_src/custom_response/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/dataclasses_/__init__.py b/docs_src/dataclasses_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/dataclasses/tutorial001_py310.py b/docs_src/dataclasses_/tutorial001_py310.py similarity index 100% rename from docs_src/dataclasses/tutorial001_py310.py rename to docs_src/dataclasses_/tutorial001_py310.py diff --git a/docs_src/dataclasses/tutorial001_py39.py b/docs_src/dataclasses_/tutorial001_py39.py similarity index 100% rename from docs_src/dataclasses/tutorial001_py39.py rename to docs_src/dataclasses_/tutorial001_py39.py diff --git a/docs_src/dataclasses/tutorial002_py310.py b/docs_src/dataclasses_/tutorial002_py310.py similarity index 100% rename from docs_src/dataclasses/tutorial002_py310.py rename to docs_src/dataclasses_/tutorial002_py310.py diff --git a/docs_src/dataclasses/tutorial002_py39.py b/docs_src/dataclasses_/tutorial002_py39.py similarity index 100% rename from docs_src/dataclasses/tutorial002_py39.py rename to docs_src/dataclasses_/tutorial002_py39.py diff --git a/docs_src/dataclasses/tutorial003_py310.py b/docs_src/dataclasses_/tutorial003_py310.py similarity index 100% rename from docs_src/dataclasses/tutorial003_py310.py rename to docs_src/dataclasses_/tutorial003_py310.py diff --git a/docs_src/dataclasses/tutorial003_py39.py b/docs_src/dataclasses_/tutorial003_py39.py similarity index 100% rename from docs_src/dataclasses/tutorial003_py39.py rename to docs_src/dataclasses_/tutorial003_py39.py diff --git a/docs_src/debugging/__init__.py b/docs_src/debugging/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/dependencies/__init__.py b/docs_src/dependencies/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/dependency_testing/__init__.py b/docs_src/dependency_testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/encoder/__init__.py b/docs_src/encoder/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/events/__init__.py b/docs_src/events/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/extending_openapi/__init__.py b/docs_src/extending_openapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/extra_data_types/__init__.py b/docs_src/extra_data_types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/extra_models/__init__.py b/docs_src/extra_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/first_steps/__init__.py b/docs_src/first_steps/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/generate_clients/__init__.py b/docs_src/generate_clients/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/graphql_/__init__.py b/docs_src/graphql_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/graphql/tutorial001_py39.py b/docs_src/graphql_/tutorial001_py39.py similarity index 100% rename from docs_src/graphql/tutorial001_py39.py rename to docs_src/graphql_/tutorial001_py39.py diff --git a/docs_src/handling_errors/__init__.py b/docs_src/handling_errors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/header_param_models/__init__.py b/docs_src/header_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/header_params/__init__.py b/docs_src/header_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/metadata/__init__.py b/docs_src/metadata/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/middleware/__init__.py b/docs_src/middleware/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/openapi_callbacks/__init__.py b/docs_src/openapi_callbacks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/openapi_webhooks/__init__.py b/docs_src/openapi_webhooks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/path_operation_advanced_configuration/__init__.py b/docs_src/path_operation_advanced_configuration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/path_operation_configuration/__init__.py b/docs_src/path_operation_configuration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/path_params/__init__.py b/docs_src/path_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/path_params_numeric_validations/__init__.py b/docs_src/path_params_numeric_validations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/pydantic_v1_in_v2/__init__.py b/docs_src/pydantic_v1_in_v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/python_types/__init__.py b/docs_src/python_types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/query_param_models/__init__.py b/docs_src/query_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/query_params/__init__.py b/docs_src/query_params/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/query_params_str_validations/__init__.py b/docs_src/query_params_str_validations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/request_files/__init__.py b/docs_src/request_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/request_form_models/__init__.py b/docs_src/request_form_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/request_forms/__init__.py b/docs_src/request_forms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/request_forms_and_files/__init__.py b/docs_src/request_forms_and_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_change_status_code/__init__.py b/docs_src/response_change_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_cookies/__init__.py b/docs_src/response_cookies/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_directly/__init__.py b/docs_src/response_directly/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_headers/__init__.py b/docs_src/response_headers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_model/__init__.py b/docs_src/response_model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/response_status_code/__init__.py b/docs_src/response_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/schema_extra_example/__init__.py b/docs_src/schema_extra_example/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/security/__init__.py b/docs_src/security/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/separate_openapi_schemas/__init__.py b/docs_src/separate_openapi_schemas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/__init__.py b/docs_src/settings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/static_files/__init__.py b/docs_src/static_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sub_applications/__init__.py b/docs_src/sub_applications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/templates/__init__.py b/docs_src/templates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/templates/static/__init__.py b/docs_src/templates/static/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/templates/templates/__init__.py b/docs_src/templates/templates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/using_request_directly/__init__.py b/docs_src/using_request_directly/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/wsgi/__init__.py b/docs_src/wsgi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyproject.toml b/pyproject.toml index ae97cb71b..8f824af5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,6 +196,17 @@ dynamic_context = "test_function" omit = [ "docs_src/response_model/tutorial003_04_py39.py", "docs_src/response_model/tutorial003_04_py310.py", + "docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock + "docs_src/dependencies/tutorial013_an_py310.py", # temporary code example? + "docs_src/dependencies/tutorial014_an_py310.py", # temporary code example? + # Pydantic V1 + "docs_src/schema_extra_example/tutorial001_pv1_py310.py", + "docs_src/query_param_models/tutorial002_pv1_py310.py", + "docs_src/query_param_models/tutorial002_pv1_an_py310.py", + "docs_src/header_param_models/tutorial002_pv1_py310.py", + "docs_src/header_param_models/tutorial002_pv1_an_py310.py", + "docs_src/cookie_param_models/tutorial002_pv1_py310.py", + "docs_src/cookie_param_models/tutorial002_pv1_an_py310.py", ] [tool.coverage.report] diff --git a/requirements-tests.txt b/requirements-tests.txt index ee188b496..1604a2858 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -6,6 +6,7 @@ mypy ==1.14.1 dirty-equals ==0.9.0 sqlmodel==0.0.27 flask >=1.1.2,<4.0.0 +strawberry-graphql >=0.200.0,< 1.0.0 anyio[trio] >=3.2.1,<5.0.0 PyJWT==2.9.0 pyyaml >=5.3.1,<7.0.0 diff --git a/tests/test_tutorial/test_body/test_tutorial002.py b/tests/test_tutorial/test_body/test_tutorial002.py new file mode 100644 index 000000000..b6d51d523 --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial002.py @@ -0,0 +1,161 @@ +import importlib +from typing import Union + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize("price", ["50.5", 50.5]) +def test_post_with_tax(client: TestClient, price: Union[str, float]): + response = client.post( + "/items/", + json={"name": "Foo", "price": price, "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.3, + "price_with_tax": 50.8, + } + + +@pytest.mark.parametrize("price", ["50.5", 50.5]) +def test_post_without_tax(client: TestClient, price: Union[str, float]): + response = client.post( + "/items/", json={"name": "Foo", "price": price, "description": "Some Foo"} + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": None, + } + + +def test_post_with_no_data(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +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": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body/test_tutorial003.py b/tests/test_tutorial/test_body/test_tutorial003.py new file mode 100644 index 000000000..227a125e7 --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial003.py @@ -0,0 +1,171 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1, "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": "Some Foo", + "tax": 0.3, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": None, + "tax": None, + } + + +def test_put_with_no_data(client: TestClient): + response = client.put("/items/123", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body/test_tutorial004.py b/tests/test_tutorial/test_body/test_tutorial004.py new file mode 100644 index 000000000..10212843e --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial004.py @@ -0,0 +1,182 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1, "description": "Some Foo", "tax": 0.3}, + params={"q": "somequery"}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": "Some Foo", + "tax": 0.3, + "q": "somequery", + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/123", + json={"name": "Foo", "price": 50.1}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "name": "Foo", + "price": 50.1, + "description": None, + "tax": None, + } + + +def test_put_with_no_data(client: TestClient): + response = client.put("/items/123", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py new file mode 100644 index 000000000..e98d5860f --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py @@ -0,0 +1,361 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_all(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + "user": {"username": "johndoe", "full_name": "John Doe"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + "user": {"username": "johndoe", "full_name": "John Doe"}, + } + + +def test_post_required(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "johndoe"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "johndoe", "full_name": None}, + } + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + { + "input": None, + "loc": [ + "body", + "user", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_no_item(client: TestClient): + response = client.put("/items/5", json={"user": {"username": "johndoe"}}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_no_user(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 50.5}}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "user", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_missing_required_field_in_item(client: TestClient): + response = client.put( + "/items/5", json={"item": {"name": "Foo"}, "user": {"username": "johndoe"}} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": {"name": "Foo"}, + "loc": [ + "body", + "item", + "price", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_missing_required_field_in_user(client: TestClient): + response = client.put( + "/items/5", + json={"item": {"name": "Foo", "price": 50.5}, "user": {"ful_name": "John Doe"}}, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": {"ful_name": "John Doe"}, + "loc": [ + "body", + "user", + "username", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_id_foo(client: TestClient): + response = client.put( + "/items/foo", + json={ + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "johndoe"}, + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{item_id}": { + "put": { + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "Body_update_item_items__item_id__put": { + "properties": { + "item": { + "$ref": "#/components/schemas/Item", + }, + "user": { + "$ref": "#/components/schemas/User", + }, + }, + "required": [ + "item", + "user", + ], + "title": "Body_update_item_items__item_id__put", + "type": "object", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "User": { + "properties": { + "username": { + "title": "Username", + "type": "string", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + "required": [ + "username", + ], + "title": "User", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py new file mode 100644 index 000000000..979c054cd --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py @@ -0,0 +1,290 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + pytest.param("tutorial004_an_py39"), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + params={"q": "somequery"}, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + "q": "somequery", + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +def test_put_missing_body(client: TestClient): + response = client.put("/items/5") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + { + "input": None, + "loc": [ + "body", + "user", + ], + "msg": "Field required", + "type": "missing", + }, + { + "input": None, + "loc": [ + "body", + "importance", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_put_empty_body(client: TestClient): + response = client.put("/items/5", json={}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + }, + ] + } + + +def test_put_invalid_importance(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 0, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["body", "importance"], + "msg": "Input should be greater than 0", + "type": "greater_than", + "input": 0, + "ctx": {"gt": 0}, + }, + ], + } + + +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": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": { + "title": "Importance", + "type": "integer", + "exclusiveMinimum": 0.0, + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py new file mode 100644 index 000000000..d47aa1b4f --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py @@ -0,0 +1,272 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_py310), + pytest.param("tutorial005_an_py39"), + pytest.param("tutorial005_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_all(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.1, + }, + } + + +def test_post_required(client: TestClient): + response = client.put( + "/items/5", + json={ + "item": {"name": "Foo", "price": 50.5}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + } + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_like_not_embeded(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 50.5, + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": None, + "loc": [ + "body", + "item", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_post_missing_required_field_in_item(client: TestClient): + response = client.put( + "/items/5", json={"item": {"name": "Foo"}, "user": {"username": "johndoe"}} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "input": {"name": "Foo"}, + "loc": [ + "body", + "item", + "price", + ], + "msg": "Field required", + "type": "missing", + }, + ], + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/{item_id}": { + "put": { + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "Body_update_item_items__item_id__put": { + "properties": { + "item": { + "$ref": "#/components/schemas/Item", + }, + }, + "required": ["item"], + "title": "Body_update_item_items__item_id__put", + "type": "object", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py new file mode 100644 index 000000000..d452929c3 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py @@ -0,0 +1,251 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +UNTYPED_LIST_SCHEMA = {"type": "array", "items": {}} + +LIST_OF_STR_SCHEMA = {"type": "array", "items": {"type": "string"}} + +SET_OF_STR_SCHEMA = {"type": "array", "items": {"type": "string"}, "uniqueItems": True} + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_mod_name(request: pytest.FixtureRequest): + return request.param + + +@pytest.fixture(name="client") +def get_client(mod_name: str): + mod = importlib.import_module(f"docs_src.body_nested_models.{mod_name}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient, mod_name: str): + if mod_name.startswith("tutorial003"): + tags_expected = IsList("foo", "bar", check_order=False) + else: + tags_expected = ["foo", "bar", "foo"] + + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": tags_expected, + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required(client: TestClient): + response = client.put( + "/items/5", + json={"description": "A very nice Item"}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_openapi_schema(client: TestClient, mod_name: str): + tags_schema = {"default": [], "title": "Tags"} + if mod_name.startswith("tutorial001"): + tags_schema.update(UNTYPED_LIST_SCHEMA) + elif mod_name.startswith("tutorial002"): + tags_schema.update(LIST_OF_STR_SCHEMA) + elif mod_name.startswith("tutorial003"): + tags_schema.update(SET_OF_STR_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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": tags_schema, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial004.py b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py new file mode 100644 index 000000000..ff9596943 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py @@ -0,0 +1,275 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": IsList("foo", "bar", check_order=False), + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "image": None, + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_item(client: TestClient): + response = client.put( + "/items/5", + json={"description": "A very nice Item"}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_image(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "image": {"url": "http://example.com/image.png"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "image", "name"], + "input": {"url": "http://example.com/image.png"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "image": { + "anyOf": [ + {"$ref": "#/components/schemas/Image"}, + {"type": "null"}, + ], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial005.py b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py new file mode 100644 index 000000000..9a07a904e --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py @@ -0,0 +1,301 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": IsList("foo", "bar", check_order=False), + "image": {"url": "http://example.com/image.png", "name": "example image"}, + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "image": None, + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_item(client: TestClient): + response = client.put( + "/items/5", + json={"description": "A very nice Item"}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {"description": "A very nice Item"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_image(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "image": {"url": "http://example.com/image.png"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "image", "name"], + "input": {"url": "http://example.com/image.png"}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_wrong_url(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "image": {"url": "not a valid url", "name": "example image"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "image", "url"], + "input": "not a valid url", + "msg": "Input should be a valid URL, relative URL without a base", + "type": "url_parsing", + "ctx": {"error": "relative URL without a base"}, + }, + ] + } + + +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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "image": { + "anyOf": [ + {"$ref": "#/components/schemas/Image"}, + {"type": "null"}, + ], + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial006.py b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py new file mode 100644 index 000000000..088177cb9 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py @@ -0,0 +1,269 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_put_all(client: TestClient): + response = client.put( + "/items/123", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo", "bar", "foo"], + "images": [ + {"url": "http://example.com/image.png", "name": "example image"} + ], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 123, + "item": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": IsList("foo", "bar", check_order=False), + "images": [ + {"url": "http://example.com/image.png", "name": "example image"} + ], + }, + } + + +def test_put_only_required(client: TestClient): + response = client.put( + "/items/5", + json={"name": "Foo", "price": 35.4}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "images": None, + }, + } + + +def test_put_empty_body(client: TestClient): + response = client.put( + "/items/5", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_images_not_list(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "price": 35.4, + "images": {"url": "http://example.com/image.png", "name": "example image"}, + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "images"], + "input": { + "url": "http://example.com/image.png", + "name": "example image", + }, + "msg": "Input should be a valid list", + "type": "list_type", + }, + ] + } + + +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": { + "/items/{item_id}": { + "put": { + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Image", + }, + "type": "array", + }, + { + "type": "null", + }, + ], + "title": "Images", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial007.py b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py new file mode 100644 index 000000000..a30281950 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py @@ -0,0 +1,344 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + pytest.param("tutorial007_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_all(client: TestClient): + data = { + "name": "Special Offer", + "description": "This is a special offer", + "price": 38.6, + "items": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + "tags": ["foo"], + "images": [ + { + "url": "http://example.com/image.png", + "name": "example image", + } + ], + } + ], + } + + response = client.post( + "/offers/", + json=data, + ) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_put_only_required(client: TestClient): + response = client.post( + "/offers/", + json={ + "name": "Special Offer", + "price": 38.6, + "items": [ + { + "name": "Foo", + "price": 35.4, + "images": [ + { + "url": "http://example.com/image.png", + "name": "example image", + } + ], + } + ], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Special Offer", + "description": None, + "price": 38.6, + "items": [ + { + "name": "Foo", + "description": None, + "price": 35.4, + "tax": None, + "tags": [], + "images": [ + { + "url": "http://example.com/image.png", + "name": "example image", + } + ], + } + ], + } + + +def test_put_empty_body(client: TestClient): + response = client.post( + "/offers/", + json={}, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "items"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_items(client: TestClient): + response = client.post( + "/offers/", + json={ + "name": "Special Offer", + "price": 38.6, + "items": [{}], + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "items", 0, "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "items", 0, "price"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_put_missing_required_in_images(client: TestClient): + response = client.post( + "/offers/", + json={ + "name": "Special Offer", + "price": 38.6, + "items": [ + {"name": "Foo", "price": 35.4, "images": [{}]}, + ], + }, + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "items", 0, "images", 0, "url"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + { + "loc": ["body", "items", 0, "images", 0, "name"], + "input": {}, + "msg": "Field required", + "type": "missing", + }, + ] + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/offers/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Offer", + "operationId": "create_offer_offers__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Offer", + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "default": [], + "type": "array", + "items": {"type": "string"}, + "uniqueItems": True, + }, + "images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Image", + }, + "type": "array", + }, + { + "type": "null", + }, + ], + "title": "Images", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "Offer": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "type": "number", + }, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + }, + }, + "required": ["name", "price", "items"], + "title": "Offer", + "type": "object", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial008.py b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py new file mode 100644 index 000000000..32eb8ee75 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py @@ -0,0 +1,157 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial008_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_body(client: TestClient): + data = [ + {"url": "http://example.com/", "name": "Example"}, + {"url": "http://fastapi.tiangolo.com/", "name": "FastAPI"}, + ] + response = client.post("/images/multiple", json=data) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_post_invalid_list_item(client: TestClient): + data = [{"url": "not a valid url", "name": "Example"}] + response = client.post("/images/multiple", json=data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", 0, "url"], + "input": "not a valid url", + "msg": "Input should be a valid URL, relative URL without a base", + "type": "url_parsing", + "ctx": {"error": "relative URL without a base"}, + }, + ] + } + + +def test_post_not_a_list(client: TestClient): + data = {"url": "http://example.com/", "name": "Example"} + response = client.post("/images/multiple", json=data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body"], + "input": { + "name": "Example", + "url": "http://example.com/", + }, + "msg": "Input should be a valid list", + "type": "list_type", + } + ] + } + + +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": { + "/images/multiple/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Multiple Images", + "operationId": "create_multiple_images_images_multiple__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Images", + "type": "array", + "items": {"$ref": "#/components/schemas/Image"}, + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Image": { + "properties": { + "url": { + "title": "Url", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + }, + "name": { + "title": "Name", + "type": "string", + }, + }, + "required": ["url", "name"], + "title": "Image", + "type": "object", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial002.py b/tests/test_tutorial/test_body_updates/test_tutorial002.py new file mode 100644 index 000000000..466e6af8f --- /dev/null +++ b/tests/test_tutorial/test_body_updates/test_tutorial002.py @@ -0,0 +1,207 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_updates.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_get(client: TestClient): + response = client.get("/items/baz") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [], + } + + +def test_patch_all(client: TestClient): + response = client.patch( + "/items/foo", + json={ + "name": "Fooz", + "description": "Item description", + "price": 3, + "tax": 10.5, + "tags": ["tag1", "tag2"], + }, + ) + assert response.json() == { + "name": "Fooz", + "description": "Item description", + "price": 3, + "tax": 10.5, + "tags": ["tag1", "tag2"], + } + + +def test_patch_name(client: TestClient): + response = client.patch( + "/items/bar", + json={"name": "Barz"}, + ) + assert response.json() == { + "name": "Barz", + "description": "The bartenders", + "price": 62, + "tax": 20.2, + "tags": [], + } + + +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": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "patch": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "type": "object", + "title": "Item", + "properties": { + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Price", + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index c81e991eb..f1d2accef 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -1,17 +1,29 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial001_py39 import app -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial010_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_response.{request.param}") + client = TestClient(mod.app) + return client -def test_get_custom_response(): +def test_get_custom_response(client: TestClient): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py new file mode 100644 index 000000000..22e2e0254 --- /dev/null +++ b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py @@ -0,0 +1,68 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial003_py39"), + pytest.param("tutorial004_py39"), + ], +) +def get_mod_name(request: pytest.FixtureRequest) -> str: + return request.param + + +@pytest.fixture(name="client") +def get_client(mod_name: str) -> TestClient: + mod = importlib.import_module(f"docs_src.custom_response.{mod_name}") + return TestClient(mod.app) + + +html_contents = """ + + + Some HTML in here + + +

Look ma! HTML!

+ + + """ + + +def test_get_custom_response(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.text == html_contents + + +def test_openapi_schema(client: TestClient, mod_name: str): + if mod_name.startswith("tutorial003"): + response_content = {"application/json": {"schema": {}}} + else: + response_content = {"text/html": {"schema": {"type": "string"}}} + + 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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": response_content, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index d5f230bc4..bc407234a 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -15,7 +15,7 @@ from tests.utils import needs_py310 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.dataclasses.{request.param}") + mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}") client = TestClient(mod.app) client.headers.clear() diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 4cf893380..995d92675 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -15,7 +15,7 @@ from tests.utils import needs_py310 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.dataclasses.{request.param}") + mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}") client = TestClient(mod.app) client.headers.clear() diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index cddf4a9be..a6a9fc1c7 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -14,7 +14,7 @@ from ...utils import needs_py310 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.dataclasses.{request.param}") + mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}") client = TestClient(mod.app) client.headers.clear() diff --git a/tests/test_tutorial/test_debugging/__init__.py b/tests/test_tutorial/test_debugging/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_debugging/test_tutorial001.py b/tests/test_tutorial/test_debugging/test_tutorial001.py new file mode 100644 index 000000000..cf62c3b19 --- /dev/null +++ b/tests/test_tutorial/test_debugging/test_tutorial001.py @@ -0,0 +1,64 @@ +import importlib +import runpy +import sys +import unittest + +import pytest +from fastapi.testclient import TestClient + +MOD_NAME = "docs_src.debugging.tutorial001_py39" + + +@pytest.fixture(name="client") +def get_client(): + mod = importlib.import_module(MOD_NAME) + client = TestClient(mod.app) + return client + + +def test_uvicorn_run_is_not_called_on_import(): + if sys.modules.get(MOD_NAME): + del sys.modules[MOD_NAME] # pragma: no cover + with unittest.mock.patch("uvicorn.run") as uvicorn_run_mock: + importlib.import_module(MOD_NAME) + uvicorn_run_mock.assert_not_called() + + +def test_get_root(client: TestClient): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"hello world": "ba"} + + +def test_uvicorn_run_called_when_run_as_main(): # Just for coverage + if sys.modules.get(MOD_NAME): + del sys.modules[MOD_NAME] + with unittest.mock.patch("uvicorn.run") as uvicorn_run_mock: + runpy.run_module(MOD_NAME, run_name="__main__") + + uvicorn_run_mock.assert_called_once_with( + unittest.mock.ANY, host="0.0.0.0", port=8000 + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py similarity index 86% rename from tests/test_tutorial/test_dependencies/test_tutorial001.py rename to tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py index 8dac99cf3..50d7c4108 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -14,6 +13,8 @@ from ...utils import needs_py310 pytest.param("tutorial001_py310", marks=needs_py310), pytest.param("tutorial001_an_py39"), pytest.param("tutorial001_an_py310", marks=needs_py310), + pytest.param("tutorial001_02_an_py39"), + pytest.param("tutorial001_02_an_py310", marks=needs_py310), ], ) def get_client(request: pytest.FixtureRequest): @@ -69,16 +70,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, @@ -128,16 +123,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py similarity index 89% rename from tests/test_tutorial/test_dependencies/test_tutorial004.py rename to tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py index 8a1346d0d..f09d6f268 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -10,6 +9,14 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial003_an_py39"), + pytest.param("tutorial003_an_py310", marks=needs_py310), pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), pytest.param("tutorial004_an_py39"), @@ -107,16 +114,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial005.py b/tests/test_tutorial/test_dependencies/test_tutorial005.py new file mode 100644 index 000000000..a914936ba --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial005.py @@ -0,0 +1,139 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_py310), + pytest.param("tutorial005_an_py39"), + pytest.param("tutorial005_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + "path,cookie,expected_status,expected_response", + [ + ( + "/items", + "from_cookie", + 200, + {"q_or_cookie": "from_cookie"}, + ), + ( + "/items?q=foo", + "from_cookie", + 200, + {"q_or_cookie": "foo"}, + ), + ( + "/items", + None, + 200, + {"q_or_cookie": None}, + ), + ], +) +def test_get(path, cookie, expected_status, expected_response, client: TestClient): + if cookie is not None: + client.cookies.set("last_query", cookie) + else: + client.cookies.clear() + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Query", + "operationId": "read_query_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Last Query", + }, + "name": "last_query", + "in": "cookie", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial007.py b/tests/test_tutorial/test_dependencies/test_tutorial007.py new file mode 100644 index 000000000..3e188abcf --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial007.py @@ -0,0 +1,24 @@ +import asyncio +from contextlib import asynccontextmanager +from unittest.mock import Mock, patch + +from docs_src.dependencies.tutorial007_py39 import get_db + + +def test_get_db(): # Just for coverage + async def test_async_gen(): + cm = asynccontextmanager(get_db) + async with cm() as db_session: + return db_session + + dbsession_moock = Mock() + + with patch( + "docs_src.dependencies.tutorial007_py39.DBSession", + return_value=dbsession_moock, + create=True, + ): + value = asyncio.run(test_async_gen()) + + assert value is dbsession_moock + dbsession_moock.close.assert_called_once() diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008.py b/tests/test_tutorial/test_dependencies/test_tutorial008.py new file mode 100644 index 000000000..9d7377ebe --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008.py @@ -0,0 +1,58 @@ +import importlib +from types import ModuleType +from typing import Annotated, Any +from unittest.mock import Mock, patch + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="module", + params=[ + "tutorial008_py39", + # Fails with `NameError: name 'DepA' is not defined` + pytest.param("tutorial008_an_py39", marks=pytest.mark.xfail), + ], +) +def get_module(request: pytest.FixtureRequest): + mod_name = f"docs_src.dependencies.{request.param}" + mod = importlib.import_module(mod_name) + return mod + + +def test_get_db(module: ModuleType): + app = FastAPI() + + @app.get("/") + def read_root(c: Annotated[Any, Depends(module.dependency_c)]): + return {"c": str(c)} + + client = TestClient(app) + + a_mock = Mock() + b_mock = Mock() + c_mock = Mock() + + with ( + patch( + f"{module.__name__}.generate_dep_a", + return_value=a_mock, + create=True, + ), + patch( + f"{module.__name__}.generate_dep_b", + return_value=b_mock, + create=True, + ), + patch( + f"{module.__name__}.generate_dep_c", + return_value=c_mock, + create=True, + ), + ): + response = client.get("/") + + assert response.status_code == 200 + assert response.json() == {"c": str(c_mock)} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial010.py b/tests/test_tutorial/test_dependencies/test_tutorial010.py new file mode 100644 index 000000000..6d3815ada --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial010.py @@ -0,0 +1,29 @@ +from typing import Annotated, Any +from unittest.mock import Mock, patch + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial010_py39 import get_db + + +def test_get_db(): + app = FastAPI() + + @app.get("/") + def read_root(c: Annotated[Any, Depends(get_db)]): + return {"c": str(c)} + + client = TestClient(app) + + dbsession_mock = Mock() + + with patch( + "docs_src.dependencies.tutorial010_py39.DBSession", + return_value=dbsession_mock, + create=True, + ): + response = client.get("/") + + assert response.status_code == 200 + assert response.json() == {"c": str(dbsession_mock)} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial011.py b/tests/test_tutorial/test_dependencies/test_tutorial011.py new file mode 100644 index 000000000..4868254c0 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial011.py @@ -0,0 +1,120 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + "tutorial011_py39", + pytest.param("tutorial011_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/query-checker/", + 200, + {"fixed_content_in_query": False}, + ), + ( + "/query-checker/?q=qwerty", + 200, + {"fixed_content_in_query": False}, + ), + ( + "/query-checker/?q=foobar", + 200, + {"fixed_content_in_query": True}, + ), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +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": { + "/query-checker/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Query Check", + "operationId": "read_query_check_query_checker__get", + "parameters": [ + { + "required": False, + "schema": { + "type": "string", + "default": "", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_encoder/__init__.py b/tests/test_tutorial/test_encoder/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_encoder/test_tutorial001.py b/tests/test_tutorial/test_encoder/test_tutorial001.py new file mode 100644 index 000000000..5c8ee054d --- /dev/null +++ b/tests/test_tutorial/test_encoder/test_tutorial001.py @@ -0,0 +1,208 @@ +import importlib +from types import ModuleType + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest): + module = importlib.import_module(f"docs_src.encoder.{request.param}") + return module + + +@pytest.fixture(name="client") +def get_client(mod: ModuleType): + client = TestClient(mod.app) + return client + + +def test_put(client: TestClient, mod: ModuleType): + fake_db = mod.fake_db + + response = client.put( + "/items/123", + json={ + "title": "Foo", + "timestamp": "2023-01-01T12:00:00", + "description": "An optional description", + }, + ) + assert response.status_code == 200 + assert "123" in fake_db + assert fake_db["123"] == { + "title": "Foo", + "timestamp": "2023-01-01T12:00:00", + "description": "An optional description", + } + + +def test_put_invalid_data(client: TestClient, mod: ModuleType): + fake_db = mod.fake_db + + response = client.put( + "/items/345", + json={ + "title": "Foo", + "timestamp": "not a date", + }, + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["body", "timestamp"], + "msg": "Input should be a valid datetime or date, invalid character in year", + "type": "datetime_from_date_parsing", + "input": "not a date", + "ctx": {"error": "invalid character in year"}, + } + ] + } + assert "345" not in fake_db + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{id}": { + "put": { + "operationId": "update_item_items__id__put", + "parameters": [ + { + "in": "path", + "name": "id", + "required": True, + "schema": { + "title": "Id", + "type": "string", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Update Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string", + }, + "title": { + "title": "Title", + "type": "string", + }, + }, + "required": [ + "title", + "timestamp", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py new file mode 100644 index 000000000..3f2f508a1 --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py @@ -0,0 +1,156 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.extra_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post(client: TestClient): + response = client.post( + "/user/", + json={ + "username": "johndoe", + "password": "secret", + "email": "johndoe@example.com", + "full_name": "John Doe", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + } + + +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": { + "/user/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserOut", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "UserIn": { + "title": "UserIn", + "required": IsList( + "username", "password", "email", check_order=False + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "UserOut": { + "title": "UserOut", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_first_steps/test_tutorial001.py b/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py similarity index 70% rename from tests/test_tutorial/test_first_steps/test_tutorial001.py rename to tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py index c102bb999..aa65218cd 100644 --- a/tests/test_tutorial/test_first_steps/test_tutorial001.py +++ b/tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py @@ -1,9 +1,20 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from docs_src.first_steps.tutorial001_py39 import app -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial001_py39", + "tutorial003_py39", + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.first_steps.{request.param}") + client = TestClient(mod.app) + return client @pytest.mark.parametrize( @@ -13,13 +24,13 @@ client = TestClient(app) ("/nonexistent", 404, {"detail": "Not Found"}), ], ) -def test_get_path(path, expected_status, expected_response): +def test_get_path(client: TestClient, path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial001.py b/tests/test_tutorial/test_generate_clients/test_tutorial001.py new file mode 100644 index 000000000..bbb66b451 --- /dev/null +++ b/tests/test_tutorial/test_generate_clients/test_tutorial001.py @@ -0,0 +1,142 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.generate_clients.{request.param}") + client = TestClient(mod.app) + return client + + +def test_post_items(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 5}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "item received"} + + +def test_get_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Plumbus", "price": 3}, + {"name": "Portal Gun", "price": 9001}, + ] + + +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": { + "/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "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"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ResponseMessage": { + "title": "ResponseMessage", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "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"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial002.py b/tests/test_tutorial/test_generate_clients/test_tutorial002.py new file mode 100644 index 000000000..ab8bc4c11 --- /dev/null +++ b/tests/test_tutorial/test_generate_clients/test_tutorial002.py @@ -0,0 +1,187 @@ +from fastapi.testclient import TestClient + +from docs_src.generate_clients.tutorial002_py39 import app + +client = TestClient(app) + + +def test_post_items(): + response = client.post("/items/", json={"name": "Foo", "price": 5}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Item received"} + + +def test_post_users(): + response = client.post( + "/users/", json={"username": "Foo", "email": "foo@example.com"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "User received"} + + +def test_get_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Plumbus", "price": 3}, + {"name": "Portal Gun", "price": 9001}, + ] + + +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": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + }, + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "post": { + "tags": ["users"], + "summary": "Create User", + "operationId": "create_user_users__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "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"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ResponseMessage": { + "title": "ResponseMessage", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "User": { + "title": "User", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "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"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial004.py b/tests/test_tutorial/test_generate_clients/test_tutorial004.py new file mode 100644 index 000000000..e66f6d2a1 --- /dev/null +++ b/tests/test_tutorial/test_generate_clients/test_tutorial004.py @@ -0,0 +1,230 @@ +import importlib +import json +import pathlib +from unittest.mock import patch + +from docs_src.generate_clients import tutorial003_py39 + + +def test_remove_tags(tmp_path: pathlib.Path): + tmp_file = tmp_path / "openapi.json" + openapi_json = tutorial003_py39.app.openapi() + tmp_file.write_text(json.dumps(openapi_json)) + + with patch("pathlib.Path", return_value=tmp_file): + importlib.import_module("docs_src.generate_clients.tutorial004_py39") + + modified_openapi = json.loads(tmp_file.read_text()) + assert modified_openapi == { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ResponseMessage": { + "properties": { + "message": { + "title": "Message", + "type": "string", + }, + }, + "required": [ + "message", + ], + "title": "ResponseMessage", + "type": "object", + }, + "User": { + "properties": { + "email": { + "title": "Email", + "type": "string", + }, + "username": { + "title": "Username", + "type": "string", + }, + }, + "required": [ + "username", + "email", + ], + "title": "User", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/items/": { + "get": { + "operationId": "get_items", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item", + }, + "title": "Response Items-Get Items", + "type": "array", + }, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Get Items", + "tags": [ + "items", + ], + }, + "post": { + "operationId": "create_item", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage", + }, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Create Item", + "tags": [ + "items", + ], + }, + }, + "/users/": { + "post": { + "operationId": "create_user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage", + }, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Create User", + "tags": [ + "users", + ], + }, + }, + }, + } diff --git a/tests/test_tutorial/test_graphql/__init__.py b/tests/test_tutorial/test_graphql/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_graphql/test_tutorial001.py b/tests/test_tutorial/test_graphql/test_tutorial001.py new file mode 100644 index 000000000..9ba7147b5 --- /dev/null +++ b/tests/test_tutorial/test_graphql/test_tutorial001.py @@ -0,0 +1,70 @@ +import warnings + +import pytest +from starlette.testclient import TestClient + +warnings.filterwarnings( + "ignore", + message=r"The 'lia' package has been renamed to 'cross_web'\..*", + category=DeprecationWarning, +) + +from docs_src.graphql_.tutorial001_py39 import app # noqa: E402 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + return TestClient(app) + + +def test_query(client: TestClient): + response = client.post("/graphql", json={"query": "{ user { name, age } }"}) + assert response.status_code == 200 + assert response.json() == {"data": {"user": {"name": "Patrick", "age": 100}}} + + +def test_openapi(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/graphql": { + "get": { + "operationId": "handle_http_get_graphql_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "The GraphiQL integrated development environment.", + }, + "404": { + "description": "Not found if GraphiQL or query via GET are not enabled.", + }, + }, + "summary": "Handle Http Get", + }, + "post": { + "operationId": "handle_http_post_graphql_post", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Handle Http Post", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial002.py similarity index 61% rename from tests/test_tutorial/test_custom_response/test_tutorial004.py rename to tests/test_tutorial/test_metadata/test_tutorial002.py index 0e7d69791..e2814c88f 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial004.py +++ b/tests/test_tutorial/test_metadata/test_tutorial002.py @@ -1,45 +1,41 @@ from fastapi.testclient import TestClient -from docs_src.custom_response.tutorial004_py39 import app +from docs_src.metadata.tutorial002_py39 import app client = TestClient(app) -html_contents = """ - - - Some HTML in here - - -

Look ma! HTML!

- - - """ - - -def test_get_custom_response(): +def test_items(): response = client.get("/items/") assert response.status_code == 200, response.text - assert response.text == html_contents + assert response.json() == [{"name": "Foo"}] -def test_openapi_schema(): +def test_get_openapi_json_default_url(): response = client.get("/openapi.json") + assert response.status_code == 404, response.text + + +def test_openapi_schema(): + response = client.get("/api/v1/openapi.json") assert response.status_code == 200, response.text assert response.json() == { "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, "paths": { "/items/": { "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", "responses": { "200": { "description": "Successful Response", - "content": {"text/html": {"schema": {"type": "string"}}}, + "content": {"application/json": {"schema": {}}}, } }, - "summary": "Read Items", - "operationId": "read_items_items__get", } } }, diff --git a/tests/test_tutorial/test_metadata/test_tutorial003.py b/tests/test_tutorial/test_metadata/test_tutorial003.py new file mode 100644 index 000000000..085c271cd --- /dev/null +++ b/tests/test_tutorial/test_metadata/test_tutorial003.py @@ -0,0 +1,53 @@ +from fastapi.testclient import TestClient + +from docs_src.metadata.tutorial003_py39 import app + +client = TestClient(app) + + +def test_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [{"name": "Foo"}] + + +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": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } + + +def test_swagger_ui_default_url(): + response = client.get("/docs") + assert response.status_code == 404, response.text + + +def test_swagger_ui_custom_url(): + response = client.get("/documentation") + assert response.status_code == 200, response.text + assert "FastAPI - Swagger UI" in response.text + + +def test_redoc_ui_default_url(): + response = client.get("/redoc") + assert response.status_code == 404, response.text diff --git a/tests/test_tutorial/test_middleware/__init__.py b/tests/test_tutorial/test_middleware/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_middleware/test_tutorial001.py b/tests/test_tutorial/test_middleware/test_tutorial001.py new file mode 100644 index 000000000..cbcfd4146 --- /dev/null +++ b/tests/test_tutorial/test_middleware/test_tutorial001.py @@ -0,0 +1,24 @@ +from fastapi.testclient import TestClient + +from docs_src.middleware.tutorial001_py39 import app + +client = TestClient(app) + + +def test_response_headers(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert "X-Process-Time" in response.headers + + +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": {}, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py new file mode 100644 index 000000000..085d1f5e1 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py @@ -0,0 +1,186 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_operation_configuration.{request.param}" + ) + return TestClient(mod.app) + + +def test_post_items(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": ["bar", "baz"], + }, + ) + assert response.status_code == 201, response.text + assert response.json() == { + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": IsList("bar", "baz", check_order=False), + } + + +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": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + "tags": { + "default": [], + "items": { + "type": "string", + }, + "title": "Tags", + "type": "array", + "uniqueItems": True, + }, + "tax": { + "anyOf": [ + { + "type": "number", + }, + { + "type": "null", + }, + ], + "title": "Tax", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py new file mode 100644 index 000000000..c7414d756 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py @@ -0,0 +1,223 @@ +import importlib + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_operation_configuration.{request.param}" + ) + return TestClient(mod.app) + + +def test_post_items(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": ["bar", "baz"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": IsList("bar", "baz", check_order=False), + } + + +def test_get_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [{"name": "Foo", "price": 42}] + + +def test_get_users(client: TestClient): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == [{"username": "johndoe"}] + + +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": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + }, + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + "tags": { + "default": [], + "items": { + "type": "string", + }, + "title": "Tags", + "type": "array", + "uniqueItems": True, + }, + "tax": { + "anyOf": [ + { + "type": "number", + }, + { + "type": "null", + }, + ], + "title": "Tax", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py new file mode 100644 index 000000000..791db2462 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py @@ -0,0 +1,208 @@ +import importlib +from textwrap import dedent + +import pytest +from dirty_equals import IsList +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +DESCRIPTIONS = { + "tutorial003": "Create an item with all the information, name, description, price, tax and a set of unique tags", + "tutorial004": dedent(""" + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """).strip(), +} + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_mod_name(request: pytest.FixtureRequest) -> str: + return request.param + + +@pytest.fixture(name="client") +def get_client(mod_name: str) -> TestClient: + mod = importlib.import_module(f"docs_src.path_operation_configuration.{mod_name}") + return TestClient(mod.app) + + +def test_post_items(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": ["bar", "baz"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": "Item description", + "price": 42.0, + "tax": 3.2, + "tags": IsList("bar", "baz", check_order=False), + } + + +def test_openapi_schema(client: TestClient, mod_name: str): + mod_name = mod_name[:11] + + 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": { + "/items/": { + "post": { + "summary": "Create an item", + "description": DESCRIPTIONS[mod_name], + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "Item": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Description", + }, + "name": { + "title": "Name", + "type": "string", + }, + "price": { + "title": "Price", + "type": "number", + }, + "tags": { + "default": [], + "items": { + "type": "string", + }, + "title": "Tags", + "type": "array", + "uniqueItems": True, + }, + "tax": { + "anyOf": [ + { + "type": "number", + }, + { + "type": "null", + }, + ], + "title": "Tax", + }, + }, + "required": [ + "name", + "price", + ], + "title": "Item", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial001.py b/tests/test_tutorial/test_path_params/test_tutorial001.py new file mode 100644 index 000000000..a898e386f --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial001.py @@ -0,0 +1,116 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial001_py39 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + ("item_id", "expected_response"), + [ + (1, {"item_id": "1"}), + ("alice", {"item_id": "alice"}), + ], +) +def test_get_items(item_id, expected_response): + response = client.get(f"/items/{item_id}") + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +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": { + "/items/{item_id}": { + "get": { + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial002.py b/tests/test_tutorial/test_path_params/test_tutorial002.py new file mode 100644 index 000000000..0bfc9f807 --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial002.py @@ -0,0 +1,124 @@ +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial002_py39 import app + +client = TestClient(app) + + +def test_get_items(): + response = client.get("/items/1") + assert response.status_code == 200, response.text + assert response.json() == {"item_id": 1} + + +def test_get_items_invalid_id(): + response = client.get("/items/item1") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "input": "item1", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +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": { + "/items/{item_id}": { + "get": { + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "in": "path", + "name": "item_id", + "required": True, + "schema": { + "title": "Item Id", + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read Item", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial003.py b/tests/test_tutorial/test_path_params/test_tutorial003.py new file mode 100644 index 000000000..cd2c39ab0 --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial003.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial003_py39 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + ("user_id", "expected_response"), + [ + ("me", {"user_id": "the current user"}), + ("alice", {"user_id": "alice"}), + ], +) +def test_get_users(user_id: str, expected_response: dict): + response = client.get(f"/users/{user_id}") + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +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": { + "/users/me": { + "get": { + "operationId": "read_user_me_users_me_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Read User Me", + }, + }, + "/users/{user_id}": { + "get": { + "operationId": "read_user_users__user_id__get", + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": True, + "schema": { + "title": "User Id", + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + "summary": "Read User", + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial003b.py b/tests/test_tutorial/test_path_params/test_tutorial003b.py new file mode 100644 index 000000000..8e4a26a1c --- /dev/null +++ b/tests/test_tutorial/test_path_params/test_tutorial003b.py @@ -0,0 +1,44 @@ +import asyncio + +from fastapi.testclient import TestClient + +from docs_src.path_params.tutorial003b_py39 import app, read_users2 + +client = TestClient(app) + + +def test_get_users(): + response = client.get("/users") + assert response.status_code == 200, response.text + assert response.json() == ["Rick", "Morty"] + + +def test_read_users2(): # Just for coverage + assert asyncio.run(read_users2()) == ["Bean", "Elfo"] + + +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": { + "/users": { + "get": { + "operationId": "read_users2_users_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Read Users2", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/__init__.py b/tests/test_tutorial/test_path_params_numeric_validations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py new file mode 100644 index 000000000..f1e304103 --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py @@ -0,0 +1,164 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_an_py39"), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/42", {"item_id": 42}), + ("/items/123?item-query=somequery", {"item_id": 123, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_invalid_item_id(client: TestClient): + response = client.get("/items/invalid_id") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +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": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "title": "Item-Query", + }, + "name": "item-query", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py new file mode 100644 index 000000000..467c915dc --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py @@ -0,0 +1,170 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/42?q=", {"item_id": 42}), + ("/items/123?q=somequery", {"item_id": 123, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_invalid_item_id(client: TestClient): + response = client.get("/items/invalid_id?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_read_items_missing_q(client: TestClient): + response = client.get("/items/42") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "q"], + "input": None, + "msg": "Field required", + "type": "missing", + } + ] + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py new file mode 100644 index 000000000..d3593c984 --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py @@ -0,0 +1,185 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/42?q=", {"item_id": 42}), + ("/items/1?q=somequery", {"item_id": 1, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_non_int_item_id(client: TestClient): + response = client.get("/items/invalid_id?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_read_items_item_id_less_than_one(client: TestClient): + response = client.get("/items/0?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "0", + "msg": "Input should be greater than or equal to 1", + "type": "greater_than_equal", + "ctx": {"ge": 1}, + } + ] + } + + +def test_read_items_missing_q(client: TestClient): + response = client.get("/items/42") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "q"], + "input": None, + "msg": "Field required", + "type": "missing", + } + ] + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + "minimum": 1, + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py new file mode 100644 index 000000000..296192593 --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py @@ -0,0 +1,202 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ("/items/1?q=", {"item_id": 1}), + ("/items/1000?q=somequery", {"item_id": 1000, "q": "somequery"}), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_non_int_item_id(client: TestClient): + response = client.get("/items/invalid_id?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "invalid_id", + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + } + ] + } + + +def test_read_items_item_id_less_than_one(client: TestClient): + response = client.get("/items/0?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "0", + "msg": "Input should be greater than 0", + "type": "greater_than", + "ctx": {"gt": 0}, + } + ] + } + + +def test_read_items_item_id_greater_than_one_thousand(client: TestClient): + response = client.get("/items/1001?q=somequery") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "1001", + "msg": "Input should be less than or equal to 1000", + "type": "less_than_equal", + "ctx": {"le": 1000}, + } + ] + } + + +def test_read_items_missing_q(client: TestClient): + response = client.get("/items/42") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "q"], + "input": None, + "msg": "Field required", + "type": "missing", + } + ] + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 1000, + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py new file mode 100644 index 000000000..9dc7d7aac --- /dev/null +++ b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py @@ -0,0 +1,221 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module( + f"docs_src.path_params_numeric_validations.{request.param}" + ) + return TestClient(mod.app) + + +@pytest.mark.parametrize( + "path,expected_response", + [ + ( + "/items/0?q=&size=0.1", + {"item_id": 0, "size": 0.1}, + ), + ( + "/items/1000?q=somequery&size=10.4", + {"item_id": 1000, "q": "somequery", "size": 10.4}, + ), + ], +) +def test_read_items(client: TestClient, path, expected_response): + response = client.get(path) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_read_items_item_id_less_than_zero(client: TestClient): + response = client.get("/items/-1?q=somequery&size=5") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "-1", + "msg": "Input should be greater than or equal to 0", + "type": "greater_than_equal", + "ctx": {"ge": 0}, + } + ] + } + + +def test_read_items_item_id_greater_than_one_thousand(client: TestClient): + response = client.get("/items/1001?q=somequery&size=5") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["path", "item_id"], + "input": "1001", + "msg": "Input should be less than or equal to 1000", + "type": "less_than_equal", + "ctx": {"le": 1000}, + } + ] + } + + +def test_read_items_size_too_small(client: TestClient): + response = client.get("/items/1?q=somequery&size=0.0") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "size"], + "input": "0.0", + "msg": "Input should be greater than 0", + "type": "greater_than", + "ctx": {"gt": 0.0}, + } + ] + } + + +def test_read_items_size_too_large(client: TestClient): + response = client.get("/items/1?q=somequery&size=10.5") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "size"], + "input": "10.5", + "msg": "Input should be less than 10.5", + "type": "less_than", + "ctx": {"lt": 10.5}, + } + ] + } + + +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": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "type": "integer", + "minimum": 0, + "maximum": 1000, + }, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": { + "type": "string", + "title": "Q", + }, + "name": "q", + "in": "query", + }, + { + "in": "query", + "name": "size", + "required": True, + "schema": { + "exclusiveMaximum": 10.5, + "exclusiveMinimum": 0, + "title": "Size", + "type": "number", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {}, + } + }, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + "title": "Detail", + "type": "array", + }, + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "integer", + }, + ], + }, + "title": "Location", + "type": "array", + }, + "msg": { + "title": "Message", + "type": "string", + }, + "type": { + "title": "Error Type", + "type": "string", + }, + }, + "required": [ + "loc", + "msg", + "type", + ], + "title": "ValidationError", + "type": "object", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_python_types/__init__.py b/tests/test_tutorial/test_python_types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py new file mode 100644 index 000000000..ccb096857 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py @@ -0,0 +1,18 @@ +import runpy +from unittest.mock import patch + +import pytest + + +@pytest.mark.parametrize( + "module_name", + [ + "tutorial001_py39", + "tutorial002_py39", + ], +) +def test_run_module(module_name: str): + with patch("builtins.print") as mock_print: + runpy.run_module(f"docs_src.python_types.{module_name}", run_name="__main__") + + mock_print.assert_called_with("John Doe") diff --git a/tests/test_tutorial/test_python_types/test_tutorial003.py b/tests/test_tutorial/test_python_types/test_tutorial003.py new file mode 100644 index 000000000..34d264917 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial003.py @@ -0,0 +1,12 @@ +import pytest + +from docs_src.python_types.tutorial003_py39 import get_name_with_age + + +def test_get_name_with_age_pass_int(): + with pytest.raises(TypeError): + get_name_with_age("John", 30) + + +def test_get_name_with_age_pass_str(): + assert get_name_with_age("John", "30") == "John is this old: 30" diff --git a/tests/test_tutorial/test_python_types/test_tutorial004.py b/tests/test_tutorial/test_python_types/test_tutorial004.py new file mode 100644 index 000000000..24af32883 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial004.py @@ -0,0 +1,5 @@ +from docs_src.python_types.tutorial004_py39 import get_name_with_age + + +def test_get_name_with_age_pass_int(): + assert get_name_with_age("John", 30) == "John is this old: 30" diff --git a/tests/test_tutorial/test_python_types/test_tutorial005.py b/tests/test_tutorial/test_python_types/test_tutorial005.py new file mode 100644 index 000000000..6d67ec471 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial005.py @@ -0,0 +1,12 @@ +from docs_src.python_types.tutorial005_py39 import get_items + + +def test_get_items(): + res = get_items( + "item_a", + "item_b", + "item_c", + "item_d", + "item_e", + ) + assert res == ("item_a", "item_b", "item_c", "item_d", "item_e") diff --git a/tests/test_tutorial/test_python_types/test_tutorial006.py b/tests/test_tutorial/test_python_types/test_tutorial006.py new file mode 100644 index 000000000..50976926e --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial006.py @@ -0,0 +1,16 @@ +from unittest.mock import patch + +from docs_src.python_types.tutorial006_py39 import process_items + + +def test_process_items(): + with patch("builtins.print") as mock_print: + process_items(["item_a", "item_b", "item_c"]) + + assert mock_print.call_count == 3 + call_args = [arg.args for arg in mock_print.call_args_list] + assert call_args == [ + ("item_a",), + ("item_b",), + ("item_c",), + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial007.py b/tests/test_tutorial/test_python_types/test_tutorial007.py new file mode 100644 index 000000000..c04529465 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial007.py @@ -0,0 +1,8 @@ +from docs_src.python_types.tutorial007_py39 import process_items + + +def test_process_items(): + items_t = (1, 2, "foo") + items_s = {b"a", b"b", b"c"} + + assert process_items(items_t, items_s) == (items_t, items_s) diff --git a/tests/test_tutorial/test_python_types/test_tutorial008.py b/tests/test_tutorial/test_python_types/test_tutorial008.py new file mode 100644 index 000000000..33cf6cbfb --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial008.py @@ -0,0 +1,17 @@ +from unittest.mock import patch + +from docs_src.python_types.tutorial008_py39 import process_items + + +def test_process_items(): + with patch("builtins.print") as mock_print: + process_items({"a": 1.0, "b": 2.5}) + + assert mock_print.call_count == 4 + call_args = [arg.args for arg in mock_print.call_args_list] + assert call_args == [ + ("a",), + (1.0,), + ("b",), + (2.5,), + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial008b.py b/tests/test_tutorial/test_python_types/test_tutorial008b.py new file mode 100644 index 000000000..1ef0d4ea1 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial008b.py @@ -0,0 +1,27 @@ +import importlib +from types import ModuleType +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial008b_py39"), + pytest.param("tutorial008b_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.python_types.{request.param}") + return mod + + +def test_process_items(module: ModuleType): + with patch("builtins.print") as mock_print: + module.process_item("a") + + assert mock_print.call_count == 1 + mock_print.assert_called_with("a") diff --git a/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py b/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py new file mode 100644 index 000000000..34046c5c4 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py @@ -0,0 +1,33 @@ +import importlib +from types import ModuleType +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial009_py39"), + pytest.param("tutorial009_py310", marks=needs_py310), + pytest.param("tutorial009b_py39"), + ], +) +def get_module(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.python_types.{request.param}") + return mod + + +def test_say_hi(module: ModuleType): + with patch("builtins.print") as mock_print: + module.say_hi("FastAPI") + module.say_hi() + + assert mock_print.call_count == 2 + call_args = [arg.args for arg in mock_print.call_args_list] + assert call_args == [ + ("Hey FastAPI!",), + ("Hello World",), + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial009c.py b/tests/test_tutorial/test_python_types/test_tutorial009c.py new file mode 100644 index 000000000..7bd404911 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial009c.py @@ -0,0 +1,33 @@ +import importlib +import re +from types import ModuleType +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + pytest.param("tutorial009c_py39"), + pytest.param("tutorial009c_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.python_types.{request.param}") + return mod + + +def test_say_hi(module: ModuleType): + with patch("builtins.print") as mock_print: + module.say_hi("FastAPI") + + mock_print.assert_called_once_with("Hey FastAPI!") + + with pytest.raises( + TypeError, + match=re.escape("say_hi() missing 1 required positional argument: 'name'"), + ): + module.say_hi() diff --git a/tests/test_tutorial/test_python_types/test_tutorial010.py b/tests/test_tutorial/test_python_types/test_tutorial010.py new file mode 100644 index 000000000..9e4d2e36b --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial010.py @@ -0,0 +1,5 @@ +from docs_src.python_types.tutorial010_py39 import Person, get_person_name + + +def test_get_person_name(): + assert get_person_name(Person("John Doe")) == "John Doe" diff --git a/tests/test_tutorial/test_python_types/test_tutorial011.py b/tests/test_tutorial/test_python_types/test_tutorial011.py new file mode 100644 index 000000000..a05751b97 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial011.py @@ -0,0 +1,25 @@ +import runpy +from unittest.mock import patch + +import pytest + +from ...utils import needs_py310 + + +@pytest.mark.parametrize( + "module_name", + [ + pytest.param("tutorial011_py39"), + pytest.param("tutorial011_py310", marks=needs_py310), + ], +) +def test_run_module(module_name: str): + with patch("builtins.print") as mock_print: + runpy.run_module(f"docs_src.python_types.{module_name}", run_name="__main__") + + assert mock_print.call_count == 2 + call_args = [str(arg.args[0]) for arg in mock_print.call_args_list] + assert call_args == [ + "id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]", + "123", + ] diff --git a/tests/test_tutorial/test_python_types/test_tutorial012.py b/tests/test_tutorial/test_python_types/test_tutorial012.py new file mode 100644 index 000000000..e57804820 --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial012.py @@ -0,0 +1,7 @@ +from docs_src.python_types.tutorial012_py39 import User + + +def test_user(): + user = User(name="John Doe", age=30) + assert user.name == "John Doe" + assert user.age == 30 diff --git a/tests/test_tutorial/test_python_types/test_tutorial013.py b/tests/test_tutorial/test_python_types/test_tutorial013.py new file mode 100644 index 000000000..5602ef76f --- /dev/null +++ b/tests/test_tutorial/test_python_types/test_tutorial013.py @@ -0,0 +1,5 @@ +from docs_src.python_types.tutorial013_py39 import say_hello + + +def test_say_hello(): + assert say_hello("FastAPI") == "Hello FastAPI" diff --git a/tests/test_tutorial/test_query_params/test_tutorial001.py b/tests/test_tutorial/test_query_params/test_tutorial001.py new file mode 100644 index 000000000..4c92b57b8 --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial001.py @@ -0,0 +1,126 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/items/", + [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}], + ), + ( + "/items/?skip=1", + [{"item_name": "Bar"}, {"item_name": "Baz"}], + ), + ( + "/items/?skip=1&limit=1", + [{"item_name": "Bar"}], + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Item", + "operationId": "read_item_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 10, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial002.py b/tests/test_tutorial/test_query_params/test_tutorial002.py new file mode 100644 index 000000000..ae3ee7613 --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial002.py @@ -0,0 +1,127 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/items/foo", + {"item_id": "foo"}, + ), + ( + "/items/bar?q=somequery", + {"item_id": "bar", "q": "somequery"}, + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial003.py b/tests/test_tutorial/test_query_params/test_tutorial003.py new file mode 100644 index 000000000..c0b7e3b13 --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial003.py @@ -0,0 +1,148 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/items/foo", + { + "item_id": "foo", + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/items/bar?q=somequery", + { + "item_id": "bar", + "q": "somequery", + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/items/baz?short=true", + {"item_id": "baz"}, + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Short", + "type": "boolean", + "default": False, + }, + "name": "short", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial004.py b/tests/test_tutorial/test_query_params/test_tutorial004.py new file mode 100644 index 000000000..9be18b74d --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial004.py @@ -0,0 +1,156 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") + + client = TestClient(mod.app) + return client + + +@pytest.mark.parametrize( + ("path", "expected_json"), + [ + ( + "/users/123/items/foo", + { + "item_id": "foo", + "owner_id": 123, + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/users/1/items/bar?q=somequery", + { + "item_id": "bar", + "owner_id": 1, + "q": "somequery", + "description": "This is an amazing item that has a long description", + }, + ), + ( + "/users/42/items/baz?short=true", + {"item_id": "baz", "owner_id": 42}, + ), + ], +) +def test_read_user_item(client: TestClient, path, expected_json): + response = client.get(path) + assert response.status_code == 200 + assert response.json() == expected_json + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/{user_id}/items/{item_id}": { + "get": { + "summary": "Read User Item", + "operationId": "read_user_item_users__user_id__items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": { + "title": "Q", + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Short", + "type": "boolean", + "default": False, + }, + "name": "short", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError", + }, + }, + }, + "description": "Validation Error", + }, + }, + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py new file mode 100644 index 000000000..f1af7e08c --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py @@ -0,0 +1,121 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_empty_str(client: TestClient): + response = client.get("/items/", params={"q": ""}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py new file mode 100644 index 000000000..62018b80b --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py @@ -0,0 +1,142 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_empty_str(client: TestClient): + response = client.get("/items/", params={"q": ""}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_too_long(client: TestClient): + response = client.get("/items/", params={"q": "q" * 51}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["query", "q"], + "msg": "String should have at most 50 characters", + "input": "q" * 51, + "ctx": {"max_length": 50}, + } + ] + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 50, + }, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py new file mode 100644 index 000000000..a4ad7a63b --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py @@ -0,0 +1,153 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial003_an_py39"), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_too_short(client: TestClient): + response = client.get("/items/", params={"q": "qu"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "qu", + "ctx": {"min_length": 3}, + } + ] + } + + +def test_query_params_str_validations_q_too_long(client: TestClient): + response = client.get("/items/", params={"q": "q" * 51}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["query", "q"], + "msg": "String should have at most 50 characters", + "input": "q" * 51, + "ctx": {"max_length": 50}, + } + ] + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + }, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py new file mode 100644 index 000000000..95efab2dc --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py @@ -0,0 +1,147 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + pytest.param("tutorial004_an_py39"), + pytest.param("tutorial004_an_py310", marks=needs_py310), + pytest.param( + "tutorial004_regex_an_py310", + marks=( + needs_py310, + pytest.mark.filterwarnings( + "ignore:`regex` has been deprecated, please use `pattern` instead:DeprecationWarning" + ), + ), + ), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_nonregexquery(client: TestClient): + response = client.get("/items/", params={"q": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py new file mode 100644 index 000000000..52462fe33 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py @@ -0,0 +1,131 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "type": "string", + "default": "fixedquery", + "minLength": 3, + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py new file mode 100644 index 000000000..640cedce1 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py @@ -0,0 +1,136 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": { + "type": "string", + "minLength": 3, + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py new file mode 100644 index 000000000..f287b5dcd --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py @@ -0,0 +1,148 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial006c_py39"), + pytest.param("tutorial006c_py310", marks=needs_py310), + pytest.param("tutorial006c_an_py39"), + pytest.param("tutorial006c_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + client = TestClient(mod.app) + return client + + +@pytest.mark.xfail( + reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419" +) +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { # pragma: no cover + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + } + + +@pytest.mark.xfail( + reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419" +) +def test_query_params_str_validations_empty_str(client: TestClient): + response = client.get("/items/?q=") + assert response.status_code == 200 + assert response.json() == { # pragma: no cover + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + } + + +def test_query_params_str_validations_q_query(client: TestClient): + response = client.get("/items/", params={"q": "query"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "query", + } + + +def test_query_params_str_validations_q_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": { + "anyOf": [ + {"type": "string", "minLength": 3}, + {"type": "null"}, + ], + "title": "Q", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py new file mode 100644 index 000000000..b17bc2771 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py @@ -0,0 +1,136 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + pytest.param("tutorial007_py310", marks=needs_py310), + pytest.param("tutorial007_an_py39"), + pytest.param("tutorial007_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + }, + {"type": "null"}, + ], + "title": "Query string", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py new file mode 100644 index 000000000..c63111574 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py @@ -0,0 +1,138 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial008_py39"), + pytest.param("tutorial008_py310", marks=needs_py310), + pytest.param("tutorial008_an_py39"), + pytest.param("tutorial008_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient): + response = client.get("/items/", params={"q": "fa"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["query", "q"], + "msg": "String should have at least 3 characters", + "input": "fa", + "ctx": {"min_length": 3}, + } + ] + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py new file mode 100644 index 000000000..7e9d69d41 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py @@ -0,0 +1,123 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial009_py39"), + pytest.param("tutorial009_py310", marks=needs_py310), + pytest.param("tutorial009_an_py39"), + pytest.param("tutorial009_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "schema": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Item-Query", + }, + "required": False, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_directly/test_tutorial002.py b/tests/test_tutorial/test_response_directly/test_tutorial002.py new file mode 100644 index 000000000..ef8457572 --- /dev/null +++ b/tests/test_tutorial/test_response_directly/test_tutorial002.py @@ -0,0 +1,65 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_directly.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_path_operation(client: TestClient): + expected_content = """ + +
+ Apply shampoo here. +
+ + You'll have to use soap here. + +
+ """ + + response = client.get("/legacy/") + assert response.status_code == 200, response.text + assert response.headers["content-type"] == "application/xml" + assert response.text == expected_content + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "info": { + "title": "FastAPI", + "version": "0.1.0", + }, + "openapi": "3.1.0", + "paths": { + "/legacy/": { + "get": { + "operationId": "get_legacy_data_legacy__get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + }, + }, + "description": "Successful Response", + }, + }, + "summary": "Get Legacy Data", + }, + }, + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py new file mode 100644 index 000000000..10692f990 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py @@ -0,0 +1,193 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_01_py39"), + pytest.param("tutorial001_01_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_read_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": None, + "price": 42.0, + "tags": [], + "tax": None, + }, + { + "name": "Plumbus", + "description": None, + "price": 32.0, + "tags": [], + "tax": None, + }, + ] + + +def test_create_item(client: TestClient): + item_data = { + "name": "Test Item", + "description": "A test item", + "price": 10.5, + "tax": 1.5, + "tags": ["test", "item"], + } + response = client.post("/items/", json=item_data) + assert response.status_code == 200, response.text + assert response.json() == item_data + + +def test_create_item_only_required(client: TestClient): + response = client.post( + "/items/", + json={ + "name": "Test Item", + "price": 10.5, + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Test Item", + "price": 10.5, + "description": None, + "tax": None, + "tags": [], + } + + +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": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "title": "Response Read Items Items Get", + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + }, + }, + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial002.py b/tests/test_tutorial/test_response_model/test_tutorial002.py new file mode 100644 index 000000000..216d4c420 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial002.py @@ -0,0 +1,129 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_user(client: TestClient): + user_data = { + "username": "foo", + "password": "fighter", + "email": "foo@example.com", + "full_name": "Grave Dohl", + } + response = client.post( + "/user/", + json=user_data, + ) + assert response.status_code == 200, response.text + assert response.json() == user_data + + +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": { + "/user/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "UserIn": { + "title": "UserIn", + "required": ["username", "password", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_status_code/__init__.py b/tests/test_tutorial/test_response_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py new file mode 100644 index 000000000..ddf55a045 --- /dev/null +++ b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py @@ -0,0 +1,96 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial002_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_status_code.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_create_item(client: TestClient): + response = client.post("/items/", params={"name": "Test Item"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Test Item"} + + +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": { + "/items/": { + "post": { + "parameters": [ + { + "name": "name", + "in": "query", + "required": True, + "schema": {"title": "Name", "type": "string"}, + } + ], + "summary": "Create Item", + "operationId": "create_item_items__post", + "responses": { + "201": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py new file mode 100644 index 000000000..4f5240860 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py @@ -0,0 +1,141 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(client: TestClient): + 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/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "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", + }, + "Item": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "examples": ["Foo"], + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + "examples": ["A very nice Item"], + }, + "price": { + "type": "number", + "title": "Price", + "examples": [35.4], + }, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + "examples": [3.2], + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py new file mode 100644 index 000000000..3529a9bf0 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py @@ -0,0 +1,143 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_py310", marks=needs_py310), + pytest.param("tutorial003_an_py39"), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(client: TestClient): + 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/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + } + }, + }, + "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", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"type": "number", "title": "Price"}, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial002.py b/tests/test_tutorial/test_security/test_tutorial002.py new file mode 100644 index 000000000..85c076b1d --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial002.py @@ -0,0 +1,71 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") + client = TestClient(mod.app) + return client + + +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "testtokenfakedecoded", + "email": "john@example.com", + "full_name": "John Doe", + "disabled": None, + } + + +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": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial004.py b/tests/test_tutorial/test_security/test_tutorial004.py new file mode 100644 index 000000000..b5e3d39ef --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial004.py @@ -0,0 +1,363 @@ +import importlib +from types import ModuleType +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_py310", marks=needs_py310), + pytest.param("tutorial004_an_py39"), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") + + return mod + + +def get_access_token(*, username="johndoe", password="secret", client: TestClient): + data = {"username": username, "password": password} + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +def test_login(mod: ModuleType): + client = TestClient(mod.app) + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +def test_login_incorrect_password(mod: ModuleType): + client = TestClient(mod.app) + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(mod: ModuleType): + client = TestClient(mod.app) + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(mod: ModuleType): + client = TestClient(mod.app) + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +def test_incorrect_token(mod: ModuleType): + client = TestClient(mod.app) + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_incorrect_token_type(mod: ModuleType): + client = TestClient(mod.app) + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_verify_password(mod: ModuleType): + assert mod.verify_password( + "secret", mod.fake_users_db["johndoe"]["hashed_password"] + ) + + +def test_get_password_hash(mod: ModuleType): + assert mod.get_password_hash("johndoe") + + +def test_create_access_token(mod: ModuleType): + access_token = mod.create_access_token(data={"data": "foo"}) + assert access_token + + +def test_token_no_sub(mod: ModuleType): + client = TestClient(mod.app) + + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token_no_username(mod: ModuleType): + client = TestClient(mod.app) + + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token_nonexistent_user(mod: ModuleType): + client = TestClient(mod.app) + + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token_inactive_user(mod: ModuleType): + client = TestClient(mod.app) + alice_user_data = { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": mod.get_password_hash("secretalice"), + "disabled": True, + } + with patch.dict(f"{mod.__name__}.fake_users_db", {"alice": alice_user_data}): + access_token = get_access_token( + username="alice", password="secretalice", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +def test_read_items(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +def test_openapi_schema(mod: ModuleType): + client = TestClient(mod.app) + 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": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "disabled": { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + }, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "anyOf": [ + {"pattern": "^password$", "type": "string"}, + {"type": "null"}, + ], + }, + "username": {"title": "Username", "type": "string"}, + "password": { + "title": "Password", + "type": "string", + "format": "password", + }, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + "format": "password", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": {}, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial007.py b/tests/test_tutorial/test_security/test_tutorial007.py new file mode 100644 index 000000000..28b70a2d4 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial007.py @@ -0,0 +1,89 @@ +import importlib +from base64 import b64encode + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial007_py39"), + pytest.param("tutorial007_an_py39"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") + return TestClient(mod.app) + + +def test_security_http_basic(client: TestClient): + response = client.get("/users/me", auth=("stanleyjobson", "swordfish")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "stanleyjobson"} + + +def test_security_http_basic_no_credentials(client: TestClient): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_credentials(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Not authenticated"} + + +def test_security_http_basic_non_basic_credentials(client: TestClient): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + response = client.get("/users/me", headers={"Authorization": auth_header}) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Not authenticated"} + + +def test_security_http_basic_invalid_username(client: TestClient): + response = client.get("/users/me", auth=("alice", "swordfish")) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_password(client: TestClient): + response = client.get("/users/me", auth=("stanleyjobson", "wrongpassword")) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Incorrect username or password"} + assert response.headers["WWW-Authenticate"] == "Basic" + + +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": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/tests/test_tutorial/test_settings/test_app01.py b/tests/test_tutorial/test_settings/test_app01.py new file mode 100644 index 000000000..0c5e440f1 --- /dev/null +++ b/tests/test_tutorial/test_settings/test_app01.py @@ -0,0 +1,78 @@ +import importlib +import sys + +import pytest +from dirty_equals import IsAnyStr +from fastapi.testclient import TestClient +from pydantic import ValidationError +from pytest import MonkeyPatch + + +@pytest.fixture( + name="mod_name", + params=[ + pytest.param("app01_py39"), + ], +) +def get_mod_name(request: pytest.FixtureRequest): + return f"docs_src.settings.{request.param}.main" + + +@pytest.fixture(name="client") +def get_test_client(mod_name: str, monkeypatch: MonkeyPatch) -> TestClient: + if mod_name in sys.modules: + del sys.modules[mod_name] + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + main_mod = importlib.import_module(mod_name) + return TestClient(main_mod.app) + + +def test_settings_validation_error(mod_name: str, monkeypatch: MonkeyPatch): + monkeypatch.delenv("ADMIN_EMAIL", raising=False) + if mod_name in sys.modules: + del sys.modules[mod_name] # pragma: no cover + + with pytest.raises(ValidationError) as exc_info: + importlib.import_module(mod_name) + assert exc_info.value.errors() == [ + { + "loc": ("admin_email",), + "msg": "Field required", + "type": "missing", + "input": {}, + "url": IsAnyStr, + } + ] + + +def test_app(client: TestClient): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } + + +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": { + "/info": { + "get": { + "operationId": "info_info_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Info", + } + } + }, + } diff --git a/tests/test_tutorial/test_static_files/__init__.py b/tests/test_tutorial/test_static_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_static_files/test_tutorial001.py b/tests/test_tutorial/test_static_files/test_tutorial001.py new file mode 100644 index 000000000..4fbf19ae8 --- /dev/null +++ b/tests/test_tutorial/test_static_files/test_tutorial001.py @@ -0,0 +1,40 @@ +import os +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture(scope="module") +def client(): + static_dir: Path = Path(os.getcwd()) / "static" + static_dir.mkdir(exist_ok=True) + sample_file = static_dir / "sample.txt" + sample_file.write_text("This is a sample static file.") + from docs_src.static_files.tutorial001_py39 import app + + with TestClient(app) as client: + yield client + sample_file.unlink() + static_dir.rmdir() + + +def test_static_files(client: TestClient): + response = client.get("/static/sample.txt") + assert response.status_code == 200, response.text + assert response.text == "This is a sample static file." + + +def test_static_files_not_found(client: TestClient): + response = client.get("/static/non_existent_file.txt") + assert response.status_code == 404, response.text + + +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": {}, + } From 8cefc4b7cc9acff703f3e7327aba7274d83022c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 10:43:27 +0000 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bc2441f7f..11dae3333 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* ✅ Add missing tests for code examples. PR [#14569](https://github.com/fastapi/fastapi/pull/14569) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Remove `lint` job from `test` CI workflow. PR [#14593](https://github.com/fastapi/fastapi/pull/14593) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). * 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo). From d98f4eb56eae3155034fe631d9639d95171fb9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 03:36:58 -0800 Subject: [PATCH 09/13] =?UTF-8?q?=F0=9F=94=A7=20Update=20pre-commit=20to?= =?UTF-8?q?=20use=20local=20Ruff=20instead=20of=20hook=20(#14604)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77e06bd96..10a0949e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,15 +12,23 @@ repos: - --unsafe - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 - hooks: - - id: ruff - args: - - --fix - - id: ruff-format + - repo: local hooks: + - id: local-ruff-check + name: ruff check + entry: uv run ruff check --force-exclude --fix --exit-non-zero-on-fix + require_serial: true + language: unsupported + types: [python] + + - id: local-ruff-format + name: ruff format + entry: uv run ruff format --force-exclude --exit-non-zero-on-format + require_serial: true + language: unsupported + types: [python] + - id: add-permalinks-pages language: unsupported name: add-permalinks-pages @@ -28,18 +36,21 @@ repos: args: - --update-existing files: ^docs/en/docs/.*\.md$ + - id: generate-readme language: unsupported name: generate README.md from index.md entry: uv run ./scripts/docs.py generate-readme files: ^docs/en/docs/index\.md|docs/en/data/sponsors\.yml|scripts/docs\.py$ pass_filenames: false + - id: update-languages language: unsupported name: update languages entry: uv run ./scripts/docs.py update-languages files: ^docs/.*|scripts/docs\.py$ pass_filenames: false + - id: ensure-non-translated language: unsupported name: ensure non-translated files are not modified From 6b53786f626db95a42fd0f66afd360bab161f4b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 11:37:18 +0000 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 11dae3333..98fd67da2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* 🔧 Update pre-commit to use local Ruff instead of hook. PR [#14604](https://github.com/fastapi/fastapi/pull/14604) by [@tiangolo](https://github.com/tiangolo). * ✅ Add missing tests for code examples. PR [#14569](https://github.com/fastapi/fastapi/pull/14569) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Remove `lint` job from `test` CI workflow. PR [#14593](https://github.com/fastapi/fastapi/pull/14593) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). From 535b5daa317a9d1d1f9a1058e57650a7beefa861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 04:45:20 -0800 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=94=8A=20Add=20a=20custom=20`FastAP?= =?UTF-8?q?IDeprecationWarning`=20(#14605)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 4 ++-- fastapi/exceptions.py | 7 +++++++ fastapi/openapi/utils.py | 5 +++-- fastapi/params.py | 9 ++++---- fastapi/routing.py | 5 +++-- fastapi/temp_pydantic_v1_params.py | 9 ++++---- fastapi/utils.py | 5 +++-- tests/benchmarks/test_general_performance.py | 3 ++- tests/test_compat_params_v1.py | 9 ++++---- .../test_pydantic_v1_deprecation_warnings.py | 9 ++++---- tests/test_regex_deprecated_body.py | 3 ++- tests/test_regex_deprecated_params.py | 3 ++- tests/test_schema_extra_examples.py | 21 ++++++++++--------- .../test_tutorial002.py | 3 ++- .../test_tutorial003.py | 3 ++- .../test_tutorial004.py | 3 ++- .../test_tutorial004.py | 2 +- .../test_tutorial002_pv1.py | 3 ++- .../test_tutorial001_pv1.py | 3 ++- 19 files changed, 66 insertions(+), 43 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 39d0bd89c..af2bed9ad 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -51,7 +51,7 @@ from fastapi.concurrency import ( contextmanager_in_threadpool, ) from fastapi.dependencies.models import Dependant -from fastapi.exceptions import DependencyScopeError +from fastapi.exceptions import DependencyScopeError, FastAPIDeprecationWarning from fastapi.logger import logger from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey @@ -327,7 +327,7 @@ def get_dependant( warnings.warn( "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." f" Please update the param {param_name}: {param_details.type_annotation!r}.", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=5, ) if isinstance( diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 8e0c55902..53e505281 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -231,3 +231,10 @@ class ResponseValidationError(ValidationException): ) -> None: super().__init__(errors, endpoint_ctx=endpoint_ctx) self.body = body + + +class FastAPIDeprecationWarning(UserWarning): + """ + A custom deprecation warning as DeprecationWarning is ignored + Ref: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries + """ diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index a99d4188e..6180fcde6 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -23,6 +23,7 @@ from fastapi.dependencies.utils import ( get_validation_alias, ) from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX from fastapi.openapi.models import OpenAPI from fastapi.params import Body, ParamTypes @@ -215,9 +216,9 @@ def generate_operation_id( *, route: routing.APIRoute, method: str ) -> str: # pragma: nocover warnings.warn( - "fastapi.openapi.utils.generate_operation_id() was deprecated, " + message="fastapi.openapi.utils.generate_operation_id() was deprecated, " "it is not used internally, and will be removed soon", - DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=2, ) if route.operation_id: diff --git a/fastapi/params.py b/fastapi/params.py index c776c4a59..cc2934f44 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from enum import Enum from typing import Annotated, Any, Callable, Optional, Union +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.models import Example from pydantic.fields import FieldInfo from typing_extensions import Literal, deprecated @@ -75,7 +76,7 @@ class Param(FieldInfo): # type: ignore[misc] if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -105,7 +106,7 @@ class Param(FieldInfo): # type: ignore[misc] if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra @@ -530,7 +531,7 @@ class Body(FieldInfo): # type: ignore[misc] if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -560,7 +561,7 @@ class Body(FieldInfo): # type: ignore[misc] if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra diff --git a/fastapi/routing.py b/fastapi/routing.py index 2770e3253..3f78e93e8 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -47,6 +47,7 @@ from fastapi.dependencies.utils import ( from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( EndpointContext, + FastAPIDeprecationWarning, FastAPIError, RequestValidationError, ResponseValidationError, @@ -640,7 +641,7 @@ class APIRoute(routing.Route): warnings.warn( "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." f" Please update the response model {self.response_model!r}.", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.response_field = create_model_field( @@ -680,7 +681,7 @@ class APIRoute(routing.Route): warnings.warn( "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." f" In responses={{}}, please update {model}.", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) response_field = create_model_field( diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py index 1bda0ea9b..62230e42c 100644 --- a/fastapi/temp_pydantic_v1_params.py +++ b/fastapi/temp_pydantic_v1_params.py @@ -1,6 +1,7 @@ import warnings from typing import Annotated, Any, Callable, Optional, Union +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.models import Example from fastapi.params import ParamTypes from typing_extensions import deprecated @@ -63,7 +64,7 @@ class Param(FieldInfo): if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -93,7 +94,7 @@ class Param(FieldInfo): if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra @@ -503,7 +504,7 @@ class Body(FieldInfo): if example is not _Unset: warnings.warn( "`example` has been deprecated, please use `examples` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) self.example = example @@ -533,7 +534,7 @@ class Body(FieldInfo): if regex is not None: warnings.warn( "`regex` has been deprecated, please use `pattern` instead", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra diff --git a/fastapi/utils.py b/fastapi/utils.py index c4631d7ed..8ae50aa14 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -23,6 +23,7 @@ from fastapi._compat import ( may_v1, ) from fastapi.datastructures import DefaultPlaceholder, DefaultType +from fastapi.exceptions import FastAPIDeprecationWarning from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal @@ -195,9 +196,9 @@ def generate_operation_id_for_path( *, name: str, path: str, method: str ) -> str: # pragma: nocover warnings.warn( - "fastapi.utils.generate_operation_id_for_path() was deprecated, " + message="fastapi.utils.generate_operation_id_for_path() was deprecated, " "it is not used internally, and will be removed soon", - DeprecationWarning, + category=FastAPIDeprecationWarning, stacklevel=2, ) operation_id = f"{name}{path}" diff --git a/tests/benchmarks/test_general_performance.py b/tests/benchmarks/test_general_performance.py index 2da74b95c..dac297e4e 100644 --- a/tests/benchmarks/test_general_performance.py +++ b/tests/benchmarks/test_general_performance.py @@ -6,6 +6,7 @@ from typing import Annotated, Any import pytest from fastapi import Depends, FastAPI +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient if "--codspeed" not in sys.argv: @@ -89,7 +90,7 @@ def app(basemodel_class: type[Any]) -> FastAPI: warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) @app.post("/sync/validated", response_model=ItemOut) diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py index 2ac96993a..704b3f77a 100644 --- a/tests/test_compat_params_v1.py +++ b/tests/test_compat_params_v1.py @@ -3,6 +3,7 @@ import warnings from typing import Optional import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from tests.utils import skip_module_if_py_gte_314 @@ -504,23 +505,23 @@ def test_body_repr(): # Deprecation warning tests for regex parameter def test_query_regex_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + with pytest.warns(FastAPIDeprecationWarning, match="`regex` has been deprecated"): Query(regex="^test$") def test_body_regex_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + with pytest.warns(FastAPIDeprecationWarning, match="`regex` has been deprecated"): Body(regex="^test$") # Deprecation warning tests for example parameter def test_query_example_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + with pytest.warns(FastAPIDeprecationWarning, match="`example` has been deprecated"): Query(example="test example") def test_body_example_deprecation_warning(): - with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + with pytest.warns(FastAPIDeprecationWarning, match="`example` has been deprecated"): Body(example={"test": "example"}) diff --git a/tests/test_pydantic_v1_deprecation_warnings.py b/tests/test_pydantic_v1_deprecation_warnings.py index e0008e218..89ca6a865 100644 --- a/tests/test_pydantic_v1_deprecation_warnings.py +++ b/tests/test_pydantic_v1_deprecation_warnings.py @@ -1,6 +1,7 @@ import sys import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from tests.utils import skip_module_if_py_gte_314 @@ -19,7 +20,7 @@ def test_warns_pydantic_v1_model_in_endpoint_param() -> None: app = FastAPI() with pytest.warns( - DeprecationWarning, + FastAPIDeprecationWarning, match=r"pydantic\.v1 is deprecated.*Please update the param data:", ): @@ -40,7 +41,7 @@ def test_warns_pydantic_v1_model_in_return_type() -> None: app = FastAPI() with pytest.warns( - DeprecationWarning, + FastAPIDeprecationWarning, match=r"pydantic\.v1 is deprecated.*Please update the response model", ): @@ -61,7 +62,7 @@ def test_warns_pydantic_v1_model_in_response_model() -> None: app = FastAPI() with pytest.warns( - DeprecationWarning, + FastAPIDeprecationWarning, match=r"pydantic\.v1 is deprecated.*Please update the response model", ): @@ -82,7 +83,7 @@ def test_warns_pydantic_v1_model_in_additional_responses_model() -> None: app = FastAPI() with pytest.warns( - DeprecationWarning, + FastAPIDeprecationWarning, match=r"pydantic\.v1 is deprecated.*In responses=\{\}, please update", ): diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py index cfbff19c8..9d58c5dae 100644 --- a/tests/test_regex_deprecated_body.py +++ b/tests/test_regex_deprecated_body.py @@ -3,6 +3,7 @@ from typing import Annotated import pytest from dirty_equals import IsDict from fastapi import FastAPI, Form +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from .utils import needs_py310 @@ -10,7 +11,7 @@ from .utils import needs_py310 def get_client(): app = FastAPI() - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.post("/items/") async def read_items( diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py index 7d9988f9f..8973657a9 100644 --- a/tests/test_regex_deprecated_params.py +++ b/tests/test_regex_deprecated_params.py @@ -3,6 +3,7 @@ from typing import Annotated import pytest from dirty_equals import IsDict from fastapi import FastAPI, Query +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from .utils import needs_py310 @@ -10,7 +11,7 @@ from .utils import needs_py310 def get_client(): app = FastAPI() - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/items/") async def read_items( diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 176b5588d..8f5195ba1 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -3,6 +3,7 @@ from typing import Union import pytest from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict @@ -21,7 +22,7 @@ def create_app(): def schema_extra(item: Item): return item - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.post("/example/") def example(item: Item = Body(example={"data": "Data in Body example"})): @@ -38,7 +39,7 @@ def create_app(): ): return item - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.post("/example_examples/") def example_examples( @@ -83,7 +84,7 @@ def create_app(): # ): # return lastname - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/path_example/{item_id}") def path_example( @@ -101,7 +102,7 @@ def create_app(): ): return item_id - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/path_example_examples/{item_id}") def path_example_examples( @@ -112,7 +113,7 @@ def create_app(): ): return item_id - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/query_example/") def query_example( @@ -132,7 +133,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/query_example_examples/") def query_example_examples( @@ -144,7 +145,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/header_example/") def header_example( @@ -167,7 +168,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/header_example_examples/") def header_example_examples( @@ -179,7 +180,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/cookie_example/") def cookie_example( @@ -199,7 +200,7 @@ def create_app(): ): return data - with pytest.warns(DeprecationWarning): + with pytest.warns(FastAPIDeprecationWarning): @app.get("/cookie_example_examples/") def cookie_example_examples( diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py index ab7e1d8a7..9d1baf853 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py @@ -2,6 +2,7 @@ import sys import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from inline_snapshot import snapshot from tests.utils import skip_module_if_py_gte_314 @@ -29,7 +30,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py index c45e04248..23b236888 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py @@ -2,6 +2,7 @@ import sys import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from inline_snapshot import snapshot from tests.utils import skip_module_if_py_gte_314 @@ -29,7 +30,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py index f3da849e0..61c0f6357 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py @@ -2,6 +2,7 @@ import sys import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from inline_snapshot import snapshot from tests.utils import skip_module_if_py_gte_314 @@ -29,7 +30,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py index 95efab2dc..585989a82 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py @@ -18,7 +18,7 @@ from ...utils import needs_py310 marks=( needs_py310, pytest.mark.filterwarnings( - "ignore:`regex` has been deprecated, please use `pattern` instead:DeprecationWarning" + "ignore:`regex` has been deprecated, please use `pattern` instead:fastapi.exceptions.FastAPIDeprecationWarning" ), ), ), diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py index 515a5a8d7..50be45896 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py @@ -2,6 +2,7 @@ import importlib import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from ...utils import needs_pydanticv1 @@ -19,7 +20,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py index c5526b19c..83c717656 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py @@ -2,6 +2,7 @@ import importlib import warnings import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -20,7 +21,7 @@ def get_client(request: pytest.FixtureRequest): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=DeprecationWarning, + category=FastAPIDeprecationWarning, ) mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") From 93f4dfd88b7d62bda89eb521ebd9322e357e6fff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 12:46:00 +0000 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 98fd67da2..2af8abdfd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* 🔊 Add a custom `FastAPIDeprecationWarning`. PR [#14605](https://github.com/fastapi/fastapi/pull/14605) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Add documentary to website. PR [#14600](https://github.com/fastapi/fastapi/pull/14600) by [@tiangolo](https://github.com/tiangolo). From cd90c78391f86f3fd06d5b327bffad9666b05b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 14:02:41 +0100 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.127.?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2af8abdfd..3e9084d47 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.127.1 + ### Refactors * 🔊 Add a custom `FastAPIDeprecationWarning`. PR [#14605](https://github.com/fastapi/fastapi/pull/14605) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 73df6dc6c..dc447c8bf 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.127.0" +__version__ = "0.127.1" from starlette import status as status