From 09ab90ed3598fbb2fa255229191b90b74d67505f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 19 Dec 2025 04:44:55 -0800 Subject: [PATCH 01/59] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Use=20prek=20as=20a?= =?UTF-8?q?=20pre-commit=20alternative=20(#14572)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .github/workflows/pre-commit.yml | 16 ++++++++++------ requirements.txt | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index fa0574d7d1..e628ce541d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -21,14 +21,21 @@ jobs: name: Checkout PR for own repo if: env.IS_FORK == 'false' with: - # To be able to commit it needs more than the last commit + # To be able to commit it needs to fetch the head of the branch, not the + # merge commit ref: ${{ github.head_ref }} + # And it needs the full history to be able to compute diffs + fetch-depth: 0 # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI token: ${{ secrets.PRE_COMMIT }} # pre-commit lite ci needs the default checkout configs to work - uses: actions/checkout@v5 name: Checkout PR for fork if: env.IS_FORK == 'true' + with: + # To be able to commit it needs the head branch of the PR, the remote one + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: @@ -44,12 +51,9 @@ jobs: run: | uv venv uv pip install -r requirements.txt - - name: Run pre-commit + - name: Run prek - pre-commit id: precommit - run: | - # Fetch the base branch for comparison - git fetch origin ${{ github.base_ref }} - uvx pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD --show-diff-on-failure + run: uvx prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure continue-on-error: true - name: Commit and push changes if: env.IS_FORK == 'false' diff --git a/requirements.txt b/requirements.txt index 1cff1a5a9b..3bbab64a42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,6 @@ -r requirements-tests.txt -r requirements-docs.txt -r requirements-translations.txt -pre-commit >=4.5.0,<5.0.0 +prek==0.2.22 # For generating screenshots playwright From 19abc42efe7385d836267782818043ef0876fd0e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 12:45:20 +0000 Subject: [PATCH 02/59] =?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 ebec6bff66..ee1a6ab135 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆️ Use prek as a pre-commit alternative. PR [#14572](https://github.com/fastapi/fastapi/pull/14572) by [@tiangolo](https://github.com/tiangolo). * 👷 Add performance tests with CodSpeed. PR [#14558](https://github.com/fastapi/fastapi/pull/14558) by [@tiangolo](https://github.com/tiangolo). ## 0.125.0 From 75d4f9c09837901d60fe910cad40d0a42a99b586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 19 Dec 2025 04:51:53 -0800 Subject: [PATCH 03/59] =?UTF-8?q?=F0=9F=94=A7=20Add=20LLM=20prompt=20file?= =?UTF-8?q?=20for=20Ukrainian,=20generated=20from=20the=20existing=20trans?= =?UTF-8?q?lations=20(#14548)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/uk/llm-prompt.md | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/uk/llm-prompt.md diff --git a/docs/uk/llm-prompt.md b/docs/uk/llm-prompt.md new file mode 100644 index 0000000000..d55d36ab5c --- /dev/null +++ b/docs/uk/llm-prompt.md @@ -0,0 +1,46 @@ +### Target language + +Translate to Ukrainian (українська). + +Language code: uk. + +### Grammar and tone + +1) Use polite/formal address consistent with existing Ukrainian docs (use “ви/ваш”). +2) Keep the tone concise and technical. + +### Headings + +1) Follow existing Ukrainian heading style; keep headings short and instructional. +2) Do not add trailing punctuation to headings. + +### Quotes + +1) Prefer Ukrainian guillemets «…» for quoted terms in prose, matching existing Ukrainian docs. +2) Never change quotes inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +1) Keep ellipsis style consistent with existing Ukrainian docs. +2) Never change `...` in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): запит +- response (HTTP): відповідь +- path operation: операція шляху +- path operation function: функція операції шляху + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) If a title is present, prefer these canonical titles (choose one canonical form where variants exist): + +- `/// note | Примітка` +- `/// note | Технічні деталі` +- `/// tip | Порада` +- `/// warning | Попередження` +- `/// info | Інформація` +- `/// danger | Обережно` From 261c11b218e0af717d6b08ee33c875c9c32595f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 12:52:40 +0000 Subject: [PATCH 04/59] =?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 ee1a6ab135..2576a062ec 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🔧 Add LLM prompt file for Ukrainian, generated from the existing translations. PR [#14548](https://github.com/fastapi/fastapi/pull/14548) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ⬆️ Use prek as a pre-commit alternative. PR [#14572](https://github.com/fastapi/fastapi/pull/14572) by [@tiangolo](https://github.com/tiangolo). From d70ed5eceb01ebd3056e31f88805b0cc3d09ed83 Mon Sep 17 00:00:00 2001 From: Paras Verma <134628559+paras-verma7454@users.noreply.github.com> Date: Sat, 20 Dec 2025 12:10:21 +0530 Subject: [PATCH 05/59] =?UTF-8?q?=F0=9F=93=9D=20Fix=20duplicated=20variabl?= =?UTF-8?q?e=20in=20`docs=5Fsrc/python=5Ftypes/tutorial005=5Fpy39.py`=20(#?= =?UTF-8?q?14565)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix duplicated variable in python types example --- docs_src/python_types/tutorial005_py39.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/python_types/tutorial005_py39.py b/docs_src/python_types/tutorial005_py39.py index 08ab44a941..6c8edb0ec4 100644 --- a/docs_src/python_types/tutorial005_py39.py +++ b/docs_src/python_types/tutorial005_py39.py @@ -1,2 +1,2 @@ def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes): - return item_a, item_b, item_c, item_d, item_d, item_e + return item_a, item_b, item_c, item_d, item_e From 5c7dceb80f7a5ec49356c8ff72b39596596d385b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 06:40:44 +0000 Subject: [PATCH 06/59] =?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 2576a062ec..259546d7ed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Fix duplicated variable in `docs_src/python_types/tutorial005_py39.py`. PR [#14565](https://github.com/fastapi/fastapi/pull/14565) by [@paras-verma7454](https://github.com/paras-verma7454). + ### Translations * 🔧 Add LLM prompt file for Ukrainian, generated from the existing translations. PR [#14548](https://github.com/fastapi/fastapi/pull/14548) by [@tiangolo](https://github.com/tiangolo). From e2cd8a420110132bd08893d743fdb617f59fd15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Dec 2025 07:55:38 -0800 Subject: [PATCH 07/59] =?UTF-8?q?=E2=9E=96=20Drop=20support=20for=20Pydant?= =?UTF-8?q?ic=20v1,=20keeping=20short=20temporary=20support=20for=20Pydant?= =?UTF-8?q?ic=20v2's=20`pydantic.v1`=20(#14575)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .github/workflows/test.yml | 25 +- .../path-operation-advanced-configuration.md | 32 - docs/en/docs/advanced/settings.md | 44 - ...migrate-from-pydantic-v1-to-pydantic-v2.md | 14 +- .../docs/how-to/separate-openapi-schemas.md | 4 +- docs/en/docs/tutorial/body-updates.md | 16 - docs/en/docs/tutorial/body.md | 8 - docs/en/docs/tutorial/extra-models.md | 30 +- .../tutorial/query-params-str-validations.md | 14 - docs/en/docs/tutorial/response-model.md | 14 - docs/en/docs/tutorial/schema-extra-example.md | 24 +- docs_src/body/tutorial002_py310.py | 2 +- docs_src/body/tutorial002_py39.py | 2 +- docs_src/body/tutorial003_py310.py | 2 +- docs_src/body/tutorial003_py39.py | 2 +- docs_src/body/tutorial004_py310.py | 2 +- docs_src/body/tutorial004_py39.py | 2 +- docs_src/body_updates/tutorial002_py310.py | 4 +- docs_src/body_updates/tutorial002_py39.py | 4 +- .../tutorial002_pv1_an_py310.py | 20 - .../tutorial002_pv1_an_py39.py | 20 - .../tutorial002_pv1_py310.py | 18 - .../tutorial002_pv1_py39.py | 20 - docs_src/extra_models/tutorial001_py310.py | 2 +- docs_src/extra_models/tutorial001_py39.py | 2 +- docs_src/extra_models/tutorial002_py310.py | 2 +- docs_src/extra_models/tutorial002_py39.py | 2 +- .../tutorial002_pv1_an_py310.py | 22 - .../tutorial002_pv1_an_py39.py | 22 - .../tutorial002_pv1_py310.py | 20 - .../tutorial002_pv1_py39.py | 22 - .../tutorial007_pv1_py39.py | 2 +- .../tutorial002_pv1_an_py310.py | 21 - .../tutorial002_pv1_an_py39.py | 21 - .../tutorial002_pv1_py310.py | 21 - .../tutorial002_pv1_py39.py | 21 - .../tutorial002_pv1_an_py39.py | 5 +- .../tutorial002_pv1_py39.py | 5 +- .../tutorial001_pv1_py310.py | 2 +- .../tutorial001_pv1_py39.py | 2 +- docs_src/settings/app03_an_py39/config_pv1.py | 2 +- docs_src/settings/app03_py39/config_pv1.py | 2 +- docs_src/settings/tutorial001_pv1_py39.py | 2 +- fastapi/_compat/__init__.py | 1 - fastapi/_compat/main.py | 204 ++--- fastapi/_compat/may_v1.py | 2 +- fastapi/_compat/v1.py | 179 ++--- fastapi/_compat/v2.py | 4 - fastapi/datastructures.py | 16 - fastapi/dependencies/utils.py | 13 +- fastapi/encoders.py | 4 +- fastapi/openapi/models.py | 24 +- fastapi/params.py | 82 +- fastapi/routing.py | 12 +- fastapi/temp_pydantic_v1_params.py | 27 +- fastapi/utils.py | 21 +- pyproject.toml | 15 +- tests/test_additional_properties_bool.py | 8 +- tests/test_ambiguous_params.py | 6 +- tests/test_arbitrary_types.py | 5 - tests/test_compat.py | 8 +- tests/test_compat_params_v1.py | 133 +-- tests/test_computed_fields.py | 4 - tests/test_custom_schema_fields.py | 30 +- tests/test_datastructures.py | 6 - tests/test_datetime_custom_encoder.py | 7 +- .../test_filter_pydantic_sub_model/app_pv1.py | 10 +- .../test_filter_pydantic_sub_model_pv1.py | 178 ++-- tests/test_filter_pydantic_sub_model_pv2.py | 213 ++--- tests/test_forms_single_model.py | 8 +- ...t_get_model_definitions_formfeed_escape.py | 316 ++++---- tests/test_inherited_custom_class.py | 7 +- tests/test_jsonable_encoder.py | 68 +- tests/test_no_schema_split.py | 46 +- ...t_openapi_separate_input_output_schemas.py | 40 +- tests/test_pydantic_v1_v2_01.py | 78 +- tests/test_pydantic_v1_v2_list.py | 40 +- tests/test_pydantic_v1_v2_mixed.py | 226 ++---- .../test_multifile.py | 757 ++++++------------ tests/test_pydantic_v1_v2_noneable.py | 193 ++--- ..._query_cookie_header_model_extra_params.py | 8 +- tests/test_read_with_orm_mode.py | 7 +- tests/test_request_param_model_by_alias.py | 10 +- .../test_body/test_list.py | 11 - .../test_body/test_optional_list.py | 9 - .../test_body/test_optional_str.py | 15 - .../test_body/test_required_str.py | 11 - .../test_cookie/test_optional_str.py | 11 - .../test_cookie/test_required_str.py | 11 - .../test_file/test_list.py | 11 - .../test_file/test_optional.py | 11 - .../test_file/test_optional_list.py | 11 - .../test_file/test_required.py | 11 - .../test_form/test_list.py | 11 - .../test_form/test_optional_list.py | 11 - .../test_form/test_optional_str.py | 11 - .../test_form/test_required_str.py | 11 - .../test_header/test_list.py | 11 - .../test_header/test_optional_list.py | 11 - .../test_header/test_optional_str.py | 11 - .../test_header/test_required_str.py | 11 - .../test_path/test_required_str.py | 6 - .../test_query/test_list.py | 11 - .../test_query/test_optional_list.py | 11 - .../test_query/test_optional_str.py | 11 - .../test_query/test_required_str.py | 11 - tests/test_response_by_alias.py | 27 +- tests/test_schema_compat_pydantic_v2.py | 4 +- tests/test_schema_extra_examples.py | 12 +- tests/test_schema_ref_pydantic_v2.py | 4 - .../test_body_updates/test_tutorial001.py | 137 +--- .../test_tutorial001.py | 5 - .../test_tutorial002.py | 75 +- .../test_dataclasses/test_tutorial003.py | 137 +--- .../test_tutorial002.py | 14 +- .../test_tutorial004.py | 100 +-- .../test_tutorial007.py | 6 - .../test_tutorial005.py | 100 +-- .../test_tutorial001.py | 6 - .../test_tutorial002.py | 5 - .../test_tutorial003.py | 4 - .../test_tutorial004.py | 4 - .../test_tutorial002.py | 14 +- .../test_tutorial015.py | 6 +- .../test_tutorial002.py | 9 - .../test_tutorial002_pv1.py | 84 -- .../test_tutorial001.py | 138 +--- .../test_tutorial001.py | 4 +- .../test_tutorial001_pv1.py | 179 +++-- .../test_tutorial001.py | 3 +- .../test_tutorial002.py | 3 +- .../test_tutorial/test_settings/test_app02.py | 4 - .../test_tutorial/test_settings/test_app03.py | 4 +- .../test_settings/test_tutorial001.py | 4 +- tests/test_union_body_discriminator.py | 3 - ...test_union_body_discriminator_annotated.py | 5 - tests/test_validate_response_recursive/app.py | 11 +- tests/utils.py | 30 +- 138 files changed, 1272 insertions(+), 3658 deletions(-) delete mode 100644 docs_src/cookie_param_models/tutorial002_pv1_an_py310.py delete mode 100644 docs_src/cookie_param_models/tutorial002_pv1_an_py39.py delete mode 100644 docs_src/cookie_param_models/tutorial002_pv1_py310.py delete mode 100644 docs_src/cookie_param_models/tutorial002_pv1_py39.py delete mode 100644 docs_src/header_param_models/tutorial002_pv1_an_py310.py delete mode 100644 docs_src/header_param_models/tutorial002_pv1_an_py39.py delete mode 100644 docs_src/header_param_models/tutorial002_pv1_py310.py delete mode 100644 docs_src/header_param_models/tutorial002_pv1_py39.py delete mode 100644 docs_src/query_param_models/tutorial002_pv1_an_py310.py delete mode 100644 docs_src/query_param_models/tutorial002_pv1_an_py39.py delete mode 100644 docs_src/query_param_models/tutorial002_pv1_py310.py delete mode 100644 docs_src/query_param_models/tutorial002_pv1_py39.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc906eaf6d..eb2b6b64ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,35 +44,22 @@ jobs: matrix: os: [ windows-latest, macos-latest ] python-version: [ "3.14" ] - pydantic-version: [ "pydantic>=2.0.2,<3.0.0" ] include: - os: ubuntu-latest python-version: "3.9" - pydantic-version: "pydantic>=1.10.0,<2.0.0" coverage: coverage - os: macos-latest python-version: "3.10" - pydantic-version: "pydantic>=2.0.2,<3.0.0" - - os: windows-latest - python-version: "3.11" - pydantic-version: "pydantic>=1.10.0,<2.0.0" - - os: ubuntu-latest - python-version: "3.12" - pydantic-version: "pydantic>=2.0.2,<3.0.0" - - os: macos-latest - python-version: "3.13" - pydantic-version: "pydantic>=1.10.0,<2.0.0" - - os: windows-latest - python-version: "3.13" - pydantic-version: "pydantic>=2.0.2,<3.0.0" coverage: coverage + - os: windows-latest + python-version: "3.12" + coverage: coverage + # Ubuntu with 3.13 needs coverage for CodSpeed benchmarks - os: ubuntu-latest python-version: "3.13" - pydantic-version: "pydantic>=2.0.2,<3.0.0" coverage: coverage - os: ubuntu-latest python-version: "3.14" - pydantic-version: "pydantic>=2.0.2,<3.0.0" coverage: coverage fail-fast: false runs-on: ${{ matrix.os }} @@ -96,8 +83,6 @@ jobs: pyproject.toml - name: Install Dependencies run: uv pip install -r requirements-tests.txt - - name: Install Pydantic - run: uv pip install "${{ matrix.pydantic-version }}" - run: mkdir coverage - name: Test run: bash scripts/test.sh @@ -105,7 +90,7 @@ jobs: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - name: CodSpeed benchmarks - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && matrix.pydantic-version == 'pydantic>=2.0.2,<3.0.0' + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' uses: CodSpeedHQ/action@v4 env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index 01196af79e..e0e3c96a0e 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -153,48 +153,16 @@ And you could do this even if the data type in the request is not JSON. For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON: -//// tab | Pydantic v2 - {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} - -//// - -/// info - -In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_json_schema()`. - -/// - Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML. Then we use the request directly, and extract the body as `bytes`. This means that FastAPI won't even try to parse the request payload as JSON. And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content: -//// tab | Pydantic v2 - {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} - -//// - -/// info - -In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`. - -/// - /// tip Here we reuse the same Pydantic model. diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index 268983f1ef..cded9b80b8 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -46,12 +46,6 @@ $ pip install "fastapi[all]" -/// info - -In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality. - -/// - ### Create the `Settings` object { #create-the-settings-object } Import `BaseSettings` from Pydantic and create a sub-class, very much like with a Pydantic model. @@ -60,24 +54,8 @@ The same way as with Pydantic models, you declare class attributes with type ann You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`. -//// tab | Pydantic v2 - {* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} -//// - -//// tab | Pydantic v1 - -/// info - -In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`. - -/// - -{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *} - -//// - /// tip If you want something quick to copy and paste, don't use this example, use the last one below. @@ -215,8 +193,6 @@ APP_NAME="ChimichangApp" And then update your `config.py` with: -//// tab | Pydantic v2 - {* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip @@ -225,26 +201,6 @@ The `model_config` attribute is used just for Pydantic configuration. You can re /// -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} - -/// tip - -The `Config` class is used just for Pydantic configuration. You can read more at Pydantic Model Config. - -/// - -//// - -/// info - -In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`. - -/// - Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use. ### Creating the `Settings` only once with `lru_cache` { #creating-the-settings-only-once-with-lru-cache } diff --git a/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md index e85d122be8..fc90220b89 100644 --- a/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md +++ b/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -2,21 +2,23 @@ If you have an old FastAPI app, you might be using Pydantic version 1. -FastAPI has had support for either Pydantic v1 or v2 since version 0.100.0. +FastAPI version 0.100.0 had support for either Pydantic v1 or v2. It would use whichever you had installed. -If you had installed Pydantic v2, it would use it. If instead you had Pydantic v1, it would use that. +FastAPI version 0.119.0 introduced partial support for Pydantic v1 from inside of Pydantic v2 (as `pydantic.v1`), to facilitate the migration to v2. -Pydantic v1 is now deprecated and support for it will be removed in the next versions of FastAPI, you should **migrate to Pydantic v2**. This way you will get the latest features, improvements, and fixes. +FastAPI 0.126.0 dropped support for Pydantic v1, while still supporting `pydantic.v1` for a little while. /// warning -Also, the Pydantic team stopped support for Pydantic v1 for the latest versions of Python, starting with **Python 3.14**. +The Pydantic team stopped support for Pydantic v1 for the latest versions of Python, starting with **Python 3.14**. + +This includes `pydantic.v1`, which is no longer supported in Python 3.14 and above. If you want to use the latest features of Python, you will need to make sure you use Pydantic v2. /// -If you have an old FastAPI app with Pydantic v1, here I'll show you how to migrate it to Pydantic v2, and the **new features in FastAPI 0.119.0** to help you with a gradual migration. +If you have an old FastAPI app with Pydantic v1, here I'll show you how to migrate it to Pydantic v2, and the **features in FastAPI 0.119.0** to help you with a gradual migration. ## Official Guide { #official-guide } @@ -44,7 +46,7 @@ After this, you can run the tests and check if everything works. If it does, you ## Pydantic v1 in v2 { #pydantic-v1-in-v2 } -Pydantic v2 includes everything from Pydantic v1 as a submodule `pydantic.v1`. +Pydantic v2 includes everything from Pydantic v1 as a submodule `pydantic.v1`. But this is no longer supported in versions above Python 3.13. This means that you can install the latest version of Pydantic v2 and import and use the old Pydantic v1 components from this submodule, as if you had the old Pydantic v1 installed. diff --git a/docs/en/docs/how-to/separate-openapi-schemas.md b/docs/en/docs/how-to/separate-openapi-schemas.md index 3c78a56d36..d790c600bb 100644 --- a/docs/en/docs/how-to/separate-openapi-schemas.md +++ b/docs/en/docs/how-to/separate-openapi-schemas.md @@ -1,6 +1,6 @@ # Separate OpenAPI Schemas for Input and Output or Not { #separate-openapi-schemas-for-input-and-output-or-not } -When using **Pydantic v2**, the generated OpenAPI is a bit more exact and **correct** than before. 😎 +Since **Pydantic v2** was released, the generated OpenAPI is a bit more exact and **correct** than before. 😎 In fact, in some cases, it will even have **two JSON Schemas** in OpenAPI for the same Pydantic model, for input and output, depending on if they have **default values**. @@ -100,5 +100,3 @@ And now there will be one single schema for input and output for the model, only
- -This is the same behavior as in Pydantic v1. 🤓 diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index baeb53ec6f..1b7fd70661 100644 --- a/docs/en/docs/tutorial/body-updates.md +++ b/docs/en/docs/tutorial/body-updates.md @@ -50,14 +50,6 @@ If you want to receive partial updates, it's very useful to use the parameter `e Like `item.model_dump(exclude_unset=True)`. -/// info - -In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. - -The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. - -/// - That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values. Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values: @@ -68,14 +60,6 @@ Then you can use this to generate a `dict` with only the data that was set (sent Now, you can create a copy of the existing model using `.model_copy()`, and pass the `update` parameter with a `dict` containing the data to update. -/// info - -In Pydantic v1 the method was called `.copy()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_copy()`. - -The examples here use `.copy()` for compatibility with Pydantic v1, but you should use `.model_copy()` instead if you can use Pydantic v2. - -/// - Like `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 25087b8406..2d0dfcbb59 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -128,14 +128,6 @@ Inside of the function, you can access all the attributes of the model object di {* ../../docs_src/body/tutorial002_py310.py *} -/// info - -In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. - -The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. - -/// - ## Request body + path parameters { #request-body-path-parameters } You can declare path parameters and request body at the same time. diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index 7079bf73da..d82df561fa 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -22,22 +22,13 @@ Here's a general idea of how the models could look like with their password fiel {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} +### About `**user_in.model_dump()` { #about-user-in-model-dump } -/// info - -In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. - -The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. - -/// - -### About `**user_in.dict()` { #about-user-in-dict } - -#### Pydantic's `.dict()` { #pydantics-dict } +#### Pydantic's `.model_dump()` { #pydantics-model-dump } `user_in` is a Pydantic model of class `UserIn`. -Pydantic models have a `.dict()` method that returns a `dict` with the model's data. +Pydantic models have a `.model_dump()` method that returns a `dict` with the model's data. So, if we create a Pydantic object `user_in` like: @@ -48,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com and then we call: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` we now have a `dict` with the data in the variable `user_dict` (it's a `dict` instead of a Pydantic model object). @@ -104,20 +95,20 @@ UserInDB( #### A Pydantic model from the contents of another { #a-pydantic-model-from-the-contents-of-another } -As in the example above we got `user_dict` from `user_in.dict()`, this code: +As in the example above we got `user_dict` from `user_in.model_dump()`, this code: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` would be equivalent to: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -...because `user_in.dict()` is a `dict`, and then we make Python "unpack" it by passing it to `UserInDB` prefixed with `**`. +...because `user_in.model_dump()` is a `dict`, and then we make Python "unpack" it by passing it to `UserInDB` prefixed with `**`. So, we get a Pydantic model from the data in another Pydantic model. @@ -126,7 +117,7 @@ So, we get a Pydantic model from the data in another Pydantic model. And then adding the extra keyword argument `hashed_password=hashed_password`, like in: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ...ends up being like: @@ -181,7 +172,6 @@ When defining a its `exclude_unset` parameter to achieve this. - -/// - -/// info - You can also use: * `response_model_exclude_defaults=True` diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index f3ddaf3697..daaea73c87 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -8,36 +8,14 @@ Here are several ways to do it. You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs. -//// tab | Pydantic v2 - -In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in Pydantic's docs: Configuration. +You can use the attribute `model_config` that takes a `dict` as described in Pydantic's docs: Configuration. You can set `"json_schema_extra"` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`. -//// - -//// tab | Pydantic v1 - -In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization. - -You can set `schema_extra` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`. - -//// - /// tip You could use the same technique to extend the JSON Schema and add your own custom extra info. diff --git a/docs_src/body/tutorial002_py310.py b/docs_src/body/tutorial002_py310.py index 454c45c886..a829a4dc9d 100644 --- a/docs_src/body/tutorial002_py310.py +++ b/docs_src/body/tutorial002_py310.py @@ -14,7 +14,7 @@ app = FastAPI() @app.post("/items/") async def create_item(item: Item): - item_dict = item.dict() + item_dict = item.model_dump() if item.tax is not None: price_with_tax = item.price + item.tax item_dict.update({"price_with_tax": price_with_tax}) diff --git a/docs_src/body/tutorial002_py39.py b/docs_src/body/tutorial002_py39.py index 5cd86216b3..fb212e8e79 100644 --- a/docs_src/body/tutorial002_py39.py +++ b/docs_src/body/tutorial002_py39.py @@ -16,7 +16,7 @@ app = FastAPI() @app.post("/items/") async def create_item(item: Item): - item_dict = item.dict() + item_dict = item.model_dump() if item.tax is not None: price_with_tax = item.price + item.tax item_dict.update({"price_with_tax": price_with_tax}) diff --git a/docs_src/body/tutorial003_py310.py b/docs_src/body/tutorial003_py310.py index 440b210e6b..51ac8aafac 100644 --- a/docs_src/body/tutorial003_py310.py +++ b/docs_src/body/tutorial003_py310.py @@ -14,4 +14,4 @@ app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item): - return {"item_id": item_id, **item.dict()} + return {"item_id": item_id, **item.model_dump()} diff --git a/docs_src/body/tutorial003_py39.py b/docs_src/body/tutorial003_py39.py index 2f33cc0389..636ba2275a 100644 --- a/docs_src/body/tutorial003_py39.py +++ b/docs_src/body/tutorial003_py39.py @@ -16,4 +16,4 @@ app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item): - return {"item_id": item_id, **item.dict()} + return {"item_id": item_id, **item.model_dump()} diff --git a/docs_src/body/tutorial004_py310.py b/docs_src/body/tutorial004_py310.py index b352b70ab6..53b10d97b6 100644 --- a/docs_src/body/tutorial004_py310.py +++ b/docs_src/body/tutorial004_py310.py @@ -14,7 +14,7 @@ app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item, q: str | None = None): - result = {"item_id": item_id, **item.dict()} + result = {"item_id": item_id, **item.model_dump()} if q: result.update({"q": q}) return result diff --git a/docs_src/body/tutorial004_py39.py b/docs_src/body/tutorial004_py39.py index 0671e0a278..2c157abfa1 100644 --- a/docs_src/body/tutorial004_py39.py +++ b/docs_src/body/tutorial004_py39.py @@ -16,7 +16,7 @@ app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item, q: Union[str, None] = None): - result = {"item_id": item_id, **item.dict()} + result = {"item_id": item_id, **item.model_dump()} if q: result.update({"q": q}) return result diff --git a/docs_src/body_updates/tutorial002_py310.py b/docs_src/body_updates/tutorial002_py310.py index 3498414966..e5db711108 100644 --- a/docs_src/body_updates/tutorial002_py310.py +++ b/docs_src/body_updates/tutorial002_py310.py @@ -29,7 +29,7 @@ async def read_item(item_id: str): async def update_item(item_id: str, item: Item): stored_item_data = items[item_id] stored_item_model = Item(**stored_item_data) - update_data = item.dict(exclude_unset=True) - updated_item = stored_item_model.copy(update=update_data) + update_data = item.model_dump(exclude_unset=True) + updated_item = stored_item_model.model_copy(update=update_data) items[item_id] = jsonable_encoder(updated_item) return updated_item diff --git a/docs_src/body_updates/tutorial002_py39.py b/docs_src/body_updates/tutorial002_py39.py index eb35b35215..eddd7af716 100644 --- a/docs_src/body_updates/tutorial002_py39.py +++ b/docs_src/body_updates/tutorial002_py39.py @@ -31,7 +31,7 @@ async def read_item(item_id: str): async def update_item(item_id: str, item: Item): stored_item_data = items[item_id] stored_item_model = Item(**stored_item_data) - update_data = item.dict(exclude_unset=True) - updated_item = stored_item_model.copy(update=update_data) + update_data = item.model_dump(exclude_unset=True) + updated_item = stored_item_model.model_copy(update=update_data) items[item_id] = jsonable_encoder(updated_item) return updated_item diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py b/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py deleted file mode 100644 index ac00360b60..0000000000 --- a/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated - -from fastapi import Cookie, FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Cookies(BaseModel): - class Config: - extra = "forbid" - - session_id: str - fatebook_tracker: str | None = None - googall_tracker: str | None = None - - -@app.get("/items/") -async def read_items(cookies: Annotated[Cookies, Cookie()]): - return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py b/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index 573caea4b1..0000000000 --- a/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated, Union - -from fastapi import Cookie, FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Cookies(BaseModel): - class Config: - extra = "forbid" - - session_id: str - fatebook_tracker: Union[str, None] = None - googall_tracker: Union[str, None] = None - - -@app.get("/items/") -async def read_items(cookies: Annotated[Cookies, Cookie()]): - return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_py310.py b/docs_src/cookie_param_models/tutorial002_pv1_py310.py deleted file mode 100644 index 2c59aad123..0000000000 --- a/docs_src/cookie_param_models/tutorial002_pv1_py310.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import Cookie, FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Cookies(BaseModel): - class Config: - extra = "forbid" - - session_id: str - fatebook_tracker: str | None = None - googall_tracker: str | None = None - - -@app.get("/items/") -async def read_items(cookies: Cookies = Cookie()): - return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_py39.py b/docs_src/cookie_param_models/tutorial002_pv1_py39.py deleted file mode 100644 index 13f78b850e..0000000000 --- a/docs_src/cookie_param_models/tutorial002_pv1_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Union - -from fastapi import Cookie, FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Cookies(BaseModel): - class Config: - extra = "forbid" - - session_id: str - fatebook_tracker: Union[str, None] = None - googall_tracker: Union[str, None] = None - - -@app.get("/items/") -async def read_items(cookies: Cookies = Cookie()): - return cookies diff --git a/docs_src/extra_models/tutorial001_py310.py b/docs_src/extra_models/tutorial001_py310.py index 669386ae6e..cf39142e4a 100644 --- a/docs_src/extra_models/tutorial001_py310.py +++ b/docs_src/extra_models/tutorial001_py310.py @@ -30,7 +30,7 @@ def fake_password_hasher(raw_password: str): def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) - user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password) print("User saved! ..not really") return user_in_db diff --git a/docs_src/extra_models/tutorial001_py39.py b/docs_src/extra_models/tutorial001_py39.py index 4be56cd2a7..327ffcdf09 100644 --- a/docs_src/extra_models/tutorial001_py39.py +++ b/docs_src/extra_models/tutorial001_py39.py @@ -32,7 +32,7 @@ def fake_password_hasher(raw_password: str): def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) - user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password) print("User saved! ..not really") return user_in_db diff --git a/docs_src/extra_models/tutorial002_py310.py b/docs_src/extra_models/tutorial002_py310.py index 5b8ed7de3d..e8a4f5f29b 100644 --- a/docs_src/extra_models/tutorial002_py310.py +++ b/docs_src/extra_models/tutorial002_py310.py @@ -28,7 +28,7 @@ def fake_password_hasher(raw_password: str): def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) - user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password) print("User saved! ..not really") return user_in_db diff --git a/docs_src/extra_models/tutorial002_py39.py b/docs_src/extra_models/tutorial002_py39.py index 70fa16441d..6543796015 100644 --- a/docs_src/extra_models/tutorial002_py39.py +++ b/docs_src/extra_models/tutorial002_py39.py @@ -30,7 +30,7 @@ def fake_password_hasher(raw_password: str): def fake_save_user(user_in: UserIn): hashed_password = fake_password_hasher(user_in.password) - user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password) print("User saved! ..not really") return user_in_db diff --git a/docs_src/header_param_models/tutorial002_pv1_an_py310.py b/docs_src/header_param_models/tutorial002_pv1_an_py310.py deleted file mode 100644 index e99e24ea55..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1_an_py310.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: str | None = None - traceparent: str | None = None - x_tag: list[str] = [] - - -@app.get("/items/") -async def read_items(headers: Annotated[CommonHeaders, Header()]): - return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an_py39.py b/docs_src/header_param_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index 18398b726c..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Annotated, Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: list[str] = [] - - -@app.get("/items/") -async def read_items(headers: Annotated[CommonHeaders, Header()]): - return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_py310.py b/docs_src/header_param_models/tutorial002_pv1_py310.py deleted file mode 100644 index 3dbff9d7bf..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: str | None = None - traceparent: str | None = None - x_tag: list[str] = [] - - -@app.get("/items/") -async def read_items(headers: CommonHeaders = Header()): - return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_py39.py b/docs_src/header_param_models/tutorial002_pv1_py39.py deleted file mode 100644 index 86e19be0d1..0000000000 --- a/docs_src/header_param_models/tutorial002_pv1_py39.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Header -from pydantic import BaseModel - -app = FastAPI() - - -class CommonHeaders(BaseModel): - class Config: - extra = "forbid" - - host: str - save_data: bool - if_modified_since: Union[str, None] = None - traceparent: Union[str, None] = None - x_tag: list[str] = [] - - -@app.get("/items/") -async def read_items(headers: CommonHeaders = Header()): - return headers diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py b/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py index 831966553f..849f648e12 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py +++ b/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py @@ -1,6 +1,6 @@ import yaml from fastapi import FastAPI, HTTPException, Request -from pydantic import BaseModel, ValidationError +from pydantic.v1 import BaseModel, ValidationError app = FastAPI() diff --git a/docs_src/query_param_models/tutorial002_pv1_an_py310.py b/docs_src/query_param_models/tutorial002_pv1_an_py310.py deleted file mode 100644 index d635aae88f..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1_an_py310.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Annotated, Literal - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: list[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: Annotated[FilterParams, Query()]): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an_py39.py b/docs_src/query_param_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index d635aae88f..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Annotated, Literal - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: list[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: Annotated[FilterParams, Query()]): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_py310.py b/docs_src/query_param_models/tutorial002_pv1_py310.py deleted file mode 100644 index 9ffdeefc06..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1_py310.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Literal - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: list[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: FilterParams = Query()): - return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_py39.py b/docs_src/query_param_models/tutorial002_pv1_py39.py deleted file mode 100644 index 9ffdeefc06..0000000000 --- a/docs_src/query_param_models/tutorial002_pv1_py39.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Literal - -from fastapi import FastAPI, Query -from pydantic import BaseModel, Field - -app = FastAPI() - - -class FilterParams(BaseModel): - class Config: - extra = "forbid" - - limit: int = Field(100, gt=0, le=100) - offset: int = Field(0, ge=0) - order_by: Literal["created_at", "updated_at"] = "created_at" - tags: list[str] = [] - - -@app.get("/items/") -async def read_items(filter_query: FilterParams = Query()): - return filter_query diff --git a/docs_src/request_form_models/tutorial002_pv1_an_py39.py b/docs_src/request_form_models/tutorial002_pv1_an_py39.py index 942d5d4118..392e6873cb 100644 --- a/docs_src/request_form_models/tutorial002_pv1_an_py39.py +++ b/docs_src/request_form_models/tutorial002_pv1_an_py39.py @@ -1,7 +1,8 @@ from typing import Annotated -from fastapi import FastAPI, Form -from pydantic import BaseModel +from fastapi import FastAPI +from fastapi.temp_pydantic_v1_params import Form +from pydantic.v1 import BaseModel app = FastAPI() diff --git a/docs_src/request_form_models/tutorial002_pv1_py39.py b/docs_src/request_form_models/tutorial002_pv1_py39.py index d5f7db2a67..da160b3a54 100644 --- a/docs_src/request_form_models/tutorial002_pv1_py39.py +++ b/docs_src/request_form_models/tutorial002_pv1_py39.py @@ -1,5 +1,6 @@ -from fastapi import FastAPI, Form -from pydantic import BaseModel +from fastapi import FastAPI +from fastapi.temp_pydantic_v1_params import Form +from pydantic.v1 import BaseModel app = FastAPI() diff --git a/docs_src/schema_extra_example/tutorial001_pv1_py310.py b/docs_src/schema_extra_example/tutorial001_pv1_py310.py index ec83f1112f..b13b8a8c2c 100644 --- a/docs_src/schema_extra_example/tutorial001_pv1_py310.py +++ b/docs_src/schema_extra_example/tutorial001_pv1_py310.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseModel +from pydantic.v1 import BaseModel app = FastAPI() diff --git a/docs_src/schema_extra_example/tutorial001_pv1_py39.py b/docs_src/schema_extra_example/tutorial001_pv1_py39.py index 6ab96ff859..3240b35d6d 100644 --- a/docs_src/schema_extra_example/tutorial001_pv1_py39.py +++ b/docs_src/schema_extra_example/tutorial001_pv1_py39.py @@ -1,7 +1,7 @@ from typing import Union from fastapi import FastAPI -from pydantic import BaseModel +from pydantic.v1 import BaseModel app = FastAPI() diff --git a/docs_src/settings/app03_an_py39/config_pv1.py b/docs_src/settings/app03_an_py39/config_pv1.py index e1c3ee3006..7ae66ef77c 100644 --- a/docs_src/settings/app03_an_py39/config_pv1.py +++ b/docs_src/settings/app03_an_py39/config_pv1.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic.v1 import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app03_py39/config_pv1.py b/docs_src/settings/app03_py39/config_pv1.py index e1c3ee3006..7ae66ef77c 100644 --- a/docs_src/settings/app03_py39/config_pv1.py +++ b/docs_src/settings/app03_py39/config_pv1.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic.v1 import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/tutorial001_pv1_py39.py b/docs_src/settings/tutorial001_pv1_py39.py index 0cfd1b6632..20ad2bbf62 100644 --- a/docs_src/settings/tutorial001_pv1_py39.py +++ b/docs_src/settings/tutorial001_pv1_py39.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseSettings +from pydantic.v1 import BaseSettings class Settings(BaseSettings): diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index 0aadd68de2..fd1df8c6a7 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -11,7 +11,6 @@ from .main import _is_model_class as _is_model_class from .main import _is_model_field as _is_model_field from .main import _is_undefined as _is_undefined from .main import _model_dump as _model_dump -from .main import _model_rebuild as _model_rebuild from .main import copy_field_info as copy_field_info from .main import create_body_model as create_body_model from .main import evaluate_forwardref as evaluate_forwardref diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py index 2043a66781..95053a2374 100644 --- a/fastapi/_compat/main.py +++ b/fastapi/_compat/main.py @@ -6,43 +6,26 @@ from typing import ( ) from fastapi._compat import may_v1 -from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass +from fastapi._compat.shared import lenient_issubclass from fastapi.types import ModelNameMap from pydantic import BaseModel from typing_extensions import Literal +from . import v2 from .model_field import ModelField - -if PYDANTIC_V2: - from .v2 import BaseConfig as BaseConfig - from .v2 import FieldInfo as FieldInfo - from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError - from .v2 import RequiredParam as RequiredParam - from .v2 import Undefined as Undefined - from .v2 import UndefinedType as UndefinedType - from .v2 import Url as Url - from .v2 import Validator as Validator - from .v2 import evaluate_forwardref as evaluate_forwardref - from .v2 import get_missing_field_error as get_missing_field_error - from .v2 import ( - with_info_plain_validator_function as with_info_plain_validator_function, - ) -else: - from .v1 import BaseConfig as BaseConfig # type: ignore[assignment] - from .v1 import FieldInfo as FieldInfo - from .v1 import ( # type: ignore[assignment] - PydanticSchemaGenerationError as PydanticSchemaGenerationError, - ) - from .v1 import RequiredParam as RequiredParam - from .v1 import Undefined as Undefined - from .v1 import UndefinedType as UndefinedType - from .v1 import Url as Url # type: ignore[assignment] - from .v1 import Validator as Validator - from .v1 import evaluate_forwardref as evaluate_forwardref - from .v1 import get_missing_field_error as get_missing_field_error - from .v1 import ( # type: ignore[assignment] - with_info_plain_validator_function as with_info_plain_validator_function, - ) +from .v2 import BaseConfig as BaseConfig +from .v2 import FieldInfo as FieldInfo +from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from .v2 import RequiredParam as RequiredParam +from .v2 import Undefined as Undefined +from .v2 import UndefinedType as UndefinedType +from .v2 import Url as Url +from .v2 import Validator as Validator +from .v2 import evaluate_forwardref as evaluate_forwardref +from .v2 import get_missing_field_error as get_missing_field_error +from .v2 import ( + with_info_plain_validator_function as with_info_plain_validator_function, +) @lru_cache @@ -50,7 +33,7 @@ def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: if lenient_issubclass(model, may_v1.BaseModel): from fastapi._compat import v1 - return v1.get_model_fields(model) + return v1.get_model_fields(model) # type: ignore[arg-type,return-value] else: from . import v2 @@ -60,11 +43,8 @@ def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: def _is_undefined(value: object) -> bool: if isinstance(value, may_v1.UndefinedType): return True - elif PYDANTIC_V2: - from . import v2 - return isinstance(value, v2.UndefinedType) - return False + return isinstance(value, v2.UndefinedType) def _get_model_config(model: BaseModel) -> Any: @@ -72,10 +52,8 @@ def _get_model_config(model: BaseModel) -> Any: from fastapi._compat import v1 return v1._get_model_config(model) - elif PYDANTIC_V2: - from . import v2 - return v2._get_model_config(model) + return v2._get_model_config(model) def _model_dump( @@ -85,20 +63,15 @@ def _model_dump( from fastapi._compat import v1 return v1._model_dump(model, mode=mode, **kwargs) - elif PYDANTIC_V2: - from . import v2 - return v2._model_dump(model, mode=mode, **kwargs) + return v2._model_dump(model, mode=mode, **kwargs) def _is_error_wrapper(exc: Exception) -> bool: if isinstance(exc, may_v1.ErrorWrapper): return True - elif PYDANTIC_V2: - from . import v2 - return isinstance(exc, v2.ErrorWrapper) - return False + return isinstance(exc, v2.ErrorWrapper) def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: @@ -106,11 +79,8 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: from fastapi._compat import v1 return v1.copy_field_info(field_info=field_info, annotation=annotation) - else: - assert PYDANTIC_V2 - from . import v2 - return v2.copy_field_info(field_info=field_info, annotation=annotation) + return v2.copy_field_info(field_info=field_info, annotation=annotation) def create_body_model( @@ -120,11 +90,8 @@ def create_body_model( from fastapi._compat import v1 return v1.create_body_model(fields=fields, model_name=model_name) - else: - assert PYDANTIC_V2 - from . import v2 - return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type] + return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type] def get_annotation_from_field_info( @@ -136,13 +103,10 @@ def get_annotation_from_field_info( return v1.get_annotation_from_field_info( annotation=annotation, field_info=field_info, field_name=field_name ) - else: - assert PYDANTIC_V2 - from . import v2 - return v2.get_annotation_from_field_info( - annotation=annotation, field_info=field_info, field_name=field_name - ) + return v2.get_annotation_from_field_info( + annotation=annotation, field_info=field_info, field_name=field_name + ) def is_bytes_field(field: ModelField) -> bool: @@ -150,11 +114,8 @@ def is_bytes_field(field: ModelField) -> bool: from fastapi._compat import v1 return v1.is_bytes_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - return v2.is_bytes_field(field) # type: ignore[arg-type] + return v2.is_bytes_field(field) # type: ignore[arg-type] def is_bytes_sequence_field(field: ModelField) -> bool: @@ -162,11 +123,8 @@ def is_bytes_sequence_field(field: ModelField) -> bool: from fastapi._compat import v1 return v1.is_bytes_sequence_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - return v2.is_bytes_sequence_field(field) # type: ignore[arg-type] + return v2.is_bytes_sequence_field(field) # type: ignore[arg-type] def is_scalar_field(field: ModelField) -> bool: @@ -174,23 +132,12 @@ def is_scalar_field(field: ModelField) -> bool: from fastapi._compat import v1 return v1.is_scalar_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - return v2.is_scalar_field(field) # type: ignore[arg-type] + return v2.is_scalar_field(field) # type: ignore[arg-type] def is_scalar_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_scalar_sequence_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - - return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] + return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] def is_sequence_field(field: ModelField) -> bool: @@ -198,11 +145,8 @@ def is_sequence_field(field: ModelField) -> bool: from fastapi._compat import v1 return v1.is_sequence_field(field) - else: - assert PYDANTIC_V2 - from . import v2 - return v2.is_sequence_field(field) # type: ignore[arg-type] + return v2.is_sequence_field(field) # type: ignore[arg-type] def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: @@ -210,22 +154,8 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: from fastapi._compat import v1 return v1.serialize_sequence_value(field=field, value=value) - else: - assert PYDANTIC_V2 - from . import v2 - return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type] - - -def _model_rebuild(model: type[BaseModel]) -> None: - if lenient_issubclass(model, may_v1.BaseModel): - from fastapi._compat import v1 - - v1._model_rebuild(model) - elif PYDANTIC_V2: - from . import v2 - - v2._model_rebuild(model) + return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type] def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: @@ -236,27 +166,18 @@ def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: from fastapi._compat import v1 v1_flat_models = v1.get_flat_models_from_fields( - v1_model_fields, known_models=set() + v1_model_fields, # type: ignore[arg-type] + known_models=set(), ) all_flat_models = v1_flat_models else: all_flat_models = set() - if PYDANTIC_V2: - from . import v2 - v2_model_fields = [ - field for field in fields if isinstance(field, v2.ModelField) - ] - v2_flat_models = v2.get_flat_models_from_fields( - v2_model_fields, known_models=set() - ) - all_flat_models = all_flat_models.union(v2_flat_models) + v2_model_fields = [field for field in fields if isinstance(field, v2.ModelField)] + v2_flat_models = v2.get_flat_models_from_fields(v2_model_fields, known_models=set()) + all_flat_models = all_flat_models.union(v2_flat_models) # type: ignore[arg-type] - model_name_map = v2.get_model_name_map(all_flat_models) - return model_name_map - from fastapi._compat import v1 - - model_name_map = v1.get_model_name_map(all_flat_models) + model_name_map = v2.get_model_name_map(all_flat_models) # type: ignore[arg-type] return model_name_map @@ -275,29 +196,23 @@ def get_definitions( if sys.version_info < (3, 14): v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)] v1_field_maps, v1_definitions = may_v1.get_definitions( - fields=v1_fields, + fields=v1_fields, # type: ignore[arg-type] model_name_map=model_name_map, separate_input_output_schemas=separate_input_output_schemas, ) - if not PYDANTIC_V2: - return v1_field_maps, v1_definitions - else: - from . import v2 - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_field_maps, v2_definitions = v2.get_definitions( - fields=v2_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - all_definitions = {**v1_definitions, **v2_definitions} - all_field_maps = {**v1_field_maps, **v2_field_maps} - return all_field_maps, all_definitions + v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] + v2_field_maps, v2_definitions = v2.get_definitions( + fields=v2_fields, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + ) + all_definitions = {**v1_definitions, **v2_definitions} + all_field_maps = {**v1_field_maps, **v2_field_maps} # type: ignore[misc] + return all_field_maps, all_definitions # Pydantic v1 is not supported since Python 3.14 else: - from . import v2 - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] v2_field_maps, v2_definitions = v2.get_definitions( fields=v2_fields, @@ -326,33 +241,24 @@ def get_schema_from_model_field( field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, ) - else: - assert PYDANTIC_V2 - from . import v2 - return v2.get_schema_from_model_field( - field=field, # type: ignore[arg-type] - model_name_map=model_name_map, - field_mapping=field_mapping, # type: ignore[arg-type] - separate_input_output_schemas=separate_input_output_schemas, - ) + return v2.get_schema_from_model_field( + field=field, # type: ignore[arg-type] + model_name_map=model_name_map, + field_mapping=field_mapping, # type: ignore[arg-type] + separate_input_output_schemas=separate_input_output_schemas, + ) def _is_model_field(value: Any) -> bool: if isinstance(value, may_v1.ModelField): return True - elif PYDANTIC_V2: - from . import v2 - return isinstance(value, v2.ModelField) - return False + return isinstance(value, v2.ModelField) def _is_model_class(value: Any) -> bool: if lenient_issubclass(value, may_v1.BaseModel): return True - elif PYDANTIC_V2: - from . import v2 - return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] - return False + return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] diff --git a/fastapi/_compat/may_v1.py b/fastapi/_compat/may_v1.py index c772162283..3ac86aa98b 100644 --- a/fastapi/_compat/may_v1.py +++ b/fastapi/_compat/may_v1.py @@ -102,7 +102,7 @@ def _normalize_errors(errors: Sequence[Any]) -> list[dict[str, Any]]: use_errors: list[Any] = [] for error in errors: if isinstance(error, ErrorWrapper): - new_errors = ValidationError( # type: ignore[call-arg] + new_errors = ValidationError( errors=[error], model=RequestErrorModel ).errors() use_errors.extend(new_errors) diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py index b29a61f734..b0a9dd35f1 100644 --- a/fastapi/_compat/v1.py +++ b/fastapi/_compat/v1.py @@ -11,6 +11,44 @@ from typing import ( from fastapi._compat import shared from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from fastapi.types import ModelNameMap +from pydantic.v1 import BaseConfig as BaseConfig +from pydantic.v1 import BaseModel as BaseModel +from pydantic.v1 import ValidationError as ValidationError +from pydantic.v1 import create_model as create_model +from pydantic.v1.class_validators import Validator as Validator +from pydantic.v1.color import Color as Color +from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper +from pydantic.v1.fields import ( + SHAPE_FROZENSET, + SHAPE_LIST, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_SINGLETON, + SHAPE_TUPLE, + SHAPE_TUPLE_ELLIPSIS, +) +from pydantic.v1.fields import FieldInfo as FieldInfo +from pydantic.v1.fields import ModelField as ModelField +from pydantic.v1.fields import Undefined as Undefined +from pydantic.v1.fields import UndefinedType as UndefinedType +from pydantic.v1.networks import AnyUrl as AnyUrl +from pydantic.v1.networks import NameEmail as NameEmail +from pydantic.v1.schema import TypeModelSet as TypeModelSet +from pydantic.v1.schema import field_schema, model_process_schema +from pydantic.v1.schema import ( + get_annotation_from_field_info as get_annotation_from_field_info, +) +from pydantic.v1.schema import ( + get_flat_models_from_field as get_flat_models_from_field, +) +from pydantic.v1.schema import ( + get_flat_models_from_fields as get_flat_models_from_fields, +) +from pydantic.v1.schema import get_model_name_map as get_model_name_map +from pydantic.v1.types import SecretBytes as SecretBytes +from pydantic.v1.types import SecretStr as SecretStr +from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref +from pydantic.v1.utils import lenient_issubclass as lenient_issubclass from pydantic.version import VERSION as PYDANTIC_VERSION from typing_extensions import Literal @@ -20,103 +58,6 @@ PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 # shadowing typing.Required. RequiredParam: Any = Ellipsis -if not PYDANTIC_V2: - from pydantic import BaseConfig as BaseConfig - from pydantic import BaseModel as BaseModel - from pydantic import ValidationError as ValidationError - from pydantic import create_model as create_model - from pydantic.class_validators import Validator as Validator - from pydantic.color import Color as Color - from pydantic.error_wrappers import ErrorWrapper as ErrorWrapper - from pydantic.errors import MissingError - from pydantic.fields import ( # type: ignore[attr-defined] - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, - ) - from pydantic.fields import FieldInfo as FieldInfo - from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined] - from pydantic.fields import Undefined as Undefined # type: ignore[attr-defined] - from pydantic.fields import ( # type: ignore[attr-defined] - UndefinedType as UndefinedType, - ) - from pydantic.networks import AnyUrl as AnyUrl - from pydantic.networks import NameEmail as NameEmail - from pydantic.schema import TypeModelSet as TypeModelSet - from pydantic.schema import ( - field_schema, - model_process_schema, - ) - from pydantic.schema import ( - get_annotation_from_field_info as get_annotation_from_field_info, - ) - from pydantic.schema import get_flat_models_from_field as get_flat_models_from_field - from pydantic.schema import ( - get_flat_models_from_fields as get_flat_models_from_fields, - ) - from pydantic.schema import get_model_name_map as get_model_name_map - from pydantic.types import SecretBytes as SecretBytes - from pydantic.types import SecretStr as SecretStr - from pydantic.typing import evaluate_forwardref as evaluate_forwardref - from pydantic.utils import lenient_issubclass as lenient_issubclass - - -else: - from pydantic.v1 import BaseConfig as BaseConfig # type: ignore[assignment] - from pydantic.v1 import BaseModel as BaseModel # type: ignore[assignment] - from pydantic.v1 import ( # type: ignore[assignment] - ValidationError as ValidationError, - ) - from pydantic.v1 import create_model as create_model # type: ignore[no-redef] - from pydantic.v1.class_validators import Validator as Validator - from pydantic.v1.color import Color as Color # type: ignore[assignment] - from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper - from pydantic.v1.errors import MissingError - from pydantic.v1.fields import ( - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, - ) - from pydantic.v1.fields import FieldInfo as FieldInfo # type: ignore[assignment] - from pydantic.v1.fields import ModelField as ModelField - from pydantic.v1.fields import Undefined as Undefined - from pydantic.v1.fields import UndefinedType as UndefinedType - from pydantic.v1.networks import AnyUrl as AnyUrl - from pydantic.v1.networks import ( # type: ignore[assignment] - NameEmail as NameEmail, - ) - from pydantic.v1.schema import TypeModelSet as TypeModelSet - from pydantic.v1.schema import ( - field_schema, - model_process_schema, - ) - from pydantic.v1.schema import ( - get_annotation_from_field_info as get_annotation_from_field_info, - ) - from pydantic.v1.schema import ( - get_flat_models_from_field as get_flat_models_from_field, - ) - from pydantic.v1.schema import ( - get_flat_models_from_fields as get_flat_models_from_fields, - ) - from pydantic.v1.schema import get_model_name_map as get_model_name_map - from pydantic.v1.types import ( # type: ignore[assignment] - SecretBytes as SecretBytes, - ) - from pydantic.v1.types import ( # type: ignore[assignment] - SecretStr as SecretStr, - ) - from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref - from pydantic.v1.utils import lenient_issubclass as lenient_issubclass - GetJsonSchemaHandler = Any JsonSchemaValue = dict[str, Any] @@ -200,24 +141,6 @@ def is_pv1_scalar_field(field: ModelField) -> bool: return True -def is_pv1_scalar_sequence_field(field: ModelField) -> bool: - if (field.shape in sequence_shapes) and not lenient_issubclass( - field.type_, BaseModel - ): - if field.sub_fields is not None: - for sub_field in field.sub_fields: - if not is_pv1_scalar_field(sub_field): - return False - return True - if shared._annotation_is_sequence(field.type_): - return True - return False - - -def _model_rebuild(model: type[BaseModel]) -> None: - model.update_forward_refs() - - def _model_dump( model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any ) -> Any: @@ -225,7 +148,7 @@ def _model_dump( def _get_model_config(model: BaseModel) -> Any: - return model.__config__ # type: ignore[attr-defined] + return model.__config__ def get_schema_from_model_field( @@ -237,8 +160,10 @@ def get_schema_from_model_field( ], separate_input_output_schemas: bool = True, ) -> dict[str, Any]: - return field_schema( # type: ignore[no-any-return] - field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + return field_schema( + field, + model_name_map=model_name_map, # type: ignore[arg-type] + ref_prefix=REF_PREFIX, )[0] @@ -257,7 +182,7 @@ def get_definitions( dict[str, dict[str, Any]], ]: models = get_flat_models_from_fields(fields, known_models=set()) - return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) + return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) # type: ignore[arg-type] def is_scalar_field(field: ModelField) -> bool: @@ -268,12 +193,8 @@ def is_sequence_field(field: ModelField) -> bool: return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_) -def is_scalar_sequence_field(field: ModelField) -> bool: - return is_pv1_scalar_sequence_field(field) - - def is_bytes_field(field: ModelField) -> bool: - return lenient_issubclass(field.type_, bytes) # type: ignore[no-any-return] + return lenient_issubclass(field.type_, bytes) def is_bytes_sequence_field(field: ModelField) -> bool: @@ -288,20 +209,14 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return] -def get_missing_field_error(loc: tuple[str, ...]) -> dict[str, Any]: - missing_field_error = ErrorWrapper(MissingError(), loc=loc) - new_error = ValidationError([missing_field_error], RequestErrorModel) - return new_error.errors()[0] # type: ignore[return-value] - - def create_body_model( *, fields: Sequence[ModelField], model_name: str ) -> type[BaseModel]: BodyModel = create_model(model_name) for f in fields: - BodyModel.__fields__[f.name] = f # type: ignore[index] + BodyModel.__fields__[f.name] = f return BodyModel def get_model_fields(model: type[BaseModel]) -> list[ModelField]: - return list(model.__fields__.values()) # type: ignore[attr-defined] + return list(model.__fields__.values()) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index c4fa49e403..cbcb98e1a2 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -216,10 +216,6 @@ def get_annotation_from_field_info( return annotation -def _model_rebuild(model: type[BaseModel]) -> None: - model.model_rebuild() - - def _model_dump( model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any ) -> Any: diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index b38a326def..492cbfcccb 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,4 +1,3 @@ -from collections.abc import Iterable from typing import ( Annotated, Any, @@ -135,27 +134,12 @@ class UploadFile(StarletteUploadFile): """ return await super().close() - @classmethod - def __get_validators__(cls: type["UploadFile"]) -> Iterable[Callable[..., Any]]: - yield cls.validate - - @classmethod - def validate(cls: type["UploadFile"], v: Any) -> Any: - if not isinstance(v, StarletteUploadFile): - raise ValueError(f"Expected UploadFile, received: {type(v)}") - return v - @classmethod def _validate(cls, __input_value: Any, _: Any) -> "UploadFile": if not isinstance(__input_value, StarletteUploadFile): raise ValueError(f"Expected UploadFile, received: {type(__input_value)}") return cast(UploadFile, __input_value) - # TODO: remove when deprecating Pydantic v1 - @classmethod - def __modify_schema__(cls, field_schema: dict[str, Any]) -> None: - field_schema.update({"type": "string", "format": "binary"}) - @classmethod def __get_pydantic_json_schema__( cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 3db006a918..0ba93524c4 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -18,7 +18,6 @@ from typing import ( import anyio from fastapi import params from fastapi._compat import ( - PYDANTIC_V2, ModelField, RequiredParam, Undefined, @@ -410,7 +409,8 @@ def analyze_param( if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( - field_info=fastapi_annotation, annotation=use_annotation + field_info=fastapi_annotation, # type: ignore[arg-type] + annotation=use_annotation, ) assert field_info.default in { Undefined, @@ -444,10 +444,9 @@ def analyze_param( "Cannot specify FastAPI annotations in `Annotated` and default value" f" together for {param_name!r}" ) - field_info = value - if PYDANTIC_V2: - if isinstance(field_info, FieldInfo): - field_info.annotation = type_annotation + field_info = value # type: ignore[assignment] + if isinstance(field_info, FieldInfo): + field_info.annotation = type_annotation # Get Depends from type annotation if depends is not None and depends.dependency is None: @@ -485,7 +484,7 @@ def analyze_param( field_info = params.File(annotation=use_annotation, default=default_value) elif not field_annotation_is_scalar(annotation=type_annotation): if annotation_is_pydantic_v1(use_annotation): - field_info = temp_pydantic_v1_params.Body( + field_info = temp_pydantic_v1_params.Body( # type: ignore[assignment] annotation=use_annotation, default=default_value ) else: diff --git a/fastapi/encoders.py b/fastapi/encoders.py index cbeeee4559..549da32797 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -228,11 +228,11 @@ def jsonable_encoder( # TODO: remove when deprecating Pydantic v1 encoders: dict[Any, Any] = {} if isinstance(obj, may_v1.BaseModel): - encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] + encoders = getattr(obj.__config__, "json_encoders", {}) if custom_encoder: encoders = {**encoders, **custom_encoder} obj_dict = _model_dump( - obj, + obj, # type: ignore[arg-type] mode="json", include=include, exclude=exclude, diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 7aa80f5cb0..680f678325 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -3,11 +3,9 @@ from enum import Enum from typing import Annotated, Any, Callable, Optional, Union from fastapi._compat import ( - PYDANTIC_V2, CoreSchema, GetJsonSchemaHandler, JsonSchemaValue, - _model_rebuild, with_info_plain_validator_function, ) from fastapi.logger import logger @@ -57,13 +55,7 @@ except ImportError: # pragma: no cover class BaseModelWithConfig(BaseModel): - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" + model_config = {"extra": "allow"} class Contact(BaseModelWithConfig): @@ -226,13 +218,7 @@ class Example(TypedDict, total=False): value: Optional[Any] externalValue: Optional[AnyUrl] - if PYDANTIC_V2: # type: ignore [misc] - __pydantic_config__ = {"extra": "allow"} - - else: - - class Config: - extra = "allow" + __pydantic_config__ = {"extra": "allow"} # type: ignore[misc] class ParameterInType(Enum): @@ -447,6 +433,6 @@ class OpenAPI(BaseModelWithConfig): externalDocs: Optional[ExternalDocumentation] = None -_model_rebuild(Schema) -_model_rebuild(Operation) -_model_rebuild(Encoding) +Schema.model_rebuild() +Operation.model_rebuild() +Encoding.model_rebuild() diff --git a/fastapi/params.py b/fastapi/params.py index 4990d0e70e..c776c4a59e 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -9,8 +9,6 @@ from pydantic.fields import FieldInfo from typing_extensions import Literal, deprecated from ._compat import ( - PYDANTIC_V2, - PYDANTIC_VERSION_MINOR_TUPLE, Undefined, ) @@ -111,29 +109,24 @@ class Param(FieldInfo): # type: ignore[misc] stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra - if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): - self.deprecated = deprecated - else: - kwargs["deprecated"] = deprecated - if PYDANTIC_V2: - if serialization_alias in (_Unset, None) and isinstance(alias, str): - serialization_alias = alias - if validation_alias in (_Unset, None): - validation_alias = alias - kwargs.update( - { - "annotation": annotation, - "alias_priority": alias_priority, - "validation_alias": validation_alias, - "serialization_alias": serialization_alias, - "strict": strict, - "json_schema_extra": current_json_schema_extra, - } - ) - kwargs["pattern"] = pattern or regex - else: - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) + kwargs["deprecated"] = deprecated + + if serialization_alias in (_Unset, None) and isinstance(alias, str): + serialization_alias = alias + if validation_alias in (_Unset, None): + validation_alias = alias + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} super().__init__(**use_kwargs) @@ -571,29 +564,22 @@ class Body(FieldInfo): # type: ignore[misc] stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra - if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): - self.deprecated = deprecated - else: - kwargs["deprecated"] = deprecated - if PYDANTIC_V2: - if serialization_alias in (_Unset, None) and isinstance(alias, str): - serialization_alias = alias - if validation_alias in (_Unset, None): - validation_alias = alias - kwargs.update( - { - "annotation": annotation, - "alias_priority": alias_priority, - "validation_alias": validation_alias, - "serialization_alias": serialization_alias, - "strict": strict, - "json_schema_extra": current_json_schema_extra, - } - ) - kwargs["pattern"] = pattern or regex - else: - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) + kwargs["deprecated"] = deprecated + if serialization_alias in (_Unset, None) and isinstance(alias, str): + serialization_alias = alias + if validation_alias in (_Unset, None): + validation_alias = alias + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} diff --git a/fastapi/routing.py b/fastapi/routing.py index fa6904a6b6..a1f2e44bb4 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,4 +1,3 @@ -import dataclasses import email.message import functools import inspect @@ -30,6 +29,7 @@ from fastapi._compat import ( _model_dump, _normalize_errors, lenient_issubclass, + may_v1, ) from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.dependencies.models import Dependant @@ -58,7 +58,6 @@ from fastapi.utils import ( get_value_or_default, is_body_allowed_for_status_code, ) -from pydantic import BaseModel from starlette import routing from starlette._exception_handler import wrap_app_handling_exceptions from starlette._utils import is_async_callable @@ -153,8 +152,8 @@ def _prepare_response_content( exclude_defaults: bool = False, exclude_none: bool = False, ) -> Any: - if isinstance(res, BaseModel): - read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) + if isinstance(res, may_v1.BaseModel): + read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) # type: ignore[arg-type] if read_with_orm_mode: # Let from_orm extract the data from this model instead of converting # it now to a dict. @@ -162,7 +161,7 @@ def _prepare_response_content( # access instead of dict iteration, e.g. lazy relationships. return res return _model_dump( - res, + res, # type: ignore[arg-type] by_alias=True, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, @@ -188,9 +187,6 @@ def _prepare_response_content( ) for k, v in res.items() } - elif dataclasses.is_dataclass(res): - assert not isinstance(res, type) - return dataclasses.asdict(res) return res diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py index b6c804dc44..1bda0ea9b2 100644 --- a/fastapi/temp_pydantic_v1_params.py +++ b/fastapi/temp_pydantic_v1_params.py @@ -6,12 +6,11 @@ from fastapi.params import ParamTypes from typing_extensions import deprecated from ._compat.may_v1 import FieldInfo, Undefined -from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE _Unset: Any = Undefined -class Param(FieldInfo): # type: ignore[misc] +class Param(FieldInfo): in_: ParamTypes def __init__( @@ -98,10 +97,7 @@ class Param(FieldInfo): # type: ignore[misc] stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra - if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): - self.deprecated = deprecated - else: - kwargs["deprecated"] = deprecated + kwargs["deprecated"] = deprecated kwargs["regex"] = pattern or regex kwargs.update(**current_json_schema_extra) use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} @@ -112,7 +108,7 @@ class Param(FieldInfo): # type: ignore[misc] return f"{self.__class__.__name__}({self.default})" -class Path(Param): # type: ignore[misc] +class Path(Param): in_ = ParamTypes.path def __init__( @@ -198,7 +194,7 @@ class Path(Param): # type: ignore[misc] ) -class Query(Param): # type: ignore[misc] +class Query(Param): in_ = ParamTypes.query def __init__( @@ -282,7 +278,7 @@ class Query(Param): # type: ignore[misc] ) -class Header(Param): # type: ignore[misc] +class Header(Param): in_ = ParamTypes.header def __init__( @@ -368,7 +364,7 @@ class Header(Param): # type: ignore[misc] ) -class Cookie(Param): # type: ignore[misc] +class Cookie(Param): in_ = ParamTypes.cookie def __init__( @@ -452,7 +448,7 @@ class Cookie(Param): # type: ignore[misc] ) -class Body(FieldInfo): # type: ignore[misc] +class Body(FieldInfo): def __init__( self, default: Any = Undefined, @@ -541,10 +537,7 @@ class Body(FieldInfo): # type: ignore[misc] stacklevel=4, ) current_json_schema_extra = json_schema_extra or extra - if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7): - self.deprecated = deprecated - else: - kwargs["deprecated"] = deprecated + kwargs["deprecated"] = deprecated kwargs["regex"] = pattern or regex kwargs.update(**current_json_schema_extra) @@ -556,7 +549,7 @@ class Body(FieldInfo): # type: ignore[misc] return f"{self.__class__.__name__}({self.default})" -class Form(Body): # type: ignore[misc] +class Form(Body): def __init__( self, default: Any = Undefined, @@ -640,7 +633,7 @@ class Form(Body): # type: ignore[misc] ) -class File(Form): # type: ignore[misc] +class File(Form): def __init__( self, default: Any = Undefined, diff --git a/fastapi/utils.py b/fastapi/utils.py index 5b93b9af8e..c4631d7ed2 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,7 +1,6 @@ import re import warnings from collections.abc import MutableMapping -from dataclasses import is_dataclass from typing import ( TYPE_CHECKING, Any, @@ -13,7 +12,6 @@ from weakref import WeakKeyDictionary import fastapi from fastapi._compat import ( - PYDANTIC_V2, BaseConfig, ModelField, PydanticSchemaGenerationError, @@ -29,6 +27,8 @@ from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal +from ._compat import v2 + if TYPE_CHECKING: # pragma: nocover from .routing import APIRoute @@ -105,14 +105,12 @@ def create_model_field( from fastapi._compat import v1 try: - return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] + return v1.ModelField(**v1_kwargs) # type: ignore[return-value] except RuntimeError: raise fastapi.exceptions.FastAPIError( _invalid_args_message.format(type_=type_) ) from None - elif PYDANTIC_V2: - from ._compat import v2 - + else: field_info = field_info or FieldInfo( annotation=type_, default=default, alias=alias ) @@ -128,7 +126,7 @@ def create_model_field( from fastapi._compat import v1 try: - return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] + return v1.ModelField(**v1_kwargs) except RuntimeError: raise fastapi.exceptions.FastAPIError( _invalid_args_message.format(type_=type_) @@ -140,11 +138,8 @@ def create_cloned_field( *, cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None, ) -> ModelField: - if PYDANTIC_V2: - from ._compat import v2 - - if isinstance(field, v2.ModelField): - return field + if isinstance(field, v2.ModelField): + return field from fastapi._compat import v1 @@ -154,8 +149,6 @@ def create_cloned_field( cloned_types = _CLONED_TYPES_CACHE original_type = field.type_ - if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"): - original_type = original_type.__pydantic_model__ use_type = original_type if lenient_issubclass(original_type, v1.BaseModel): original_type = cast(type[v1.BaseModel], original_type) diff --git a/pyproject.toml b/pyproject.toml index dd16e1f8fa..ae97cb71bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ classifiers = [ "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", - "Framework :: Pydantic :: 1", "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "Programming Language :: Python :: 3 :: Only", @@ -45,7 +44,7 @@ classifiers = [ ] dependencies = [ "starlette>=0.40.0,<0.51.0", - "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", + "pydantic>=2.7.0", "typing-extensions>=4.8.0", "annotated-doc>=0.0.2", ] @@ -71,11 +70,10 @@ standard = [ "email-validator >=2.0.0", # Uvicorn with uvloop "uvicorn[standard] >=0.12.0", - # TODO: this should be part of some pydantic optional extra dependencies # # Settings management - # "pydantic-settings >=2.0.0", + "pydantic-settings >=2.0.0", # # Extra Pydantic data types - # "pydantic-extra-types >=2.0.0", + "pydantic-extra-types >=2.0.0", ] standard-no-fastapi-cloud-cli = [ @@ -90,11 +88,10 @@ standard-no-fastapi-cloud-cli = [ "email-validator >=2.0.0", # Uvicorn with uvloop "uvicorn[standard] >=0.12.0", - # TODO: this should be part of some pydantic optional extra dependencies # # Settings management - # "pydantic-settings >=2.0.0", + "pydantic-settings >=2.0.0", # # Extra Pydantic data types - # "pydantic-extra-types >=2.0.0", + "pydantic-extra-types >=2.0.0", ] all = [ @@ -183,8 +180,6 @@ filterwarnings = [ # Ref: https://github.com/python-trio/trio/pull/3054 # Remove once there's a new version of Trio 'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio', - # Ignore flaky coverage / pytest warning about SQLite connection, only applies to Python 3.13 and Pydantic v1 - 'ignore:Exception ignored in. = (3, 14): skip_module_if_py_gte_314() @@ -588,23 +588,14 @@ def test_openapi_schema(): "required": True, "content": { "application/json": { - "schema": pydantic_snapshot( - v1=snapshot( + "schema": { + "title": "Body", + "allOf": [ { "$ref": "#/components/schemas/Body_update_item_items__item_id__put" } - ), - v2=snapshot( - { - "title": "Body", - "allOf": [ - { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - ], - } - ), - ), + ], + } } }, }, @@ -809,23 +800,14 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v1=snapshot( + "schema": { + "allOf": [ { "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" } - ), - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" - } - ], - "title": "Body", - } - ), - ), + ], + "title": "Body", + } } }, "required": True, @@ -855,23 +837,14 @@ def test_openapi_schema(): "requestBody": { "content": { "application/x-www-form-urlencoded": { - "schema": pydantic_snapshot( - v1=snapshot( + "schema": { + "allOf": [ { "$ref": "#/components/schemas/Body_submit_form_form_data__post" } - ), - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_submit_form_form_data__post" - } - ], - "title": "Body", - } - ), - ), + ], + "title": "Body", + } } }, "required": True, @@ -901,23 +874,14 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": pydantic_snapshot( - v1=snapshot( + "schema": { + "allOf": [ { "$ref": "#/components/schemas/Body_upload_file_upload__post" } - ), - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_upload_file_upload__post" - } - ], - "title": "Body", - } - ), - ), + ], + "title": "Body", + }, } }, "required": True, @@ -947,23 +911,14 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": pydantic_snapshot( - v1=snapshot( + "schema": { + "allOf": [ { "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" } - ), - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" - } - ], - "title": "Body", - } - ), - ), + ], + "title": "Body", + } } }, "required": True, @@ -990,21 +945,12 @@ def test_openapi_schema(): "components": { "schemas": { "Body_create_item_embed_items_embed__post": { - "properties": pydantic_snapshot( - v1=snapshot( - {"item": {"$ref": "#/components/schemas/Item"}} - ), - v2=snapshot( - { - "item": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - } - ), - ), + "properties": { + "item": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + } + }, "type": "object", "required": ["item"], "title": "Body_create_item_embed_items_embed__post", @@ -1030,17 +976,10 @@ def test_openapi_schema(): }, "Body_update_item_items__item_id__put": { "properties": { - "item": pydantic_snapshot( - v1=snapshot({"$ref": "#/components/schemas/Item"}), - v2=snapshot( - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - ), + "item": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + }, "importance": { "type": "integer", "maximum": 10.0, diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py index f2e42999b3..e7f969f7cf 100644 --- a/tests/test_computed_fields.py +++ b/tests/test_computed_fields.py @@ -2,8 +2,6 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -from .utils import needs_pydanticv2 - @pytest.fixture(name="client") def get_client(request): @@ -35,7 +33,6 @@ def get_client(request): @pytest.mark.parametrize("client", [True, False], indirect=True) @pytest.mark.parametrize("path", ["/", "/responses"]) -@needs_pydanticv2 def test_get(client: TestClient, path: str): response = client.get(path) assert response.status_code == 200, response.text @@ -43,7 +40,6 @@ def test_get(client: TestClient, path: str): @pytest.mark.parametrize("client", [True, False], indirect=True) -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_custom_schema_fields.py b/tests/test_custom_schema_fields.py index bc3e1c5ec3..60b795e9ba 100644 --- a/tests/test_custom_schema_fields.py +++ b/tests/test_custom_schema_fields.py @@ -1,12 +1,8 @@ from typing import Annotated, Optional from fastapi import FastAPI -from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel - -if PYDANTIC_V2: - from pydantic import WithJsonSchema +from pydantic import BaseModel, WithJsonSchema app = FastAPI() @@ -14,23 +10,15 @@ app = FastAPI() class Item(BaseModel): name: str - if PYDANTIC_V2: - description: Annotated[ - Optional[str], WithJsonSchema({"type": ["string", "null"]}) - ] = None + description: Annotated[ + Optional[str], WithJsonSchema({"type": ["string", "null"]}) + ] = None - model_config = { - "json_schema_extra": { - "x-something-internal": {"level": 4}, - } + model_config = { + "json_schema_extra": { + "x-something-internal": {"level": 4}, } - else: - description: Optional[str] = None # type: ignore[no-redef] - - class Config: - schema_extra = { - "x-something-internal": {"level": 4}, - } + } @app.get("/foo", response_model=Item) @@ -55,7 +43,7 @@ item_schema = { }, "description": { "title": "Description", - "type": ["string", "null"] if PYDANTIC_V2 else "string", + "type": ["string", "null"], }, }, } diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index c175147bc3..29a70cae0c 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -7,12 +7,6 @@ from fastapi.datastructures import Default from fastapi.testclient import TestClient -# TODO: remove when deprecating Pydantic v1 -def test_upload_file_invalid(): - with pytest.raises(ValueError): - UploadFile.validate("not a Starlette UploadFile") - - def test_upload_file_invalid_pydantic_v2(): with pytest.raises(ValueError): UploadFile._validate("not a Starlette UploadFile", {}) diff --git a/tests/test_datetime_custom_encoder.py b/tests/test_datetime_custom_encoder.py index 3aa77c0b1d..822651f4f1 100644 --- a/tests/test_datetime_custom_encoder.py +++ b/tests/test_datetime_custom_encoder.py @@ -4,10 +4,9 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1, needs_pydanticv2 +from .utils import needs_pydanticv1 -@needs_pydanticv2 def test_pydanticv2(): from pydantic import field_serializer @@ -34,7 +33,9 @@ def test_pydanticv2(): # TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_pydanticv1(): - class ModelWithDatetimeField(BaseModel): + from pydantic import v1 + + class ModelWithDatetimeField(v1.BaseModel): dt_field: datetime class Config: diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py index 657e8c5d12..0b6ab53e04 100644 --- a/tests/test_filter_pydantic_sub_model/app_pv1.py +++ b/tests/test_filter_pydantic_sub_model/app_pv1.py @@ -1,7 +1,7 @@ from typing import Optional from fastapi import Depends, FastAPI -from pydantic import BaseModel, validator +from pydantic.v1 import BaseModel, validator app = FastAPI() @@ -18,6 +18,7 @@ class ModelA(BaseModel): name: str description: Optional[str] = None model_b: ModelB + tags: dict[str, str] = {} @validator("name") def lower_username(cls, name: str, values): @@ -32,4 +33,9 @@ async def get_model_c() -> ModelC: @app.get("/model/{name}", response_model=ModelA) async def get_model_a(name: str, model_c=Depends(get_model_c)): - return {"name": name, "description": "model-a-desc", "model_b": model_c} + return { + "name": name, + "description": "model-a-desc", + "model_b": model_c, + "tags": {"key1": "value1", "key2": "value2"}, + } diff --git a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py index 48732dbf06..b464b4f572 100644 --- a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py +++ b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py @@ -1,6 +1,7 @@ import pytest from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ..utils import needs_pydanticv1 @@ -21,6 +22,7 @@ def test_filter_sub_model(client: TestClient): "name": "modelA", "description": "model-a-desc", "model_b": {"username": "test-user"}, + "tags": {"key1": "value1", "key2": "value2"}, } @@ -41,90 +43,104 @@ def test_validator_is_cloned(client: TestClient): def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/model/{name}": { - "get": { - "summary": "Get Model A", - "operationId": "get_model_a_model__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ModelA"} - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model/{name}": { + "get": { + "summary": "Get Model A", + "operationId": "get_model_a_model__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelA" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "ModelA": { + "title": "ModelA", + "required": ["name", "model_b"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "model_b": {"$ref": "#/components/schemas/ModelB"}, + "tags": { + "additionalProperties": {"type": "string"}, + "type": "object", + "title": "Tags", + "default": {}, }, }, }, + "ModelB": { + "title": "ModelB", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ModelA": { - "title": "ModelA", - "required": ["name", "model_b"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "model_b": {"$ref": "#/components/schemas/ModelB"}, - }, - }, - "ModelB": { - "title": "ModelB", - "required": ["username"], - "type": "object", - "properties": {"username": {"title": "Username", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index 2e2c26ddcb..d70f530435 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -5,8 +5,7 @@ from dirty_equals import HasRepr, IsDict, IsOneOf from fastapi import Depends, FastAPI from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient - -from .utils import needs_pydanticv2 +from inline_snapshot import snapshot @pytest.fixture(name="client") @@ -25,6 +24,7 @@ def get_client(): name: str description: Optional[str] = None foo: ModelB + tags: dict[str, str] = {} @field_validator("name") def lower_username(cls, name: str, info: ValidationInfo): @@ -37,13 +37,17 @@ def get_client(): @app.get("/model/{name}", response_model=ModelA) async def get_model_a(name: str, model_c=Depends(get_model_c)): - return {"name": name, "description": "model-a-desc", "foo": model_c} + return { + "name": name, + "description": "model-a-desc", + "foo": model_c, + "tags": {"key1": "value1", "key2": "value2"}, + } client = TestClient(app) return client -@needs_pydanticv2 def test_filter_sub_model(client: TestClient): response = client.get("/model/modelA") assert response.status_code == 200, response.text @@ -51,10 +55,10 @@ def test_filter_sub_model(client: TestClient): "name": "modelA", "description": "model-a-desc", "foo": {"username": "test-user"}, + "tags": {"key1": "value1", "key2": "value2"}, } -@needs_pydanticv2 def test_validator_is_cloned(client: TestClient): with pytest.raises(ResponseValidationError) as err: client.get("/model/modelX") @@ -79,106 +83,119 @@ def test_validator_is_cloned(client: TestClient): ] -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/model/{name}": { - "get": { - "summary": "Get Model A", - "operationId": "get_model_a_model__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ModelA"} - } + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model/{name}": { + "get": { + "summary": "Get Model A", + "operationId": "get_model_a_model__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelA" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "ModelA": { + "title": "ModelA", + "required": IsOneOf( + ["name", "description", "foo"], + # TODO remove when deprecating Pydantic v1 + ["name", "foo"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], } + ) + | + # TODO remove when deprecating Pydantic v1 + IsDict({"title": "Description", "type": "string"}), + "foo": {"$ref": "#/components/schemas/ModelB"}, + "tags": { + "additionalProperties": {"type": "string"}, + "type": "object", + "title": "Tags", + "default": {}, }, }, }, + "ModelB": { + "title": "ModelB", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ModelA": { - "title": "ModelA", - "required": IsOneOf( - ["name", "description", "foo"], - # TODO remove when deprecating Pydantic v1 - ["name", "foo"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | - # TODO remove when deprecating Pydantic v1 - IsDict({"title": "Description", "type": "string"}), - "foo": {"$ref": "#/components/schemas/ModelB"}, - }, - }, - "ModelB": { - "title": "ModelB", - "required": ["username"], - "type": "object", - "properties": {"username": {"title": "Username", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py index b149b76539..c401cc9374 100644 --- a/tests/test_forms_single_model.py +++ b/tests/test_forms_single_model.py @@ -2,7 +2,6 @@ from typing import Annotated, Optional from dirty_equals import IsDict from fastapi import FastAPI, Form -from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -20,12 +19,7 @@ class FormModel(BaseModel): class FormModelExtraAllow(BaseModel): param: str - if PYDANTIC_V2: - model_config = {"extra": "allow"} - else: - - class Config: - extra = "allow" + model_config = {"extra": "allow"} @app.post("/form/") diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index 215d06a072..50d799a571 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -1,181 +1,169 @@ -from collections.abc import Iterator -from typing import Any - -import fastapi._compat -import fastapi.openapi.utils -import pydantic.schema import pytest from fastapi import FastAPI -from pydantic import BaseModel -from starlette.testclient import TestClient +from fastapi.testclient import TestClient +from inline_snapshot import snapshot from .utils import needs_pydanticv1 -class Address(BaseModel): - """ - This is a public description of an Address - \f - You can't see this part of the docstring, it's private! - """ +@pytest.fixture( + name="client", + params=[ + pytest.param("pydantic-v1", marks=needs_pydanticv1), + "pydantic-v2", + ], +) +def client_fixture(request: pytest.FixtureRequest) -> TestClient: + if request.param == "pydantic-v1": + from pydantic.v1 import BaseModel + else: + from pydantic import BaseModel - line_1: str - city: str - state_province: str + class Address(BaseModel): + """ + This is a public description of an Address + \f + You can't see this part of the docstring, it's private! + """ + + line_1: str + city: str + state_province: str + + class Facility(BaseModel): + id: str + address: Address + + app = FastAPI() + + @app.get("/facilities/{facility_id}") + def get_facility(facility_id: str) -> Facility: + return Facility( + id=facility_id, + address=Address(line_1="123 Main St", city="Anytown", state_province="CA"), + ) + + client = TestClient(app) + return client -class Facility(BaseModel): - id: str - address: Address +def test_get(client: TestClient): + response = client.get("/facilities/42") + assert response.status_code == 200, response.text + assert response.json() == { + "id": "42", + "address": { + "line_1": "123 Main St", + "city": "Anytown", + "state_province": "CA", + }, + } -app = FastAPI() - -client = TestClient(app) - - -@app.get("/facilities/{facility_id}") -def get_facility(facility_id: str) -> Facility: ... - - -openapi_schema = { - "components": { - "schemas": { - "Address": { - # NOTE: the description of this model shows only the public-facing text, before the `\f` in docstring - "description": "This is a public description of an Address\n", - "properties": { - "city": {"title": "City", "type": "string"}, - "line_1": {"title": "Line 1", "type": "string"}, - "state_province": {"title": "State Province", "type": "string"}, - }, - "required": ["line_1", "city", "state_province"], - "title": "Address", - "type": "object", - }, - "Facility": { - "properties": { - "address": {"$ref": "#/components/schemas/Address"}, - "id": {"title": "Id", "type": "string"}, - }, - "required": ["id", "address"], - "title": "Facility", - "type": "object", - }, - "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", - }, - } - }, - "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.1.0", - "paths": { - "/facilities/{facility_id}": { - "get": { - "operationId": "get_facility_facilities__facility_id__get", - "parameters": [ - { - "in": "path", - "name": "facility_id", - "required": True, - "schema": {"title": "Facility Id", "type": "string"}, - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Facility"} - } - }, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error", - }, - }, - "summary": "Get Facility", - } - } - }, -} - - -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): """ Sanity check to ensure our app's openapi schema renders as we expect """ response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -class SortedTypeSet(set): - """ - Set of Types whose `__iter__()` method yields results sorted by the type names - """ - - def __init__(self, seq: set[type[Any]], *, sort_reversed: bool): - super().__init__(seq) - self.sort_reversed = sort_reversed - - def __iter__(self) -> Iterator[type[Any]]: - members_sorted = sorted( - super().__iter__(), - key=lambda type_: type_.__name__, - reverse=self.sort_reversed, - ) - yield from members_sorted - - -@needs_pydanticv1 -@pytest.mark.parametrize("sort_reversed", [True, False]) -def test_model_description_escaped_with_formfeed(sort_reversed: bool): - """ - Regression test for bug fixed by https://github.com/fastapi/fastapi/pull/6039. - - Test `get_model_definitions` with models passed in different order. - """ - from fastapi._compat import v1 - - all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes) - - flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set()) - model_name_map = pydantic.schema.get_model_name_map(flat_models) - - expected_address_description = "This is a public description of an Address\n" - - models = v1.get_model_definitions( - flat_models=SortedTypeSet(flat_models, sort_reversed=sort_reversed), - model_name_map=model_name_map, + assert response.json() == snapshot( + { + "components": { + "schemas": { + "Address": { + # NOTE: the description of this model shows only the public-facing text, before the `\f` in docstring + "description": "This is a public description of an Address\n", + "properties": { + "city": {"title": "City", "type": "string"}, + "line_1": {"title": "Line 1", "type": "string"}, + "state_province": { + "title": "State Province", + "type": "string", + }, + }, + "required": ["line_1", "city", "state_province"], + "title": "Address", + "type": "object", + }, + "Facility": { + "properties": { + "address": {"$ref": "#/components/schemas/Address"}, + "id": {"title": "Id", "type": "string"}, + }, + "required": ["id", "address"], + "title": "Facility", + "type": "object", + }, + "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", + }, + } + }, + "info": {"title": "FastAPI", "version": "0.1.0"}, + "openapi": "3.1.0", + "paths": { + "/facilities/{facility_id}": { + "get": { + "operationId": "get_facility_facilities__facility_id__get", + "parameters": [ + { + "in": "path", + "name": "facility_id", + "required": True, + "schema": {"title": "Facility Id", "type": "string"}, + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Facility" + } + } + }, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error", + }, + }, + "summary": "Get Facility", + } + } + }, + } ) - assert models["Address"]["description"] == expected_address_description diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py index fe9350f4ef..7f29fe33ed 100644 --- a/tests/test_inherited_custom_class.py +++ b/tests/test_inherited_custom_class.py @@ -5,7 +5,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1, needs_pydanticv2 +from .utils import needs_pydanticv1 class MyUuid: @@ -26,7 +26,6 @@ class MyUuid: raise TypeError("vars() argument must have __dict__ attribute") -@needs_pydanticv2 def test_pydanticv2(): from pydantic import field_serializer @@ -73,6 +72,8 @@ def test_pydanticv2(): # TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_pydanticv1(): + from pydantic import v1 + app = FastAPI() @app.get("/fast_uuid") @@ -84,7 +85,7 @@ def test_pydanticv1(): vars(asyncpg_uuid) return {"fast_uuid": asyncpg_uuid} - class SomeCustomClass(BaseModel): + class SomeCustomClass(v1.BaseModel): class Config: arbitrary_types_allowed = True json_encoders = {uuid.UUID: str} diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 3b6513e27b..81bf94ece0 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -8,11 +8,11 @@ from pathlib import PurePath, PurePosixPath, PureWindowsPath from typing import Optional import pytest -from fastapi._compat import PYDANTIC_V2, Undefined +from fastapi._compat import Undefined from fastapi.encoders import jsonable_encoder from pydantic import BaseModel, Field, ValidationError -from .utils import needs_pydanticv1, needs_pydanticv2 +from .utils import needs_pydanticv1 class Person: @@ -59,12 +59,7 @@ class RoleEnum(Enum): class ModelWithConfig(BaseModel): role: Optional[RoleEnum] = None - if PYDANTIC_V2: - model_config = {"use_enum_values": True} - else: - - class Config: - use_enum_values = True + model_config = {"use_enum_values": True} class ModelWithAlias(BaseModel): @@ -89,6 +84,18 @@ def test_encode_dict(): } +def test_encode_dict_include_exclude_list(): + pet = {"name": "Firulais", "owner": {"name": "Foo"}} + assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} + assert jsonable_encoder(pet, include=["name"]) == {"name": "Firulais"} + assert jsonable_encoder(pet, exclude=["owner"]) == {"name": "Firulais"} + assert jsonable_encoder(pet, include=[]) == {} + assert jsonable_encoder(pet, exclude=[]) == { + "name": "Firulais", + "owner": {"name": "Foo"}, + } + + def test_encode_class(): person = Person(name="Foo") pet = Pet(owner=person, name="Firulais") @@ -130,7 +137,6 @@ def test_encode_unsupported(): jsonable_encoder(unserializable) -@needs_pydanticv2 def test_encode_custom_json_encoders_model_pydanticv2(): from pydantic import field_serializer @@ -153,7 +159,9 @@ def test_encode_custom_json_encoders_model_pydanticv2(): # TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_encode_custom_json_encoders_model_pydanticv1(): - class ModelWithCustomEncoder(BaseModel): + from pydantic import v1 + + class ModelWithCustomEncoder(v1.BaseModel): dt_field: datetime class Config: @@ -208,10 +216,12 @@ def test_encode_model_with_default(): @needs_pydanticv1 def test_custom_encoders(): + from pydantic import v1 + class safe_datetime(datetime): pass - class MyModel(BaseModel): + class MyModel(v1.BaseModel): dt_field: safe_datetime instance = MyModel(dt_field=safe_datetime.now()) @@ -244,12 +254,7 @@ def test_encode_model_with_pure_path(): class ModelWithPath(BaseModel): path: PurePath - if PYDANTIC_V2: - model_config = {"arbitrary_types_allowed": True} - else: - - class Config: - arbitrary_types_allowed = True + model_config = {"arbitrary_types_allowed": True} test_path = PurePath("/foo", "bar") obj = ModelWithPath(path=test_path) @@ -260,12 +265,7 @@ def test_encode_model_with_pure_posix_path(): class ModelWithPath(BaseModel): path: PurePosixPath - if PYDANTIC_V2: - model_config = {"arbitrary_types_allowed": True} - else: - - class Config: - arbitrary_types_allowed = True + model_config = {"arbitrary_types_allowed": True} obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) assert jsonable_encoder(obj) == {"path": "/foo/bar"} @@ -275,45 +275,44 @@ def test_encode_model_with_pure_windows_path(): class ModelWithPath(BaseModel): path: PureWindowsPath - if PYDANTIC_V2: - model_config = {"arbitrary_types_allowed": True} - else: - - class Config: - arbitrary_types_allowed = True + model_config = {"arbitrary_types_allowed": True} obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} +def test_encode_pure_path(): + test_path = PurePath("/foo", "bar") + + assert jsonable_encoder({"path": test_path}) == {"path": str(test_path)} + + @needs_pydanticv1 def test_encode_root(): - class ModelWithRoot(BaseModel): + from pydantic import v1 + + class ModelWithRoot(v1.BaseModel): __root__: str model = ModelWithRoot(__root__="Foo") assert jsonable_encoder(model) == "Foo" -@needs_pydanticv2 def test_decimal_encoder_float(): data = {"value": Decimal(1.23)} assert jsonable_encoder(data) == {"value": 1.23} -@needs_pydanticv2 def test_decimal_encoder_int(): data = {"value": Decimal(2)} assert jsonable_encoder(data) == {"value": 2} -@needs_pydanticv2 def test_decimal_encoder_nan(): data = {"value": Decimal("NaN")} assert isnan(jsonable_encoder(data)["value"]) -@needs_pydanticv2 def test_decimal_encoder_infinity(): data = {"value": Decimal("Infinity")} assert isinf(jsonable_encoder(data)["value"]) @@ -330,7 +329,6 @@ def test_encode_deque_encodes_child_models(): assert jsonable_encoder(dq)[0]["test"] == "test" -@needs_pydanticv2 def test_encode_pydantic_undefined(): data = {"value": Undefined} assert jsonable_encoder(data) == {"value": None} diff --git a/tests/test_no_schema_split.py b/tests/test_no_schema_split.py index 6169867ba3..131a3755e7 100644 --- a/tests/test_no_schema_split.py +++ b/tests/test_no_schema_split.py @@ -9,8 +9,6 @@ from fastapi.testclient import TestClient from inline_snapshot import snapshot from pydantic import BaseModel, Field -from tests.utils import pydantic_snapshot - class MessageEventType(str, Enum): alpha = "alpha" @@ -126,47 +124,21 @@ def test_openapi_schema(): }, "MessageEvent": { "properties": { - "event_type": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/MessageEventType", - "default": "alpha", - } - ), - v1=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/MessageEventType" - } - ], - "default": "alpha", - } - ), - ), + "event_type": { + "$ref": "#/components/schemas/MessageEventType", + "default": "alpha", + }, "output": {"type": "string", "title": "Output"}, }, "type": "object", "required": ["output"], "title": "MessageEvent", }, - "MessageEventType": pydantic_snapshot( - v2=snapshot( - { - "type": "string", - "enum": ["alpha", "beta"], - "title": "MessageEventType", - } - ), - v1=snapshot( - { - "type": "string", - "enum": ["alpha", "beta"], - "title": "MessageEventType", - "description": "An enumeration.", - } - ), - ), + "MessageEventType": { + "type": "string", + "enum": ["alpha", "beta"], + "title": "MessageEventType", + }, "MessageOutput": { "properties": { "body": {"type": "string", "title": "Body", "default": ""}, diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index bfe5e9a712..1891f0bde0 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -3,37 +3,30 @@ from typing import Optional from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot -from pydantic import BaseModel - -from .utils import PYDANTIC_V2, needs_pydanticv2 +from pydantic import BaseModel, computed_field class SubItem(BaseModel): subname: str sub_description: Optional[str] = None tags: list[str] = [] - if PYDANTIC_V2: - model_config = {"json_schema_serialization_defaults_required": True} + model_config = {"json_schema_serialization_defaults_required": True} class Item(BaseModel): name: str description: Optional[str] = None sub: Optional[SubItem] = None - if PYDANTIC_V2: - model_config = {"json_schema_serialization_defaults_required": True} + model_config = {"json_schema_serialization_defaults_required": True} -if PYDANTIC_V2: - from pydantic import computed_field +class WithComputedField(BaseModel): + name: str - class WithComputedField(BaseModel): - name: str - - @computed_field - @property - def computed_field(self) -> str: - return f"computed {self.name}" + @computed_field + @property + def computed_field(self) -> str: + return f"computed {self.name}" def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: @@ -58,13 +51,11 @@ def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: Item(name="Plumbus"), ] - if PYDANTIC_V2: - - @app.post("/with-computed-field/") - def create_with_computed_field( - with_computed_field: WithComputedField, - ) -> WithComputedField: - return with_computed_field + @app.post("/with-computed-field/") + def create_with_computed_field( + with_computed_field: WithComputedField, + ) -> WithComputedField: + return with_computed_field client = TestClient(app) return client @@ -151,7 +142,6 @@ def test_read_items(): ) -@needs_pydanticv2 def test_with_computed_field(): client = get_app_client() client_no = get_app_client(separate_input_output_schemas=False) @@ -168,7 +158,6 @@ def test_with_computed_field(): ) -@needs_pydanticv2 def test_openapi_schema(): client = get_app_client() response = client.get("/openapi.json") @@ -449,7 +438,6 @@ def test_openapi_schema(): ) -@needs_pydanticv2 def test_openapi_schema_no_separate(): client = get_app_client(separate_input_output_schemas=False) response = client.get("/openapi.json") diff --git a/tests/test_pydantic_v1_v2_01.py b/tests/test_pydantic_v1_v2_01.py index ebf86b99f0..83536cafa2 100644 --- a/tests/test_pydantic_v1_v2_01.py +++ b/tests/test_pydantic_v1_v2_01.py @@ -1,7 +1,7 @@ import sys from typing import Any, Union -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 +from tests.utils import skip_module_if_py_gte_314 if sys.version_info >= (3, 14): skip_module_if_py_gte_314() @@ -225,21 +225,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/SubItem" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/SubItem"} - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -275,21 +266,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/SubItem" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/SubItem"} - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -325,21 +307,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/Item"} - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -373,21 +346,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/Item"} - ), - ) + ], + "title": "Data", + } } }, "required": True, diff --git a/tests/test_pydantic_v1_v2_list.py b/tests/test_pydantic_v1_v2_list.py index 8879b010de..4ddcbf240d 100644 --- a/tests/test_pydantic_v1_v2_list.py +++ b/tests/test_pydantic_v1_v2_list.py @@ -1,7 +1,7 @@ import sys from typing import Any, Union -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 +from tests.utils import skip_module_if_py_gte_314 if sys.version_info >= (3, 14): skip_module_if_py_gte_314() @@ -375,21 +375,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/Item"} - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -429,21 +420,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/Item"} - ), - ) + ], + "title": "Data", + } } }, "required": True, diff --git a/tests/test_pydantic_v1_v2_mixed.py b/tests/test_pydantic_v1_v2_mixed.py index e66583cd54..61e5bb5827 100644 --- a/tests/test_pydantic_v1_v2_mixed.py +++ b/tests/test_pydantic_v1_v2_mixed.py @@ -1,7 +1,7 @@ import sys from typing import Any, Union -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 +from tests.utils import skip_module_if_py_gte_314 if sys.version_info >= (3, 14): skip_module_if_py_gte_314() @@ -668,38 +668,20 @@ def test_v2_to_v1_validation_error(): assert response.status_code == 422, response.text assert response.json() == snapshot( { - "detail": pydantic_snapshot( - v2=snapshot( - [ - { - "type": "missing", - "loc": ["body", "new_size"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - { - "type": "missing", - "loc": ["body", "new_sub"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - ] - ), - v1=snapshot( - [ - { - "loc": ["body", "new_size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "new_sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - ), - ) + "detail": [ + { + "type": "missing", + "loc": ["body", "new_size"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + { + "type": "missing", + "loc": ["body", "new_sub"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + ] } ) @@ -717,23 +699,12 @@ def test_v2_to_v1_nested_validation_error(): assert response.json() == snapshot( { "detail": [ - pydantic_snapshot( - v2=snapshot( - { - "type": "missing", - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "Field required", - "input": {"wrong_field": "value"}, - } - ), - v1=snapshot( - { - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "field required", - "type": "value_error.missing", - } - ), - ) + { + "type": "missing", + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "Field required", + "input": {"wrong_field": "value"}, + } ] } ) @@ -777,38 +748,20 @@ def test_v2_list_validation_error(): assert response.status_code == 422, response.text assert response.json() == snapshot( { - "detail": pydantic_snapshot( - v2=snapshot( - [ - { - "type": "missing", - "loc": ["body", 1, "new_size"], - "msg": "Field required", - "input": {"new_title": "Invalid"}, - }, - { - "type": "missing", - "loc": ["body", 1, "new_sub"], - "msg": "Field required", - "input": {"new_title": "Invalid"}, - }, - ] - ), - v1=snapshot( - [ - { - "loc": ["body", 1, "new_size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "new_sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - ), - ) + "detail": [ + { + "type": "missing", + "loc": ["body", 1, "new_size"], + "msg": "Field required", + "input": {"new_title": "Invalid"}, + }, + { + "type": "missing", + "loc": ["body", 1, "new_sub"], + "msg": "Field required", + "input": {"new_title": "Invalid"}, + }, + ] } ) @@ -844,31 +797,18 @@ def test_invalid_list_structure_v2(): assert response.status_code == 422, response.text assert response.json() == snapshot( { - "detail": pydantic_snapshot( - v2=snapshot( - [ - { - "type": "list_type", - "loc": ["body"], - "msg": "Input should be a valid list", - "input": { - "new_title": "Not a list", - "new_size": 100, - "new_sub": {"new_sub_name": "Sub"}, - }, - } - ] - ), - v1=snapshot( - [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ] - ), - ) + "detail": [ + { + "type": "list_type", + "loc": ["body"], + "msg": "Input should be a valid list", + "input": { + "new_title": "Not a list", + "new_size": 100, + "new_sub": {"new_sub_name": "Sub"}, + }, + } + ] } ) @@ -888,21 +828,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/Item"} - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -938,21 +869,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/Item"} - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -1056,21 +978,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/Item"} - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -1440,17 +1353,10 @@ def test_openapi_schema(): "properties": { "new_title": {"type": "string", "title": "New Title"}, "new_size": {"type": "integer", "title": "New Size"}, - "new_description": pydantic_snapshot( - v2=snapshot( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - } - ), - v1=snapshot( - {"type": "string", "title": "New Description"} - ), - ), + "new_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + }, "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, "new_multi": { "items": {"$ref": "#/components/schemas/NewSubItem"}, diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py index e66d102fb3..32d9019616 100644 --- a/tests/test_pydantic_v1_v2_multifile/test_multifile.py +++ b/tests/test_pydantic_v1_v2_multifile/test_multifile.py @@ -1,6 +1,6 @@ import sys -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 +from tests.utils import skip_module_if_py_gte_314 if sys.version_info >= (3, 14): skip_module_if_py_gte_314() @@ -292,23 +292,14 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ { "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" } - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -344,18 +335,9 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - ), - ), + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + }, } }, "required": True, @@ -391,23 +373,14 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ { "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" } - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -535,18 +508,9 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - ), - ), + "schema": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + }, } }, "required": True, @@ -587,18 +551,9 @@ def test_openapi_schema(): "content": { "application/json": { "schema": { - "items": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - ), - ), + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + }, "type": "array", "title": "Data", } @@ -642,18 +597,9 @@ def test_openapi_schema(): "content": { "application/json": { "schema": { - "items": pydantic_snapshot( - v2=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - } - ), - v1=snapshot( - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - ), - ), + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + }, "type": "array", "title": "Data", } @@ -767,460 +713,239 @@ def test_openapi_schema(): }, }, "components": { - "schemas": pydantic_snapshot( - v1=snapshot( - { - "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { - "properties": { - "data1": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" - }, - "type": "array", - "title": "Data1", - }, - "data2": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" - }, - "type": "array", - "title": "Data2", - }, + "schemas": { + "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { + "properties": { + "data1": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" }, - "type": "object", - "required": ["data1", "data2"], - "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", + "type": "array", + "title": "Data1", }, - "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { - "properties": { - "item1": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "item2": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" - }, + "data2": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" }, - "type": "object", - "required": ["item1", "item2"], - "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", + "type": "array", + "title": "Data2", }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } + }, + "type": "object", + "required": ["data1", "data2"], + "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", + }, + "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { + "properties": { + "item1": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" + }, + "item2": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" + }, + }, + "type": "object", + "required": ["item1", "item2"], + "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - {"type": "string"}, - {"type": "integer"}, - ] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + ] }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", + "type": "array", + "title": "Location", }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": { - "type": "string", - "title": "Description", - }, - "sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "type": "array", - "title": "Multi", - "default": [], - }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { + "properties": { + "title": {"type": "string", "title": "Title"}, + "size": {"type": "integer", "title": "Size"}, + "description": { + "type": "string", + "title": "Description", + }, + "sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" + }, + "multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", + "type": "array", + "title": "Multi", + "default": [], }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { - "properties": { - "name1": {"type": "string", "title": "Name1"} + }, + "type": "object", + "required": ["title", "size", "sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { + "properties": {"name1": {"type": "string", "title": "Name1"}}, + "type": "object", + "required": ["name1"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { + "properties": {"name": {"type": "string", "title": "Name"}}, + "type": "object", + "required": ["name"], + "title": "SubItem", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { + "properties": { + "new_title": { + "type": "string", + "title": "New Title", + }, + "new_size": { + "type": "integer", + "title": "New Size", + }, + "new_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + }, + "new_sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" + }, + "new_multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" }, - "type": "object", - "required": ["name1"], - "title": "ItemInList", + "type": "array", + "title": "New Multi", + "default": [], }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { - "properties": { - "name": {"type": "string", "title": "Name"} + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input": { + "properties": { + "new_title": { + "type": "string", + "title": "New Title", + }, + "new_size": { + "type": "integer", + "title": "New Size", + }, + "new_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + }, + "new_sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" + }, + "new_multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" }, - "type": "object", - "required": ["name"], - "title": "SubItem", + "type": "array", + "title": "New Multi", + "default": [], }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "type": "string", - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, + }, + "type": "object", + "required": ["new_title", "new_size", "new_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { + "properties": {"name2": {"type": "string", "title": "Name2"}}, + "type": "object", + "required": ["name2"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { + "properties": { + "new_sub_name": { + "type": "string", + "title": "New Sub Name", + } + }, + "type": "object", + "required": ["new_sub_name"], + "title": "SubItem", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { + "properties": { + "dup_title": { + "type": "string", + "title": "Dup Title", + }, + "dup_size": { + "type": "integer", + "title": "Dup Size", + }, + "dup_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Dup Description", + }, + "dup_sub": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" + }, + "dup_multi": { + "items": { + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", + "type": "array", + "title": "Dup Multi", + "default": [], }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { - "properties": { - "name2": {"type": "string", "title": "Name2"} - }, - "type": "object", - "required": ["name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { - "properties": { - "new_sub_name": { - "type": "string", - "title": "New Sub Name", - } - }, - "type": "object", - "required": ["new_sub_name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { - "properties": { - "dup_title": { - "type": "string", - "title": "Dup Title", - }, - "dup_size": { - "type": "integer", - "title": "Dup Size", - }, - "dup_description": { - "type": "string", - "title": "Dup Description", - }, - "dup_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "dup_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "type": "array", - "title": "Dup Multi", - "default": [], - }, - }, - "type": "object", - "required": ["dup_title", "dup_size", "dup_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { - "properties": { - "dup_name2": { - "type": "string", - "title": "Dup Name2", - } - }, - "type": "object", - "required": ["dup_name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { - "properties": { - "dup_sub_name": { - "type": "string", - "title": "Dup Sub Name", - } - }, - "type": "object", - "required": ["dup_sub_name"], - "title": "SubItem", - }, - } - ), - v2=snapshot( - { - "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { - "properties": { - "data1": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" - }, - "type": "array", - "title": "Data1", - }, - "data2": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" - }, - "type": "array", - "title": "Data2", - }, - }, - "type": "object", - "required": ["data1", "data2"], - "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - }, - "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { - "properties": { - "item1": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "item2": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" - }, - }, - "type": "object", - "required": ["item1", "item2"], - "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - {"type": "string"}, - {"type": "integer"}, - ] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": { - "type": "string", - "title": "Description", - }, - "sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { - "properties": { - "name1": {"type": "string", "title": "Name1"} - }, - "type": "object", - "required": ["name1"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { - "properties": { - "name": {"type": "string", "title": "Name"} - }, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { - "properties": { - "name2": {"type": "string", "title": "Name2"} - }, - "type": "object", - "required": ["name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { - "properties": { - "new_sub_name": { - "type": "string", - "title": "New Sub Name", - } - }, - "type": "object", - "required": ["new_sub_name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { - "properties": { - "dup_title": { - "type": "string", - "title": "Dup Title", - }, - "dup_size": { - "type": "integer", - "title": "Dup Size", - }, - "dup_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Dup Description", - }, - "dup_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "dup_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "type": "array", - "title": "Dup Multi", - "default": [], - }, - }, - "type": "object", - "required": ["dup_title", "dup_size", "dup_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { - "properties": { - "dup_name2": { - "type": "string", - "title": "Dup Name2", - } - }, - "type": "object", - "required": ["dup_name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { - "properties": { - "dup_sub_name": { - "type": "string", - "title": "Dup Sub Name", - } - }, - "type": "object", - "required": ["dup_sub_name"], - "title": "SubItem", - }, - } - ), - ), + }, + "type": "object", + "required": ["dup_title", "dup_size", "dup_sub"], + "title": "Item", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { + "properties": { + "dup_name2": { + "type": "string", + "title": "Dup Name2", + } + }, + "type": "object", + "required": ["dup_name2"], + "title": "ItemInList", + }, + "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { + "properties": { + "dup_sub_name": { + "type": "string", + "title": "Dup Sub Name", + } + }, + "type": "object", + "required": ["dup_sub_name"], + "title": "SubItem", + }, + }, }, } ) diff --git a/tests/test_pydantic_v1_v2_noneable.py b/tests/test_pydantic_v1_v2_noneable.py index 3e86469908..2cb6c3d6b4 100644 --- a/tests/test_pydantic_v1_v2_noneable.py +++ b/tests/test_pydantic_v1_v2_noneable.py @@ -1,7 +1,7 @@ import sys from typing import Any, Union -from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 +from tests.utils import skip_module_if_py_gte_314 if sys.version_info >= (3, 14): skip_module_if_py_gte_314() @@ -312,38 +312,20 @@ def test_v2_to_v1_validation_error(): assert response.status_code == 422, response.text assert response.json() == snapshot( { - "detail": pydantic_snapshot( - v2=snapshot( - [ - { - "type": "missing", - "loc": ["body", "new_size"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - { - "type": "missing", - "loc": ["body", "new_sub"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - ] - ), - v1=snapshot( - [ - { - "loc": ["body", "new_size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "new_sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - ), - ) + "detail": [ + { + "type": "missing", + "loc": ["body", "new_size"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + { + "type": "missing", + "loc": ["body", "new_sub"], + "msg": "Field required", + "input": {"new_title": "Missing fields"}, + }, + ] } ) @@ -361,23 +343,12 @@ def test_v2_to_v1_nested_validation_error(): assert response.json() == snapshot( { "detail": [ - pydantic_snapshot( - v2=snapshot( - { - "type": "missing", - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "Field required", - "input": {"wrong_field": "value"}, - } - ), - v1=snapshot( - { - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "field required", - "type": "value_error.missing", - } - ), - ) + { + "type": "missing", + "loc": ["body", "new_sub", "new_sub_name"], + "msg": "Field required", + "input": {"wrong_field": "value"}, + } ] } ) @@ -396,23 +367,12 @@ def test_v2_to_v1_type_validation_error(): assert response.json() == snapshot( { "detail": [ - pydantic_snapshot( - v2=snapshot( - { - "type": "int_parsing", - "loc": ["body", "new_size"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "not_a_number", - } - ), - v1=snapshot( - { - "loc": ["body", "new_size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ), - ) + { + "type": "int_parsing", + "loc": ["body", "new_size"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "not_a_number", + } ] } ) @@ -483,21 +443,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/Item"} - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -507,22 +458,15 @@ def test_openapi_schema(): "description": "Successful Response", "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( + "schema": { + "anyOf": [ { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 V1 To V2 Post", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/NewItem"} - ), - ) + "$ref": "#/components/schemas/NewItem" + }, + {"type": "null"}, + ], + "title": "Response Handle V1 Item To V2 V1 To V2 Post", + } } }, }, @@ -546,21 +490,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( - { - "allOf": [ - { - "$ref": "#/components/schemas/Item" - } - ], - "title": "Data", - } - ), - v1=snapshot( + "schema": { + "allOf": [ {"$ref": "#/components/schemas/Item"} - ), - ) + ], + "title": "Data", + } } }, "required": True, @@ -570,22 +505,15 @@ def test_openapi_schema(): "description": "Successful Response", "content": { "application/json": { - "schema": pydantic_snapshot( - v2=snapshot( + "schema": { + "anyOf": [ { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post", - } - ), - v1=snapshot( - {"$ref": "#/components/schemas/NewItem"} - ), - ) + "$ref": "#/components/schemas/NewItem" + }, + {"type": "null"}, + ], + "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post", + } } }, }, @@ -707,17 +635,10 @@ def test_openapi_schema(): "properties": { "new_title": {"type": "string", "title": "New Title"}, "new_size": {"type": "integer", "title": "New Size"}, - "new_description": pydantic_snapshot( - v2=snapshot( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - } - ), - v1=snapshot( - {"type": "string", "title": "New Description"} - ), - ), + "new_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "New Description", + }, "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, "new_multi": { "items": {"$ref": "#/components/schemas/NewSubItem"}, diff --git a/tests/test_query_cookie_header_model_extra_params.py b/tests/test_query_cookie_header_model_extra_params.py index f4ebefb3f3..d361e1e533 100644 --- a/tests/test_query_cookie_header_model_extra_params.py +++ b/tests/test_query_cookie_header_model_extra_params.py @@ -1,5 +1,4 @@ from fastapi import Cookie, FastAPI, Header, Query -from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel @@ -9,12 +8,7 @@ app = FastAPI() class Model(BaseModel): param: str - if PYDANTIC_V2: - model_config = {"extra": "allow"} - else: - - class Config: - extra = "allow" + model_config = {"extra": "allow"} @app.get("/query") diff --git a/tests/test_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py index b359874439..5858f8e801 100644 --- a/tests/test_read_with_orm_mode.py +++ b/tests/test_read_with_orm_mode.py @@ -4,10 +4,9 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict -from .utils import needs_pydanticv1, needs_pydanticv2 +from .utils import needs_pydanticv1 -@needs_pydanticv2 def test_read_with_orm_mode() -> None: class PersonBase(BaseModel): name: str @@ -48,7 +47,9 @@ def test_read_with_orm_mode() -> None: @needs_pydanticv1 def test_read_with_orm_mode_pv1() -> None: - class PersonBase(BaseModel): + from pydantic import v1 + + class PersonBase(v1.BaseModel): name: str lastname: str diff --git a/tests/test_request_param_model_by_alias.py b/tests/test_request_param_model_by_alias.py index a6f759f23b..c29130d7a6 100644 --- a/tests/test_request_param_model_by_alias.py +++ b/tests/test_request_param_model_by_alias.py @@ -1,6 +1,5 @@ from dirty_equals import IsPartialDict from fastapi import Cookie, FastAPI, Header, Query -from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -53,8 +52,7 @@ def test_query_model_with_alias_by_name(): response = client.get("/query", params={"param": "value"}) assert response.status_code == 422, response.text details = response.json() - if PYDANTIC_V2: - assert details["detail"][0]["input"] == {"param": "value"} + assert details["detail"][0]["input"] == {"param": "value"} def test_header_model_with_alias_by_name(): @@ -62,8 +60,7 @@ def test_header_model_with_alias_by_name(): response = client.get("/header", headers={"param": "value"}) assert response.status_code == 422, response.text details = response.json() - if PYDANTIC_V2: - assert details["detail"][0]["input"] == IsPartialDict({"param": "value"}) + assert details["detail"][0]["input"] == IsPartialDict({"param": "value"}) def test_cookie_model_with_alias_by_name(): @@ -72,5 +69,4 @@ def test_cookie_model_with_alias_by_name(): response = client.get("/cookie") assert response.status_code == 422, response.text details = response.json() - if PYDANTIC_V2: - assert details["detail"][0]["input"] == {"param": "value"} + assert details["detail"][0]["input"] == {"param": "value"} diff --git a/tests/test_request_params/test_body/test_list.py b/tests/test_request_params/test_body/test_list.py index b2503a1c44..0048da0f8b 100644 --- a/tests/test_request_params/test_body/test_list.py +++ b/tests/test_request_params/test_body/test_list.py @@ -6,8 +6,6 @@ from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -246,7 +244,6 @@ async def read_model_required_list_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-list-validation-alias", "/model-required-list-validation-alias"], @@ -269,7 +266,6 @@ def test_required_list_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize("json", [None, {}]) @pytest.mark.parametrize( "path", @@ -294,7 +290,6 @@ def test_required_list_validation_alias_missing(path: str, json: Union[dict, Non } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -319,7 +314,6 @@ def test_required_list_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -364,7 +358,6 @@ def read_model_required_list_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -390,7 +383,6 @@ def test_required_list_alias_and_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize("json", [None, {}]) @pytest.mark.parametrize( "path", @@ -415,7 +407,6 @@ def test_required_list_alias_and_validation_alias_missing(path: str, json): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -442,7 +433,6 @@ def test_required_list_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -467,7 +457,6 @@ def test_required_list_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_body/test_optional_list.py b/tests/test_request_params/test_body/test_optional_list.py index ede635f840..960e8890f3 100644 --- a/tests/test_request_params/test_body/test_optional_list.py +++ b/tests/test_request_params/test_body/test_optional_list.py @@ -6,8 +6,6 @@ from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -283,7 +281,6 @@ def read_model_optional_list_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -369,7 +366,6 @@ def test_optional_list_validation_alias_missing_empty_dict(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -384,7 +380,6 @@ def test_optional_list_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -432,7 +427,6 @@ def read_model_optional_list_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -524,7 +518,6 @@ def test_optional_list_alias_and_validation_alias_missing_empty_dict(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -539,7 +532,6 @@ def test_optional_list_alias_and_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -554,7 +546,6 @@ def test_optional_list_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_body/test_optional_str.py b/tests/test_request_params/test_body/test_optional_str.py index 9e0c7217cc..59732688a2 100644 --- a/tests/test_request_params/test_body/test_optional_str.py +++ b/tests/test_request_params/test_body/test_optional_str.py @@ -6,8 +6,6 @@ from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -268,7 +266,6 @@ def read_model_optional_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -300,7 +297,6 @@ def test_optional_validation_alias_schema(path: str): ) -@needs_pydanticv2 def test_optional_validation_alias_missing(): client = TestClient(app) response = client.post("/optional-validation-alias") @@ -308,7 +304,6 @@ def test_optional_validation_alias_missing(): assert response.json() == {"p": None} -@needs_pydanticv2 def test_model_optional_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-validation-alias") @@ -338,7 +333,6 @@ def test_model_optional_validation_alias_missing(): ) -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -350,7 +344,6 @@ def test_model_optional_validation_alias_missing_empty_dict(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -365,7 +358,6 @@ def test_optional_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -410,7 +402,6 @@ def read_model_optional_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -445,7 +436,6 @@ def test_optional_alias_and_validation_alias_schema(path: str): ) -@needs_pydanticv2 def test_optional_alias_and_validation_alias_missing(): client = TestClient(app) response = client.post("/optional-alias-and-validation-alias") @@ -453,7 +443,6 @@ def test_optional_alias_and_validation_alias_missing(): assert response.json() == {"p": None} -@needs_pydanticv2 def test_model_optional_alias_and_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-alias-and-validation-alias") @@ -483,7 +472,6 @@ def test_model_optional_alias_and_validation_alias_missing(): ) -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -498,7 +486,6 @@ def test_model_optional_alias_and_validation_alias_missing_empty_dict(path: str) assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -513,7 +500,6 @@ def test_optional_alias_and_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -528,7 +514,6 @@ def test_optional_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_body/test_required_str.py b/tests/test_request_params/test_body/test_required_str.py index aa47a4ede3..5571ba5d5a 100644 --- a/tests/test_request_params/test_body/test_required_str.py +++ b/tests/test_request_params/test_body/test_required_str.py @@ -6,8 +6,6 @@ from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -236,7 +234,6 @@ def read_model_required_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-validation-alias", "/model-required-validation-alias"], @@ -255,7 +252,6 @@ def test_required_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize("json", [None, {}]) @pytest.mark.parametrize( "path", @@ -282,7 +278,6 @@ def test_required_validation_alias_missing( } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -307,7 +302,6 @@ def test_required_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -353,7 +347,6 @@ def read_model_required_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -375,7 +368,6 @@ def test_required_alias_and_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize("json", [None, {}]) @pytest.mark.parametrize( "path", @@ -402,7 +394,6 @@ def test_required_alias_and_validation_alias_missing( } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -430,7 +421,6 @@ def test_required_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -455,7 +445,6 @@ def test_required_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_cookie/test_optional_str.py b/tests/test_request_params/test_cookie/test_optional_str.py index f2d02dae54..b2f7f9cef5 100644 --- a/tests/test_request_params/test_cookie/test_optional_str.py +++ b/tests/test_request_params/test_cookie/test_optional_str.py @@ -6,8 +6,6 @@ from fastapi import Cookie, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -189,7 +187,6 @@ def read_model_optional_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -208,7 +205,6 @@ def test_optional_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -220,7 +216,6 @@ def test_optional_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -236,7 +231,6 @@ def test_optional_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -276,7 +270,6 @@ def read_model_optional_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -298,7 +291,6 @@ def test_optional_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -313,7 +305,6 @@ def test_optional_alias_and_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -329,7 +320,6 @@ def test_optional_alias_and_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -345,7 +335,6 @@ def test_optional_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_cookie/test_required_str.py b/tests/test_request_params/test_cookie/test_required_str.py index 3255857d44..58bb7af5b9 100644 --- a/tests/test_request_params/test_cookie/test_required_str.py +++ b/tests/test_request_params/test_cookie/test_required_str.py @@ -6,8 +6,6 @@ from fastapi import Cookie, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -231,7 +229,6 @@ def read_model_required_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-validation-alias", "/model-required-validation-alias"], @@ -247,7 +244,6 @@ def test_required_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -274,7 +270,6 @@ def test_required_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -300,7 +295,6 @@ def test_required_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -339,7 +333,6 @@ def read_model_required_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -358,7 +351,6 @@ def test_required_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -385,7 +377,6 @@ def test_required_alias_and_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -417,7 +408,6 @@ def test_required_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -446,7 +436,6 @@ def test_required_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py index 52b032e441..90f36e5f7c 100644 --- a/tests/test_request_params/test_file/test_list.py +++ b/tests/test_request_params/test_file/test_list.py @@ -5,8 +5,6 @@ from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -280,7 +278,6 @@ def read_list_uploadfile_validation_alias( return {"file_size": [file.size for file in p]} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -322,7 +319,6 @@ def test_list_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -349,7 +345,6 @@ def test_list_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -374,7 +369,6 @@ def test_list_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -417,7 +411,6 @@ def read_list_uploadfile_alias_and_validation_alias( return {"file_size": [file.size for file in p]} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -459,7 +452,6 @@ def test_list_alias_and_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -486,7 +478,6 @@ def test_list_alias_and_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -514,7 +505,6 @@ def test_list_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -539,7 +529,6 @@ def test_list_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_file/test_optional.py b/tests/test_request_params/test_file/test_optional.py index b7177e071d..4e9564873c 100644 --- a/tests/test_request_params/test_file/test_optional.py +++ b/tests/test_request_params/test_file/test_optional.py @@ -5,8 +5,6 @@ from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -204,7 +202,6 @@ def read_optional_uploadfile_validation_alias( return {"file_size": p.size if p else None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -239,7 +236,6 @@ def test_optional_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -254,7 +250,6 @@ def test_optional_validation_alias_missing(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -269,7 +264,6 @@ def test_optional_validation_alias_by_name(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -312,7 +306,6 @@ def read_optional_uploadfile_alias_and_validation_alias( return {"file_size": p.size if p else None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -347,7 +340,6 @@ def test_optional_alias_and_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -362,7 +354,6 @@ def test_optional_alias_and_validation_alias_missing(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -377,7 +368,6 @@ def test_optional_alias_and_validation_alias_by_name(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -392,7 +382,6 @@ def test_optional_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_file/test_optional_list.py b/tests/test_request_params/test_file/test_optional_list.py index af9fe552c9..e18f36e152 100644 --- a/tests/test_request_params/test_file/test_optional_list.py +++ b/tests/test_request_params/test_file/test_optional_list.py @@ -5,8 +5,6 @@ from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -217,7 +215,6 @@ def read_optional_list_uploadfile_validation_alias( return {"file_size": [file.size for file in p] if p else None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -259,7 +256,6 @@ def test_optional_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -274,7 +270,6 @@ def test_optional_validation_alias_missing(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -289,7 +284,6 @@ def test_optional_validation_alias_by_name(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -329,7 +323,6 @@ def read_optional_list_uploadfile_alias_and_validation_alias( return {"file_size": [file.size for file in p] if p else None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -371,7 +364,6 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -386,7 +378,6 @@ def test_optional_list_alias_and_validation_alias_missing(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -401,7 +392,6 @@ def test_optional_list_alias_and_validation_alias_by_name(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -416,7 +406,6 @@ def test_optional_list_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"file_size": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_file/test_required.py b/tests/test_request_params/test_file/test_required.py index 2979a1040a..9783f4bceb 100644 --- a/tests/test_request_params/test_file/test_required.py +++ b/tests/test_request_params/test_file/test_required.py @@ -5,8 +5,6 @@ from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -242,7 +240,6 @@ def read_required_uploadfile_validation_alias( return {"file_size": p.size} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -268,7 +265,6 @@ def test_required_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -295,7 +291,6 @@ def test_required_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -320,7 +315,6 @@ def test_required_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -359,7 +353,6 @@ def read_required_uploadfile_alias_and_validation_alias( return {"file_size": p.size} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -385,7 +378,6 @@ def test_required_alias_and_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -412,7 +404,6 @@ def test_required_alias_and_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -440,7 +431,6 @@ def test_required_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -465,7 +455,6 @@ def test_required_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_form/test_list.py b/tests/test_request_params/test_form/test_list.py index 9f45aa755d..600dba4ae9 100644 --- a/tests/test_request_params/test_form/test_list.py +++ b/tests/test_request_params/test_form/test_list.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -247,7 +245,6 @@ async def read_model_required_list_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-list-validation-alias", "/model-required-list-validation-alias"], @@ -270,7 +267,6 @@ def test_required_list_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -297,7 +293,6 @@ def test_required_list_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -322,7 +317,6 @@ def test_required_list_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-list-validation-alias", "/model-required-list-validation-alias"], @@ -363,7 +357,6 @@ def read_model_required_list_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -389,7 +382,6 @@ def test_required_list_alias_and_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -416,7 +408,6 @@ def test_required_list_alias_and_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -446,7 +437,6 @@ def test_required_list_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -470,7 +460,6 @@ def test_required_list_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_form/test_optional_list.py b/tests/test_request_params/test_form/test_optional_list.py index 0af6d34777..4552623f51 100644 --- a/tests/test_request_params/test_form/test_optional_list.py +++ b/tests/test_request_params/test_form/test_optional_list.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -211,7 +209,6 @@ def read_model_optional_list_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -250,7 +247,6 @@ def test_optional_list_validation_alias_schema(path: str): ) -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -262,7 +258,6 @@ def test_optional_list_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -277,7 +272,6 @@ def test_optional_list_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -321,7 +315,6 @@ def read_model_optional_list_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -363,7 +356,6 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): ) -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -378,7 +370,6 @@ def test_optional_list_alias_and_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -393,7 +384,6 @@ def test_optional_list_alias_and_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -408,7 +398,6 @@ def test_optional_list_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_form/test_optional_str.py b/tests/test_request_params/test_form/test_optional_str.py index 92329216ec..1b08299046 100644 --- a/tests/test_request_params/test_form/test_optional_str.py +++ b/tests/test_request_params/test_form/test_optional_str.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -194,7 +192,6 @@ def read_model_optional_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -226,7 +223,6 @@ def test_optional_validation_alias_schema(path: str): ) -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -238,7 +234,6 @@ def test_optional_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -253,7 +248,6 @@ def test_optional_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -298,7 +292,6 @@ def read_model_optional_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -333,7 +326,6 @@ def test_optional_alias_and_validation_alias_schema(path: str): ) -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -348,7 +340,6 @@ def test_optional_alias_and_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -363,7 +354,6 @@ def test_optional_alias_and_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -378,7 +368,6 @@ def test_optional_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_form/test_required_str.py b/tests/test_request_params/test_form/test_required_str.py index 1e33038040..1d2431b333 100644 --- a/tests/test_request_params/test_form/test_required_str.py +++ b/tests/test_request_params/test_form/test_required_str.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - from .utils import get_body_model_name app = FastAPI() @@ -232,7 +230,6 @@ def read_model_required_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-validation-alias", "/model-required-validation-alias"], @@ -251,7 +248,6 @@ def test_required_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -278,7 +274,6 @@ def test_required_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -303,7 +298,6 @@ def test_required_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -347,7 +341,6 @@ def read_model_required_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -369,7 +362,6 @@ def test_required_alias_and_validation_alias_schema(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -396,7 +388,6 @@ def test_required_alias_and_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -424,7 +415,6 @@ def test_required_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -449,7 +439,6 @@ def test_required_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_header/test_list.py b/tests/test_request_params/test_header/test_list.py index 62f9f36ff6..2eba17559e 100644 --- a/tests/test_request_params/test_header/test_list.py +++ b/tests/test_request_params/test_header/test_list.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -234,7 +232,6 @@ async def read_model_required_list_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-list-validation-alias", "/model-required-list-validation-alias"], @@ -254,7 +251,6 @@ def test_required_list_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -281,7 +277,6 @@ def test_required_list_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -306,7 +301,6 @@ def test_required_list_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-list-validation-alias", "/model-required-list-validation-alias"], @@ -343,7 +337,6 @@ def read_model_required_list_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -366,7 +359,6 @@ def test_required_list_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -393,7 +385,6 @@ def test_required_list_alias_and_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -423,7 +414,6 @@ def test_required_list_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -450,7 +440,6 @@ def test_required_list_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_header/test_optional_list.py b/tests/test_request_params/test_header/test_optional_list.py index 88243f05a9..cd6167a183 100644 --- a/tests/test_request_params/test_header/test_optional_list.py +++ b/tests/test_request_params/test_header/test_optional_list.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -202,7 +200,6 @@ def read_model_optional_list_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -224,7 +221,6 @@ def test_optional_list_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -236,7 +232,6 @@ def test_optional_list_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -251,7 +246,6 @@ def test_optional_list_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -291,7 +285,6 @@ def read_model_optional_list_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -316,7 +309,6 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -331,7 +323,6 @@ def test_optional_list_alias_and_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -346,7 +337,6 @@ def test_optional_list_alias_and_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -361,7 +351,6 @@ def test_optional_list_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_header/test_optional_str.py b/tests/test_request_params/test_header/test_optional_str.py index e40b1669ee..d4f25cc1e7 100644 --- a/tests/test_request_params/test_header/test_optional_str.py +++ b/tests/test_request_params/test_header/test_optional_str.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -186,7 +184,6 @@ def read_model_optional_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -205,7 +202,6 @@ def test_optional_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -217,7 +213,6 @@ def test_optional_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -232,7 +227,6 @@ def test_optional_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -271,7 +265,6 @@ def read_model_optional_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -293,7 +286,6 @@ def test_optional_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -308,7 +300,6 @@ def test_optional_alias_and_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -323,7 +314,6 @@ def test_optional_alias_and_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -338,7 +328,6 @@ def test_optional_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_header/test_required_str.py b/tests/test_request_params/test_header/test_required_str.py index 23554d3e4a..85bb43d5a8 100644 --- a/tests/test_request_params/test_header/test_required_str.py +++ b/tests/test_request_params/test_header/test_required_str.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -225,7 +223,6 @@ def read_model_required_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-validation-alias", "/model-required-validation-alias"], @@ -241,7 +238,6 @@ def test_required_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -268,7 +264,6 @@ def test_required_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -293,7 +288,6 @@ def test_required_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -331,7 +325,6 @@ def read_model_required_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -350,7 +343,6 @@ def test_required_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -377,7 +369,6 @@ def test_required_alias_and_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -408,7 +399,6 @@ def test_required_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -436,7 +426,6 @@ def test_required_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_path/test_required_str.py b/tests/test_request_params/test_path/test_required_str.py index ecd4eb61cd..b2d63667e2 100644 --- a/tests/test_request_params/test_path/test_required_str.py +++ b/tests/test_request_params/test_path/test_required_str.py @@ -4,8 +4,6 @@ import pytest from fastapi import FastAPI, Path from fastapi.testclient import TestClient -from tests.utils import needs_pydanticv2 - app = FastAPI() @@ -45,14 +43,12 @@ def read_required_alias_and_validation_alias( "p_val_alias", "P Val Alias", id="required-validation-alias", - marks=needs_pydanticv2, ), pytest.param( "/required-alias-and-validation-alias/{p_val_alias}", "p_val_alias", "P Val Alias", id="required-alias-and-validation-alias", - marks=needs_pydanticv2, ), ], ) @@ -75,12 +71,10 @@ def test_schema(path: str, expected_name: str, expected_title: str): pytest.param( "/required-validation-alias", id="required-validation-alias", - marks=needs_pydanticv2, ), pytest.param( "/required-alias-and-validation-alias", id="required-alias-and-validation-alias", - marks=needs_pydanticv2, ), ], ) diff --git a/tests/test_request_params/test_query/test_list.py b/tests/test_request_params/test_query/test_list.py index 6a3000fbf6..dc21a85006 100644 --- a/tests/test_request_params/test_query/test_list.py +++ b/tests/test_request_params/test_query/test_list.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -234,7 +232,6 @@ async def read_model_required_list_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-list-validation-alias", "/model-required-list-validation-alias"], @@ -254,7 +251,6 @@ def test_required_list_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -281,7 +277,6 @@ def test_required_list_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -306,7 +301,6 @@ def test_required_list_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-list-validation-alias", "/model-required-list-validation-alias"], @@ -341,7 +335,6 @@ def read_model_required_list_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -364,7 +357,6 @@ def test_required_list_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -391,7 +383,6 @@ def test_required_list_alias_and_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -426,7 +417,6 @@ def test_required_list_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -453,7 +443,6 @@ def test_required_list_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_query/test_optional_list.py b/tests/test_request_params/test_query/test_optional_list.py index f4b8ec6a82..2a8f63a36a 100644 --- a/tests/test_request_params/test_query/test_optional_list.py +++ b/tests/test_request_params/test_query/test_optional_list.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -202,7 +200,6 @@ def read_model_optional_list_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -224,7 +221,6 @@ def test_optional_list_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -236,7 +232,6 @@ def test_optional_list_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -251,7 +246,6 @@ def test_optional_list_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-list-validation-alias", "/model-optional-list-validation-alias"], @@ -289,7 +283,6 @@ def read_model_optional_list_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -314,7 +307,6 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -329,7 +321,6 @@ def test_optional_list_alias_and_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -344,7 +335,6 @@ def test_optional_list_alias_and_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -359,7 +349,6 @@ def test_optional_list_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_query/test_optional_str.py b/tests/test_request_params/test_query/test_optional_str.py index c7d20e37d1..c6a70bc283 100644 --- a/tests/test_request_params/test_query/test_optional_str.py +++ b/tests/test_request_params/test_query/test_optional_str.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -186,7 +184,6 @@ def read_model_optional_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -205,7 +202,6 @@ def test_optional_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/optional-validation-alias", "/model-optional-validation-alias"], @@ -217,7 +213,6 @@ def test_optional_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -232,7 +227,6 @@ def test_optional_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -271,7 +265,6 @@ def read_model_optional_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -293,7 +286,6 @@ def test_optional_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -308,7 +300,6 @@ def test_optional_alias_and_validation_alias_missing(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -323,7 +314,6 @@ def test_optional_alias_and_validation_alias_by_name(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -338,7 +328,6 @@ def test_optional_alias_and_validation_alias_by_alias(path: str): assert response.json() == {"p": None} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_request_params/test_query/test_required_str.py b/tests/test_request_params/test_query/test_required_str.py index ce30f3b1f8..2ef1b0373d 100644 --- a/tests/test_request_params/test_query/test_required_str.py +++ b/tests/test_request_params/test_query/test_required_str.py @@ -6,8 +6,6 @@ from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field -from tests.utils import needs_pydanticv2 - app = FastAPI() # ===================================================================================== @@ -228,7 +226,6 @@ def read_model_required_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", ["/required-validation-alias", "/model-required-validation-alias"], @@ -244,7 +241,6 @@ def test_required_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -271,7 +267,6 @@ def test_required_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -296,7 +291,6 @@ def test_required_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -334,7 +328,6 @@ def read_model_required_alias_and_validation_alias( return {"p": p.p} -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -353,7 +346,6 @@ def test_required_alias_and_validation_alias_schema(path: str): ] -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -380,7 +372,6 @@ def test_required_alias_and_validation_alias_missing(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -411,7 +402,6 @@ def test_required_alias_and_validation_alias_by_name(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ @@ -439,7 +429,6 @@ def test_required_alias_and_validation_alias_by_alias(path: str): } -@needs_pydanticv2 @pytest.mark.parametrize( "path", [ diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index 5b241c76b6..807d2600b9 100644 --- a/tests/test_response_by_alias.py +++ b/tests/test_response_by_alias.py @@ -1,5 +1,4 @@ from fastapi import FastAPI -from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict, Field @@ -13,24 +12,14 @@ class Model(BaseModel): class ModelNoAlias(BaseModel): name: str - if PYDANTIC_V2: - model_config = ConfigDict( - json_schema_extra={ - "description": ( - "response_model_by_alias=False is basically a quick hack, to support " - "proper OpenAPI use another model with the correct field names" - ) - } - ) - else: - - class Config: - schema_extra = { - "description": ( - "response_model_by_alias=False is basically a quick hack, to support " - "proper OpenAPI use another model with the correct field names" - ) - } + model_config = ConfigDict( + json_schema_extra={ + "description": ( + "response_model_by_alias=False is basically a quick hack, to support " + "proper OpenAPI use another model with the correct field names" + ) + } + ) @app.get("/dict", response_model=Model, response_model_by_alias=False) diff --git a/tests/test_schema_compat_pydantic_v2.py b/tests/test_schema_compat_pydantic_v2.py index 39626c0eca..737687f256 100644 --- a/tests/test_schema_compat_pydantic_v2.py +++ b/tests/test_schema_compat_pydantic_v2.py @@ -4,7 +4,7 @@ from fastapi.testclient import TestClient from inline_snapshot import snapshot from pydantic import BaseModel -from tests.utils import needs_py310, needs_pydanticv2 +from tests.utils import needs_py310 @pytest.fixture(name="client") @@ -32,14 +32,12 @@ def get_client(): @needs_py310 -@needs_pydanticv2 def test_get(client: TestClient): response = client.get("/users") assert response.json() == {"username": "alice", "role": "admin"} @needs_py310 -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("openapi.json") assert response.json() == snapshot( diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index b313f47e90..176b5588d7 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -3,7 +3,6 @@ from typing import Union import pytest from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query -from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict @@ -14,14 +13,9 @@ def create_app(): class Item(BaseModel): data: str - if PYDANTIC_V2: - model_config = ConfigDict( - json_schema_extra={"example": {"data": "Data in schema_extra"}} - ) - else: - - class Config: - schema_extra = {"example": {"data": "Data in schema_extra"}} + model_config = ConfigDict( + json_schema_extra={"example": {"data": "Data in schema_extra"}} + ) @app.post("/schema_extra/") def schema_extra(item: Item): diff --git a/tests/test_schema_ref_pydantic_v2.py b/tests/test_schema_ref_pydantic_v2.py index 119b76a529..69cb82a356 100644 --- a/tests/test_schema_ref_pydantic_v2.py +++ b/tests/test_schema_ref_pydantic_v2.py @@ -6,8 +6,6 @@ from fastapi.testclient import TestClient from inline_snapshot import snapshot from pydantic import BaseModel, ConfigDict, Field -from tests.utils import needs_pydanticv2 - @pytest.fixture(name="client") def get_client(): @@ -25,13 +23,11 @@ def get_client(): return client -@needs_pydanticv2 def test_get(client: TestClient): response = client.get("/") assert response.json() == {"$ref": "some-ref"} -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("openapi.json") assert response.json() == snapshot( diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index 8bbc4d6992..0401eb7d0d 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -3,7 +3,7 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( @@ -45,7 +45,6 @@ def test_put(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text @@ -185,137 +184,3 @@ def test_openapi_schema(client: TestClient): } }, } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema_pv1(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", - } - ], - }, - "put": { - "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__put", - "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": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "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_conditional_openapi/test_tutorial001.py b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py index a425e893d8..ddc282d85e 100644 --- a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -2,8 +2,6 @@ import importlib from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 - def get_client() -> TestClient: from docs_src.conditional_openapi import tutorial001_py39 @@ -14,7 +12,6 @@ def get_client() -> TestClient: return client -@needs_pydanticv2 def test_disable_openapi(monkeypatch): monkeypatch.setenv("OPENAPI_URL", "") # Load the client after setting the env var @@ -27,7 +24,6 @@ def test_disable_openapi(monkeypatch): assert response.status_code == 404, response.text -@needs_pydanticv2 def test_root(): client = get_client() response = client.get("/") @@ -35,7 +31,6 @@ def test_root(): assert response.json() == {"message": "Hello World"} -@needs_pydanticv2 def test_default_openapi(): client = get_client() response = client.get("/docs") diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py index 4a826a5374..0fbf141e05 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py @@ -5,25 +5,16 @@ from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import ( - needs_py310, - needs_pydanticv1, - needs_pydanticv2, - pydantic_snapshot, -) +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - pytest.param("tutorial002_py39", marks=needs_pydanticv2), - pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_an_py39", marks=needs_pydanticv2), - pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_pv1_py39", marks=needs_pydanticv1), - pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), - pytest.param("tutorial002_pv1_an_py39", marks=needs_pydanticv1), - pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + 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): @@ -62,31 +53,16 @@ def test_cookie_param_model_defaults(client: TestClient): def test_cookie_param_model_invalid(client: TestClient): response = client.get("/items/") assert response.status_code == 422 - assert response.json() == pydantic_snapshot( - v2=snapshot( + assert response.json() == { + "detail": [ { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "session_id"], - "msg": "Field required", - "input": {}, - } - ] + "type": "missing", + "loc": ["cookie", "session_id"], + "msg": "Field required", + "input": {}, } - ), - v1=snapshot( - { - "detail": [ - { - "type": "value_error.missing", - "loc": ["cookie", "session_id"], - "msg": "field required", - } - ] - } - ), - ) + ] + } def test_cookie_param_model_extra(client: TestClient): @@ -146,24 +122,13 @@ def test_openapi_schema(client: TestClient): "name": "fatebook_tracker", "in": "cookie", "required": False, - "schema": pydantic_snapshot( - v2=snapshot( - { - "anyOf": [ - {"type": "string"}, - {"type": "null"}, - ], - "title": "Fatebook Tracker", - } - ), - v1=snapshot( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Fatebook Tracker", - } - ), - ), + "schema": { + "anyOf": [ + {"type": "string"}, + {"type": "null"}, + ], + "title": "Fatebook Tracker", + }, }, { "name": "googall_tracker", diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index d8cc45dd69..cddf4a9be8 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -3,7 +3,7 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( @@ -67,7 +67,6 @@ def test_get_authors(client: TestClient): ] -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 @@ -201,137 +200,3 @@ def test_openapi_schema(client: TestClient): } }, } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema_pv1(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": { - "/authors/{author_id}/items/": { - "post": { - "summary": "Create Author Items", - "operationId": "create_author_items_authors__author_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Author Id", "type": "string"}, - "name": "author_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Author"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/authors/": { - "get": { - "summary": "Get Authors", - "operationId": "get_authors_authors__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Get Authors Authors Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/Author" - }, - } - } - }, - } - }, - } - }, - }, - "components": { - "schemas": { - "Author": { - "title": "Author", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - }, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "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_header_param_models/test_tutorial002.py b/tests/test_tutorial/test_header_param_models/test_tutorial002.py index a7a271ba41..ed4743ebf9 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial002.py @@ -5,20 +5,16 @@ from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import needs_py310, needs_pydanticv1, needs_pydanticv2 +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - pytest.param("tutorial002_py39", marks=needs_pydanticv2), - pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_an_py39", marks=needs_pydanticv2), - pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_pv1_py39", marks=needs_pydanticv1), - pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), - pytest.param("tutorial002_pv1_an_py39", marks=needs_pydanticv1), - pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + 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): diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index 3805536e76..a95540731d 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -3,7 +3,7 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( @@ -35,7 +35,6 @@ def test_query_params_str_validations(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text @@ -135,100 +134,3 @@ def test_openapi_schema(client: TestClient): } }, } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema_pv1(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": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this 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"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "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_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index 204cca09ed..d5f284e3bd 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -3,8 +3,6 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 - @pytest.fixture( name="client", @@ -21,7 +19,6 @@ def get_client(request: pytest.FixtureRequest): return client -@needs_pydanticv2 def test_post(client: TestClient): yaml_data = """ name: Deadpoolio @@ -38,7 +35,6 @@ def test_post(client: TestClient): } -@needs_pydanticv2 def test_post_broken_yaml(client: TestClient): yaml_data = """ name: Deadpoolio @@ -52,7 +48,6 @@ def test_post_broken_yaml(client: TestClient): assert response.json() == {"detail": "Invalid YAML"} -@needs_pydanticv2 def test_post_invalid(client: TestClient): yaml_data = """ name: Deadpoolio @@ -77,7 +72,6 @@ def test_post_invalid(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index 2a7a2b78b2..c5a3aec1d9 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -3,7 +3,7 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( @@ -34,7 +34,6 @@ def test_query_params_str_validations(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text @@ -134,100 +133,3 @@ def test_openapi_schema(client: TestClient): } }, } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema_pv1(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": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this 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"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "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_pydantic_v1_in_v2/test_tutorial001.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py index ce53a36eb8..4090eba012 100644 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py +++ b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py @@ -2,7 +2,6 @@ import sys from typing import Any import pytest -from fastapi._compat import PYDANTIC_V2 from tests.utils import skip_module_if_py_gte_314 @@ -10,13 +9,8 @@ if sys.version_info >= (3, 14): skip_module_if_py_gte_314() -if not PYDANTIC_V2: - pytest.skip("This test is only for Pydantic v2", allow_module_level=True) - import importlib -import pytest - from ...utils import needs_py310 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 57720bf489..266d25944d 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 @@ -1,7 +1,6 @@ import sys import pytest -from fastapi._compat import PYDANTIC_V2 from inline_snapshot import snapshot from tests.utils import skip_module_if_py_gte_314 @@ -10,12 +9,8 @@ if sys.version_info >= (3, 14): skip_module_if_py_gte_314() -if not PYDANTIC_V2: - pytest.skip("This test is only for Pydantic v2", allow_module_level=True) - import importlib -import pytest from fastapi.testclient import TestClient from ...utils import needs_py310 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 f020d4a975..693c3ba290 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 @@ -1,7 +1,6 @@ import sys import pytest -from fastapi._compat import PYDANTIC_V2 from inline_snapshot import snapshot from tests.utils import skip_module_if_py_gte_314 @@ -9,9 +8,6 @@ from tests.utils import skip_module_if_py_gte_314 if sys.version_info >= (3, 14): skip_module_if_py_gte_314() -if not PYDANTIC_V2: - pytest.skip("This test is only for Pydantic v2", allow_module_level=True) - import importlib 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 daa6f0b050..0fd084c84b 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 @@ -1,7 +1,6 @@ import sys import pytest -from fastapi._compat import PYDANTIC_V2 from inline_snapshot import snapshot from tests.utils import skip_module_if_py_gte_314 @@ -9,9 +8,6 @@ from tests.utils import skip_module_if_py_gte_314 if sys.version_info >= (3, 14): skip_module_if_py_gte_314() -if not PYDANTIC_V2: - pytest.skip("This test is only for Pydantic v2", allow_module_level=True) - import importlib diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial002.py b/tests/test_tutorial/test_query_param_models/test_tutorial002.py index e7de73f804..0e9c3351a0 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial002.py @@ -5,20 +5,16 @@ from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot -from tests.utils import needs_py310, needs_pydanticv1, needs_pydanticv2 +from tests.utils import needs_py310 @pytest.fixture( name="client", params=[ - pytest.param("tutorial002_py39", marks=needs_pydanticv2), - pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_an_py39", marks=needs_pydanticv2), - pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), - pytest.param("tutorial002_pv1_py39", marks=needs_pydanticv1), - pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), - pytest.param("tutorial002_pv1_an_py39", marks=needs_pydanticv1), - pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + 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): diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py index 50ebf711f5..82bb606a9b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial015.py @@ -5,14 +5,14 @@ from dirty_equals import IsStr from fastapi.testclient import TestClient from inline_snapshot import snapshot -from ...utils import needs_py310, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - pytest.param("tutorial015_an_py39", marks=needs_pydanticv2), - pytest.param("tutorial015_an_py310", marks=(needs_py310, needs_pydanticv2)), + pytest.param("tutorial015_an_py39"), + pytest.param("tutorial015_an_py310", marks=[needs_py310]), ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002.py b/tests/test_tutorial/test_request_form_models/test_tutorial002.py index 9bb90fa064..238f8fa2ef 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002.py @@ -3,8 +3,6 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 - @pytest.fixture( name="client", @@ -20,14 +18,12 @@ def get_client(request: pytest.FixtureRequest): return client -@needs_pydanticv2 def test_post_body_form(client: TestClient): response = client.post("/login/", data={"username": "Foo", "password": "secret"}) assert response.status_code == 200 assert response.json() == {"username": "Foo", "password": "secret"} -@needs_pydanticv2 def test_post_body_extra_form(client: TestClient): response = client.post( "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} @@ -45,7 +41,6 @@ def test_post_body_extra_form(client: TestClient): } -@needs_pydanticv2 def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) assert response.status_code == 422 @@ -61,7 +56,6 @@ def test_post_body_form_no_password(client: TestClient): } -@needs_pydanticv2 def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) assert response.status_code == 422 @@ -77,7 +71,6 @@ def test_post_body_form_no_username(client: TestClient): } -@needs_pydanticv2 def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") assert response.status_code == 422 @@ -99,7 +92,6 @@ def test_post_body_form_no_data(client: TestClient): } -@needs_pydanticv2 def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text @@ -121,7 +113,6 @@ def test_post_body_json(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text 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 41c7833bef..9ab30086bb 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 @@ -118,87 +118,3 @@ def test_post_body_json(client: TestClient): }, ] } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_response_directly/test_tutorial001.py b/tests/test_tutorial/test_response_directly/test_tutorial001.py index 127f0e4c1d..2d0c387195 100644 --- a/tests/test_tutorial/test_response_directly/test_tutorial001.py +++ b/tests/test_tutorial/test_response_directly/test_tutorial001.py @@ -3,7 +3,7 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( @@ -37,7 +37,6 @@ def test_path_operation(client: TestClient): } -@needs_pydanticv2 def test_openapi_schema_pv2(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text @@ -151,138 +150,3 @@ def test_openapi_schema_pv2(client: TestClient): }, }, } - - -@needs_pydanticv1 -def test_openapi_schema_pv1(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/{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": { - "title": "Description", - "type": "string", - }, - "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_schema_extra_example/test_tutorial001.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py index 4d1808cf6a..82f69fd463 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py @@ -3,7 +3,7 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( @@ -20,7 +20,6 @@ def get_client(request: pytest.FixtureRequest): return client -@needs_pydanticv2 def test_post_body_example(client: TestClient): response = client.put( "/items/5", @@ -34,7 +33,6 @@ def test_post_body_example(client: TestClient): assert response.status_code == 200 -@needs_pydanticv2 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text 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 552bb66970..6059962897 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 pytest from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310, needs_pydanticv1 @@ -38,98 +39,106 @@ def test_post_body_example(client: TestClient): 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": [ - { - "required": True, - "schema": {"type": "integer", "title": "Item Id"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + assert response.json() == snapshot( + { + "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": [ + { + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + "name": "item_id", + "in": "path", } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "title": "Item", + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], } } }, + "required": True, }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "price": {"type": "number", "title": "Price"}, + "tax": {"type": "number", "title": "Tax"}, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "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", }, } - } - }, - "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": {"type": "string", "title": "Description"}, - "price": {"type": "number", "title": "Price"}, - "tax": {"type": "number", "title": "Tax"}, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } - ], - }, - "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_separate_openapi_schemas/test_tutorial001.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py index b59d799a31..275b234877 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py @@ -3,7 +3,7 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( @@ -38,7 +38,6 @@ def test_read_items(client: TestClient) -> None: ] -@needs_pydanticv2 def test_openapi_schema(client: TestClient) -> None: response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py index 61fbacfc34..8230e39226 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py @@ -3,7 +3,7 @@ import importlib import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310, needs_pydanticv2 +from ...utils import needs_py310 @pytest.fixture( @@ -38,7 +38,6 @@ def test_read_items(client: TestClient) -> None: ] -@needs_pydanticv2 def test_openapi_schema(client: TestClient) -> None: response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_settings/test_app02.py b/tests/test_tutorial/test_settings/test_app02.py index 9cedc5a526..9cbed4fd1b 100644 --- a/tests/test_tutorial/test_settings/test_app02.py +++ b/tests/test_tutorial/test_settings/test_app02.py @@ -4,8 +4,6 @@ from types import ModuleType import pytest from pytest import MonkeyPatch -from ...utils import needs_pydanticv2 - @pytest.fixture( name="mod_path", @@ -31,7 +29,6 @@ def get_test_main_mod(mod_path: str) -> ModuleType: return test_main_mod -@needs_pydanticv2 def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") settings = main_mod.get_settings() @@ -39,6 +36,5 @@ def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): assert settings.items_per_user == 50 -@needs_pydanticv2 def test_override_settings(test_main_mod: ModuleType): test_main_mod.test_app() diff --git a/tests/test_tutorial/test_settings/test_app03.py b/tests/test_tutorial/test_settings/test_app03.py index dbaf8f3f90..06e82398d1 100644 --- a/tests/test_tutorial/test_settings/test_app03.py +++ b/tests/test_tutorial/test_settings/test_app03.py @@ -5,7 +5,7 @@ import pytest from fastapi.testclient import TestClient from pytest import MonkeyPatch -from ...utils import needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_pydanticv1 @pytest.fixture( @@ -26,7 +26,6 @@ def get_main_mod(mod_path: str) -> ModuleType: return main_mod -@needs_pydanticv2 def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") settings = main_mod.get_settings() @@ -45,7 +44,6 @@ def test_settings_pv1(mod_path: str, monkeypatch: MonkeyPatch): assert settings.items_per_user == 50 -@needs_pydanticv2 def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") client = TestClient(main_mod.app) diff --git a/tests/test_tutorial/test_settings/test_tutorial001.py b/tests/test_tutorial/test_settings/test_tutorial001.py index 2c6dce261f..6a08096989 100644 --- a/tests/test_tutorial/test_settings/test_tutorial001.py +++ b/tests/test_tutorial/test_settings/test_tutorial001.py @@ -4,13 +4,13 @@ import pytest from fastapi.testclient import TestClient from pytest import MonkeyPatch -from ...utils import needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_pydanticv1 @pytest.fixture( name="app", params=[ - pytest.param("tutorial001_py39", marks=needs_pydanticv2), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_pv1_py39", marks=needs_pydanticv1), ], ) diff --git a/tests/test_union_body_discriminator.py b/tests/test_union_body_discriminator.py index bf41a72915..40fd0065a9 100644 --- a/tests/test_union_body_discriminator.py +++ b/tests/test_union_body_discriminator.py @@ -7,10 +7,7 @@ from inline_snapshot import snapshot from pydantic import BaseModel, Field from typing_extensions import Literal -from .utils import needs_pydanticv2 - -@needs_pydanticv2 def test_discriminator_pydantic_v2() -> None: from pydantic import Tag diff --git a/tests/test_union_body_discriminator_annotated.py b/tests/test_union_body_discriminator_annotated.py index f8108c8df4..42a6aed24c 100644 --- a/tests/test_union_body_discriminator_annotated.py +++ b/tests/test_union_body_discriminator_annotated.py @@ -8,8 +8,6 @@ from fastapi.testclient import TestClient from inline_snapshot import snapshot from pydantic import BaseModel -from .utils import needs_pydanticv2 - @pytest.fixture(name="client") def client_fixture() -> TestClient: @@ -47,21 +45,18 @@ def client_fixture() -> TestClient: return client -@needs_pydanticv2 def test_union_body_discriminator_assignment(client: TestClient) -> None: response = client.post("/pet/assignment", json={"pet_type": "cat", "meows": 5}) assert response.status_code == 200, response.text assert response.json() == {"pet_type": "cat", "meows": 5} -@needs_pydanticv2 def test_union_body_discriminator_annotated(client: TestClient) -> None: response = client.post("/pet/annotated", json={"pet_type": "dog", "barks": 3.5}) assert response.status_code == 200, response.text assert response.json() == {"pet_type": "dog", "barks": 3.5} -@needs_pydanticv2 def test_openapi_schema(client: TestClient) -> None: response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_validate_response_recursive/app.py b/tests/test_validate_response_recursive/app.py index 8f76572ba6..d51aa848d0 100644 --- a/tests/test_validate_response_recursive/app.py +++ b/tests/test_validate_response_recursive/app.py @@ -1,5 +1,4 @@ from fastapi import FastAPI -from fastapi._compat import PYDANTIC_V2 from pydantic import BaseModel app = FastAPI() @@ -20,13 +19,9 @@ class RecursiveItemViaSubmodel(BaseModel): name: str -if PYDANTIC_V2: - RecursiveItem.model_rebuild() - RecursiveSubitemInSubmodel.model_rebuild() - RecursiveItemViaSubmodel.model_rebuild() -else: - RecursiveItem.update_forward_refs() - RecursiveSubitemInSubmodel.update_forward_refs() +RecursiveItem.model_rebuild() +RecursiveSubitemInSubmodel.model_rebuild() +RecursiveItemViaSubmodel.model_rebuild() @app.get("/items/recursive", response_model=RecursiveItem) diff --git a/tests/utils.py b/tests/utils.py index 691e92bbfb..b896d4527f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,43 +1,19 @@ import sys import pytest -from fastapi._compat import PYDANTIC_V2 -from inline_snapshot import Snapshot needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) needs_py_lt_314 = pytest.mark.skipif( - sys.version_info > (3, 13), reason="requires python3.13-" + sys.version_info >= (3, 14), reason="requires python3.13-" ) -needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") -needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") + +needs_pydanticv1 = needs_py_lt_314 def skip_module_if_py_gte_314(): """Skip entire module on Python 3.14+ at import time.""" if sys.version_info >= (3, 14): pytest.skip("requires python3.13-", allow_module_level=True) - - -def pydantic_snapshot( - *, - v2: Snapshot, - v1: Snapshot, # TODO: remove v1 argument when deprecating Pydantic v1 -): - """ - This function should be used like this: - - >>> assert value == pydantic_snapshot(v2=snapshot(),v1=snapshot()) - - inline-snapshot will create the snapshots when pytest is executed for each versions of pydantic. - - It is also possible to use the function inside snapshots for version-specific values. - - >>> assert value == snapshot({ - "data": "some data", - "version_specific": pydantic_snapshot(v2=snapshot(),v1=snapshot()), - }) - """ - return v2 if PYDANTIC_V2 else v1 From eac57f6908dc7f9e9d65b1ddf638a496696a9818 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 15:56:04 +0000 Subject: [PATCH 08/59] =?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 259546d7ed..be48d562cc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Upgrades + +* ➖ Drop support for Pydantic v1, keeping short temporary support for Pydantic v2's `pydantic.v1`. PR [#14575](https://github.com/fastapi/fastapi/pull/14575) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Fix duplicated variable in `docs_src/python_types/tutorial005_py39.py`. PR [#14565](https://github.com/fastapi/fastapi/pull/14565) by [@paras-verma7454](https://github.com/paras-verma7454). From 1cb4e25651d7cb6690d658ff60289a5fb9d65167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Dec 2025 08:08:57 -0800 Subject: [PATCH 09/59] =?UTF-8?q?=F0=9F=94=A7=20Tweak=20pre-commit=20to=20?= =?UTF-8?q?allow=20committing=20release-notes=20(#14577)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e6d93fb7d..a65d97dad3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ repos: rev: v6.0.0 hooks: - id: check-added-large-files + args: ['--maxkb=750'] - id: check-toml - id: check-yaml args: From 55ec28b81b5857294a5871a937045483596a54bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 16:09:21 +0000 Subject: [PATCH 10/59] =?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 be48d562cc..35a097a7a0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -21,6 +21,7 @@ hide: ### Internal +* 🔧 Tweak pre-commit to allow committing release-notes. PR [#14577](https://github.com/fastapi/fastapi/pull/14577) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Use prek as a pre-commit alternative. PR [#14572](https://github.com/fastapi/fastapi/pull/14572) by [@tiangolo](https://github.com/tiangolo). * 👷 Add performance tests with CodSpeed. PR [#14558](https://github.com/fastapi/fastapi/pull/14558) by [@tiangolo](https://github.com/tiangolo). From 10252b193716e8b5c64d5b6760fc9aac9e3787d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Dec 2025 17:11:50 +0100 Subject: [PATCH 11/59] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.126.?= =?UTF-8?q?0?= 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 35a097a7a0..d2026b756f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.126.0 + ### Upgrades * ➖ Drop support for Pydantic v1, keeping short temporary support for Pydantic v2's `pydantic.v1`. PR [#14575](https://github.com/fastapi/fastapi/pull/14575) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 2f13448467..7ed0fa95bb 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.125.0" +__version__ = "0.126.0" from starlette import status as status From 6b591ddd7e16727c57be1dad153b49ad9741a863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Dec 2025 17:15:42 +0100 Subject: [PATCH 12/59] =?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 --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d2026b756f..3a7c2a6ce6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -12,6 +12,8 @@ hide: ### Upgrades * ➖ Drop support for Pydantic v1, keeping short temporary support for Pydantic v2's `pydantic.v1`. PR [#14575](https://github.com/fastapi/fastapi/pull/14575) by [@tiangolo](https://github.com/tiangolo). + * The minimum version of Pydantic installed is now `pydantic >=2.7.0`. + * The `standard` dependencies now include `pydantic-settings >=2.0.0` and `pydantic-extra-types >=2.0.0`. ### Docs From 026b43e5d3008c86cbf6f1a76ba471276418413c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Dec 2025 09:30:52 -0800 Subject: [PATCH 13/59] =?UTF-8?q?=F0=9F=94=A7=20Add=20LLM=20prompt=20file?= =?UTF-8?q?=20for=20Japanese,=20generated=20from=20the=20existing=20transl?= =?UTF-8?q?ations=20(#14545)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maruo.S --- docs/ja/llm-prompt.md | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/ja/llm-prompt.md diff --git a/docs/ja/llm-prompt.md b/docs/ja/llm-prompt.md new file mode 100644 index 0000000000..c47cc36df7 --- /dev/null +++ b/docs/ja/llm-prompt.md @@ -0,0 +1,47 @@ +### Target language + +Translate to Japanese (日本語). + +Language code: ja. + +### Grammar and tone + +1) Use polite, instructional Japanese (です/ます調). +2) Keep the tone concise and technical (match existing Japanese FastAPI docs). + +### Headings + +1) Follow the existing Japanese style: short, descriptive headings (often noun phrases), e.g. 「チェック」. +2) Do not add a trailing period at the end of headings. + +### Quotes + +1) Prefer Japanese corner brackets 「」 in normal prose when quoting a term. +2) Do not change quotes inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +1) Keep ellipsis style consistent with existing Japanese docs (commonly `...`). +2) Never change `...` in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): リクエスト +- response (HTTP): レスポンス +- path operation: パスオペレーション +- path operation function: パスオペレーション関数 + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) If a title is present, prefer these canonical titles: + +- `/// note | 備考` +- `/// note | 技術詳細` +- `/// tip | 豆知識` +- `/// warning | 注意` +- `/// info | 情報` +- `/// check | 確認` +- `/// danger | 警告` From 5783910d0c0fc8a3da8fff793b681121c61efb4a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 17:31:11 +0000 Subject: [PATCH 14/59] =?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 3a7c2a6ce6..174af0cb42 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🔧 Add LLM prompt file for Japanese, generated from the existing translations. PR [#14545](https://github.com/fastapi/fastapi/pull/14545) by [@tiangolo](https://github.com/tiangolo). + ## 0.126.0 ### Upgrades From 52892592753333c89076616824e3fe23181d65b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Dec 2025 09:32:05 -0800 Subject: [PATCH 15/59] =?UTF-8?q?=F0=9F=94=A7=20Add=20LLM=20prompt=20file?= =?UTF-8?q?=20for=20Korean,=20generated=20from=20the=20existing=20translat?= =?UTF-8?q?ions=20(#14546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hy.lee --- docs/ko/llm-prompt.md | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/ko/llm-prompt.md diff --git a/docs/ko/llm-prompt.md b/docs/ko/llm-prompt.md new file mode 100644 index 0000000000..008511a5b7 --- /dev/null +++ b/docs/ko/llm-prompt.md @@ -0,0 +1,51 @@ +### Target language + +Translate to Korean (한국어). + +Language code: ko. + +### Grammar and tone + +1) Use polite, instructional Korean (e.g. 합니다/하세요 style). +2) Keep the tone consistent with the existing Korean FastAPI docs. + +### Headings + +1) Follow existing Korean heading style (short, action-oriented headings like “확인하기”). +2) Do not add trailing punctuation to headings. + +### Quotes + +1) Keep quote style consistent with the existing Korean docs. +2) Never change quotes inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +1) Keep ellipsis style consistent with existing Korean docs (often `...`). +2) Never change `...` in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): 요청 +- response (HTTP): 응답 +- path operation: 경로 처리 +- path operation function: 경로 처리 함수 + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) If a title is present, prefer these canonical titles: + +- `/// note | 참고` +- `/// tip | 팁` +- `/// warning | 경고` +- `/// info | 정보` +- `/// danger | 위험` +- `/// note Technical Details | 기술 세부사항` +- `/// check | 확인` +Notes: + +- `details` blocks exist in Korean docs; keep `/// details` as-is and translate only the title after `|`. +- Example canonical title used: `/// details | 상세 설명` From c2c1cc8aec83e713326842dffacd0e38a6ff24c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 17:32:31 +0000 Subject: [PATCH 16/59] =?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 174af0cb42..7e3ef48ece 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🔧 Add LLM prompt file for Korean, generated from the existing translations. PR [#14546](https://github.com/fastapi/fastapi/pull/14546) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Japanese, generated from the existing translations. PR [#14545](https://github.com/fastapi/fastapi/pull/14545) by [@tiangolo](https://github.com/tiangolo). ## 0.126.0 From 1d93d531bc950ae880679e2e365fb6d7e028d106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 21 Dec 2025 00:06:22 -0800 Subject: [PATCH 17/59] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20OpenAI=20m?= =?UTF-8?q?odel=20for=20translations=20to=20gpt-5.2=20(#14579)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/translate.py b/scripts/translate.py index 764bc704ea..6ebd24f547 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -727,7 +727,7 @@ def translate_page( print(f"Found existing translation: {out_path}") old_translation = out_path.read_text(encoding="utf-8") print(f"Translating {en_path} to {language} ({language_name})") - agent = Agent("openai:gpt-5") + agent = Agent("openai:gpt-5.2") prompt_segments = [ general_prompt, From 6513d4daa16a536d17743de6a292f49bd06388a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 08:06:42 +0000 Subject: [PATCH 18/59] =?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 7e3ef48ece..12267e3b10 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -12,6 +12,10 @@ hide: * 🔧 Add LLM prompt file for Korean, generated from the existing translations. PR [#14546](https://github.com/fastapi/fastapi/pull/14546) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Japanese, generated from the existing translations. PR [#14545](https://github.com/fastapi/fastapi/pull/14545) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* ⬆️ Upgrade OpenAI model for translations to gpt-5.2. PR [#14579](https://github.com/fastapi/fastapi/pull/14579) by [@tiangolo](https://github.com/tiangolo). + ## 0.126.0 ### Upgrades From 6e42bcd8ce2ade33d94f478310dad20ee84f8f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 21 Dec 2025 08:44:10 -0800 Subject: [PATCH 19/59] =?UTF-8?q?=F0=9F=94=8A=20Add=20deprecation=20warnin?= =?UTF-8?q?gs=20when=20using=20`pydantic.v1`=20(#14583)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 8 + fastapi/routing.py | 16 + tests/benchmarks/test_general_performance.py | 150 ++++----- tests/test_compat_params_v1.py | 167 +++++----- tests/test_datetime_custom_encoder.py | 10 +- .../test_filter_pydantic_sub_model/app_pv1.py | 20 +- ...t_get_model_definitions_formfeed_escape.py | 30 +- .../test_pydantic_v1_deprecation_warnings.py | 98 ++++++ tests/test_pydantic_v1_v2_01.py | 40 +-- tests/test_pydantic_v1_v2_list.py | 75 +++-- tests/test_pydantic_v1_v2_mixed.py | 301 +++++++++--------- tests/test_pydantic_v1_v2_multifile/main.py | 213 ++++++------- tests/test_pydantic_v1_v2_noneable.py | 115 +++---- tests/test_read_with_orm_mode.py | 12 +- ...est_response_model_as_return_annotation.py | 12 +- .../test_tutorial002.py | 9 +- .../test_tutorial003.py | 9 +- .../test_tutorial004.py | 9 +- .../test_tutorial002_pv1.py | 9 +- .../test_tutorial001_pv1.py | 9 +- 20 files changed, 756 insertions(+), 556 deletions(-) create mode 100644 tests/test_pydantic_v1_deprecation_warnings.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 0ba93524c4..39d0bd89cd 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,6 +1,7 @@ import dataclasses import inspect import sys +import warnings from collections.abc import Coroutine, Mapping, Sequence from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy @@ -322,6 +323,13 @@ def get_dependant( ) continue assert param_details.field is not None + if isinstance(param_details.field, may_v1.ModelField): + warnings.warn( + "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." + f" Please update the param {param_name}: {param_details.type_annotation!r}.", + category=DeprecationWarning, + stacklevel=5, + ) if isinstance( param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body) ): diff --git a/fastapi/routing.py b/fastapi/routing.py index a1f2e44bb4..2770e3253d 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -2,6 +2,7 @@ import email.message import functools import inspect import json +import warnings from collections.abc import ( AsyncIterator, Awaitable, @@ -28,6 +29,7 @@ from fastapi._compat import ( _get_model_config, _model_dump, _normalize_errors, + annotation_is_pydantic_v1, lenient_issubclass, may_v1, ) @@ -634,6 +636,13 @@ class APIRoute(routing.Route): f"Status code {status_code} must not have a response body" ) response_name = "Response_" + self.unique_id + if annotation_is_pydantic_v1(self.response_model): + warnings.warn( + "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." + f" Please update the response model {self.response_model!r}.", + category=DeprecationWarning, + stacklevel=4, + ) self.response_field = create_model_field( name=response_name, type_=self.response_model, @@ -667,6 +676,13 @@ class APIRoute(routing.Route): f"Status code {additional_status_code} must not have a response body" ) response_name = f"Response_{additional_status_code}_{self.unique_id}" + if annotation_is_pydantic_v1(model): + warnings.warn( + "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." + f" In responses={{}}, please update {model}.", + category=DeprecationWarning, + stacklevel=4, + ) response_field = create_model_field( name=response_name, type_=model, mode="serialization" ) diff --git a/tests/benchmarks/test_general_performance.py b/tests/benchmarks/test_general_performance.py index dca3613d00..2da74b95c5 100644 --- a/tests/benchmarks/test_general_performance.py +++ b/tests/benchmarks/test_general_performance.py @@ -1,5 +1,6 @@ import json import sys +import warnings from collections.abc import Iterator from typing import Annotated, Any @@ -84,96 +85,103 @@ def app(basemodel_class: type[Any]) -> FastAPI: app = FastAPI() - @app.post("/sync/validated", response_model=ItemOut) - def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): - return ItemOut(name=item.name, value=item.value, dep=dep) + with warnings.catch_warnings(record=True): + warnings.filterwarnings( + "ignore", + message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", + category=DeprecationWarning, + ) - @app.get("/sync/dict-no-response-model") - def sync_dict_no_response_model(): - return {"name": "foo", "value": 123} + @app.post("/sync/validated", response_model=ItemOut) + def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name=item.name, value=item.value, dep=dep) - @app.get("/sync/dict-with-response-model", response_model=ItemOut) - def sync_dict_with_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return {"name": "foo", "value": 123, "dep": dep} + @app.get("/sync/dict-no-response-model") + def sync_dict_no_response_model(): + return {"name": "foo", "value": 123} - @app.get("/sync/model-no-response-model") - def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]): - return ItemOut(name="foo", value=123, dep=dep) + @app.get("/sync/dict-with-response-model", response_model=ItemOut) + def sync_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], + ): + return {"name": "foo", "value": 123, "dep": dep} - @app.get("/sync/model-with-response-model", response_model=ItemOut) - def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]): - return ItemOut(name="foo", value=123, dep=dep) + @app.get("/sync/model-no-response-model") + def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) - @app.post("/async/validated", response_model=ItemOut) - async def async_validated( - item: ItemIn, - dep: Annotated[int, Depends(dep_b)], - ): - return ItemOut(name=item.name, value=item.value, dep=dep) + @app.get("/sync/model-with-response-model", response_model=ItemOut) + def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) - @app.post("/sync/large-receive") - def sync_large_receive(payload: LargeIn): - return {"received": len(payload.items)} + @app.post("/async/validated", response_model=ItemOut) + async def async_validated( + item: ItemIn, + dep: Annotated[int, Depends(dep_b)], + ): + return ItemOut(name=item.name, value=item.value, dep=dep) - @app.post("/async/large-receive") - async def async_large_receive(payload: LargeIn): - return {"received": len(payload.items)} + @app.post("/sync/large-receive") + def sync_large_receive(payload: LargeIn): + return {"received": len(payload.items)} - @app.get("/sync/large-dict-no-response-model") - def sync_large_dict_no_response_model(): - return LARGE_PAYLOAD + @app.post("/async/large-receive") + async def async_large_receive(payload: LargeIn): + return {"received": len(payload.items)} - @app.get("/sync/large-dict-with-response-model", response_model=LargeOut) - def sync_large_dict_with_response_model(): - return LARGE_PAYLOAD + @app.get("/sync/large-dict-no-response-model") + def sync_large_dict_no_response_model(): + return LARGE_PAYLOAD - @app.get("/sync/large-model-no-response-model") - def sync_large_model_no_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + @app.get("/sync/large-dict-with-response-model", response_model=LargeOut) + def sync_large_dict_with_response_model(): + return LARGE_PAYLOAD - @app.get("/sync/large-model-with-response-model", response_model=LargeOut) - def sync_large_model_with_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + @app.get("/sync/large-model-no-response-model") + def sync_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - @app.get("/async/large-dict-no-response-model") - async def async_large_dict_no_response_model(): - return LARGE_PAYLOAD + @app.get("/sync/large-model-with-response-model", response_model=LargeOut) + def sync_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - @app.get("/async/large-dict-with-response-model", response_model=LargeOut) - async def async_large_dict_with_response_model(): - return LARGE_PAYLOAD + @app.get("/async/large-dict-no-response-model") + async def async_large_dict_no_response_model(): + return LARGE_PAYLOAD - @app.get("/async/large-model-no-response-model") - async def async_large_model_no_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + @app.get("/async/large-dict-with-response-model", response_model=LargeOut) + async def async_large_dict_with_response_model(): + return LARGE_PAYLOAD - @app.get("/async/large-model-with-response-model", response_model=LargeOut) - async def async_large_model_with_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + @app.get("/async/large-model-no-response-model") + async def async_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - @app.get("/async/dict-no-response-model") - async def async_dict_no_response_model(): - return {"name": "foo", "value": 123} + @app.get("/async/large-model-with-response-model", response_model=LargeOut) + async def async_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - @app.get("/async/dict-with-response-model", response_model=ItemOut) - async def async_dict_with_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return {"name": "foo", "value": 123, "dep": dep} + @app.get("/async/dict-no-response-model") + async def async_dict_no_response_model(): + return {"name": "foo", "value": 123} - @app.get("/async/model-no-response-model") - async def async_model_no_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return ItemOut(name="foo", value=123, dep=dep) + @app.get("/async/dict-with-response-model", response_model=ItemOut) + async def async_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], + ): + return {"name": "foo", "value": 123, "dep": dep} - @app.get("/async/model-with-response-model", response_model=ItemOut) - async def async_model_with_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return ItemOut(name="foo", value=123, dep=dep) + @app.get("/async/model-no-response-model") + async def async_model_no_response_model( + dep: Annotated[int, Depends(dep_b)], + ): + return ItemOut(name="foo", value=123, dep=dep) + + @app.get("/async/model-with-response-model", response_model=ItemOut) + async def async_model_with_response_model( + dep: Annotated[int, Depends(dep_b)], + ): + return ItemOut(name="foo", value=123, dep=dep) return app diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py index b4ca861be3..2ac96993a8 100644 --- a/tests/test_compat_params_v1.py +++ b/tests/test_compat_params_v1.py @@ -1,4 +1,5 @@ import sys +import warnings from typing import Optional import pytest @@ -33,94 +34,90 @@ class Item(BaseModel): app = FastAPI() +with warnings.catch_warnings(record=True): + warnings.simplefilter("always") -@app.get("/items/{item_id}") -def get_item_with_path( - item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)], -): - return {"item_id": item_id} + @app.get("/items/{item_id}") + def get_item_with_path( + item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)], + ): + return {"item_id": item_id} + @app.get("/items/") + def get_items_with_query( + q: Annotated[ + Optional[str], + Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$"), + ] = None, + skip: Annotated[int, Query(ge=0)] = 0, + limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10, + ): + return {"q": q, "skip": skip, "limit": limit} -@app.get("/items/") -def get_items_with_query( - q: Annotated[ - Optional[str], Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$") - ] = None, - skip: Annotated[int, Query(ge=0)] = 0, - limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10, -): - return {"q": q, "skip": skip, "limit": limit} + @app.get("/users/") + def get_user_with_header( + x_custom: Annotated[Optional[str], Header()] = None, + x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None, + ): + return {"x_custom": x_custom, "x_token": x_token} + @app.get("/cookies/") + def get_cookies( + session_id: Annotated[Optional[str], Cookie()] = None, + tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None, + ): + return {"session_id": session_id, "tracking_id": tracking_id} -@app.get("/users/") -def get_user_with_header( - x_custom: Annotated[Optional[str], Header()] = None, - x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None, -): - return {"x_custom": x_custom, "x_token": x_token} + @app.post("/items/") + def create_item( + item: Annotated[ + Item, + Body( + examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}] + ), + ], + ): + return {"item": item} + @app.post("/items-embed/") + def create_item_embed( + item: Annotated[Item, Body(embed=True)], + ): + return {"item": item} -@app.get("/cookies/") -def get_cookies( - session_id: Annotated[Optional[str], Cookie()] = None, - tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None, -): - return {"session_id": session_id, "tracking_id": tracking_id} + @app.put("/items/{item_id}") + def update_item( + item_id: Annotated[int, Path(ge=1)], + item: Annotated[Item, Body()], + importance: Annotated[int, Body(gt=0, le=10)], + ): + return {"item": item, "importance": importance} + @app.post("/form-data/") + def submit_form( + username: Annotated[str, Form(min_length=3, max_length=50)], + password: Annotated[str, Form(min_length=8)], + email: Annotated[Optional[str], Form()] = None, + ): + return {"username": username, "password": password, "email": email} -@app.post("/items/") -def create_item( - item: Annotated[ - Item, - Body(examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]), - ], -): - return {"item": item} + @app.post("/upload/") + def upload_file( + file: Annotated[bytes, File()], + description: Annotated[Optional[str], Form()] = None, + ): + return {"file_size": len(file), "description": description} - -@app.post("/items-embed/") -def create_item_embed( - item: Annotated[Item, Body(embed=True)], -): - return {"item": item} - - -@app.put("/items/{item_id}") -def update_item( - item_id: Annotated[int, Path(ge=1)], - item: Annotated[Item, Body()], - importance: Annotated[int, Body(gt=0, le=10)], -): - return {"item": item, "importance": importance} - - -@app.post("/form-data/") -def submit_form( - username: Annotated[str, Form(min_length=3, max_length=50)], - password: Annotated[str, Form(min_length=8)], - email: Annotated[Optional[str], Form()] = None, -): - return {"username": username, "password": password, "email": email} - - -@app.post("/upload/") -def upload_file( - file: Annotated[bytes, File()], - description: Annotated[Optional[str], Form()] = None, -): - return {"file_size": len(file), "description": description} - - -@app.post("/upload-multiple/") -def upload_multiple_files( - files: Annotated[list[bytes], File()], - note: Annotated[str, Form()] = "", -): - return { - "file_count": len(files), - "total_size": sum(len(f) for f in files), - "note": note, - } + @app.post("/upload-multiple/") + def upload_multiple_files( + files: Annotated[list[bytes], File()], + note: Annotated[str, Form()] = "", + ): + return { + "file_count": len(files), + "total_size": sum(len(f) for f in files), + "note": note, + } client = TestClient(app) @@ -211,10 +208,10 @@ def test_header_params_none(): # Cookie parameter tests def test_cookie_params(): - with TestClient(app) as client: - client.cookies.set("session_id", "abc123") - client.cookies.set("tracking_id", "1234567890abcdef") - response = client.get("/cookies/") + with TestClient(app) as test_client: + test_client.cookies.set("session_id", "abc123") + test_client.cookies.set("tracking_id", "1234567890abcdef") + response = test_client.get("/cookies/") assert response.status_code == 200 assert response.json() == { "session_id": "abc123", @@ -223,9 +220,9 @@ def test_cookie_params(): def test_cookie_tracking_id_too_short(): - with TestClient(app) as client: - client.cookies.set("tracking_id", "short") - response = client.get("/cookies/") + with TestClient(app) as test_client: + test_client.cookies.set("tracking_id", "short") + response = test_client.get("/cookies/") assert response.status_code == 422 assert response.json() == snapshot( { diff --git a/tests/test_datetime_custom_encoder.py b/tests/test_datetime_custom_encoder.py index 822651f4f1..56b6780f04 100644 --- a/tests/test_datetime_custom_encoder.py +++ b/tests/test_datetime_custom_encoder.py @@ -1,3 +1,4 @@ +import warnings from datetime import datetime, timezone from fastapi import FastAPI @@ -48,9 +49,12 @@ def test_pydanticv1(): app = FastAPI() model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) - @app.get("/model", response_model=ModelWithDatetimeField) - def get_model(): - return model + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + + @app.get("/model", response_model=ModelWithDatetimeField) + def get_model(): + return model client = TestClient(app) with client: diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py index 0b6ab53e04..d6f2ce7d2d 100644 --- a/tests/test_filter_pydantic_sub_model/app_pv1.py +++ b/tests/test_filter_pydantic_sub_model/app_pv1.py @@ -1,3 +1,4 @@ +import warnings from typing import Optional from fastapi import Depends, FastAPI @@ -31,11 +32,14 @@ async def get_model_c() -> ModelC: return ModelC(username="test-user", password="test-password") -@app.get("/model/{name}", response_model=ModelA) -async def get_model_a(name: str, model_c=Depends(get_model_c)): - return { - "name": name, - "description": "model-a-desc", - "model_b": model_c, - "tags": {"key1": "value1", "key2": "value2"}, - } +with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + + @app.get("/model/{name}", response_model=ModelA) + async def get_model_a(name: str, model_c=Depends(get_model_c)): + return { + "name": name, + "description": "model-a-desc", + "model_b": model_c, + "tags": {"key1": "value1", "key2": "value2"}, + } diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index 50d799a571..dee5955544 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -1,3 +1,5 @@ +import warnings + import pytest from fastapi import FastAPI from fastapi.testclient import TestClient @@ -36,12 +38,28 @@ def client_fixture(request: pytest.FixtureRequest) -> TestClient: app = FastAPI() - @app.get("/facilities/{facility_id}") - def get_facility(facility_id: str) -> Facility: - return Facility( - id=facility_id, - address=Address(line_1="123 Main St", city="Anytown", state_province="CA"), - ) + if request.param == "pydantic-v1": + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + + @app.get("/facilities/{facility_id}") + def get_facility(facility_id: str) -> Facility: + return Facility( + id=facility_id, + address=Address( + line_1="123 Main St", city="Anytown", state_province="CA" + ), + ) + else: + + @app.get("/facilities/{facility_id}") + def get_facility(facility_id: str) -> Facility: + return Facility( + id=facility_id, + address=Address( + line_1="123 Main St", city="Anytown", state_province="CA" + ), + ) client = TestClient(app) return client diff --git a/tests/test_pydantic_v1_deprecation_warnings.py b/tests/test_pydantic_v1_deprecation_warnings.py new file mode 100644 index 0000000000..e0008e2183 --- /dev/null +++ b/tests/test_pydantic_v1_deprecation_warnings.py @@ -0,0 +1,98 @@ +import sys + +import pytest + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi._compat.v1 import BaseModel +from fastapi.testclient import TestClient + + +def test_warns_pydantic_v1_model_in_endpoint_param() -> None: + class ParamModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.warns( + DeprecationWarning, + match=r"pydantic\.v1 is deprecated.*Please update the param data:", + ): + + @app.post("/param") + def endpoint(data: ParamModelV1): + return data + + client = TestClient(app) + response = client.post("/param", json={"name": "test"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "test"} + + +def test_warns_pydantic_v1_model_in_return_type() -> None: + class ReturnModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.warns( + DeprecationWarning, + match=r"pydantic\.v1 is deprecated.*Please update the response model", + ): + + @app.get("/return") + def endpoint() -> ReturnModelV1: + return ReturnModelV1(name="test") + + client = TestClient(app) + response = client.get("/return") + assert response.status_code == 200, response.text + assert response.json() == {"name": "test"} + + +def test_warns_pydantic_v1_model_in_response_model() -> None: + class ResponseModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.warns( + DeprecationWarning, + match=r"pydantic\.v1 is deprecated.*Please update the response model", + ): + + @app.get("/response-model", response_model=ResponseModelV1) + def endpoint(): + return {"name": "test"} + + client = TestClient(app) + response = client.get("/response-model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "test"} + + +def test_warns_pydantic_v1_model_in_additional_responses_model() -> None: + class ErrorModelV1(BaseModel): + detail: str + + app = FastAPI() + + with pytest.warns( + DeprecationWarning, + match=r"pydantic\.v1 is deprecated.*In responses=\{\}, please update", + ): + + @app.get( + "/responses", response_model=None, responses={400: {"model": ErrorModelV1}} + ) + def endpoint(): + return {"ok": True} + + client = TestClient(app) + response = client.get("/responses") + assert response.status_code == 200, response.text + assert response.json() == {"ok": True} diff --git a/tests/test_pydantic_v1_v2_01.py b/tests/test_pydantic_v1_v2_01.py index 83536cafa2..4868e5d223 100644 --- a/tests/test_pydantic_v1_v2_01.py +++ b/tests/test_pydantic_v1_v2_01.py @@ -1,4 +1,5 @@ import sys +import warnings from typing import Any, Union from tests.utils import skip_module_if_py_gte_314 @@ -26,30 +27,29 @@ class Item(BaseModel): app = FastAPI() +with warnings.catch_warnings(record=True): + warnings.simplefilter("always") -@app.post("/simple-model") -def handle_simple_model(data: SubItem) -> SubItem: - return data + @app.post("/simple-model") + def handle_simple_model(data: SubItem) -> SubItem: + return data + @app.post("/simple-model-filter", response_model=SubItem) + def handle_simple_model_filter(data: SubItem) -> Any: + extended_data = data.dict() + extended_data.update({"secret_price": 42}) + return extended_data -@app.post("/simple-model-filter", response_model=SubItem) -def handle_simple_model_filter(data: SubItem) -> Any: - extended_data = data.dict() - extended_data.update({"secret_price": 42}) - return extended_data + @app.post("/item") + def handle_item(data: Item) -> Item: + return data - -@app.post("/item") -def handle_item(data: Item) -> Item: - return data - - -@app.post("/item-filter", response_model=Item) -def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data + @app.post("/item-filter", response_model=Item) + def handle_item_filter(data: Item) -> Any: + extended_data = data.dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return extended_data client = TestClient(app) diff --git a/tests/test_pydantic_v1_v2_list.py b/tests/test_pydantic_v1_v2_list.py index 4ddcbf240d..108f231faa 100644 --- a/tests/test_pydantic_v1_v2_list.py +++ b/tests/test_pydantic_v1_v2_list.py @@ -1,4 +1,5 @@ import sys +import warnings from typing import Any, Union from tests.utils import skip_module_if_py_gte_314 @@ -27,49 +28,47 @@ class Item(BaseModel): app = FastAPI() -@app.post("/item") -def handle_item(data: Item) -> list[Item]: - return [data, data] +with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + @app.post("/item") + def handle_item(data: Item) -> list[Item]: + return [data, data] -@app.post("/item-filter", response_model=list[Item]) -def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return [extended_data, extended_data] - - -@app.post("/item-list") -def handle_item_list(data: list[Item]) -> Item: - if data: - return data[0] - return Item(title="", size=0, sub=SubItem(name="")) - - -@app.post("/item-list-filter", response_model=Item) -def handle_item_list_filter(data: list[Item]) -> Any: - if data: - extended_data = data[0].dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data - return Item(title="", size=0, sub=SubItem(name="")) - - -@app.post("/item-list-to-list") -def handle_item_list_to_list(data: list[Item]) -> list[Item]: - return data - - -@app.post("/item-list-to-list-filter", response_model=list[Item]) -def handle_item_list_to_list_filter(data: list[Item]) -> Any: - if data: - extended_data = data[0].dict() + @app.post("/item-filter", response_model=list[Item]) + def handle_item_filter(data: Item) -> Any: + extended_data = data.dict() extended_data.update({"secret_data": "classified", "internal_id": 12345}) extended_data["sub"].update({"internal_id": 67890}) return [extended_data, extended_data] - return [] + + @app.post("/item-list") + def handle_item_list(data: list[Item]) -> Item: + if data: + return data[0] + return Item(title="", size=0, sub=SubItem(name="")) + + @app.post("/item-list-filter", response_model=Item) + def handle_item_list_filter(data: list[Item]) -> Any: + if data: + extended_data = data[0].dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return extended_data + return Item(title="", size=0, sub=SubItem(name="")) + + @app.post("/item-list-to-list") + def handle_item_list_to_list(data: list[Item]) -> list[Item]: + return data + + @app.post("/item-list-to-list-filter", response_model=list[Item]) + def handle_item_list_to_list_filter(data: list[Item]) -> Any: + if data: + extended_data = data[0].dict() + extended_data.update({"secret_data": "classified", "internal_id": 12345}) + extended_data["sub"].update({"internal_id": 67890}) + return [extended_data, extended_data] + return [] client = TestClient(app) diff --git a/tests/test_pydantic_v1_v2_mixed.py b/tests/test_pydantic_v1_v2_mixed.py index 61e5bb5827..895835a4c0 100644 --- a/tests/test_pydantic_v1_v2_mixed.py +++ b/tests/test_pydantic_v1_v2_mixed.py @@ -1,4 +1,5 @@ import sys +import warnings from typing import Any, Union from tests.utils import skip_module_if_py_gte_314 @@ -39,179 +40,181 @@ class NewItem(NewBaseModel): app = FastAPI() +with warnings.catch_warnings(record=True): + warnings.simplefilter("always") -@app.post("/v1-to-v2/item") -def handle_v1_item_to_v2(data: Item) -> NewItem: - return NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) + @app.post("/v1-to-v2/item") + def handle_v1_item_to_v2(data: Item) -> NewItem: + return NewItem( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=NewSubItem(new_sub_name=data.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], + ) + @app.post("/v1-to-v2/item-filter", response_model=NewItem) + def handle_v1_item_to_v2_filter(data: Item) -> Any: + result = { + "new_title": data.title, + "new_size": data.size, + "new_description": data.description, + "new_sub": { + "new_sub_name": data.sub.name, + "new_sub_secret": "sub_hidden", + }, + "new_multi": [ + {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} + for s in data.multi + ], + "secret": "hidden_v1_to_v2", + } + return result -@app.post("/v1-to-v2/item-filter", response_model=NewItem) -def handle_v1_item_to_v2_filter(data: Item) -> Any: - result = { - "new_title": data.title, - "new_size": data.size, - "new_description": data.description, - "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi - ], - "secret": "hidden_v1_to_v2", - } - return result + @app.post("/v2-to-v1/item") + def handle_v2_item_to_v1(data: NewItem) -> Item: + return Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=SubItem(name=data.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + @app.post("/v2-to-v1/item-filter", response_model=Item) + def handle_v2_item_to_v1_filter(data: NewItem) -> Any: + result = { + "title": data.new_title, + "size": data.new_size, + "description": data.new_description, + "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, + "multi": [ + {"name": s.new_sub_name, "sub_secret": "sub_hidden"} + for s in data.new_multi + ], + "secret": "hidden_v2_to_v1", + } + return result -@app.post("/v2-to-v1/item") -def handle_v2_item_to_v1(data: NewItem) -> Item: - return Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) + @app.post("/v1-to-v2/item-to-list") + def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]: + converted = NewItem( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=NewSubItem(new_sub_name=data.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], + ) + return [converted, converted] + @app.post("/v1-to-v2/list-to-list") + def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]: + result = [] + for item in data: + result.append( + NewItem( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=NewSubItem(new_sub_name=item.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], + ) + ) + return result -@app.post("/v2-to-v1/item-filter", response_model=Item) -def handle_v2_item_to_v1_filter(data: NewItem) -> Any: - result = { - "title": data.new_title, - "size": data.new_size, - "description": data.new_description, - "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi - ], - "secret": "hidden_v2_to_v1", - } - return result + @app.post("/v1-to-v2/list-to-list-filter", response_model=list[NewItem]) + def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any: + result = [] + for item in data: + converted = { + "new_title": item.title, + "new_size": item.size, + "new_description": item.description, + "new_sub": { + "new_sub_name": item.sub.name, + "new_sub_secret": "sub_hidden", + }, + "new_multi": [ + {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} + for s in item.multi + ], + "secret": "hidden_v2_to_v1", + } + result.append(converted) + return result - -@app.post("/v1-to-v2/item-to-list") -def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]: - converted = NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - return [converted, converted] - - -@app.post("/v1-to-v2/list-to-list") -def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]: - result = [] - for item in data: - result.append( - NewItem( + @app.post("/v1-to-v2/list-to-item") + def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem: + if data: + item = data[0] + return NewItem( new_title=item.title, new_size=item.size, new_description=item.description, new_sub=NewSubItem(new_sub_name=item.sub.name), new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], ) + return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name="")) + + @app.post("/v2-to-v1/item-to-list") + def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]: + converted = Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=SubItem(name=data.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], ) - return result + return [converted, converted] + @app.post("/v2-to-v1/list-to-list") + def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]: + result = [] + for item in data: + result.append( + Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=SubItem(name=item.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], + ) + ) + return result -@app.post("/v1-to-v2/list-to-list-filter", response_model=list[NewItem]) -def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any: - result = [] - for item in data: - converted = { - "new_title": item.title, - "new_size": item.size, - "new_description": item.description, - "new_sub": {"new_sub_name": item.sub.name, "new_sub_secret": "sub_hidden"}, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} - for s in item.multi - ], - "secret": "hidden_v2_to_v1", - } - result.append(converted) - return result + @app.post("/v2-to-v1/list-to-list-filter", response_model=list[Item]) + def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any: + result = [] + for item in data: + converted = { + "title": item.new_title, + "size": item.new_size, + "description": item.new_description, + "sub": { + "name": item.new_sub.new_sub_name, + "sub_secret": "sub_hidden", + }, + "multi": [ + {"name": s.new_sub_name, "sub_secret": "sub_hidden"} + for s in item.new_multi + ], + "secret": "hidden_v2_to_v1", + } + result.append(converted) + return result - -@app.post("/v1-to-v2/list-to-item") -def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem: - if data: - item = data[0] - return NewItem( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=NewSubItem(new_sub_name=item.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], - ) - return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name="")) - - -@app.post("/v2-to-v1/item-to-list") -def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]: - converted = Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - return [converted, converted] - - -@app.post("/v2-to-v1/list-to-list") -def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]: - result = [] - for item in data: - result.append( - Item( + @app.post("/v2-to-v1/list-to-item") + def handle_v2_list_to_v1_item(data: list[NewItem]) -> Item: + if data: + item = data[0] + return Item( title=item.new_title, size=item.new_size, description=item.new_description, sub=SubItem(name=item.new_sub.new_sub_name), multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], ) - ) - return result - - -@app.post("/v2-to-v1/list-to-list-filter", response_model=list[Item]) -def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any: - result = [] - for item in data: - converted = { - "title": item.new_title, - "size": item.new_size, - "description": item.new_description, - "sub": {"name": item.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} - for s in item.new_multi - ], - "secret": "hidden_v2_to_v1", - } - result.append(converted) - return result - - -@app.post("/v2-to-v1/list-to-item") -def handle_v2_list_to_v1_item(data: list[NewItem]) -> Item: - if data: - item = data[0] - return Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=SubItem(name=item.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - return Item(title="", size=0, sub=SubItem(name="")) + return Item(title="", size=0, sub=SubItem(name="")) client = TestClient(app) diff --git a/tests/test_pydantic_v1_v2_multifile/main.py b/tests/test_pydantic_v1_v2_multifile/main.py index 9397368abf..4180ec3bf5 100644 --- a/tests/test_pydantic_v1_v2_multifile/main.py +++ b/tests/test_pydantic_v1_v2_multifile/main.py @@ -1,140 +1,137 @@ +import warnings + from fastapi import FastAPI from . import modelsv1, modelsv2, modelsv2b app = FastAPI() +with warnings.catch_warnings(record=True): + warnings.simplefilter("always") -@app.post("/v1-to-v2/item") -def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item: - return modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) + @app.post("/v1-to-v2/item") + def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item: + return modelsv2.Item( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], + ) + @app.post("/v2-to-v1/item") + def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item: + return modelsv1.Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], + ) -@app.post("/v2-to-v1/item") -def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item: - return modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) + @app.post("/v1-to-v2/item-to-list") + def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]: + converted = modelsv2.Item( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), + new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], + ) + return [converted, converted] + @app.post("/v1-to-v2/list-to-list") + def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]: + result = [] + for item in data: + result.append( + modelsv2.Item( + new_title=item.title, + new_size=item.size, + new_description=item.description, + new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), + new_multi=[ + modelsv2.SubItem(new_sub_name=s.name) for s in item.multi + ], + ) + ) + return result -@app.post("/v1-to-v2/item-to-list") -def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]: - converted = modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) - return [converted, converted] - - -@app.post("/v1-to-v2/list-to-list") -def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]: - result = [] - for item in data: - result.append( - modelsv2.Item( + @app.post("/v1-to-v2/list-to-item") + def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item: + if data: + item = data[0] + return modelsv2.Item( new_title=item.title, new_size=item.size, new_description=item.description, new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], ) - ) - return result - - -@app.post("/v1-to-v2/list-to-item") -def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item: - if data: - item = data[0] return modelsv2.Item( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], + new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="") ) - return modelsv2.Item( - new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="") - ) + @app.post("/v2-to-v1/item-to-list") + def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]: + converted = modelsv1.Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], + ) + return [converted, converted] -@app.post("/v2-to-v1/item-to-list") -def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]: - converted = modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - return [converted, converted] + @app.post("/v2-to-v1/list-to-list") + def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]: + result = [] + for item in data: + result.append( + modelsv1.Item( + title=item.new_title, + size=item.new_size, + description=item.new_description, + sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), + multi=[ + modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi + ], + ) + ) + return result - -@app.post("/v2-to-v1/list-to-list") -def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]: - result = [] - for item in data: - result.append( - modelsv1.Item( + @app.post("/v2-to-v1/list-to-item") + def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item: + if data: + item = data[0] + return modelsv1.Item( title=item.new_title, size=item.new_size, description=item.new_description, sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], ) - ) - return result + return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name="")) - -@app.post("/v2-to-v1/list-to-item") -def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item: - if data: - item = data[0] + @app.post("/v2-to-v1/same-name") + def handle_v2_same_name_to_v1( + item1: modelsv2.Item, item2: modelsv2b.Item + ) -> modelsv1.Item: return modelsv1.Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], + title=item1.new_title, + size=item2.dup_size, + description=item1.new_description, + sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name), + multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi], ) - return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name="")) - -@app.post("/v2-to-v1/same-name") -def handle_v2_same_name_to_v1( - item1: modelsv2.Item, item2: modelsv2b.Item -) -> modelsv1.Item: - return modelsv1.Item( - title=item1.new_title, - size=item2.dup_size, - description=item1.new_description, - sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi], - ) - - -@app.post("/v2-to-v1/list-of-items-to-list-of-items") -def handle_v2_items_in_list_to_v1_item_in_list( - data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList] -) -> list[modelsv1.ItemInList]: - result = [] - item1 = data1[0] - item2 = data2[0] - result = [ - modelsv1.ItemInList(name1=item1.name2), - modelsv1.ItemInList(name1=item2.dup_name2), - ] - return result + @app.post("/v2-to-v1/list-of-items-to-list-of-items") + def handle_v2_items_in_list_to_v1_item_in_list( + data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList] + ) -> list[modelsv1.ItemInList]: + item1 = data1[0] + item2 = data2[0] + return [ + modelsv1.ItemInList(name1=item1.name2), + modelsv1.ItemInList(name1=item2.dup_name2), + ] diff --git a/tests/test_pydantic_v1_v2_noneable.py b/tests/test_pydantic_v1_v2_noneable.py index 2cb6c3d6b4..ba98b5653c 100644 --- a/tests/test_pydantic_v1_v2_noneable.py +++ b/tests/test_pydantic_v1_v2_noneable.py @@ -1,4 +1,5 @@ import sys +import warnings from typing import Any, Union from tests.utils import skip_module_if_py_gte_314 @@ -39,65 +40,69 @@ class NewItem(NewBaseModel): app = FastAPI() +with warnings.catch_warnings(record=True): + warnings.simplefilter("always") -@app.post("/v1-to-v2/") -def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]: - if data.size < 0: - return None - return NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) + @app.post("/v1-to-v2/") + def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]: + if data.size < 0: + return None + return NewItem( + new_title=data.title, + new_size=data.size, + new_description=data.description, + new_sub=NewSubItem(new_sub_name=data.sub.name), + new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], + ) + @app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None]) + def handle_v1_item_to_v2_filter(data: Item) -> Any: + if data.size < 0: + return None + result = { + "new_title": data.title, + "new_size": data.size, + "new_description": data.description, + "new_sub": { + "new_sub_name": data.sub.name, + "new_sub_secret": "sub_hidden", + }, + "new_multi": [ + {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} + for s in data.multi + ], + "secret": "hidden_v1_to_v2", + } + return result -@app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None]) -def handle_v1_item_to_v2_filter(data: Item) -> Any: - if data.size < 0: - return None - result = { - "new_title": data.title, - "new_size": data.size, - "new_description": data.description, - "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi - ], - "secret": "hidden_v1_to_v2", - } - return result + @app.post("/v2-to-v1/item") + def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]: + if data.new_size < 0: + return None + return Item( + title=data.new_title, + size=data.new_size, + description=data.new_description, + sub=SubItem(name=data.new_sub.new_sub_name), + multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], + ) - -@app.post("/v2-to-v1/item") -def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]: - if data.new_size < 0: - return None - return Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - -@app.post("/v2-to-v1/item-filter", response_model=Union[Item, None]) -def handle_v2_item_to_v1_filter(data: NewItem) -> Any: - if data.new_size < 0: - return None - result = { - "title": data.new_title, - "size": data.new_size, - "description": data.new_description, - "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi - ], - "secret": "hidden_v2_to_v1", - } - return result + @app.post("/v2-to-v1/item-filter", response_model=Union[Item, None]) + def handle_v2_item_to_v1_filter(data: NewItem) -> Any: + if data.new_size < 0: + return None + result = { + "title": data.new_title, + "size": data.new_size, + "description": data.new_description, + "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, + "multi": [ + {"name": s.new_sub_name, "sub_secret": "sub_hidden"} + for s in data.new_multi + ], + "secret": "hidden_v2_to_v1", + } + return result client = TestClient(app) diff --git a/tests/test_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py index 5858f8e801..a195634b8a 100644 --- a/tests/test_read_with_orm_mode.py +++ b/tests/test_read_with_orm_mode.py @@ -1,3 +1,4 @@ +import warnings from typing import Any from fastapi import FastAPI @@ -73,10 +74,13 @@ def test_read_with_orm_mode_pv1() -> None: app = FastAPI() - @app.post("/people/", response_model=PersonRead) - def create_person(person: PersonCreate) -> Any: - db_person = Person.from_orm(person) - return db_person + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + + @app.post("/people/", response_model=PersonRead) + def create_person(person: PersonCreate) -> Any: + db_person = Person.from_orm(person) + return db_person client = TestClient(app) diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 44e882a76e..9e527d6a01 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -1,3 +1,4 @@ +import warnings from typing import Union import pytest @@ -521,11 +522,14 @@ def test_invalid_response_model_field_pv1(): class Model(v1.BaseModel): foo: str - with pytest.raises(FastAPIError) as e: + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") - @app.get("/") - def read_root() -> Union[Response, Model, None]: - return Response(content="Foo") # pragma: no cover + with pytest.raises(FastAPIError) as e: + + @app.get("/") + def read_root() -> Union[Response, Model, None]: + return Response(content="Foo") # pragma: no cover assert "valid Pydantic field type" in e.value.args[0] assert "parameter response_model=None" in e.value.args[0] 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 266d25944d..ab7e1d8a77 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 @@ -1,4 +1,5 @@ import sys +import warnings import pytest from inline_snapshot import snapshot @@ -24,7 +25,13 @@ from ...utils import needs_py310 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + with warnings.catch_warnings(record=True): + warnings.filterwarnings( + "ignore", + message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", + category=DeprecationWarning, + ) + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") c = TestClient(mod.app) return c 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 693c3ba290..c45e042484 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 @@ -1,4 +1,5 @@ import sys +import warnings import pytest from inline_snapshot import snapshot @@ -24,7 +25,13 @@ from ...utils import needs_py310 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + with warnings.catch_warnings(record=True): + warnings.filterwarnings( + "ignore", + message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", + category=DeprecationWarning, + ) + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") c = TestClient(mod.app) return c 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 0fd084c84b..f3da849e04 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 @@ -1,4 +1,5 @@ import sys +import warnings import pytest from inline_snapshot import snapshot @@ -24,7 +25,13 @@ from ...utils import needs_py310 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") + with warnings.catch_warnings(record=True): + warnings.filterwarnings( + "ignore", + message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", + category=DeprecationWarning, + ) + mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") c = TestClient(mod.app) return c 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 9ab30086bb..515a5a8d78 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 @@ -1,4 +1,5 @@ import importlib +import warnings import pytest from fastapi.testclient import TestClient @@ -14,7 +15,13 @@ from ...utils import needs_pydanticv1 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") + with warnings.catch_warnings(record=True): + warnings.filterwarnings( + "ignore", + message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", + category=DeprecationWarning, + ) + mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") client = TestClient(mod.app) return client 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 6059962897..c5526b19cd 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 @@ -1,4 +1,5 @@ import importlib +import warnings import pytest from fastapi.testclient import TestClient @@ -15,7 +16,13 @@ from ...utils import needs_py310, needs_pydanticv1 ], ) def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") + with warnings.catch_warnings(record=True): + warnings.filterwarnings( + "ignore", + message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", + category=DeprecationWarning, + ) + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") client = TestClient(mod.app) return client From 22c7200ebb9a7cfb3f938985fba55bd557faa4ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 16:44:32 +0000 Subject: [PATCH 20/59] =?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 12267e3b10..ed0592468c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Breaking Changes + +* 🔊 Add deprecation warnings when using `pydantic.v1`. PR [#14583](https://github.com/fastapi/fastapi/pull/14583) by [@tiangolo](https://github.com/tiangolo). + ### Translations * 🔧 Add LLM prompt file for Korean, generated from the existing translations. PR [#14546](https://github.com/fastapi/fastapi/pull/14546) by [@tiangolo](https://github.com/tiangolo). From c4a1ab503635918938e3741d1fb6f2fff73dc1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 21 Dec 2025 17:45:43 +0100 Subject: [PATCH 21/59] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.127.?= =?UTF-8?q?0?= 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 ed0592468c..f22b5dc95b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.127.0 + ### Breaking Changes * 🔊 Add deprecation warnings when using `pydantic.v1`. PR [#14583](https://github.com/fastapi/fastapi/pull/14583) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 7ed0fa95bb..73df6dc6c9 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.126.0" +__version__ = "0.127.0" from starlette import status as status From b9b2793bda6898295593302b1213b4a61b91dd31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 21 Dec 2025 09:40:17 -0800 Subject: [PATCH 22/59] =?UTF-8?q?=F0=9F=94=A8=20Update=20scripts=20and=20p?= =?UTF-8?q?re-commit=20to=20autofix=20files=20(#14585)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 - .pre-commit-config.yaml | 22 +++++++- scripts/docs.py | 94 ++++++++++++-------------------- 3 files changed, 54 insertions(+), 64 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 73e1c6b67a..cd27179f57 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -60,8 +60,6 @@ jobs: pyproject.toml - name: Install docs extras run: uv pip install -r requirements-docs.txt - - name: Verify Docs - run: python ./scripts/docs.py verify-docs - name: Export Language Codes id: show-langs run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a65d97dad3..77e06bd96c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,10 +21,28 @@ repos: - id: ruff-format - repo: local hooks: - - id: local-script + - id: add-permalinks-pages language: unsupported - name: local script + name: add-permalinks-pages entry: uv run ./scripts/docs.py add-permalinks-pages args: - --update-existing files: ^docs/en/docs/.*\.md$ + - id: generate-readme + language: unsupported + name: generate README.md from index.md + entry: uv run ./scripts/docs.py generate-readme + files: ^docs/en/docs/index\.md|docs/en/data/sponsors\.yml|scripts/docs\.py$ + pass_filenames: false + - id: update-languages + language: unsupported + name: update languages + entry: uv run ./scripts/docs.py update-languages + files: ^docs/.*|scripts/docs\.py$ + pass_filenames: false + - id: ensure-non-translated + language: unsupported + name: ensure non-translated files are not modified + entry: uv run ./scripts/docs.py ensure-non-translated + files: ^docs/(?!en/).*|^scripts/docs\.py$ + pass_filenames: false diff --git a/scripts/docs.py b/scripts/docs.py index bf7d9de395..fbde1eca4f 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -19,7 +19,13 @@ from slugify import slugify as py_slugify logging.basicConfig(level=logging.INFO) -SUPPORTED_LANGS = {"en", "de", "es", "pt", "ru"} +SUPPORTED_LANGS = { + "en", + "de", + "es", + "pt", + "ru", +} app = typer.Typer() @@ -232,27 +238,15 @@ def generate_readme() -> None: """ Generate README.md content from main index.md """ - typer.echo("Generating README") readme_path = Path("README.md") + old_content = readme_path.read_text() new_content = generate_readme_content() - readme_path.write_text(new_content, encoding="utf-8") - - -@app.command() -def verify_readme() -> None: - """ - Verify README.md content from main index.md - """ - typer.echo("Verifying README") - readme_path = Path("README.md") - generated_content = generate_readme_content() - readme_content = readme_path.read_text("utf-8") - if generated_content != readme_content: - typer.secho( - "README.md outdated from the latest index.md", color=typer.colors.RED - ) - raise typer.Abort() - typer.echo("Valid README ✅") + if new_content != old_content: + print("README.md outdated from the latest index.md") + print("Updating README.md") + readme_path.write_text(new_content, encoding="utf-8") + raise typer.Exit(1) + print("README.md is up to date ✅") @app.command() @@ -280,7 +274,17 @@ def update_languages() -> None: """ Update the mkdocs.yml file Languages section including all the available languages. """ - update_config() + old_config = get_en_config() + updated_config = get_updated_config_content() + if old_config != updated_config: + print("docs/en/mkdocs.yml outdated") + print("Updating docs/en/mkdocs.yml") + en_config_path.write_text( + yaml.dump(updated_config, sort_keys=False, width=200, allow_unicode=True), + encoding="utf-8", + ) + raise typer.Exit(1) + print("docs/en/mkdocs.yml is up to date ✅") @app.command() @@ -367,39 +371,12 @@ def get_updated_config_content() -> dict[str, Any]: return config -def update_config() -> None: - config = get_updated_config_content() - en_config_path.write_text( - yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) - - @app.command() -def verify_config() -> None: +def ensure_non_translated() -> None: """ - Verify main mkdocs.yml content to make sure it uses the latest language names. + Ensure there are no files in the non translatable pages. """ - typer.echo("Verifying mkdocs.yml") - config = get_en_config() - updated_config = get_updated_config_content() - if config != updated_config: - typer.secho( - "docs/en/mkdocs.yml outdated from docs/language_names.yml, " - "update language_names.yml and run " - "python ./scripts/docs.py update-languages", - color=typer.colors.RED, - ) - raise typer.Abort() - typer.echo("Valid mkdocs.yml ✅") - - -@app.command() -def verify_non_translated() -> None: - """ - Verify there are no files in the non translatable pages. - """ - print("Verifying non translated pages") + print("Ensuring no non translated pages") lang_paths = get_lang_paths() error_paths = [] for lang in lang_paths: @@ -410,20 +387,17 @@ def verify_non_translated() -> None: if non_translatable_path.exists(): error_paths.append(non_translatable_path) if error_paths: - print("Non-translated pages found, remove them:") + print("Non-translated pages found, removing them:") for error_path in error_paths: print(error_path) - raise typer.Abort() + if error_path.is_file(): + error_path.unlink() + else: + shutil.rmtree(error_path) + raise typer.Exit(1) print("No non-translated pages found ✅") -@app.command() -def verify_docs(): - verify_readme() - verify_config() - verify_non_translated() - - @app.command() def langs_json(): langs = [] From e1bd9f3e33073f8b07bf69bf89f34b230130447a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 17:40:41 +0000 Subject: [PATCH 23/59] =?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 f22b5dc95b..4316e53bf1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 🔨 Update scripts and pre-commit to autofix files. PR [#14585](https://github.com/fastapi/fastapi/pull/14585) by [@tiangolo](https://github.com/tiangolo). + ## 0.127.0 ### Breaking Changes From 6539b80d9f798af4143d772b7962218861e4d1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 21 Dec 2025 09:51:45 -0800 Subject: [PATCH 24/59] =?UTF-8?q?=F0=9F=91=B7=20Run=20CodSpeed=20tests=20i?= =?UTF-8?q?n=20parallel=20to=20other=20tests=20to=20speed=20up=20CI=20(#14?= =?UTF-8?q?586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb2b6b64ee..5a12d69c8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,10 +54,14 @@ jobs: - os: windows-latest python-version: "3.12" coverage: coverage - # Ubuntu with 3.13 needs coverage for CodSpeed benchmarks - os: ubuntu-latest python-version: "3.13" coverage: coverage + # Ubuntu with 3.13 needs coverage for CodSpeed benchmarks + - os: ubuntu-latest + python-version: "3.13" + coverage: coverage + codspeed: codspeed - os: ubuntu-latest python-version: "3.14" coverage: coverage @@ -85,12 +89,13 @@ jobs: run: uv pip install -r requirements-tests.txt - run: mkdir coverage - name: Test + if: matrix.codspeed != 'codspeed' run: bash scripts/test.sh env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - name: CodSpeed benchmarks - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' + if: matrix.codspeed == 'codspeed' uses: CodSpeedHQ/action@v4 env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} From a7a0aee984fb36f6f16978147fb6a87115f0f4f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 17:52:08 +0000 Subject: [PATCH 25/59] =?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 4316e53bf1..a08ac1cd83 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update scripts and pre-commit to autofix files. PR [#14585](https://github.com/fastapi/fastapi/pull/14585) by [@tiangolo](https://github.com/tiangolo). ## 0.127.0 From a329baaa5495c51aafe2f2214f15b5992e17c4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 23 Dec 2025 03:17:16 -0800 Subject: [PATCH 26/59] =?UTF-8?q?=F0=9F=91=B7=20Update=20secrets=20check?= =?UTF-8?q?=20(#14592)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pre-commit.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index e628ce541d..b397912e67 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,7 +7,8 @@ on: - synchronize env: - IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + # Forks and Dependabot don't have access to secrets + HAS_SECRETS: ${{ secrets.PRE_COMMIT != '' }} jobs: pre-commit: @@ -19,7 +20,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v5 name: Checkout PR for own repo - if: env.IS_FORK == 'false' + if: env.HAS_SECRETS == 'true' with: # To be able to commit it needs to fetch the head of the branch, not the # merge commit @@ -31,7 +32,7 @@ jobs: # pre-commit lite ci needs the default checkout configs to work - uses: actions/checkout@v5 name: Checkout PR for fork - if: env.IS_FORK == 'true' + if: env.HAS_SECRETS == 'false' with: # To be able to commit it needs the head branch of the PR, the remote one ref: ${{ github.event.pull_request.head.sha }} @@ -56,7 +57,7 @@ jobs: run: uvx prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure continue-on-error: true - name: Commit and push changes - if: env.IS_FORK == 'false' + if: env.HAS_SECRETS == 'true' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -68,7 +69,7 @@ jobs: git push fi - uses: pre-commit-ci/lite-action@v1.1.0 - if: env.IS_FORK == 'true' + if: env.HAS_SECRETS == 'false' with: msg: 🎨 Auto format - name: Error out on pre-commit errors From e55f223b4695b80ad164a150489b3c3a26a35e4d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 23 Dec 2025 11:17:37 +0000 Subject: [PATCH 27/59] =?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 a08ac1cd83..09e6341aa9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). * 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update scripts and pre-commit to autofix files. PR [#14585](https://github.com/fastapi/fastapi/pull/14585) by [@tiangolo](https://github.com/tiangolo). From 7203e860b3adacc09dfd2171ea1b3ed1a9589fa9 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Wed, 24 Dec 2025 11:28:19 +0100 Subject: [PATCH 28/59] =?UTF-8?q?=F0=9F=8C=90=20Update=20translations=20fo?= =?UTF-8?q?r=20de=20(update-outdated)=20(#14581)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync with #14575 (Drop support for Pydantic v1) * Add a word and fix a typo Found while syncing. --- .../path-operation-advanced-configuration.md | 36 +----------------- docs/de/docs/advanced/settings.md | 38 ------------------- ...migrate-from-pydantic-v1-to-pydantic-v2.md | 14 ++++--- .../docs/how-to/separate-openapi-schemas.md | 4 +- docs/de/docs/tutorial/body-updates.md | 16 -------- docs/de/docs/tutorial/body.md | 8 ---- docs/de/docs/tutorial/extra-models.md | 29 +++++--------- .../tutorial/query-params-str-validations.md | 14 ------- docs/de/docs/tutorial/response-model.md | 14 ------- docs/de/docs/tutorial/schema-extra-example.md | 24 +----------- 10 files changed, 21 insertions(+), 176 deletions(-) diff --git a/docs/de/docs/advanced/path-operation-advanced-configuration.md b/docs/de/docs/advanced/path-operation-advanced-configuration.md index e722526002..c7ac1cf61b 100644 --- a/docs/de/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/de/docs/advanced/path-operation-advanced-configuration.md @@ -48,7 +48,7 @@ Sie können die verwendeten Zeilen aus dem Docstring einer *Pfadoperation-Funkti Das Hinzufügen eines `\f` (ein maskiertes „Form Feed“-Zeichen) führt dazu, dass **FastAPI** die für OpenAPI verwendete Ausgabe an dieser Stelle abschneidet. -Sie wird nicht in der Dokumentation angezeigt, aber andere Tools (z. B. Sphinx) können den Rest verwenden. +Sie wird nicht in der Dokumentation angezeigt, aber andere Tools (wie z. B. Sphinx) können den Rest verwenden. {* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} @@ -153,48 +153,16 @@ Und Sie könnten dies auch tun, wenn der Datentyp im Request nicht JSON ist. In der folgenden Anwendung verwenden wir beispielsweise weder die integrierte Funktionalität von FastAPI zum Extrahieren des JSON-Schemas aus Pydantic-Modellen noch die automatische Validierung für JSON. Tatsächlich deklarieren wir den Request-Content-Type als YAML und nicht als JSON: -//// tab | Pydantic v2 - {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *} - -//// - -/// info | Info - -In Pydantic Version 1 hieß die Methode zum Abrufen des JSON-Schemas für ein Modell `Item.schema()`, in Pydantic Version 2 heißt die Methode `Item.model_json_schema()`. - -/// - Obwohl wir nicht die standardmäßig integrierte Funktionalität verwenden, verwenden wir dennoch ein Pydantic-Modell, um das JSON-Schema für die Daten, die wir in YAML empfangen möchten, manuell zu generieren. -Dann verwenden wir den Request direkt und extrahieren den Body als `bytes`. Das bedeutet, dass FastAPI nicht einmal versucht, den Request-Payload als JSON zu parsen. +Dann verwenden wir den Request direkt und extrahieren den Body als `bytes`. Das bedeutet, dass FastAPI nicht einmal versucht, die Request-Payload als JSON zu parsen. Und dann parsen wir in unserem Code diesen YAML-Inhalt direkt und verwenden dann wieder dasselbe Pydantic-Modell, um den YAML-Inhalt zu validieren: -//// tab | Pydantic v2 - {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *} - -//// - -/// info | Info - -In Pydantic Version 1 war die Methode zum Parsen und Validieren eines Objekts `Item.parse_obj()`, in Pydantic Version 2 heißt die Methode `Item.model_validate()`. - -/// - /// tip | Tipp Hier verwenden wir dasselbe Pydantic-Modell wieder. diff --git a/docs/de/docs/advanced/settings.md b/docs/de/docs/advanced/settings.md index ebacf76f42..ea4540e10e 100644 --- a/docs/de/docs/advanced/settings.md +++ b/docs/de/docs/advanced/settings.md @@ -60,24 +60,8 @@ Auf die gleiche Weise wie bei Pydantic-Modellen deklarieren Sie Klassenattribute Sie können dieselben Validierungs-Funktionen und -Tools verwenden, die Sie für Pydantic-Modelle verwenden, z. B. verschiedene Datentypen und zusätzliche Validierungen mit `Field()`. -//// tab | Pydantic v2 - {* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} -//// - -//// tab | Pydantic v1 - -/// info | Info - -In Pydantic v1 würden Sie `BaseSettings` direkt von `pydantic` statt von `pydantic_settings` importieren. - -/// - -{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *} - -//// - /// tip | Tipp Für ein schnelles Copy-and-paste verwenden Sie nicht dieses Beispiel, sondern das letzte unten. @@ -215,8 +199,6 @@ APP_NAME="ChimichangApp" Und dann aktualisieren Sie Ihre `config.py` mit: -//// tab | Pydantic v2 - {* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} /// tip | Tipp @@ -225,26 +207,6 @@ Das Attribut `model_config` wird nur für die Pydantic-Konfiguration verwendet. /// -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *} - -/// tip | Tipp - -Die Klasse `Config` wird nur für die Pydantic-Konfiguration verwendet. Weitere Informationen finden Sie unter Pydantic Model Config. - -/// - -//// - -/// info | Info - -In Pydantic Version 1 erfolgte die Konfiguration in einer internen Klasse `Config`, in Pydantic Version 2 erfolgt sie in einem Attribut `model_config`. Dieses Attribut akzeptiert ein `dict`. Um automatische Codevervollständigung und Inline-Fehlerberichte zu erhalten, können Sie `SettingsConfigDict` importieren und verwenden, um dieses `dict` zu definieren. - -/// - Hier definieren wir die Konfiguration `env_file` innerhalb Ihrer Pydantic-`Settings`-Klasse und setzen den Wert auf den Dateinamen mit der dotenv-Datei, die wir verwenden möchten. ### Die `Settings` nur einmal laden mittels `lru_cache` { #creating-the-settings-only-once-with-lru-cache } diff --git a/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md index 7f60492ee9..a8eff3b2b0 100644 --- a/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md +++ b/docs/de/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md @@ -2,21 +2,23 @@ Wenn Sie eine ältere FastAPI-App haben, nutzen Sie möglicherweise Pydantic Version 1. -FastAPI unterstützt seit Version 0.100.0 sowohl Pydantic v1 als auch v2. +FastAPI Version 0.100.0 unterstützte sowohl Pydantic v1 als auch v2. Es verwendete, was auch immer Sie installiert hatten. -Wenn Sie Pydantic v2 installiert hatten, wurde dieses verwendet. Wenn stattdessen Pydantic v1 installiert war, wurde jenes verwendet. +FastAPI Version 0.119.0 führte eine teilweise Unterstützung für Pydantic v1 innerhalb von Pydantic v2 (als `pydantic.v1`) ein, um die Migration zu v2 zu erleichtern. -Pydantic v1 ist jetzt deprecatet und die Unterstützung dafür wird in den nächsten Versionen von FastAPI entfernt, Sie sollten also zu **Pydantic v2 migrieren**. Auf diese Weise erhalten Sie die neuesten Features, Verbesserungen und Fixes. +FastAPI 0.126.0 entfernte die Unterstützung für Pydantic v1, während `pydantic.v1` noch eine Weile unterstützt wurde. /// warning | Achtung -Außerdem hat das Pydantic-Team die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**. +Das Pydantic-Team hat die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**. + +Dies schließt `pydantic.v1` ein, das unter Python 3.14 und höher nicht mehr unterstützt wird. Wenn Sie die neuesten Features von Python nutzen möchten, müssen Sie sicherstellen, dass Sie Pydantic v2 verwenden. /// -Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **neuen Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen. +Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen. ## Offizieller Leitfaden { #official-guide } @@ -44,7 +46,7 @@ Danach können Sie die Tests ausführen und prüfen, ob alles funktioniert. Fall ## Pydantic v1 in v2 { #pydantic-v1-in-v2 } -Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`. +Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`. Dies wird aber in Versionen oberhalb von Python 3.13 nicht mehr unterstützt. Das bedeutet, Sie können die neueste Version von Pydantic v2 installieren und die alten Pydantic‑v1‑Komponenten aus diesem Untermodul importieren und verwenden, als hätten Sie das alte Pydantic v1 installiert. diff --git a/docs/de/docs/how-to/separate-openapi-schemas.md b/docs/de/docs/how-to/separate-openapi-schemas.md index 31653590b5..16f9c8a144 100644 --- a/docs/de/docs/how-to/separate-openapi-schemas.md +++ b/docs/de/docs/how-to/separate-openapi-schemas.md @@ -1,6 +1,6 @@ # Separate OpenAPI-Schemas für Eingabe und Ausgabe oder nicht { #separate-openapi-schemas-for-input-and-output-or-not } -Bei Verwendung von **Pydantic v2** ist die generierte OpenAPI etwas genauer und **korrekter** als zuvor. 😎 +Seit der Veröffentlichung von **Pydantic v2** ist die generierte OpenAPI etwas genauer und **korrekter** als zuvor. 😎 Tatsächlich gibt es in einigen Fällen sogar **zwei JSON-Schemas** in OpenAPI für dasselbe Pydantic-Modell, für Eingabe und Ausgabe, je nachdem, ob sie **Defaultwerte** haben. @@ -100,5 +100,3 @@ Und jetzt wird es ein einziges Schema für die Eingabe und Ausgabe des Modells g
- -Dies ist das gleiche Verhalten wie in Pydantic v1. 🤓 diff --git a/docs/de/docs/tutorial/body-updates.md b/docs/de/docs/tutorial/body-updates.md index aa62199feb..d260998e91 100644 --- a/docs/de/docs/tutorial/body-updates.md +++ b/docs/de/docs/tutorial/body-updates.md @@ -50,14 +50,6 @@ Wenn Sie Teil-Aktualisierungen entgegennehmen, ist der `exclude_unset`-Parameter Wie in `item.model_dump(exclude_unset=True)`. -/// info | Info - -In Pydantic v1 hieß diese Methode `.dict()`, in Pydantic v2 wurde sie deprecatet (aber immer noch unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - Das wird ein `dict` erstellen, mit nur den Daten, die gesetzt wurden, als das `item`-Modell erstellt wurde, Defaultwerte ausgeschlossen. Sie können das verwenden, um ein `dict` zu erstellen, das nur die (im Request) gesendeten Daten enthält, ohne Defaultwerte: @@ -68,14 +60,6 @@ Sie können das verwenden, um ein `dict` zu erstellen, das nur die (im deprecatet (aber immer noch unterstützt) und in `.model_copy()` umbenannt. - -Die Beispiele hier verwenden `.copy()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_copy()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - Wie in `stored_item_model.model_copy(update=update_data)`: {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} diff --git a/docs/de/docs/tutorial/body.md b/docs/de/docs/tutorial/body.md index 0ad95b0386..cdf3122f2f 100644 --- a/docs/de/docs/tutorial/body.md +++ b/docs/de/docs/tutorial/body.md @@ -127,14 +127,6 @@ Innerhalb der Funktion können Sie alle Attribute des Modellobjekts direkt verwe {* ../../docs_src/body/tutorial002_py310.py *} -/// info | Info - -In Pydantic v1 hieß die Methode `.dict()`, sie wurde in Pydantic v2 deprecatet (aber weiterhin unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` zur Kompatibilität mit Pydantic v1, aber Sie sollten stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 nutzen können. - -/// - ## Requestbody- + Pfad-Parameter { #request-body-path-parameters } Sie können Pfad-Parameter und den Requestbody gleichzeitig deklarieren. diff --git a/docs/de/docs/tutorial/extra-models.md b/docs/de/docs/tutorial/extra-models.md index 967e8535b8..889fdb9a3b 100644 --- a/docs/de/docs/tutorial/extra-models.md +++ b/docs/de/docs/tutorial/extra-models.md @@ -22,21 +22,13 @@ Hier ist eine allgemeine Idee, wie die Modelle mit ihren Passwortfeldern aussehe {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} -/// info | Info +### Über `**user_in.model_dump()` { #about-user-in-model-dump } -In Pydantic v1 hieß die Methode `.dict()`, in Pydantic v2 wurde sie deprecatet (aber weiterhin unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, aber Sie sollten `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - -### Über `**user_in.dict()` { #about-user-in-dict } - -#### Die `.dict()`-Methode von Pydantic { #pydantics-dict } +#### Pydantics `.model_dump()` { #pydantics-model-dump } `user_in` ist ein Pydantic-Modell der Klasse `UserIn`. -Pydantic-Modelle haben eine `.dict()`-Methode, die ein `dict` mit den Daten des Modells zurückgibt. +Pydantic-Modelle haben eine `.model_dump()`-Methode, die ein `dict` mit den Daten des Modells zurückgibt. Wenn wir also ein Pydantic-Objekt `user_in` erstellen, etwa so: @@ -47,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com und dann aufrufen: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() ``` haben wir jetzt ein `dict` mit den Daten in der Variablen `user_dict` (es ist ein `dict` statt eines Pydantic-Modellobjekts). @@ -103,20 +95,20 @@ UserInDB( #### Ein Pydantic-Modell aus dem Inhalt eines anderen { #a-pydantic-model-from-the-contents-of-another } -Da wir im obigen Beispiel `user_dict` von `user_in.dict()` bekommen haben, wäre dieser Code: +Da wir im obigen Beispiel `user_dict` von `user_in.model_dump()` bekommen haben, wäre dieser Code: ```Python -user_dict = user_in.dict() +user_dict = user_in.model_dump() UserInDB(**user_dict) ``` gleichwertig zu: ```Python -UserInDB(**user_in.dict()) +UserInDB(**user_in.model_dump()) ``` -... weil `user_in.dict()` ein `dict` ist, und dann lassen wir Python es „entpacken“, indem wir es an `UserInDB` mit vorangestelltem `**` übergeben. +... weil `user_in.model_dump()` ein `dict` ist, und dann lassen wir Python es „entpacken“, indem wir es an `UserInDB` mit vorangestelltem `**` übergeben. Auf diese Weise erhalten wir ein Pydantic-Modell aus den Daten eines anderen Pydantic-Modells. @@ -125,7 +117,7 @@ Auf diese Weise erhalten wir ein Pydantic-Modell aus den Daten eines anderen Pyd Und dann fügen wir das zusätzliche Schlüsselwort-Argument `hashed_password=hashed_password` hinzu, wie in: ```Python -UserInDB(**user_in.dict(), hashed_password=hashed_password) +UserInDB(**user_in.model_dump(), hashed_password=hashed_password) ``` ... was so ist wie: @@ -180,7 +172,6 @@ Wenn Sie eine deprecatet (aber immer noch unterstützt) und in `.model_dump()` umbenannt. - -Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können. - -/// - -/// info | Info - -FastAPI verwendet `.dict()` von Pydantic Modellen, mit dessen `exclude_unset`-Parameter, um das zu erreichen. - -/// - -/// info | Info - Sie können auch: * `response_model_exclude_defaults=True` diff --git a/docs/de/docs/tutorial/schema-extra-example.md b/docs/de/docs/tutorial/schema-extra-example.md index e2ffed292e..07fe8c5d92 100644 --- a/docs/de/docs/tutorial/schema-extra-example.md +++ b/docs/de/docs/tutorial/schema-extra-example.md @@ -8,36 +8,14 @@ Hier sind mehrere Möglichkeiten, das zu tun. Sie können `examples` („Beispiele“) für ein Pydantic-Modell deklarieren, welche dem generierten JSON-Schema hinzugefügt werden. -//// tab | Pydantic v2 - {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} -//// - -//// tab | Pydantic v1 - -{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *} - -//// - Diese zusätzlichen Informationen werden unverändert zum für dieses Modell ausgegebenen **JSON-Schema** hinzugefügt und in der API-Dokumentation verwendet. -//// tab | Pydantic v2 - -In Pydantic Version 2 würden Sie das Attribut `model_config` verwenden, das ein `dict` akzeptiert, wie beschrieben in Pydantic-Dokumentation: Configuration. +Sie können das Attribut `model_config` verwenden, das ein `dict` akzeptiert, wie beschrieben in Pydantic-Dokumentation: Configuration. Sie können `json_schema_extra` setzen, mit einem `dict`, das alle zusätzlichen Daten enthält, die im generierten JSON-Schema angezeigt werden sollen, einschließlich `examples`. -//// - -//// tab | Pydantic v1 - -In Pydantic Version 1 würden Sie eine interne Klasse `Config` und `schema_extra` verwenden, wie beschrieben in Pydantic-Dokumentation: Schema customization. - -Sie können `schema_extra` setzen, mit einem `dict`, das alle zusätzlichen Daten enthält, die im generierten JSON-Schema angezeigt werden sollen, einschließlich `examples`. - -//// - /// tip | Tipp Mit derselben Technik können Sie das JSON-Schema erweitern und Ihre eigenen benutzerdefinierten Zusatzinformationen hinzufügen. From 2b212ddd7604891d42e2ffbafe764c334230bdb7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 10:28:45 +0000 Subject: [PATCH 29/59] =?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 09e6341aa9..30d5c44039 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Update translations for de (update-outdated). PR [#14581](https://github.com/fastapi/fastapi/pull/14581) by [@nilslindemann](https://github.com/nilslindemann). + ### Internal * 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo). 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 30/59] =?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 a42cedae69..1057b86942 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 31/59] =?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 30d5c44039..dbaa2db873 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 32/59] =?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 5a12d69c8b..3ad630d94b 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 33/59] =?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 dbaa2db873..d54193ffe7 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 34/59] =?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 1920df8ff3..11fb6c9832 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 5df904ac7a..35ca9f0692 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 35/59] =?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 d54193ffe7..bc2441f7f3 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 36/59] =?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 e2d59c776e..52b9634aea 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 0583faf4a3..5c908cec4a 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 574beb65f4..dbc91409a5 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 a002c08ca3..666f819b0f 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 8d96171c7e..3a07482ad1 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 2ebfb3dd08..e50c1ae0ac 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 6467376967..6dc9feb299 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 7af4c6b754..98266cc288 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 c37ce30236..b3ced37c1e 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 97278069ad..50c321e7dd 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 c74ce65c3e..4e8e77d2ac 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 0000000000..e69de29bb2 diff --git a/docs_src/additional_status_codes/__init__.py b/docs_src/additional_status_codes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/advanced_middleware/__init__.py b/docs_src/advanced_middleware/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 diff --git a/docs_src/background_tasks/__init__.py b/docs_src/background_tasks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/behind_a_proxy/__init__.py b/docs_src/behind_a_proxy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/body/__init__.py b/docs_src/body/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/body_fields/__init__.py b/docs_src/body_fields/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/body_multiple_params/__init__.py b/docs_src/body_multiple_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/body_nested_models/__init__.py b/docs_src/body_nested_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/body_updates/__init__.py b/docs_src/body_updates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/conditional_openapi/__init__.py b/docs_src/conditional_openapi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/configure_swagger_ui/__init__.py b/docs_src/configure_swagger_ui/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/cookie_param_models/__init__.py b/docs_src/cookie_param_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/cookie_params/__init__.py b/docs_src/cookie_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/cors/__init__.py b/docs_src/cors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/custom_docs_ui/__init__.py b/docs_src/custom_docs_ui/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 diff --git a/docs_src/custom_response/__init__.py b/docs_src/custom_response/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/dataclasses_/__init__.py b/docs_src/dataclasses_/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 diff --git a/docs_src/dependencies/__init__.py b/docs_src/dependencies/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/dependency_testing/__init__.py b/docs_src/dependency_testing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/encoder/__init__.py b/docs_src/encoder/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/events/__init__.py b/docs_src/events/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/extending_openapi/__init__.py b/docs_src/extending_openapi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/extra_data_types/__init__.py b/docs_src/extra_data_types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/extra_models/__init__.py b/docs_src/extra_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/first_steps/__init__.py b/docs_src/first_steps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/generate_clients/__init__.py b/docs_src/generate_clients/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/graphql_/__init__.py b/docs_src/graphql_/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 diff --git a/docs_src/header_param_models/__init__.py b/docs_src/header_param_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/header_params/__init__.py b/docs_src/header_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/metadata/__init__.py b/docs_src/metadata/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/middleware/__init__.py b/docs_src/middleware/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/openapi_callbacks/__init__.py b/docs_src/openapi_callbacks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/openapi_webhooks/__init__.py b/docs_src/openapi_webhooks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 diff --git a/docs_src/path_operation_configuration/__init__.py b/docs_src/path_operation_configuration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/path_params/__init__.py b/docs_src/path_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 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 0000000000..e69de29bb2 diff --git a/docs_src/python_types/__init__.py b/docs_src/python_types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/query_param_models/__init__.py b/docs_src/query_param_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/query_params/__init__.py b/docs_src/query_params/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 diff --git a/docs_src/request_files/__init__.py b/docs_src/request_files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/request_form_models/__init__.py b/docs_src/request_form_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/request_forms/__init__.py b/docs_src/request_forms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 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 0000000000..e69de29bb2 diff --git a/docs_src/response_cookies/__init__.py b/docs_src/response_cookies/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_directly/__init__.py b/docs_src/response_directly/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_headers/__init__.py b/docs_src/response_headers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_model/__init__.py b/docs_src/response_model/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/response_status_code/__init__.py b/docs_src/response_status_code/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/schema_extra_example/__init__.py b/docs_src/schema_extra_example/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/security/__init__.py b/docs_src/security/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/separate_openapi_schemas/__init__.py b/docs_src/separate_openapi_schemas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/settings/__init__.py b/docs_src/settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/static_files/__init__.py b/docs_src/static_files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/sub_applications/__init__.py b/docs_src/sub_applications/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/templates/__init__.py b/docs_src/templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/templates/static/__init__.py b/docs_src/templates/static/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/templates/templates/__init__.py b/docs_src/templates/templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/using_request_directly/__init__.py b/docs_src/using_request_directly/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/wsgi/__init__.py b/docs_src/wsgi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pyproject.toml b/pyproject.toml index ae97cb71bd..8f824af5d5 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 ee188b496c..1604a2858c 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 0000000000..b6d51d5235 --- /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 0000000000..227a125e78 --- /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 0000000000..10212843ee --- /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 0000000000..e98d5860fe --- /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 0000000000..979c054cd0 --- /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 0000000000..d47aa1b4f9 --- /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 0000000000..d452929c38 --- /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 0000000000..ff9596943d --- /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 0000000000..9a07a904e6 --- /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 0000000000..088177cb95 --- /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 0000000000..a302819505 --- /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 0000000000..32eb8ee75c --- /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 0000000000..466e6af8fd --- /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 c81e991ebf..f1d2accef2 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 0000000000..22e2e02540 --- /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 d5f230bc42..bc407234a1 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 4cf8933805..995d926752 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 cddf4a9be8..a6a9fc1c7e 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 0000000000..e69de29bb2 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 0000000000..cf62c3b194 --- /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 8dac99cf30..50d7c4108c 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 8a1346d0d2..f09d6f268d 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 0000000000..a914936ba1 --- /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 0000000000..3e188abcf6 --- /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 0000000000..9d7377ebe4 --- /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 0000000000..6d3815ada2 --- /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 0000000000..4868254c0b --- /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 0000000000..e69de29bb2 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 0000000000..5c8ee054d8 --- /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 0000000000..3f2f508a11 --- /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 c102bb9999..aa65218cde 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 0000000000..bbb66b4516 --- /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 0000000000..ab8bc4c11c --- /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 0000000000..e66f6d2a12 --- /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 0000000000..e69de29bb2 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 0000000000..9ba7147b55 --- /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 0e7d69791b..e2814c88f9 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_get_openapi_json_default_url(): + response = client.get("/openapi.json") + assert response.status_code == 404, response.text def test_openapi_schema(): - response = client.get("/openapi.json") + 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 0000000000..085c271cdb --- /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 0000000000..e69de29bb2 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 0000000000..cbcfd4146f --- /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 0000000000..085d1f5e19 --- /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 0000000000..c7414d756a --- /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 0000000000..791db24625 --- /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 0000000000..a898e386fb --- /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 0000000000..0bfc9f807e --- /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 0000000000..cd2c39ab06 --- /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 0000000000..8e4a26a1ca --- /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 0000000000..e69de29bb2 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 0000000000..f1e3041030 --- /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 0000000000..467c915dcd --- /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 0000000000..d3593c984c --- /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 0000000000..296192593b --- /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 0000000000..9dc7d7aac2 --- /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 0000000000..e69de29bb2 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 0000000000..ccb0968576 --- /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 0000000000..34d2649171 --- /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 0000000000..24af32883e --- /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 0000000000..6d67ec4716 --- /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 0000000000..50976926e7 --- /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 0000000000..c045294652 --- /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 0000000000..33cf6cbfbc --- /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 0000000000..1ef0d4ea16 --- /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 0000000000..34046c5c48 --- /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 0000000000..7bd4049113 --- /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 0000000000..9e4d2e36bf --- /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 0000000000..a05751b974 --- /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 0000000000..e578048204 --- /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 0000000000..5602ef76f8 --- /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 0000000000..4c92b57b8d --- /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 0000000000..ae3ee7613d --- /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 0000000000..c0b7e3b133 --- /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 0000000000..9be18b74df --- /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 0000000000..f1af7e08c1 --- /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 0000000000..62018b80b5 --- /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 0000000000..a4ad7a63ba --- /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 0000000000..95efab2dc7 --- /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 0000000000..52462fe33b --- /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 0000000000..640cedce19 --- /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 0000000000..f287b5dcd8 --- /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 0000000000..b17bc27719 --- /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 0000000000..c631115744 --- /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 0000000000..7e9d69d41c --- /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 0000000000..ef84575723 --- /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 0000000000..10692f9904 --- /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 0000000000..216d4c420c --- /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 0000000000..e69de29bb2 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 0000000000..ddf55a045d --- /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 0000000000..4f52408605 --- /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 0000000000..3529a9bf02 --- /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 0000000000..85c076b1d2 --- /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 0000000000..b5e3d39ef7 --- /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 0000000000..28b70a2d43 --- /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 0000000000..0c5e440f1a --- /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 0000000000..e69de29bb2 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 0000000000..4fbf19ae82 --- /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 37/59] =?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 bc2441f7f3..11dae3333e 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 38/59] =?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 77e06bd96c..10a0949e48 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 39/59] =?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 11dae3333e..98fd67da23 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 40/59] =?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 39d0bd89cd..af2bed9ad9 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 8e0c559023..53e5052818 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 a99d4188e7..6180fcde6a 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 c776c4a59e..cc2934f44d 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 2770e3253d..3f78e93e84 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 1bda0ea9b2..62230e42cf 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 c4631d7ed2..8ae50aa145 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 2da74b95c5..dac297e4ed 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 2ac96993a8..704b3f77a6 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 e0008e2183..89ca6a8658 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 cfbff19c87..9d58c5dae1 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 7d9988f9f8..8973657a90 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 176b5588d7..8f5195ba11 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 ab7e1d8a77..9d1baf8530 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 c45e042484..23b236888d 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 f3da849e04..61c0f63571 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 95efab2dc7..585989a827 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 515a5a8d78..50be458962 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 c5526b19cd..83c7176567 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 41/59] =?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 98fd67da23..2af8abdfd4 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 42/59] =?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 2af8abdfd4..3e9084d475 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 73df6dc6c9..dc447c8bfd 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 From 34e884156f38dff4094a7764ff895c4d4b872060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 12:40:26 -0800 Subject: [PATCH 43/59] =?UTF-8?q?=E2=9C=85=20Run=20performance=20tests=20o?= =?UTF-8?q?nly=20on=20Pydantic=20v2=20(#14608)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/benchmarks/test_general_performance.py | 272 +++++++++---------- 1 file changed, 133 insertions(+), 139 deletions(-) diff --git a/tests/benchmarks/test_general_performance.py b/tests/benchmarks/test_general_performance.py index dac297e4ed..87add6d174 100644 --- a/tests/benchmarks/test_general_performance.py +++ b/tests/benchmarks/test_general_performance.py @@ -1,13 +1,12 @@ import json import sys -import warnings from collections.abc import Iterator from typing import Annotated, Any import pytest from fastapi import Depends, FastAPI -from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient +from pydantic import BaseModel if "--codspeed" not in sys.argv: pytest.skip( @@ -47,148 +46,143 @@ def dep_b(a: Annotated[int, Depends(dep_a)]): return a + 2 -@pytest.fixture( - scope="module", - params=[ - "pydantic-v2", - "pydantic-v1", - ], -) -def basemodel_class(request: pytest.FixtureRequest) -> type[Any]: - if request.param == "pydantic-v2": - from pydantic import BaseModel +class ItemIn(BaseModel): + name: str + value: int - return BaseModel - else: - from pydantic.v1 import BaseModel - return BaseModel +class ItemOut(BaseModel): + name: str + value: int + dep: int + + +class LargeIn(BaseModel): + items: list[dict[str, Any]] + metadata: dict[str, Any] + + +class LargeOut(BaseModel): + items: list[dict[str, Any]] + metadata: dict[str, Any] + + +app = FastAPI() + + +@app.post("/sync/validated", response_model=ItemOut) +def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name=item.name, value=item.value, dep=dep) + + +@app.get("/sync/dict-no-response-model") +def sync_dict_no_response_model(): + return {"name": "foo", "value": 123} + + +@app.get("/sync/dict-with-response-model", response_model=ItemOut) +def sync_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return {"name": "foo", "value": 123, "dep": dep} + + +@app.get("/sync/model-no-response-model") +def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) + + +@app.get("/sync/model-with-response-model", response_model=ItemOut) +def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) + + +@app.post("/async/validated", response_model=ItemOut) +async def async_validated( + item: ItemIn, + dep: Annotated[int, Depends(dep_b)], +): + return ItemOut(name=item.name, value=item.value, dep=dep) + + +@app.post("/sync/large-receive") +def sync_large_receive(payload: LargeIn): + return {"received": len(payload.items)} + + +@app.post("/async/large-receive") +async def async_large_receive(payload: LargeIn): + return {"received": len(payload.items)} + + +@app.get("/sync/large-dict-no-response-model") +def sync_large_dict_no_response_model(): + return LARGE_PAYLOAD + + +@app.get("/sync/large-dict-with-response-model", response_model=LargeOut) +def sync_large_dict_with_response_model(): + return LARGE_PAYLOAD + + +@app.get("/sync/large-model-no-response-model") +def sync_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/sync/large-model-with-response-model", response_model=LargeOut) +def sync_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/async/large-dict-no-response-model") +async def async_large_dict_no_response_model(): + return LARGE_PAYLOAD + + +@app.get("/async/large-dict-with-response-model", response_model=LargeOut) +async def async_large_dict_with_response_model(): + return LARGE_PAYLOAD + + +@app.get("/async/large-model-no-response-model") +async def async_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/async/large-model-with-response-model", response_model=LargeOut) +async def async_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + +@app.get("/async/dict-no-response-model") +async def async_dict_no_response_model(): + return {"name": "foo", "value": 123} + + +@app.get("/async/dict-with-response-model", response_model=ItemOut) +async def async_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return {"name": "foo", "value": 123, "dep": dep} + + +@app.get("/async/model-no-response-model") +async def async_model_no_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return ItemOut(name="foo", value=123, dep=dep) + + +@app.get("/async/model-with-response-model", response_model=ItemOut) +async def async_model_with_response_model( + dep: Annotated[int, Depends(dep_b)], +): + return ItemOut(name="foo", value=123, dep=dep) @pytest.fixture(scope="module") -def app(basemodel_class: type[Any]) -> FastAPI: - class ItemIn(basemodel_class): - name: str - value: int - - class ItemOut(basemodel_class): - name: str - value: int - dep: int - - class LargeIn(basemodel_class): - items: list[dict[str, Any]] - metadata: dict[str, Any] - - class LargeOut(basemodel_class): - items: list[dict[str, Any]] - metadata: dict[str, Any] - - app = FastAPI() - - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - - @app.post("/sync/validated", response_model=ItemOut) - def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): - return ItemOut(name=item.name, value=item.value, dep=dep) - - @app.get("/sync/dict-no-response-model") - def sync_dict_no_response_model(): - return {"name": "foo", "value": 123} - - @app.get("/sync/dict-with-response-model", response_model=ItemOut) - def sync_dict_with_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return {"name": "foo", "value": 123, "dep": dep} - - @app.get("/sync/model-no-response-model") - def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]): - return ItemOut(name="foo", value=123, dep=dep) - - @app.get("/sync/model-with-response-model", response_model=ItemOut) - def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]): - return ItemOut(name="foo", value=123, dep=dep) - - @app.post("/async/validated", response_model=ItemOut) - async def async_validated( - item: ItemIn, - dep: Annotated[int, Depends(dep_b)], - ): - return ItemOut(name=item.name, value=item.value, dep=dep) - - @app.post("/sync/large-receive") - def sync_large_receive(payload: LargeIn): - return {"received": len(payload.items)} - - @app.post("/async/large-receive") - async def async_large_receive(payload: LargeIn): - return {"received": len(payload.items)} - - @app.get("/sync/large-dict-no-response-model") - def sync_large_dict_no_response_model(): - return LARGE_PAYLOAD - - @app.get("/sync/large-dict-with-response-model", response_model=LargeOut) - def sync_large_dict_with_response_model(): - return LARGE_PAYLOAD - - @app.get("/sync/large-model-no-response-model") - def sync_large_model_no_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - - @app.get("/sync/large-model-with-response-model", response_model=LargeOut) - def sync_large_model_with_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - - @app.get("/async/large-dict-no-response-model") - async def async_large_dict_no_response_model(): - return LARGE_PAYLOAD - - @app.get("/async/large-dict-with-response-model", response_model=LargeOut) - async def async_large_dict_with_response_model(): - return LARGE_PAYLOAD - - @app.get("/async/large-model-no-response-model") - async def async_large_model_no_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - - @app.get("/async/large-model-with-response-model", response_model=LargeOut) - async def async_large_model_with_response_model(): - return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) - - @app.get("/async/dict-no-response-model") - async def async_dict_no_response_model(): - return {"name": "foo", "value": 123} - - @app.get("/async/dict-with-response-model", response_model=ItemOut) - async def async_dict_with_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return {"name": "foo", "value": 123, "dep": dep} - - @app.get("/async/model-no-response-model") - async def async_model_no_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return ItemOut(name="foo", value=123, dep=dep) - - @app.get("/async/model-with-response-model", response_model=ItemOut) - async def async_model_with_response_model( - dep: Annotated[int, Depends(dep_b)], - ): - return ItemOut(name="foo", value=123, dep=dep) - - return app - - -@pytest.fixture(scope="module") -def client(app: FastAPI) -> Iterator[TestClient]: +def client() -> Iterator[TestClient]: with TestClient(app) as client: yield client From 1b3bea8b6be2220cc1544653ea785d2134fcb35b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 20:40:51 +0000 Subject: [PATCH 44/59] =?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 3e9084d475..10b262b489 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ✅ Run performance tests only on Pydantic v2. PR [#14608](https://github.com/fastapi/fastapi/pull/14608) by [@tiangolo](https://github.com/tiangolo). + ## 0.127.1 ### Refactors From e3006305518a56ea35f62a31748ad26fe4356fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 04:54:56 -0800 Subject: [PATCH 45/59] =?UTF-8?q?=E2=9E=96=20Drop=20support=20for=20`pydan?= =?UTF-8?q?tic.v1`=20(#14609)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial002_pv1_an_py39.py | 20 - .../tutorial002_pv1_py39.py | 18 - fastapi/_compat/__init__.py | 64 +- fastapi/_compat/main.py | 264 ---- fastapi/_compat/may_v1.py | 124 -- fastapi/_compat/model_field.py | 50 - fastapi/_compat/shared.py | 24 +- fastapi/_compat/v1.py | 222 --- fastapi/_compat/v2.py | 59 +- fastapi/datastructures.py | 15 +- fastapi/dependencies/utils.py | 116 +- fastapi/encoders.py | 37 +- fastapi/exceptions.py | 6 + fastapi/openapi/models.py | 24 +- fastapi/openapi/utils.py | 13 +- fastapi/param_functions.py | 29 +- fastapi/params.py | 33 +- fastapi/routing.py | 104 +- fastapi/temp_pydantic_v1_params.py | 718 --------- fastapi/utils.py | 109 +- pyproject.toml | 22 +- tests/test_compat.py | 67 +- tests/test_compat_params_v1.py | 1060 ------------- tests/test_datetime_custom_encoder.py | 34 - .../__init__.py | 0 .../test_filter_pydantic_sub_model/app_pv1.py | 45 - .../test_filter_pydantic_sub_model_pv1.py | 146 -- ...t_get_model_definitions_formfeed_escape.py | 47 +- tests/test_inherited_custom_class.py | 45 - tests/test_jsonable_encoder.py | 63 +- .../test_pydantic_v1_deprecation_warnings.py | 99 -- tests/test_pydantic_v1_error.py | 97 ++ tests/test_pydantic_v1_v2_01.py | 439 ----- tests/test_pydantic_v1_v2_list.py | 682 -------- tests/test_pydantic_v1_v2_mixed.py | 1408 ----------------- .../test_pydantic_v1_v2_multifile/__init__.py | 0 tests/test_pydantic_v1_v2_multifile/main.py | 137 -- .../test_pydantic_v1_v2_multifile/modelsv1.py | 19 - .../test_pydantic_v1_v2_multifile/modelsv2.py | 19 - .../modelsv2b.py | 19 - .../test_multifile.py | 951 ----------- tests/test_pydantic_v1_v2_noneable.py | 692 -------- tests/test_read_with_orm_mode.py | 50 - ...est_response_model_as_return_annotation.py | 26 - .../test_tutorial007_pv1.py | 115 -- .../test_pydantic_v1_in_v2/__init__.py | 0 .../test_tutorial001.py | 31 - .../test_tutorial002.py | 143 -- .../test_tutorial003.py | 158 -- .../test_tutorial004.py | 156 -- .../test_tutorial002_pv1.py | 128 -- .../test_tutorial001_pv1.py | 152 -- .../test_tutorial/test_settings/test_app03.py | 12 - .../test_settings/test_tutorial001.py | 10 +- tests/utils.py | 2 - 55 files changed, 338 insertions(+), 8785 deletions(-) delete mode 100644 docs_src/request_form_models/tutorial002_pv1_an_py39.py delete mode 100644 docs_src/request_form_models/tutorial002_pv1_py39.py delete mode 100644 fastapi/_compat/main.py delete mode 100644 fastapi/_compat/may_v1.py delete mode 100644 fastapi/_compat/model_field.py delete mode 100644 fastapi/_compat/v1.py delete mode 100644 fastapi/temp_pydantic_v1_params.py delete mode 100644 tests/test_compat_params_v1.py delete mode 100644 tests/test_filter_pydantic_sub_model/__init__.py delete mode 100644 tests/test_filter_pydantic_sub_model/app_pv1.py delete mode 100644 tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py delete mode 100644 tests/test_pydantic_v1_deprecation_warnings.py create mode 100644 tests/test_pydantic_v1_error.py delete mode 100644 tests/test_pydantic_v1_v2_01.py delete mode 100644 tests/test_pydantic_v1_v2_list.py delete mode 100644 tests/test_pydantic_v1_v2_mixed.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/__init__.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/main.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/modelsv1.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/modelsv2.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/modelsv2b.py delete mode 100644 tests/test_pydantic_v1_v2_multifile/test_multifile.py delete mode 100644 tests/test_pydantic_v1_v2_noneable.py delete mode 100644 tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py delete mode 100644 tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py diff --git a/docs_src/request_form_models/tutorial002_pv1_an_py39.py b/docs_src/request_form_models/tutorial002_pv1_an_py39.py deleted file mode 100644 index 392e6873cb..0000000000 --- a/docs_src/request_form_models/tutorial002_pv1_an_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI -from fastapi.temp_pydantic_v1_params import Form -from pydantic.v1 import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - class Config: - extra = "forbid" - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial002_pv1_py39.py b/docs_src/request_form_models/tutorial002_pv1_py39.py deleted file mode 100644 index da160b3a54..0000000000 --- a/docs_src/request_form_models/tutorial002_pv1_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import FastAPI -from fastapi.temp_pydantic_v1_params import Form -from pydantic.v1 import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - class Config: - extra = "forbid" - - -@app.post("/login/") -async def login(data: FormData = Form()): - return data diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index fd1df8c6a7..3dfaf9b712 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -1,43 +1,9 @@ -from .main import BaseConfig as BaseConfig -from .main import PydanticSchemaGenerationError as PydanticSchemaGenerationError -from .main import RequiredParam as RequiredParam -from .main import Undefined as Undefined -from .main import UndefinedType as UndefinedType -from .main import Url as Url -from .main import Validator as Validator -from .main import _get_model_config as _get_model_config -from .main import _is_error_wrapper as _is_error_wrapper -from .main import _is_model_class as _is_model_class -from .main import _is_model_field as _is_model_field -from .main import _is_undefined as _is_undefined -from .main import _model_dump as _model_dump -from .main import copy_field_info as copy_field_info -from .main import create_body_model as create_body_model -from .main import evaluate_forwardref as evaluate_forwardref -from .main import get_annotation_from_field_info as get_annotation_from_field_info -from .main import get_cached_model_fields as get_cached_model_fields -from .main import get_compat_model_name_map as get_compat_model_name_map -from .main import get_definitions as get_definitions -from .main import get_missing_field_error as get_missing_field_error -from .main import get_schema_from_model_field as get_schema_from_model_field -from .main import is_bytes_field as is_bytes_field -from .main import is_bytes_sequence_field as is_bytes_sequence_field -from .main import is_scalar_field as is_scalar_field -from .main import is_scalar_sequence_field as is_scalar_sequence_field -from .main import is_sequence_field as is_sequence_field -from .main import serialize_sequence_value as serialize_sequence_value -from .main import ( - with_info_plain_validator_function as with_info_plain_validator_function, -) -from .may_v1 import CoreSchema as CoreSchema -from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler -from .may_v1 import JsonSchemaValue as JsonSchemaValue -from .may_v1 import _normalize_errors as _normalize_errors -from .model_field import ModelField as ModelField from .shared import PYDANTIC_V2 as PYDANTIC_V2 from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1 from .shared import field_annotation_is_scalar as field_annotation_is_scalar +from .shared import is_pydantic_v1_model_class as is_pydantic_v1_model_class +from .shared import is_pydantic_v1_model_instance as is_pydantic_v1_model_instance from .shared import ( is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, ) @@ -47,3 +13,29 @@ from .shared import ( from .shared import lenient_issubclass as lenient_issubclass from .shared import sequence_types as sequence_types from .shared import value_is_sequence as value_is_sequence +from .v2 import BaseConfig as BaseConfig +from .v2 import ModelField as ModelField +from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError +from .v2 import RequiredParam as RequiredParam +from .v2 import Undefined as Undefined +from .v2 import UndefinedType as UndefinedType +from .v2 import Url as Url +from .v2 import Validator as Validator +from .v2 import _regenerate_error_with_loc as _regenerate_error_with_loc +from .v2 import copy_field_info as copy_field_info +from .v2 import create_body_model as create_body_model +from .v2 import evaluate_forwardref as evaluate_forwardref +from .v2 import get_cached_model_fields as get_cached_model_fields +from .v2 import get_compat_model_name_map as get_compat_model_name_map +from .v2 import get_definitions as get_definitions +from .v2 import get_missing_field_error as get_missing_field_error +from .v2 import get_schema_from_model_field as get_schema_from_model_field +from .v2 import is_bytes_field as is_bytes_field +from .v2 import is_bytes_sequence_field as is_bytes_sequence_field +from .v2 import is_scalar_field as is_scalar_field +from .v2 import is_scalar_sequence_field as is_scalar_sequence_field +from .v2 import is_sequence_field as is_sequence_field +from .v2 import serialize_sequence_value as serialize_sequence_value +from .v2 import ( + with_info_plain_validator_function as with_info_plain_validator_function, +) diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py deleted file mode 100644 index 95053a2374..0000000000 --- a/fastapi/_compat/main.py +++ /dev/null @@ -1,264 +0,0 @@ -import sys -from collections.abc import Sequence -from functools import lru_cache -from typing import ( - Any, -) - -from fastapi._compat import may_v1 -from fastapi._compat.shared import lenient_issubclass -from fastapi.types import ModelNameMap -from pydantic import BaseModel -from typing_extensions import Literal - -from . import v2 -from .model_field import ModelField -from .v2 import BaseConfig as BaseConfig -from .v2 import FieldInfo as FieldInfo -from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError -from .v2 import RequiredParam as RequiredParam -from .v2 import Undefined as Undefined -from .v2 import UndefinedType as UndefinedType -from .v2 import Url as Url -from .v2 import Validator as Validator -from .v2 import evaluate_forwardref as evaluate_forwardref -from .v2 import get_missing_field_error as get_missing_field_error -from .v2 import ( - with_info_plain_validator_function as with_info_plain_validator_function, -) - - -@lru_cache -def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: - if lenient_issubclass(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1.get_model_fields(model) # type: ignore[arg-type,return-value] - else: - from . import v2 - - return v2.get_model_fields(model) # type: ignore[return-value] - - -def _is_undefined(value: object) -> bool: - if isinstance(value, may_v1.UndefinedType): - return True - - return isinstance(value, v2.UndefinedType) - - -def _get_model_config(model: BaseModel) -> Any: - if isinstance(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1._get_model_config(model) - - return v2._get_model_config(model) - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - if isinstance(model, may_v1.BaseModel): - from fastapi._compat import v1 - - return v1._model_dump(model, mode=mode, **kwargs) - - return v2._model_dump(model, mode=mode, **kwargs) - - -def _is_error_wrapper(exc: Exception) -> bool: - if isinstance(exc, may_v1.ErrorWrapper): - return True - - return isinstance(exc, v2.ErrorWrapper) - - -def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - if isinstance(field_info, may_v1.FieldInfo): - from fastapi._compat import v1 - - return v1.copy_field_info(field_info=field_info, annotation=annotation) - - return v2.copy_field_info(field_info=field_info, annotation=annotation) - - -def create_body_model( - *, fields: Sequence[ModelField], model_name: str -) -> type[BaseModel]: - if fields and isinstance(fields[0], may_v1.ModelField): - from fastapi._compat import v1 - - return v1.create_body_model(fields=fields, model_name=model_name) - - return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type] - - -def get_annotation_from_field_info( - annotation: Any, field_info: FieldInfo, field_name: str -) -> Any: - if isinstance(field_info, may_v1.FieldInfo): - from fastapi._compat import v1 - - return v1.get_annotation_from_field_info( - annotation=annotation, field_info=field_info, field_name=field_name - ) - - return v2.get_annotation_from_field_info( - annotation=annotation, field_info=field_info, field_name=field_name - ) - - -def is_bytes_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_bytes_field(field) - - return v2.is_bytes_field(field) # type: ignore[arg-type] - - -def is_bytes_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_bytes_sequence_field(field) - - return v2.is_bytes_sequence_field(field) # type: ignore[arg-type] - - -def is_scalar_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_scalar_field(field) - - return v2.is_scalar_field(field) # type: ignore[arg-type] - - -def is_scalar_sequence_field(field: ModelField) -> bool: - return v2.is_scalar_sequence_field(field) # type: ignore[arg-type] - - -def is_sequence_field(field: ModelField) -> bool: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.is_sequence_field(field) - - return v2.is_sequence_field(field) # type: ignore[arg-type] - - -def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.serialize_sequence_value(field=field, value=value) - - return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type] - - -def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: - v1_model_fields = [ - field for field in fields if isinstance(field, may_v1.ModelField) - ] - if v1_model_fields: - from fastapi._compat import v1 - - v1_flat_models = v1.get_flat_models_from_fields( - v1_model_fields, # type: ignore[arg-type] - known_models=set(), - ) - all_flat_models = v1_flat_models - else: - all_flat_models = set() - - v2_model_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_flat_models = v2.get_flat_models_from_fields(v2_model_fields, known_models=set()) - all_flat_models = all_flat_models.union(v2_flat_models) # type: ignore[arg-type] - - model_name_map = v2.get_model_name_map(all_flat_models) # type: ignore[arg-type] - return model_name_map - - -def get_definitions( - *, - fields: list[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, -) -> tuple[ - dict[ - tuple[ModelField, Literal["validation", "serialization"]], - may_v1.JsonSchemaValue, - ], - dict[str, dict[str, Any]], -]: - if sys.version_info < (3, 14): - v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)] - v1_field_maps, v1_definitions = may_v1.get_definitions( - fields=v1_fields, # type: ignore[arg-type] - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_field_maps, v2_definitions = v2.get_definitions( - fields=v2_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - all_definitions = {**v1_definitions, **v2_definitions} - all_field_maps = {**v1_field_maps, **v2_field_maps} # type: ignore[misc] - return all_field_maps, all_definitions - - # Pydantic v1 is not supported since Python 3.14 - else: - v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] - v2_field_maps, v2_definitions = v2.get_definitions( - fields=v2_fields, - model_name_map=model_name_map, - separate_input_output_schemas=separate_input_output_schemas, - ) - return v2_field_maps, v2_definitions - - -def get_schema_from_model_field( - *, - field: ModelField, - model_name_map: ModelNameMap, - field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], - may_v1.JsonSchemaValue, - ], - separate_input_output_schemas: bool = True, -) -> dict[str, Any]: - if isinstance(field, may_v1.ModelField): - from fastapi._compat import v1 - - return v1.get_schema_from_model_field( - field=field, - model_name_map=model_name_map, - field_mapping=field_mapping, - separate_input_output_schemas=separate_input_output_schemas, - ) - - return v2.get_schema_from_model_field( - field=field, # type: ignore[arg-type] - model_name_map=model_name_map, - field_mapping=field_mapping, # type: ignore[arg-type] - separate_input_output_schemas=separate_input_output_schemas, - ) - - -def _is_model_field(value: Any) -> bool: - if isinstance(value, may_v1.ModelField): - return True - - return isinstance(value, v2.ModelField) - - -def _is_model_class(value: Any) -> bool: - if lenient_issubclass(value, may_v1.BaseModel): - return True - - return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] diff --git a/fastapi/_compat/may_v1.py b/fastapi/_compat/may_v1.py deleted file mode 100644 index 3ac86aa98b..0000000000 --- a/fastapi/_compat/may_v1.py +++ /dev/null @@ -1,124 +0,0 @@ -import sys -from collections.abc import Sequence -from typing import Any, Literal, Union - -from fastapi.types import ModelNameMap - -if sys.version_info >= (3, 14): - - class AnyUrl: - pass - - class BaseConfig: - pass - - class BaseModel: - pass - - class Color: - pass - - class CoreSchema: - pass - - class ErrorWrapper: - pass - - class FieldInfo: - pass - - class GetJsonSchemaHandler: - pass - - class JsonSchemaValue: - pass - - class ModelField: - pass - - class NameEmail: - pass - - class RequiredParam: - pass - - class SecretBytes: - pass - - class SecretStr: - pass - - class Undefined: - pass - - class UndefinedType: - pass - - class Url: - pass - - from .v2 import ValidationError, create_model - - def get_definitions( - *, - fields: list[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, - ) -> tuple[ - dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - dict[str, dict[str, Any]], - ]: - return {}, {} # pragma: no cover - - -else: - from .v1 import AnyUrl as AnyUrl - from .v1 import BaseConfig as BaseConfig - from .v1 import BaseModel as BaseModel - from .v1 import Color as Color - from .v1 import CoreSchema as CoreSchema - from .v1 import ErrorWrapper as ErrorWrapper - from .v1 import FieldInfo as FieldInfo - from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler - from .v1 import JsonSchemaValue as JsonSchemaValue - from .v1 import ModelField as ModelField - from .v1 import NameEmail as NameEmail - from .v1 import RequiredParam as RequiredParam - from .v1 import SecretBytes as SecretBytes - from .v1 import SecretStr as SecretStr - from .v1 import Undefined as Undefined - from .v1 import UndefinedType as UndefinedType - from .v1 import Url as Url - from .v1 import ValidationError, create_model - from .v1 import get_definitions as get_definitions - - -RequestErrorModel: type[BaseModel] = create_model("Request") - - -def _normalize_errors(errors: Sequence[Any]) -> list[dict[str, Any]]: - use_errors: list[Any] = [] - for error in errors: - if isinstance(error, ErrorWrapper): - new_errors = ValidationError( - errors=[error], model=RequestErrorModel - ).errors() - use_errors.extend(new_errors) - elif isinstance(error, list): - use_errors.extend(_normalize_errors(error)) - else: - use_errors.append(error) - return use_errors - - -def _regenerate_error_with_loc( - *, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...] -) -> list[dict[str, Any]]: - updated_loc_errors: list[Any] = [ - {**err, "loc": loc_prefix + err.get("loc", ())} - for err in _normalize_errors(errors) - ] - - return updated_loc_errors diff --git a/fastapi/_compat/model_field.py b/fastapi/_compat/model_field.py deleted file mode 100644 index 47d05cb946..0000000000 --- a/fastapi/_compat/model_field.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import ( - Any, - Union, -) - -from fastapi.types import IncEx -from pydantic.fields import FieldInfo -from typing_extensions import Literal, Protocol - - -class ModelField(Protocol): - field_info: "FieldInfo" - name: str - mode: Literal["validation", "serialization"] = "validation" - _version: Literal["v1", "v2"] = "v1" - - @property - def alias(self) -> str: ... - - @property - def required(self) -> bool: ... - - @property - def default(self) -> Any: ... - - @property - def type_(self) -> Any: ... - - def get_default(self) -> Any: ... - - def validate( - self, - value: Any, - values: dict[str, Any] = {}, # noqa: B006 - *, - loc: tuple[Union[int, str], ...] = (), - ) -> tuple[Any, Union[list[dict[str, Any]], None]]: ... - - def serialize( - self, - value: Any, - *, - mode: Literal["json", "python"] = "json", - include: Union[IncEx, None] = None, - exclude: Union[IncEx, None] = None, - by_alias: bool = True, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - ) -> Any: ... diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index 3a11e88ac9..419b58f7f2 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -1,6 +1,7 @@ import sys import types import typing +import warnings from collections import deque from collections.abc import Mapping, Sequence from dataclasses import is_dataclass @@ -10,7 +11,6 @@ from typing import ( Union, ) -from fastapi._compat import may_v1 from fastapi.types import UnionType from pydantic import BaseModel from pydantic.version import VERSION as PYDANTIC_VERSION @@ -81,9 +81,7 @@ def value_is_sequence(value: Any) -> bool: def _annotation_is_complex(annotation: Union[type[Any], None]) -> bool: return ( - lenient_issubclass( - annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile) - ) + lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) or _annotation_is_sequence(annotation) or is_dataclass(annotation) ) @@ -179,13 +177,27 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: ) +def is_pydantic_v1_model_instance(obj: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return isinstance(obj, v1.BaseModel) + + +def is_pydantic_v1_model_class(cls: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return lenient_issubclass(cls, v1.BaseModel) + + def annotation_is_pydantic_v1(annotation: Any) -> bool: - if lenient_issubclass(annotation, may_v1.BaseModel): + if is_pydantic_v1_model_class(annotation): return True origin = get_origin(annotation) if origin is Union or origin is UnionType: for arg in get_args(annotation): - if lenient_issubclass(arg, may_v1.BaseModel): + if is_pydantic_v1_model_class(arg): return True if field_annotation_is_sequence(annotation): for sub_annotation in get_args(annotation): diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py deleted file mode 100644 index b0a9dd35f1..0000000000 --- a/fastapi/_compat/v1.py +++ /dev/null @@ -1,222 +0,0 @@ -from collections.abc import Sequence -from copy import copy -from dataclasses import dataclass, is_dataclass -from enum import Enum -from typing import ( - Any, - Callable, - Union, -) - -from fastapi._compat import shared -from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX -from fastapi.types import ModelNameMap -from pydantic.v1 import BaseConfig as BaseConfig -from pydantic.v1 import BaseModel as BaseModel -from pydantic.v1 import ValidationError as ValidationError -from pydantic.v1 import create_model as create_model -from pydantic.v1.class_validators import Validator as Validator -from pydantic.v1.color import Color as Color -from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper -from pydantic.v1.fields import ( - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, -) -from pydantic.v1.fields import FieldInfo as FieldInfo -from pydantic.v1.fields import ModelField as ModelField -from pydantic.v1.fields import Undefined as Undefined -from pydantic.v1.fields import UndefinedType as UndefinedType -from pydantic.v1.networks import AnyUrl as AnyUrl -from pydantic.v1.networks import NameEmail as NameEmail -from pydantic.v1.schema import TypeModelSet as TypeModelSet -from pydantic.v1.schema import field_schema, model_process_schema -from pydantic.v1.schema import ( - get_annotation_from_field_info as get_annotation_from_field_info, -) -from pydantic.v1.schema import ( - get_flat_models_from_field as get_flat_models_from_field, -) -from pydantic.v1.schema import ( - get_flat_models_from_fields as get_flat_models_from_fields, -) -from pydantic.v1.schema import get_model_name_map as get_model_name_map -from pydantic.v1.types import SecretBytes as SecretBytes -from pydantic.v1.types import SecretStr as SecretStr -from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref -from pydantic.v1.utils import lenient_issubclass as lenient_issubclass -from pydantic.version import VERSION as PYDANTIC_VERSION -from typing_extensions import Literal - -PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) -PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2 -# Keeping old "Required" functionality from Pydantic V1, without -# shadowing typing.Required. -RequiredParam: Any = Ellipsis - - -GetJsonSchemaHandler = Any -JsonSchemaValue = dict[str, Any] -CoreSchema = Any -Url = AnyUrl - -sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, -} -sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, -} - - -@dataclass -class GenerateJsonSchema: - ref_template: str - - -class PydanticSchemaGenerationError(Exception): - pass - - -RequestErrorModel: type[BaseModel] = create_model("Request") - - -def with_info_plain_validator_function( - function: Callable[..., Any], - *, - ref: Union[str, None] = None, - metadata: Any = None, - serialization: Any = None, -) -> Any: - return {} - - -def get_model_definitions( - *, - flat_models: set[Union[type[BaseModel], type[Enum]]], - model_name_map: dict[Union[type[BaseModel], type[Enum]], str], -) -> dict[str, Any]: - definitions: dict[str, dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - definitions[model_name] = m_schema - for m_schema in definitions.values(): - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - return definitions - - -def is_pv1_scalar_field(field: ModelField) -> bool: - from fastapi import params - - field_info = field.field_info - if not ( - field.shape == SHAPE_SINGLETON - and not lenient_issubclass(field.type_, BaseModel) - and not lenient_issubclass(field.type_, dict) - and not shared.field_annotation_is_sequence(field.type_) - and not is_dataclass(field.type_) - and not isinstance(field_info, params.Body) - ): - return False - if field.sub_fields: - if not all(is_pv1_scalar_field(f) for f in field.sub_fields): - return False - return True - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - return model.dict(**kwargs) - - -def _get_model_config(model: BaseModel) -> Any: - return model.__config__ - - -def get_schema_from_model_field( - *, - field: ModelField, - model_name_map: ModelNameMap, - field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue - ], - separate_input_output_schemas: bool = True, -) -> dict[str, Any]: - return field_schema( - field, - model_name_map=model_name_map, # type: ignore[arg-type] - ref_prefix=REF_PREFIX, - )[0] - - -# def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: -# models = get_flat_models_from_fields(fields, known_models=set()) -# return get_model_name_map(models) # type: ignore[no-any-return] - - -def get_definitions( - *, - fields: list[ModelField], - model_name_map: ModelNameMap, - separate_input_output_schemas: bool = True, -) -> tuple[ - dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], - dict[str, dict[str, Any]], -]: - models = get_flat_models_from_fields(fields, known_models=set()) - return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) # type: ignore[arg-type] - - -def is_scalar_field(field: ModelField) -> bool: - return is_pv1_scalar_field(field) - - -def is_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_) - - -def is_bytes_field(field: ModelField) -> bool: - return lenient_issubclass(field.type_, bytes) - - -def is_bytes_sequence_field(field: ModelField) -> bool: - return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) - - -def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - return copy(field_info) - - -def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: - return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return] - - -def create_body_model( - *, fields: Sequence[ModelField], model_name: str -) -> type[BaseModel]: - BodyModel = create_model(model_name) - for f in fields: - BodyModel.__fields__[f.name] = f - return BodyModel - - -def get_model_fields(model: type[BaseModel]) -> list[ModelField]: - return list(model.__fields__.values()) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index cbcb98e1a2..25b6814536 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -4,6 +4,7 @@ from collections.abc import Sequence from copy import copy, deepcopy from dataclasses import dataclass, is_dataclass from enum import Enum +from functools import lru_cache from typing import ( Annotated, Any, @@ -11,7 +12,7 @@ from typing import ( cast, ) -from fastapi._compat import may_v1, shared +from fastapi._compat import shared from fastapi.openapi.constants import REF_TEMPLATE from fastapi.types import IncEx, ModelNameMap, UnionType from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model @@ -175,7 +176,7 @@ class ModelField: None, ) except ValidationError as exc: - return None, may_v1._regenerate_error_with_loc( + return None, _regenerate_error_with_loc( errors=exc.errors(include_url=False), loc_prefix=loc ) @@ -210,22 +211,6 @@ class ModelField: return id(self) -def get_annotation_from_field_info( - annotation: Any, field_info: FieldInfo, field_name: str -) -> Any: - return annotation - - -def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any -) -> Any: - return model.model_dump(mode=mode, **kwargs) - - -def _get_model_config(model: BaseModel) -> Any: - return model.model_config - - def _has_computed_fields(field: ModelField) -> bool: computed_fields = field._type_adapter.core_schema.get("schema", {}).get( "computed_fields", [] @@ -490,6 +475,11 @@ def get_model_fields(model: type[BaseModel]) -> list[ModelField]: return model_fields +@lru_cache +def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]: + return get_model_fields(model) # type: ignore[return-value] + + # Duplicate of several schema functions from Pydantic v1 to make them compatible with # Pydantic v2 and allow mixing the models @@ -503,22 +493,23 @@ def normalize_name(name: str) -> str: def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]: name_model_map = {} - conflicting_names: set[str] = set() for model in unique_models: model_name = normalize_name(model.__name__) - if model_name in conflicting_names: - model_name = get_long_model_name(model) - name_model_map[model_name] = model - elif model_name in name_model_map: - conflicting_names.add(model_name) - conflicting_model = name_model_map.pop(model_name) - name_model_map[get_long_model_name(conflicting_model)] = conflicting_model - name_model_map[get_long_model_name(model)] = model - else: - name_model_map[model_name] = model + name_model_map[model_name] = model return {v: k for k, v in name_model_map.items()} +def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: + all_flat_models = set() + + v2_model_fields = [field for field in fields if isinstance(field, ModelField)] + v2_flat_models = get_flat_models_from_fields(v2_model_fields, known_models=set()) + all_flat_models = all_flat_models.union(v2_flat_models) # type: ignore[arg-type] + + model_name_map = get_model_name_map(all_flat_models) # type: ignore[arg-type] + return model_name_map + + def get_flat_models_from_model( model: type["BaseModel"], known_models: Union[TypeModelSet, None] = None ) -> TypeModelSet: @@ -567,5 +558,11 @@ def get_flat_models_from_fields( return known_models -def get_long_model_name(model: TypeModelOrEnum) -> str: - return f"{model.__module__}__{model.__qualname__}".replace(".", "__") +def _regenerate_error_with_loc( + *, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...] +) -> list[dict[str, Any]]: + updated_loc_errors: list[Any] = [ + {**err, "loc": loc_prefix + err.get("loc", ())} for err in errors + ] + + return updated_loc_errors diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index 492cbfcccb..2bf5fdb262 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,3 +1,4 @@ +from collections.abc import Mapping from typing import ( Annotated, Any, @@ -9,11 +10,7 @@ from typing import ( ) from annotated_doc import Doc -from fastapi._compat import ( - CoreSchema, - GetJsonSchemaHandler, - JsonSchemaValue, -) +from pydantic import GetJsonSchemaHandler from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 from starlette.datastructures import FormData as FormData # noqa: F401 @@ -142,14 +139,14 @@ class UploadFile(StarletteUploadFile): @classmethod def __get_pydantic_json_schema__( - cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler - ) -> JsonSchemaValue: + cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler + ) -> dict[str, Any]: return {"type": "string", "format": "binary"} @classmethod def __get_pydantic_core_schema__( - cls, source: type[Any], handler: Callable[[Any], CoreSchema] - ) -> CoreSchema: + cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]] + ) -> Mapping[str, Any]: from ._compat.v2 import with_info_plain_validator_function return with_info_plain_validator_function(cls._validate) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index af2bed9ad9..45e1ff3ed1 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,7 +1,6 @@ import dataclasses import inspect import sys -import warnings from collections.abc import Coroutine, Mapping, Sequence from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy @@ -22,13 +21,11 @@ from fastapi._compat import ( ModelField, RequiredParam, Undefined, - _is_error_wrapper, - _is_model_class, + _regenerate_error_with_loc, copy_field_info, create_body_model, evaluate_forwardref, field_annotation_is_scalar, - get_annotation_from_field_info, get_cached_model_fields, get_missing_field_error, is_bytes_field, @@ -39,19 +36,17 @@ from fastapi._compat import ( is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_sequence_annotation, lenient_issubclass, - may_v1, sequence_types, serialize_sequence_value, value_is_sequence, ) -from fastapi._compat.shared import annotation_is_pydantic_v1 from fastapi.background import BackgroundTasks from fastapi.concurrency import ( asynccontextmanager, contextmanager_in_threadpool, ) from fastapi.dependencies.models import Dependant -from fastapi.exceptions import DependencyScopeError, FastAPIDeprecationWarning +from fastapi.exceptions import DependencyScopeError from fastapi.logger import logger from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey @@ -72,8 +67,6 @@ from starlette.responses import Response from starlette.websockets import WebSocket from typing_extensions import Literal, get_args, get_origin -from .. import temp_pydantic_v1_params - multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' 'You can install "python-multipart" with: \n\n' @@ -189,7 +182,7 @@ def _get_flat_fields_from_params(fields: list[ModelField]) -> list[ModelField]: if not fields: return fields first_field = fields[0] - if len(fields) == 1 and _is_model_class(first_field.type_): + if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): fields_to_extract = get_cached_model_fields(first_field.type_) return fields_to_extract return fields @@ -323,16 +316,7 @@ def get_dependant( ) continue assert param_details.field is not None - if isinstance(param_details.field, may_v1.ModelField): - warnings.warn( - "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." - f" Please update the param {param_name}: {param_details.type_annotation!r}.", - category=FastAPIDeprecationWarning, - stacklevel=5, - ) - if isinstance( - param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body) - ): + if isinstance(param_details.field.field_info, params.Body): dependant.body_params.append(param_details.field) else: add_param_to_fields(field=param_details.field, dependant=dependant) @@ -391,7 +375,7 @@ def analyze_param( fastapi_annotations = [ arg for arg in annotated_args[1:] - if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends)) + if isinstance(arg, (FieldInfo, params.Depends)) ] fastapi_specific_annotations = [ arg @@ -400,30 +384,27 @@ def analyze_param( arg, ( params.Param, - temp_pydantic_v1_params.Param, params.Body, - temp_pydantic_v1_params.Body, params.Depends, ), ) ] if fastapi_specific_annotations: - fastapi_annotation: Union[ - FieldInfo, may_v1.FieldInfo, params.Depends, None - ] = fastapi_specific_annotations[-1] + fastapi_annotation: Union[FieldInfo, params.Depends, None] = ( + fastapi_specific_annotations[-1] + ) else: fastapi_annotation = None # Set default for Annotated FieldInfo - if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)): + if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( field_info=fastapi_annotation, # type: ignore[arg-type] annotation=use_annotation, ) - assert field_info.default in { - Undefined, - may_v1.Undefined, - } or field_info.default in {RequiredParam, may_v1.RequiredParam}, ( + assert ( + field_info.default == Undefined or field_info.default == RequiredParam + ), ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." ) @@ -447,7 +428,7 @@ def analyze_param( ) depends = value # Get FieldInfo from default value - elif isinstance(value, (FieldInfo, may_v1.FieldInfo)): + elif isinstance(value, FieldInfo): assert field_info is None, ( "Cannot specify FastAPI annotations in `Annotated` and default value" f" together for {param_name!r}" @@ -491,14 +472,7 @@ def analyze_param( ) or is_uploadfile_sequence_annotation(type_annotation): field_info = params.File(annotation=use_annotation, default=default_value) elif not field_annotation_is_scalar(annotation=type_annotation): - if annotation_is_pydantic_v1(use_annotation): - field_info = temp_pydantic_v1_params.Body( # type: ignore[assignment] - annotation=use_annotation, default=default_value - ) - else: - field_info = params.Body( - annotation=use_annotation, default=default_value - ) + field_info = params.Body(annotation=use_annotation, default=default_value) else: field_info = params.Query(annotation=use_annotation, default=default_value) @@ -507,23 +481,17 @@ def analyze_param( if field_info is not None: # Handle field_info.in_ if is_path_param: - assert isinstance( - field_info, (params.Path, temp_pydantic_v1_params.Path) - ), ( + assert isinstance(field_info, params.Path), ( f"Cannot use `{field_info.__class__.__name__}` for path param" f" {param_name!r}" ) elif ( - isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)) + isinstance(field_info, params.Param) and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query - use_annotation_from_field_info = get_annotation_from_field_info( - use_annotation, - field_info, - param_name, - ) - if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)): + use_annotation_from_field_info = use_annotation + if isinstance(field_info, params.Form): ensure_multipart_is_installed() if not field_info.alias and getattr(field_info, "convert_underscores", None): alias = param_name.replace("_", "-") @@ -535,20 +503,19 @@ def analyze_param( type_=use_annotation_from_field_info, default=field_info.default, alias=alias, - required=field_info.default - in (RequiredParam, may_v1.RequiredParam, Undefined), + required=field_info.default in (RequiredParam, Undefined), field_info=field_info, ) if is_path_param: assert is_scalar_field(field=field), ( "Path params must be of one of the supported types" ) - elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)): + elif isinstance(field_info, params.Query): assert ( is_scalar_field(field) or is_scalar_sequence_field(field) or ( - _is_model_class(field.type_) + lenient_issubclass(field.type_, BaseModel) # For Pydantic v1 and getattr(field, "shape", 1) == 1 ) @@ -742,10 +709,8 @@ def _validate_value_with_model_field( else: return deepcopy(field.default), [] v_, errors_ = field.validate(value, values, loc=loc) - if _is_error_wrapper(errors_): # type: ignore[arg-type] - return None, [errors_] - elif isinstance(errors_, list): - new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=()) + if isinstance(errors_, list): + new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) return None, new_errors else: return v_, [] @@ -762,7 +727,7 @@ def _get_multidict_value( if ( value is None or ( - isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form)) + isinstance(field.field_info, params.Form) and isinstance(value, str) # For type checks and value == "" ) @@ -832,7 +797,7 @@ def request_params_to_args( if single_not_embedded_field: field_info = first_field.field_info - assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( + assert isinstance(field_info, params.Param), ( "Params must be subclasses of Param" ) loc: tuple[str, ...] = (field_info.in_.value,) @@ -844,7 +809,7 @@ def request_params_to_args( for field in fields: value = _get_multidict_value(field, received_params) field_info = field.field_info - assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( + assert isinstance(field_info, params.Param), ( "Params must be subclasses of Param" ) loc = (field_info.in_.value, get_validation_alias(field)) @@ -871,7 +836,7 @@ def is_union_of_base_models(field_type: Any) -> bool: union_args = get_args(field_type) for arg in union_args: - if not _is_model_class(arg): + if not lenient_issubclass(arg, BaseModel): return False return True @@ -893,8 +858,8 @@ def _should_embed_body_fields(fields: list[ModelField]) -> bool: # If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level # otherwise it has to be embedded, so that the key value pair can be extracted if ( - isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form)) - and not _is_model_class(first_field.type_) + isinstance(first_field.field_info, params.Form) + and not lenient_issubclass(first_field.type_, BaseModel) and not is_union_of_base_models(first_field.type_) ): return True @@ -911,14 +876,14 @@ async def _extract_form_body( value = _get_multidict_value(field, received_body) field_info = field.field_info if ( - isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) + isinstance(field_info, params.File) and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( is_bytes_sequence_field(field) - and isinstance(field_info, (params.File, temp_pydantic_v1_params.File)) + and isinstance(field_info, params.File) and value_is_sequence(value) ): # For types @@ -964,7 +929,7 @@ async def request_body_to_args( if ( single_not_embedded_field - and _is_model_class(first_field.type_) + and lenient_issubclass(first_field.type_, BaseModel) and isinstance(received_body, FormData) ): fields_to_extract = get_cached_model_fields(first_field.type_) @@ -1029,28 +994,15 @@ def get_body_field( BodyFieldInfo_kwargs["default"] = None if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): BodyFieldInfo: type[params.Body] = params.File - elif any( - isinstance(f.field_info, temp_pydantic_v1_params.File) - for f in flat_dependant.body_params - ): - BodyFieldInfo: type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File # type: ignore[no-redef] elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): BodyFieldInfo = params.Form - elif any( - isinstance(f.field_info, temp_pydantic_v1_params.Form) - for f in flat_dependant.body_params - ): - BodyFieldInfo = temp_pydantic_v1_params.Form # type: ignore[assignment] else: - if annotation_is_pydantic_v1(BodyModel): - BodyFieldInfo = temp_pydantic_v1_params.Body # type: ignore[assignment] - else: - BodyFieldInfo = params.Body + BodyFieldInfo = params.Body body_param_media_types = [ f.field_info.media_type for f in flat_dependant.body_params - if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body)) + if isinstance(f.field_info, params.Body) ] if len(set(body_param_media_types)) == 1: BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 549da32797..e8610c983b 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -18,14 +18,18 @@ from typing import Annotated, Any, Callable, Optional, Union from uuid import UUID from annotated_doc import Doc -from fastapi._compat import may_v1 +from fastapi.exceptions import PydanticV1NotSupportedError from fastapi.types import IncEx from pydantic import BaseModel from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr +from pydantic_core import PydanticUndefinedType -from ._compat import Url, _is_undefined, _model_dump +from ._compat import ( + Url, + is_pydantic_v1_model_instance, +) # Taken from Pydantic v1 as is @@ -63,7 +67,6 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { bytes: lambda o: o.decode(), Color: str, - may_v1.Color: str, datetime.date: isoformat, datetime.datetime: isoformat, datetime.time: isoformat, @@ -80,19 +83,14 @@ ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { IPv6Interface: str, IPv6Network: str, NameEmail: str, - may_v1.NameEmail: str, Path: str, Pattern: lambda o: o.pattern, SecretBytes: str, - may_v1.SecretBytes: str, SecretStr: str, - may_v1.SecretStr: str, set: list, UUID: str, Url: str, - may_v1.Url: str, AnyUrl: str, - may_v1.AnyUrl: str, } @@ -224,15 +222,8 @@ def jsonable_encoder( include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) - if isinstance(obj, (BaseModel, may_v1.BaseModel)): - # TODO: remove when deprecating Pydantic v1 - encoders: dict[Any, Any] = {} - if isinstance(obj, may_v1.BaseModel): - encoders = getattr(obj.__config__, "json_encoders", {}) - if custom_encoder: - encoders = {**encoders, **custom_encoder} - obj_dict = _model_dump( - obj, # type: ignore[arg-type] + if isinstance(obj, BaseModel): + obj_dict = obj.model_dump( mode="json", include=include, exclude=exclude, @@ -241,14 +232,10 @@ def jsonable_encoder( exclude_none=exclude_none, exclude_defaults=exclude_defaults, ) - if "__root__" in obj_dict: - obj_dict = obj_dict["__root__"] return jsonable_encoder( obj_dict, exclude_none=exclude_none, exclude_defaults=exclude_defaults, - # TODO: remove when deprecating Pydantic v1 - custom_encoder=encoders, sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): @@ -271,7 +258,7 @@ def jsonable_encoder( return str(obj) if isinstance(obj, (str, int, float, type(None))): return obj - if _is_undefined(obj): + if isinstance(obj, PydanticUndefinedType): return None if isinstance(obj, dict): encoded_dict = {} @@ -331,7 +318,11 @@ def jsonable_encoder( for encoder, classes_tuple in encoders_by_class_tuples.items(): if isinstance(obj, classes_tuple): return encoder(obj) - + if is_pydantic_v1_model_instance(obj): + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the model {obj!r}." + ) try: data = dict(obj) except Exception as e: diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 53e5052818..1a3abd80c2 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -233,6 +233,12 @@ class ResponseValidationError(ValidationException): self.body = body +class PydanticV1NotSupportedError(FastAPIError): + """ + A pydantic.v1 model is used, which is no longer supported. + """ + + class FastAPIDeprecationWarning(UserWarning): """ A custom deprecation warning as DeprecationWarning is ignored diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 680f678325..ac6a6d52c3 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,15 +1,15 @@ -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from enum import Enum from typing import Annotated, Any, Callable, Optional, Union -from fastapi._compat import ( - CoreSchema, - GetJsonSchemaHandler, - JsonSchemaValue, - with_info_plain_validator_function, -) +from fastapi._compat import with_info_plain_validator_function from fastapi.logger import logger -from pydantic import AnyUrl, BaseModel, Field +from pydantic import ( + AnyUrl, + BaseModel, + Field, + GetJsonSchemaHandler, +) from typing_extensions import Literal, TypedDict from typing_extensions import deprecated as typing_deprecated @@ -43,14 +43,14 @@ except ImportError: # pragma: no cover @classmethod def __get_pydantic_json_schema__( - cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler - ) -> JsonSchemaValue: + cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler + ) -> dict[str, Any]: return {"type": "string", "format": "email"} @classmethod def __get_pydantic_core_schema__( - cls, source: type[Any], handler: Callable[[Any], CoreSchema] - ) -> CoreSchema: + cls, source: type[Any], handler: Callable[[Any], Mapping[str, Any]] + ) -> Mapping[str, Any]: return with_info_plain_validator_function(cls._validate) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 6180fcde6a..75ff261025 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -6,7 +6,6 @@ from typing import Any, Optional, Union, cast from fastapi import routing from fastapi._compat import ( - JsonSchemaValue, ModelField, Undefined, get_compat_model_name_map, @@ -39,8 +38,6 @@ from starlette.responses import JSONResponse from starlette.routing import BaseRoute from typing_extensions import Literal -from .._compat import _is_model_field - validation_error_definition = { "title": "ValidationError", "type": "object", @@ -109,7 +106,7 @@ def _get_openapi_operation_parameters( dependant: Dependant, model_name_map: ModelNameMap, field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, ) -> list[dict[str, Any]]: @@ -182,13 +179,13 @@ def get_openapi_operation_request_body( body_field: Optional[ModelField], model_name_map: ModelNameMap, field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, ) -> Optional[dict[str, Any]]: if not body_field: return None - assert _is_model_field(body_field) + assert isinstance(body_field, ModelField) body_schema = get_schema_from_model_field( field=body_field, model_name_map=model_name_map, @@ -265,7 +262,7 @@ def get_openapi_path( operation_ids: set[str], model_name_map: ModelNameMap, field_mapping: dict[ - tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any] ], separate_input_output_schemas: bool = True, ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: @@ -457,7 +454,7 @@ def get_fields_from_routes( route, routing.APIRoute ): if route.body_field: - assert _is_model_field(route.body_field), ( + assert isinstance(route.body_field, ModelField), ( "A request body must be a Pydantic Field" ) body_fields_from_routes.append(route.body_field) diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 844542594b..0834fd741a 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -5,6 +5,7 @@ from annotated_doc import Doc from fastapi import params from fastapi._compat import Undefined from fastapi.openapi.models import Example +from pydantic import AliasChoices, AliasPath from typing_extensions import Literal, deprecated _Unset: Any = Undefined @@ -54,10 +55,8 @@ def Path( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -379,10 +378,8 @@ def Query( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -683,10 +680,8 @@ def Header( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -999,10 +994,8 @@ def Cookie( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1326,10 +1319,8 @@ def Body( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1641,10 +1632,8 @@ def Form( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one @@ -1955,10 +1944,8 @@ def File( # noqa: N802 """ ), ] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None validation_alias: Annotated[ - Union[str, None], + Union[str, AliasPath, AliasChoices, None], Doc( """ 'Whitelist' validation step. The parameter field will be the single one diff --git a/fastapi/params.py b/fastapi/params.py index cc2934f44d..72e797f833 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -6,6 +6,7 @@ from typing import Annotated, Any, Callable, Optional, Union from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.openapi.models import Example +from pydantic import AliasChoices, AliasPath from pydantic.fields import FieldInfo from typing_extensions import Literal, deprecated @@ -34,9 +35,7 @@ class Param(FieldInfo): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -147,9 +146,7 @@ class Path(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -233,9 +230,7 @@ class Query(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -317,9 +312,7 @@ class Header(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, convert_underscores: bool = True, title: Optional[str] = None, @@ -403,9 +396,7 @@ class Cookie(Param): # type: ignore[misc] annotation: Optional[Any] = None, alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -487,9 +478,7 @@ class Body(FieldInfo): # type: ignore[misc] media_type: str = "application/json", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -600,9 +589,7 @@ class Form(Body): # type: ignore[misc] media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, @@ -684,9 +671,7 @@ class File(Form): # type: ignore[misc] media_type: str = "multipart/form-data", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, + validation_alias: Union[str, AliasPath, AliasChoices, None] = None, serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, diff --git a/fastapi/routing.py b/fastapi/routing.py index 3f78e93e84..9ca2f46732 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -2,7 +2,6 @@ import email.message import functools import inspect import json -import warnings from collections.abc import ( AsyncIterator, Awaitable, @@ -22,16 +21,12 @@ from typing import ( ) from annotated_doc import Doc -from fastapi import params, temp_pydantic_v1_params +from fastapi import params from fastapi._compat import ( ModelField, Undefined, - _get_model_config, - _model_dump, - _normalize_errors, annotation_is_pydantic_v1, lenient_issubclass, - may_v1, ) from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.dependencies.models import Dependant @@ -47,8 +42,8 @@ from fastapi.dependencies.utils import ( from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( EndpointContext, - FastAPIDeprecationWarning, FastAPIError, + PydanticV1NotSupportedError, RequestValidationError, ResponseValidationError, WebSocketRequestValidationError, @@ -148,51 +143,6 @@ def websocket_session( return app -def _prepare_response_content( - res: Any, - *, - exclude_unset: bool, - exclude_defaults: bool = False, - exclude_none: bool = False, -) -> Any: - if isinstance(res, may_v1.BaseModel): - read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) # type: ignore[arg-type] - if read_with_orm_mode: - # Let from_orm extract the data from this model instead of converting - # it now to a dict. - # Otherwise, there's no way to extract lazy data that requires attribute - # access instead of dict iteration, e.g. lazy relationships. - return res - return _model_dump( - res, # type: ignore[arg-type] - by_alias=True, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - elif isinstance(res, list): - return [ - _prepare_response_content( - item, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - for item in res - ] - elif isinstance(res, dict): - return { - k: _prepare_response_content( - v, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - for k, v in res.items() - } - return res - - def _merge_lifespan_context( original_context: Lifespan[Any], nested_context: Lifespan[Any] ) -> Lifespan[Any]: @@ -252,14 +202,6 @@ async def serialize_response( ) -> Any: if field: errors = [] - if not hasattr(field, "serialize"): - # pydantic v1 - response_content = _prepare_response_content( - response_content, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) else: @@ -268,28 +210,15 @@ async def serialize_response( ) if isinstance(errors_, list): errors.extend(errors_) - elif errors_: - errors.append(errors_) if errors: ctx = endpoint_ctx or EndpointContext() raise ResponseValidationError( - errors=_normalize_errors(errors), + errors=errors, body=response_content, endpoint_ctx=ctx, ) - if hasattr(field, "serialize"): - return field.serialize( - value, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - return jsonable_encoder( + return field.serialize( value, include=include, exclude=exclude, @@ -298,6 +227,7 @@ async def serialize_response( exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) + else: return jsonable_encoder(response_content) @@ -332,9 +262,7 @@ def get_request_handler( ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" is_coroutine = dependant.is_coroutine_callable - is_body_form = body_field and isinstance( - body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) - ) + is_body_form = body_field and isinstance(body_field.field_info, params.Form) if isinstance(response_class, DefaultPlaceholder): actual_response_class: type[Response] = response_class.value else: @@ -464,7 +392,7 @@ def get_request_handler( response.headers.raw.extend(solved_result.response.headers.raw) if errors: validation_error = RequestValidationError( - _normalize_errors(errors), body=body, endpoint_ctx=endpoint_ctx + errors, body=body, endpoint_ctx=endpoint_ctx ) raise validation_error @@ -503,7 +431,7 @@ def get_websocket_app( ) if solved_result.errors: raise WebSocketRequestValidationError( - _normalize_errors(solved_result.errors), + solved_result.errors, endpoint_ctx=endpoint_ctx, ) assert dependant.call is not None, "dependant.call must be a function" @@ -638,11 +566,9 @@ class APIRoute(routing.Route): ) response_name = "Response_" + self.unique_id if annotation_is_pydantic_v1(self.response_model): - warnings.warn( - "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." - f" Please update the response model {self.response_model!r}.", - category=FastAPIDeprecationWarning, - stacklevel=4, + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the response model {self.response_model!r}." ) self.response_field = create_model_field( name=response_name, @@ -678,11 +604,9 @@ class APIRoute(routing.Route): ) response_name = f"Response_{additional_status_code}_{self.unique_id}" if annotation_is_pydantic_v1(model): - warnings.warn( - "pydantic.v1 is deprecated and will soon stop being supported by FastAPI." - f" In responses={{}}, please update {model}.", - category=FastAPIDeprecationWarning, - stacklevel=4, + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" In responses={{}}, please update {model}." ) response_field = create_model_field( name=response_name, type_=model, mode="serialization" diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py deleted file mode 100644 index 62230e42cf..0000000000 --- a/fastapi/temp_pydantic_v1_params.py +++ /dev/null @@ -1,718 +0,0 @@ -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 - -from ._compat.may_v1 import FieldInfo, Undefined - -_Unset: Any = Undefined - - -class Param(FieldInfo): - in_: ParamTypes - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - if example is not _Unset: - warnings.warn( - "`example` has been deprecated, please use `examples` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - self.example = example - self.include_in_schema = include_in_schema - self.openapi_examples = openapi_examples - kwargs = dict( - default=default, - default_factory=default_factory, - alias=alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - discriminator=discriminator, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - **extra, - ) - if examples is not None: - kwargs["examples"] = examples - if regex is not None: - warnings.warn( - "`regex` has been deprecated, please use `pattern` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - current_json_schema_extra = json_schema_extra or extra - kwargs["deprecated"] = deprecated - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) - use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} - - super().__init__(**use_kwargs) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.default})" - - -class Path(Param): - in_ = ParamTypes.path - - def __init__( - self, - default: Any = ..., - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - assert default is ..., "Path parameters cannot have a default value" - self.in_ = self.in_ - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Query(Param): - in_ = ParamTypes.query - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Header(Param): - in_ = ParamTypes.header - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - convert_underscores: bool = True, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - self.convert_underscores = convert_underscores - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Cookie(Param): - in_ = ParamTypes.cookie - - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class Body(FieldInfo): - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - embed: Union[bool, None] = None, - media_type: str = "application/json", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - self.embed = embed - self.media_type = media_type - if example is not _Unset: - warnings.warn( - "`example` has been deprecated, please use `examples` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - self.example = example - self.include_in_schema = include_in_schema - self.openapi_examples = openapi_examples - kwargs = dict( - default=default, - default_factory=default_factory, - alias=alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - discriminator=discriminator, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - **extra, - ) - if examples is not None: - kwargs["examples"] = examples - if regex is not None: - warnings.warn( - "`regex` has been deprecated, please use `pattern` instead", - category=FastAPIDeprecationWarning, - stacklevel=4, - ) - current_json_schema_extra = json_schema_extra or extra - kwargs["deprecated"] = deprecated - kwargs["regex"] = pattern or regex - kwargs.update(**current_json_schema_extra) - - use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} - - super().__init__(**use_kwargs) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.default})" - - -class Form(Body): - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - media_type: str = "application/x-www-form-urlencoded", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - media_type=media_type, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) - - -class File(Form): - def __init__( - self, - default: Any = Undefined, - *, - default_factory: Union[Callable[[], Any], None] = _Unset, - annotation: Optional[Any] = None, - media_type: str = "multipart/form-data", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, - # TODO: update when deprecating Pydantic v1, import these types - # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, - regex: Annotated[ - Optional[str], - deprecated( - "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." - ), - ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[list[Any]] = None, - example: Annotated[ - Optional[Any], - deprecated( - "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " - "although still supported. Use examples instead." - ), - ] = _Unset, - openapi_examples: Optional[dict[str, Example]] = None, - deprecated: Union[deprecated, str, bool, None] = None, - include_in_schema: bool = True, - json_schema_extra: Union[dict[str, Any], None] = None, - **extra: Any, - ): - super().__init__( - default=default, - default_factory=default_factory, - annotation=annotation, - media_type=media_type, - alias=alias, - alias_priority=alias_priority, - validation_alias=validation_alias, - serialization_alias=serialization_alias, - title=title, - description=description, - gt=gt, - ge=ge, - lt=lt, - le=le, - min_length=min_length, - max_length=max_length, - pattern=pattern, - regex=regex, - discriminator=discriminator, - strict=strict, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - deprecated=deprecated, - example=example, - examples=examples, - openapi_examples=openapi_examples, - include_in_schema=include_in_schema, - json_schema_extra=json_schema_extra, - **extra, - ) diff --git a/fastapi/utils.py b/fastapi/utils.py index 8ae50aa145..78fdcbb5b4 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -6,7 +6,6 @@ from typing import ( Any, Optional, Union, - cast, ) from weakref import WeakKeyDictionary @@ -19,11 +18,9 @@ from fastapi._compat import ( UndefinedType, Validator, annotation_is_pydantic_v1, - lenient_issubclass, - may_v1, ) from fastapi.datastructures import DefaultPlaceholder, DefaultType -from fastapi.exceptions import FastAPIDeprecationWarning +from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Literal @@ -83,52 +80,18 @@ def create_model_field( mode: Literal["validation", "serialization"] = "validation", version: Literal["1", "auto"] = "auto", ) -> ModelField: + if annotation_is_pydantic_v1(type_): + raise PydanticV1NotSupportedError( + "pydantic.v1 models are no longer supported by FastAPI." + f" Please update the response model {type_!r}." + ) class_validators = class_validators or {} - v1_model_config = may_v1.BaseConfig - v1_field_info = field_info or may_v1.FieldInfo() - v1_kwargs = { - "name": name, - "field_info": v1_field_info, - "type_": type_, - "class_validators": class_validators, - "default": default, - "required": required, - "model_config": v1_model_config, - "alias": alias, - } - - if ( - annotation_is_pydantic_v1(type_) - or isinstance(field_info, may_v1.FieldInfo) - or version == "1" - ): - from fastapi._compat import v1 - - try: - return v1.ModelField(**v1_kwargs) # type: ignore[return-value] - except RuntimeError: - raise fastapi.exceptions.FastAPIError( - _invalid_args_message.format(type_=type_) - ) from None - else: - field_info = field_info or FieldInfo( - annotation=type_, default=default, alias=alias - ) - kwargs = {"mode": mode, "name": name, "field_info": field_info} - try: - return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] - except PydanticSchemaGenerationError: - raise fastapi.exceptions.FastAPIError( - _invalid_args_message.format(type_=type_) - ) from None - # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be - # a Pydantic v1 type, like a constrained int - from fastapi._compat import v1 - + field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias) + kwargs = {"mode": mode, "name": name, "field_info": field_info} try: - return v1.ModelField(**v1_kwargs) - except RuntimeError: + return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] + except PydanticSchemaGenerationError: raise fastapi.exceptions.FastAPIError( _invalid_args_message.format(type_=type_) ) from None @@ -139,57 +102,7 @@ def create_cloned_field( *, cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None, ) -> ModelField: - if isinstance(field, v2.ModelField): - return field - - from fastapi._compat import v1 - - # cloned_types caches already cloned types to support recursive models and improve - # performance by avoiding unnecessary cloning - if cloned_types is None: - cloned_types = _CLONED_TYPES_CACHE - - original_type = field.type_ - use_type = original_type - if lenient_issubclass(original_type, v1.BaseModel): - original_type = cast(type[v1.BaseModel], original_type) - use_type = cloned_types.get(original_type) - if use_type is None: - use_type = v1.create_model(original_type.__name__, __base__=original_type) - cloned_types[original_type] = use_type - for f in original_type.__fields__.values(): - use_type.__fields__[f.name] = create_cloned_field( - f, - cloned_types=cloned_types, - ) - new_field = create_model_field(name=field.name, type_=use_type, version="1") - new_field.has_alias = field.has_alias # type: ignore[attr-defined] - new_field.alias = field.alias # type: ignore[misc] - new_field.class_validators = field.class_validators # type: ignore[attr-defined] - new_field.default = field.default # type: ignore[misc] - new_field.default_factory = field.default_factory # type: ignore[attr-defined] - new_field.required = field.required # type: ignore[misc] - new_field.model_config = field.model_config # type: ignore[attr-defined] - new_field.field_info = field.field_info - new_field.allow_none = field.allow_none # type: ignore[attr-defined] - new_field.validate_always = field.validate_always # type: ignore[attr-defined] - if field.sub_fields: # type: ignore[attr-defined] - new_field.sub_fields = [ # type: ignore[attr-defined] - create_cloned_field(sub_field, cloned_types=cloned_types) - for sub_field in field.sub_fields # type: ignore[attr-defined] - ] - if field.key_field: # type: ignore[attr-defined] - new_field.key_field = create_cloned_field( # type: ignore[attr-defined] - field.key_field, # type: ignore[attr-defined] - cloned_types=cloned_types, - ) - new_field.validators = field.validators # type: ignore[attr-defined] - new_field.pre_validators = field.pre_validators # type: ignore[attr-defined] - new_field.post_validators = field.post_validators # type: ignore[attr-defined] - new_field.parse_json = field.parse_json # type: ignore[attr-defined] - new_field.shape = field.shape # type: ignore[attr-defined] - new_field.populate_validators() # type: ignore[attr-defined] - return new_field + return field def generate_operation_id_for_path( diff --git a/pyproject.toml b/pyproject.toml index 8f824af5d5..9c2c35a9f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,14 +199,22 @@ omit = [ "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 + # Pydantic v1 migration, no longer tested + "docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial001_an_py39.py", + "docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial002_an_py39.py", + "docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial003_an_py39.py", + "docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py", + "docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py", + # TODO: remove when removing this file, after updating translations, 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", + "docs_src/schema_extra_example/tutorial001_pv1_py39.py", + "docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py", + "docs_src/settings/app03_py39/config_pv1.py", + "docs_src/settings/app03_an_py39/config_pv1.py", + "docs_src/settings/tutorial001_pv1_py39.py", ] [tool.coverage.report] diff --git a/tests/test_compat.py b/tests/test_compat.py index 8d20710303..0b5600f8f5 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,20 +1,16 @@ -from typing import Any, Union +from typing import Union from fastapi import FastAPI, UploadFile from fastapi._compat import ( Undefined, - _get_model_config, - get_cached_model_fields, - is_scalar_field, is_uploadfile_sequence_annotation, - may_v1, ) from fastapi._compat.shared import is_bytes_sequence_annotation from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict from pydantic.fields import FieldInfo -from .utils import needs_py310, needs_py_lt_314 +from .utils import needs_py310 def test_model_field_default_required(): @@ -26,35 +22,6 @@ def test_model_field_default_required(): assert field.default is Undefined -@needs_py_lt_314 -def test_v1_plain_validator_function(): - from fastapi._compat import v1 - - # For coverage - def func(v): # pragma: no cover - return v - - result = v1.with_info_plain_validator_function(func) - assert result == {} - - -def test_is_model_field(): - # For coverage - from fastapi._compat import _is_model_field - - assert not _is_model_field(str) - - -def test_get_model_config(): - # For coverage in Pydantic v2 - class Foo(BaseModel): - model_config = ConfigDict(from_attributes=True) - - foo = Foo() - config = _get_model_config(foo) - assert config == {"from_attributes": True} - - def test_complex(): app = FastAPI() @@ -165,33 +132,3 @@ def test_serialize_sequence_value_with_none_first_in_union(): result = v2.serialize_sequence_value(field=field, value=["x", "y"]) assert result == ["x", "y"] assert isinstance(result, list) - - -@needs_py_lt_314 -def test_is_pv1_scalar_field(): - from fastapi._compat import v1 - - # For coverage - class Model(v1.BaseModel): - foo: Union[str, dict[str, Any]] - - fields = v1.get_model_fields(Model) - assert not is_scalar_field(fields[0]) - - -@needs_py_lt_314 -def test_get_model_fields_cached(): - from fastapi._compat import v1 - - class Model(may_v1.BaseModel): - foo: str - - non_cached_fields = v1.get_model_fields(Model) - non_cached_fields2 = v1.get_model_fields(Model) - cached_fields = get_cached_model_fields(Model) - cached_fields2 = get_cached_model_fields(Model) - for f1, f2 in zip(cached_fields, cached_fields2): - assert f1 is f2 - - assert non_cached_fields is not non_cached_fields2 - assert cached_fields is cached_fields2 diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py deleted file mode 100644 index 704b3f77a6..0000000000 --- a/tests/test_compat_params_v1.py +++ /dev/null @@ -1,1060 +0,0 @@ -import sys -import warnings -from typing import Optional - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from typing import Annotated - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.temp_pydantic_v1_params import ( - Body, - Cookie, - File, - Form, - Header, - Path, - Query, -) -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class Item(BaseModel): - name: str - price: float - description: Optional[str] = None - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/items/{item_id}") - def get_item_with_path( - item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)], - ): - return {"item_id": item_id} - - @app.get("/items/") - def get_items_with_query( - q: Annotated[ - Optional[str], - Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$"), - ] = None, - skip: Annotated[int, Query(ge=0)] = 0, - limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10, - ): - return {"q": q, "skip": skip, "limit": limit} - - @app.get("/users/") - def get_user_with_header( - x_custom: Annotated[Optional[str], Header()] = None, - x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None, - ): - return {"x_custom": x_custom, "x_token": x_token} - - @app.get("/cookies/") - def get_cookies( - session_id: Annotated[Optional[str], Cookie()] = None, - tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None, - ): - return {"session_id": session_id, "tracking_id": tracking_id} - - @app.post("/items/") - def create_item( - item: Annotated[ - Item, - Body( - examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}] - ), - ], - ): - return {"item": item} - - @app.post("/items-embed/") - def create_item_embed( - item: Annotated[Item, Body(embed=True)], - ): - return {"item": item} - - @app.put("/items/{item_id}") - def update_item( - item_id: Annotated[int, Path(ge=1)], - item: Annotated[Item, Body()], - importance: Annotated[int, Body(gt=0, le=10)], - ): - return {"item": item, "importance": importance} - - @app.post("/form-data/") - def submit_form( - username: Annotated[str, Form(min_length=3, max_length=50)], - password: Annotated[str, Form(min_length=8)], - email: Annotated[Optional[str], Form()] = None, - ): - return {"username": username, "password": password, "email": email} - - @app.post("/upload/") - def upload_file( - file: Annotated[bytes, File()], - description: Annotated[Optional[str], Form()] = None, - ): - return {"file_size": len(file), "description": description} - - @app.post("/upload-multiple/") - def upload_multiple_files( - files: Annotated[list[bytes], File()], - note: Annotated[str, Form()] = "", - ): - return { - "file_count": len(files), - "total_size": sum(len(f) for f in files), - "note": note, - } - - -client = TestClient(app) - - -# Path parameter tests -def test_path_param_valid(): - response = client.get("/items/50") - assert response.status_code == 200 - assert response.json() == {"item_id": 50} - - -def test_path_param_too_large(): - response = client.get("/items/1001") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["path", "item_id"] - - -def test_path_param_too_small(): - response = client.get("/items/0") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["path", "item_id"] - - -# Query parameter tests -def test_query_params_valid(): - response = client.get("/items/?q=test search&skip=5&limit=20") - assert response.status_code == 200 - assert response.json() == {"q": "test search", "skip": 5, "limit": 20} - - -def test_query_params_defaults(): - response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == {"q": None, "skip": 0, "limit": 10} - - -def test_query_param_too_short(): - response = client.get("/items/?q=ab") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["query", "q"] - - -def test_query_param_invalid_pattern(): - response = client.get("/items/?q=test@#$") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["query", "q"] - - -def test_query_param_limit_too_large(): - response = client.get("/items/?limit=101") - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["query", "limit"] - - -# Header parameter tests -def test_header_params(): - response = client.get( - "/users/", - headers={"X-Custom": "Plumbus", "X-Token": "secret-token"}, - ) - assert response.status_code == 200 - assert response.json() == { - "x_custom": "Plumbus", - "x_token": "secret-token", - } - - -def test_header_underscore_conversion(): - response = client.get( - "/users/", - headers={"x-token": "secret-token-with-dash"}, - ) - assert response.status_code == 200 - assert response.json()["x_token"] == "secret-token-with-dash" - - -def test_header_params_none(): - response = client.get("/users/") - assert response.status_code == 200 - assert response.json() == {"x_custom": None, "x_token": None} - - -# Cookie parameter tests -def test_cookie_params(): - with TestClient(app) as test_client: - test_client.cookies.set("session_id", "abc123") - test_client.cookies.set("tracking_id", "1234567890abcdef") - response = test_client.get("/cookies/") - assert response.status_code == 200 - assert response.json() == { - "session_id": "abc123", - "tracking_id": "1234567890abcdef", - } - - -def test_cookie_tracking_id_too_short(): - with TestClient(app) as test_client: - test_client.cookies.set("tracking_id", "short") - response = test_client.get("/cookies/") - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["cookie", "tracking_id"], - "msg": "ensure this value has at least 10 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 10}, - } - ] - } - ) - - -def test_cookie_params_none(): - response = client.get("/cookies/") - assert response.status_code == 200 - assert response.json() == {"session_id": None, "tracking_id": None} - - -# Body parameter tests -def test_body_param(): - response = client.post( - "/items/", - json={"name": "Test Item", "price": 29.99, "description": "A test item"}, - ) - assert response.status_code == 200 - assert response.json() == { - "item": { - "name": "Test Item", - "price": 29.99, - "description": "A test item", - } - } - - -def test_body_param_minimal(): - response = client.post( - "/items/", - json={"name": "Minimal", "price": 9.99}, - ) - assert response.status_code == 200 - assert response.json() == { - "item": {"name": "Minimal", "price": 9.99, "description": None} - } - - -def test_body_param_missing_required(): - response = client.post( - "/items/", - json={"name": "Incomplete"}, - ) - assert response.status_code == 422 - error = response.json()["detail"][0] - assert error["loc"] == ["body", "price"] - - -def test_body_embed(): - response = client.post( - "/items-embed/", - json={"item": {"name": "Embedded", "price": 15.0}}, - ) - assert response.status_code == 200 - assert response.json() == { - "item": {"name": "Embedded", "price": 15.0, "description": None} - } - - -def test_body_embed_wrong_structure(): - response = client.post( - "/items-embed/", - json={"name": "Not Embedded", "price": 15.0}, - ) - assert response.status_code == 422 - - -# Multiple body parameters test -def test_multiple_body_params(): - response = client.put( - "/items/5", - json={ - "item": {"name": "Updated Item", "price": 49.99}, - "importance": 8, - }, - ) - assert response.status_code == 200 - assert response.json() == snapshot( - { - "item": {"name": "Updated Item", "price": 49.99, "description": None}, - "importance": 8, - } - ) - - -def test_multiple_body_params_importance_too_large(): - response = client.put( - "/items/5", - json={ - "item": {"name": "Item", "price": 10.0}, - "importance": 11, - }, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "importance"], - "msg": "ensure this value is less than or equal to 10", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 10}, - } - ] - } - ) - - -def test_multiple_body_params_importance_too_small(): - response = client.put( - "/items/5", - json={ - "item": {"name": "Item", "price": 10.0}, - "importance": 0, - }, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "importance"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ] - } - ) - - -# Form parameter tests -def test_form_data_valid(): - response = client.post( - "/form-data/", - data={ - "username": "testuser", - "password": "password123", - "email": "test@example.com", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "testuser", - "password": "password123", - "email": "test@example.com", - } - - -def test_form_data_optional_field(): - response = client.post( - "/form-data/", - data={"username": "testuser", "password": "password123"}, - ) - assert response.status_code == 200 - assert response.json() == { - "username": "testuser", - "password": "password123", - "email": None, - } - - -def test_form_data_username_too_short(): - response = client.post( - "/form-data/", - data={"username": "ab", "password": "password123"}, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "ensure this value has at least 3 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) - - -def test_form_data_password_too_short(): - response = client.post( - "/form-data/", - data={"username": "testuser", "password": "short"}, - ) - assert response.status_code == 422 - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "ensure this value has at least 8 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 8}, - } - ] - } - ) - - -# File upload tests -def test_upload_file(): - response = client.post( - "/upload/", - files={"file": ("test.txt", b"Hello, World!", "text/plain")}, - data={"description": "A test file"}, - ) - assert response.status_code == 200 - assert response.json() == { - "file_size": 13, - "description": "A test file", - } - - -def test_upload_file_without_description(): - response = client.post( - "/upload/", - files={"file": ("test.txt", b"Hello!", "text/plain")}, - ) - assert response.status_code == 200 - assert response.json() == { - "file_size": 6, - "description": None, - } - - -def test_upload_multiple_files(): - response = client.post( - "/upload-multiple/", - files=[ - ("files", ("file1.txt", b"Content 1", "text/plain")), - ("files", ("file2.txt", b"Content 2", "text/plain")), - ("files", ("file3.txt", b"Content 3", "text/plain")), - ], - data={"note": "Multiple files uploaded"}, - ) - assert response.status_code == 200 - assert response.json() == { - "file_count": 3, - "total_size": 27, - "note": "Multiple files uploaded", - } - - -def test_upload_multiple_files_empty_note(): - response = client.post( - "/upload-multiple/", - files=[ - ("files", ("file1.txt", b"Test", "text/plain")), - ], - ) - assert response.status_code == 200 - assert response.json()["file_count"] == 1 - assert response.json()["note"] == "" - - -# __repr__ tests -def test_query_repr(): - query_param = Query(default=None, min_length=3) - assert repr(query_param) == "Query(None)" - - -def test_body_repr(): - body_param = Body(default=None) - assert repr(body_param) == "Body(None)" - - -# Deprecation warning tests for regex parameter -def test_query_regex_deprecation_warning(): - with pytest.warns(FastAPIDeprecationWarning, match="`regex` has been deprecated"): - Query(regex="^test$") - - -def test_body_regex_deprecation_warning(): - 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(FastAPIDeprecationWarning, match="`example` has been deprecated"): - Query(example="test example") - - -def test_body_example_deprecation_warning(): - with pytest.warns(FastAPIDeprecationWarning, match="`example` has been deprecated"): - Body(example={"test": "example"}) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "summary": "Get Item With Path", - "operationId": "get_item_with_path_items__item_id__get", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": { - "title": "The ID of the item", - "minimum": 1, - "maximum": 1000, - "type": "integer", - }, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": { - "title": "Item Id", - "minimum": 1, - "type": "integer", - }, - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "title": "Body", - "allOf": [ - { - "$ref": "#/components/schemas/Body_update_item_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" - } - } - }, - }, - }, - }, - }, - "/items/": { - "get": { - "summary": "Get Items With Query", - "operationId": "get_items_with_query_items__get", - "parameters": [ - { - "name": "q", - "in": "query", - "required": False, - "schema": { - "title": "Q", - "maxLength": 50, - "minLength": 3, - "pattern": "^[a-zA-Z0-9 ]+$", - "type": "string", - }, - }, - { - "name": "skip", - "in": "query", - "required": False, - "schema": { - "title": "Skip", - "default": 0, - "minimum": 0, - "type": "integer", - }, - }, - { - "name": "limit", - "in": "query", - "required": False, - "schema": { - "title": "Limit", - "default": 10, - "minimum": 1, - "maximum": 100, - "examples": [5], - "type": "integer", - }, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "title": "Item", - "examples": [ - { - "name": "Foo", - "price": 35.4, - "description": "The Foo item", - } - ], - "allOf": [ - {"$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" - } - } - }, - }, - }, - }, - }, - "/users/": { - "get": { - "summary": "Get User With Header", - "operationId": "get_user_with_header_users__get", - "parameters": [ - { - "name": "x-custom", - "in": "header", - "required": False, - "schema": {"title": "X-Custom", "type": "string"}, - }, - { - "name": "x-token", - "in": "header", - "required": False, - "schema": {"title": "X-Token", "type": "string"}, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookies/": { - "get": { - "summary": "Get Cookies", - "operationId": "get_cookies_cookies__get", - "parameters": [ - { - "name": "session_id", - "in": "cookie", - "required": False, - "schema": {"title": "Session Id", "type": "string"}, - }, - { - "name": "tracking_id", - "in": "cookie", - "required": False, - "schema": { - "title": "Tracking Id", - "minLength": 10, - "type": "string", - }, - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items-embed/": { - "post": { - "summary": "Create Item Embed", - "operationId": "create_item_embed_items_embed__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" - } - ], - "title": "Body", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/form-data/": { - "post": { - "summary": "Submit Form", - "operationId": "submit_form_form_data__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_submit_form_form_data__post" - } - ], - "title": "Body", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/upload/": { - "post": { - "summary": "Upload File", - "operationId": "upload_file_upload__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_upload_file_upload__post" - } - ], - "title": "Body", - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/upload-multiple/": { - "post": { - "summary": "Upload Multiple Files", - "operationId": "upload_multiple_files_upload_multiple__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" - } - ], - "title": "Body", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_item_embed_items_embed__post": { - "properties": { - "item": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - } - }, - "type": "object", - "required": ["item"], - "title": "Body_create_item_embed_items_embed__post", - }, - "Body_submit_form_form_data__post": { - "properties": { - "username": { - "type": "string", - "maxLength": 50, - "minLength": 3, - "title": "Username", - }, - "password": { - "type": "string", - "minLength": 8, - "title": "Password", - }, - "email": {"type": "string", "title": "Email"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "Body_submit_form_form_data__post", - }, - "Body_update_item_items__item_id__put": { - "properties": { - "item": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - }, - "importance": { - "type": "integer", - "maximum": 10.0, - "exclusiveMinimum": 0.0, - "title": "Importance", - }, - }, - "type": "object", - "required": ["item", "importance"], - "title": "Body_update_item_items__item_id__put", - }, - "Body_upload_file_upload__post": { - "properties": { - "file": { - "type": "string", - "format": "binary", - "title": "File", - }, - "description": {"type": "string", "title": "Description"}, - }, - "type": "object", - "required": ["file"], - "title": "Body_upload_file_upload__post", - }, - "Body_upload_multiple_files_upload_multiple__post": { - "properties": { - "files": { - "items": {"type": "string", "format": "binary"}, - "type": "array", - "title": "Files", - }, - "note": {"type": "string", "title": "Note", "default": ""}, - }, - "type": "object", - "required": ["files"], - "title": "Body_upload_multiple_files_upload_multiple__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "price": {"type": "number", "title": "Price"}, - "description": {"type": "string", "title": "Description"}, - }, - "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_datetime_custom_encoder.py b/tests/test_datetime_custom_encoder.py index 56b6780f04..f154ede029 100644 --- a/tests/test_datetime_custom_encoder.py +++ b/tests/test_datetime_custom_encoder.py @@ -1,12 +1,9 @@ -import warnings from datetime import datetime, timezone from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1 - def test_pydanticv2(): from pydantic import field_serializer @@ -29,34 +26,3 @@ def test_pydanticv2(): with client: response = client.get("/model") assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_pydanticv1(): - from pydantic import v1 - - class ModelWithDatetimeField(v1.BaseModel): - dt_field: datetime - - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } - - app = FastAPI() - model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/model", response_model=ModelWithDatetimeField) - def get_model(): - return model - - client = TestClient(app) - with client: - response = client.get("/model") - assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} diff --git a/tests/test_filter_pydantic_sub_model/__init__.py b/tests/test_filter_pydantic_sub_model/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py deleted file mode 100644 index d6f2ce7d2d..0000000000 --- a/tests/test_filter_pydantic_sub_model/app_pv1.py +++ /dev/null @@ -1,45 +0,0 @@ -import warnings -from typing import Optional - -from fastapi import Depends, FastAPI -from pydantic.v1 import BaseModel, validator - -app = FastAPI() - - -class ModelB(BaseModel): - username: str - - -class ModelC(ModelB): - password: str - - -class ModelA(BaseModel): - name: str - description: Optional[str] = None - model_b: ModelB - tags: dict[str, str] = {} - - @validator("name") - def lower_username(cls, name: str, values): - if not name.endswith("A"): - raise ValueError("name must end in A") - return name - - -async def get_model_c() -> ModelC: - return ModelC(username="test-user", password="test-password") - - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/model/{name}", response_model=ModelA) - async def get_model_a(name: str, model_c=Depends(get_model_c)): - return { - "name": name, - "description": "model-a-desc", - "model_b": model_c, - "tags": {"key1": "value1", "key2": "value2"}, - } diff --git a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py deleted file mode 100644 index b464b4f572..0000000000 --- a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py +++ /dev/null @@ -1,146 +0,0 @@ -import pytest -from fastapi.exceptions import ResponseValidationError -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from ..utils import needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from .app_pv1 import app - - client = TestClient(app) - return client - - -@needs_pydanticv1 -def test_filter_sub_model(client: TestClient): - response = client.get("/model/modelA") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "modelA", - "description": "model-a-desc", - "model_b": {"username": "test-user"}, - "tags": {"key1": "value1", "key2": "value2"}, - } - - -@needs_pydanticv1 -def test_validator_is_cloned(client: TestClient): - with pytest.raises(ResponseValidationError) as err: - client.get("/model/modelX") - assert err.value.errors() == [ - { - "loc": ("response", "name"), - "msg": "name must end in A", - "type": "value_error", - } - ] - - -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/model/{name}": { - "get": { - "summary": "Get Model A", - "operationId": "get_model_a_model__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelA" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "ModelA": { - "title": "ModelA", - "required": ["name", "model_b"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "model_b": {"$ref": "#/components/schemas/ModelB"}, - "tags": { - "additionalProperties": {"type": "string"}, - "type": "object", - "title": "Tags", - "default": {}, - }, - }, - }, - "ModelB": { - "title": "ModelB", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - ) diff --git a/tests/test_get_model_definitions_formfeed_escape.py b/tests/test_get_model_definitions_formfeed_escape.py index dee5955544..eb7939b69a 100644 --- a/tests/test_get_model_definitions_formfeed_escape.py +++ b/tests/test_get_model_definitions_formfeed_escape.py @@ -1,25 +1,12 @@ -import warnings - import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot -from .utils import needs_pydanticv1 - -@pytest.fixture( - name="client", - params=[ - pytest.param("pydantic-v1", marks=needs_pydanticv1), - "pydantic-v2", - ], -) -def client_fixture(request: pytest.FixtureRequest) -> TestClient: - if request.param == "pydantic-v1": - from pydantic.v1 import BaseModel - else: - from pydantic import BaseModel +@pytest.fixture(name="client") +def client_fixture() -> TestClient: + from pydantic import BaseModel class Address(BaseModel): """ @@ -38,28 +25,12 @@ def client_fixture(request: pytest.FixtureRequest) -> TestClient: app = FastAPI() - if request.param == "pydantic-v1": - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.get("/facilities/{facility_id}") - def get_facility(facility_id: str) -> Facility: - return Facility( - id=facility_id, - address=Address( - line_1="123 Main St", city="Anytown", state_province="CA" - ), - ) - else: - - @app.get("/facilities/{facility_id}") - def get_facility(facility_id: str) -> Facility: - return Facility( - id=facility_id, - address=Address( - line_1="123 Main St", city="Anytown", state_province="CA" - ), - ) + @app.get("/facilities/{facility_id}") + def get_facility(facility_id: str) -> Facility: + return Facility( + id=facility_id, + address=Address(line_1="123 Main St", city="Anytown", state_province="CA"), + ) client = TestClient(app) return client diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py index 7f29fe33ed..8cf8952f92 100644 --- a/tests/test_inherited_custom_class.py +++ b/tests/test_inherited_custom_class.py @@ -5,8 +5,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv1 - class MyUuid: def __init__(self, uuid_string: str): @@ -67,46 +65,3 @@ def test_pydanticv2(): assert response_pydantic.json() == { "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_pydanticv1(): - from pydantic import v1 - - app = FastAPI() - - @app.get("/fast_uuid") - def return_fast_uuid(): - asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") - assert isinstance(asyncpg_uuid, uuid.UUID) - assert type(asyncpg_uuid) is not uuid.UUID - with pytest.raises(TypeError): - vars(asyncpg_uuid) - return {"fast_uuid": asyncpg_uuid} - - class SomeCustomClass(v1.BaseModel): - class Config: - arbitrary_types_allowed = True - json_encoders = {uuid.UUID: str} - - a_uuid: MyUuid - - @app.get("/get_custom_class") - def return_some_user(): - # Test that the fix also works for custom pydantic classes - return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) - - client = TestClient(app) - - with client: - response_simple = client.get("/fast_uuid") - response_pydantic = client.get("/get_custom_class") - - assert response_simple.json() == { - "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51" - } - - assert response_pydantic.json() == { - "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" - } diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 81bf94ece0..4528dff440 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,3 +1,4 @@ +import warnings from collections import deque from dataclasses import dataclass from datetime import datetime, timezone @@ -5,15 +6,14 @@ from decimal import Decimal from enum import Enum from math import isinf, isnan from pathlib import PurePath, PurePosixPath, PureWindowsPath -from typing import Optional +from typing import Optional, TypedDict import pytest from fastapi._compat import Undefined from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import PydanticV1NotSupportedError from pydantic import BaseModel, Field, ValidationError -from .utils import needs_pydanticv1 - class Person: def __init__(self, name: str): @@ -156,29 +156,17 @@ def test_encode_custom_json_encoders_model_pydanticv2(): assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_encode_custom_json_encoders_model_pydanticv1(): - from pydantic import v1 +def test_json_encoder_error_with_pydanticv1(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 - class ModelWithCustomEncoder(v1.BaseModel): - dt_field: datetime + class ModelV1(v1.BaseModel): + name: str - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } - - class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): - class Config: - pass - - model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) - assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} - subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) - assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + data = ModelV1(name="test") + with pytest.raises(PydanticV1NotSupportedError): + jsonable_encoder(data) def test_encode_model_with_config(): @@ -214,25 +202,27 @@ def test_encode_model_with_default(): } -@needs_pydanticv1 def test_custom_encoders(): - from pydantic import v1 - class safe_datetime(datetime): pass - class MyModel(v1.BaseModel): + class MyDict(TypedDict): dt_field: safe_datetime - instance = MyModel(dt_field=safe_datetime.now()) + instance = MyDict(dt_field=safe_datetime.now()) encoded_instance = jsonable_encoder( instance, custom_encoder={safe_datetime: lambda o: o.strftime("%H:%M:%S")} ) - assert encoded_instance["dt_field"] == instance.dt_field.strftime("%H:%M:%S") + assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S") + + encoded_instance = jsonable_encoder( + instance, custom_encoder={datetime: lambda o: o.strftime("%H:%M:%S")} + ) + assert encoded_instance["dt_field"] == instance["dt_field"].strftime("%H:%M:%S") encoded_instance2 = jsonable_encoder(instance) - assert encoded_instance2["dt_field"] == instance.dt_field.isoformat() + assert encoded_instance2["dt_field"] == instance["dt_field"].isoformat() def test_custom_enum_encoders(): @@ -287,17 +277,6 @@ def test_encode_pure_path(): assert jsonable_encoder({"path": test_path}) == {"path": str(test_path)} -@needs_pydanticv1 -def test_encode_root(): - from pydantic import v1 - - class ModelWithRoot(v1.BaseModel): - __root__: str - - model = ModelWithRoot(__root__="Foo") - assert jsonable_encoder(model) == "Foo" - - def test_decimal_encoder_float(): data = {"value": Decimal(1.23)} assert jsonable_encoder(data) == {"value": 1.23} diff --git a/tests/test_pydantic_v1_deprecation_warnings.py b/tests/test_pydantic_v1_deprecation_warnings.py deleted file mode 100644 index 89ca6a8658..0000000000 --- a/tests/test_pydantic_v1_deprecation_warnings.py +++ /dev/null @@ -1,99 +0,0 @@ -import sys - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient - - -def test_warns_pydantic_v1_model_in_endpoint_param() -> None: - class ParamModelV1(BaseModel): - name: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*Please update the param data:", - ): - - @app.post("/param") - def endpoint(data: ParamModelV1): - return data - - client = TestClient(app) - response = client.post("/param", json={"name": "test"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "test"} - - -def test_warns_pydantic_v1_model_in_return_type() -> None: - class ReturnModelV1(BaseModel): - name: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*Please update the response model", - ): - - @app.get("/return") - def endpoint() -> ReturnModelV1: - return ReturnModelV1(name="test") - - client = TestClient(app) - response = client.get("/return") - assert response.status_code == 200, response.text - assert response.json() == {"name": "test"} - - -def test_warns_pydantic_v1_model_in_response_model() -> None: - class ResponseModelV1(BaseModel): - name: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*Please update the response model", - ): - - @app.get("/response-model", response_model=ResponseModelV1) - def endpoint(): - return {"name": "test"} - - client = TestClient(app) - response = client.get("/response-model") - assert response.status_code == 200, response.text - assert response.json() == {"name": "test"} - - -def test_warns_pydantic_v1_model_in_additional_responses_model() -> None: - class ErrorModelV1(BaseModel): - detail: str - - app = FastAPI() - - with pytest.warns( - FastAPIDeprecationWarning, - match=r"pydantic\.v1 is deprecated.*In responses=\{\}, please update", - ): - - @app.get( - "/responses", response_model=None, responses={400: {"model": ErrorModelV1}} - ) - def endpoint(): - return {"ok": True} - - client = TestClient(app) - response = client.get("/responses") - assert response.status_code == 200, response.text - assert response.json() == {"ok": True} diff --git a/tests/test_pydantic_v1_error.py b/tests/test_pydantic_v1_error.py new file mode 100644 index 0000000000..13229a3137 --- /dev/null +++ b/tests/test_pydantic_v1_error.py @@ -0,0 +1,97 @@ +import sys +import warnings +from typing import Union + +import pytest + +from tests.utils import skip_module_if_py_gte_314 + +if sys.version_info >= (3, 14): + skip_module_if_py_gte_314() + +from fastapi import FastAPI +from fastapi.exceptions import PydanticV1NotSupportedError + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic.v1 import BaseModel + + +def test_raises_pydantic_v1_model_in_endpoint_param() -> None: + class ParamModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/param") + def endpoint(data: ParamModelV1): # pragma: no cover + return data + + +def test_raises_pydantic_v1_model_in_return_type() -> None: + class ReturnModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get("/return") + def endpoint() -> ReturnModelV1: # pragma: no cover + return ReturnModelV1(name="test") + + +def test_raises_pydantic_v1_model_in_response_model() -> None: + class ResponseModelV1(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get("/response-model", response_model=ResponseModelV1) + def endpoint(): # pragma: no cover + return {"name": "test"} + + +def test_raises_pydantic_v1_model_in_additional_responses_model() -> None: + class ErrorModelV1(BaseModel): + detail: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.get( + "/responses", response_model=None, responses={400: {"model": ErrorModelV1}} + ) + def endpoint(): # pragma: no cover + return {"ok": True} + + +def test_raises_pydantic_v1_model_in_union() -> None: + class ModelV1A(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/union") + def endpoint(data: Union[dict, ModelV1A]): # pragma: no cover + return data + + +def test_raises_pydantic_v1_model_in_sequence() -> None: + class ModelV1A(BaseModel): + name: str + + app = FastAPI() + + with pytest.raises(PydanticV1NotSupportedError): + + @app.post("/sequence") + def endpoint(data: list[ModelV1A]): # pragma: no cover + return data diff --git a/tests/test_pydantic_v1_v2_01.py b/tests/test_pydantic_v1_v2_01.py deleted file mode 100644 index 4868e5d223..0000000000 --- a/tests/test_pydantic_v1_v2_01.py +++ /dev/null @@ -1,439 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/simple-model") - def handle_simple_model(data: SubItem) -> SubItem: - return data - - @app.post("/simple-model-filter", response_model=SubItem) - def handle_simple_model_filter(data: SubItem) -> Any: - extended_data = data.dict() - extended_data.update({"secret_price": 42}) - return extended_data - - @app.post("/item") - def handle_item(data: Item) -> Item: - return data - - @app.post("/item-filter", response_model=Item) - def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data - - -client = TestClient(app) - - -def test_old_simple_model(): - response = client.post( - "/simple-model", - json={"name": "Foo"}, - ) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo"} - - -def test_old_simple_model_validation_error(): - response = client.post( - "/simple-model", - json={"wrong_name": "Foo"}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_old_simple_model_filter(): - response = client.post( - "/simple-model-filter", - json={"name": "Foo"}, - ) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo"} - - -def test_item_model(): - response = client.post( - "/item", - json={ - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - } - - -def test_item_model_minimal(): - response = client.post( - "/item", - json={"title": "Minimal Item", "size": 50, "sub": {"name": "SubMin"}}, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "Minimal Item", - "size": 50, - "description": None, - "sub": {"name": "SubMin"}, - "multi": [], - } - - -def test_item_model_validation_errors(): - response = client.post( - "/item", - json={"title": "Missing fields"}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 2 - assert { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - assert { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - - -def test_item_model_nested_validation_error(): - response = client.post( - "/item", - json={"title": "Test Item", "size": 100, "sub": {"wrong_field": "test"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_item_model_invalid_type(): - response = client.post( - "/item", - json={"title": "Test Item", "size": "not_a_number", "sub": {"name": "SubItem"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_item_filter(): - response = client.post( - "/item-filter", - json={ - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == { - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - } - assert "secret_data" not in result - assert "internal_id" not in result - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/simple-model": { - "post": { - "summary": "Handle Simple Model", - "operationId": "handle_simple_model_simple_model_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/SubItem"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/simple-model-filter": { - "post": { - "summary": "Handle Simple Model Filter", - "operationId": "handle_simple_model_filter_simple_model_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/SubItem"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item": { - "post": { - "summary": "Handle Item", - "operationId": "handle_item_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "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" - } - } - }, - }, - }, - } - }, - "/item-filter": { - "post": { - "summary": "Handle Item Filter", - "operationId": "handle_item_filter_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "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" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "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_pydantic_v1_v2_list.py b/tests/test_pydantic_v1_v2_list.py deleted file mode 100644 index 108f231faa..0000000000 --- a/tests/test_pydantic_v1_v2_list.py +++ /dev/null @@ -1,682 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -app = FastAPI() - - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/item") - def handle_item(data: Item) -> list[Item]: - return [data, data] - - @app.post("/item-filter", response_model=list[Item]) - def handle_item_filter(data: Item) -> Any: - extended_data = data.dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return [extended_data, extended_data] - - @app.post("/item-list") - def handle_item_list(data: list[Item]) -> Item: - if data: - return data[0] - return Item(title="", size=0, sub=SubItem(name="")) - - @app.post("/item-list-filter", response_model=Item) - def handle_item_list_filter(data: list[Item]) -> Any: - if data: - extended_data = data[0].dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return extended_data - return Item(title="", size=0, sub=SubItem(name="")) - - @app.post("/item-list-to-list") - def handle_item_list_to_list(data: list[Item]) -> list[Item]: - return data - - @app.post("/item-list-to-list-filter", response_model=list[Item]) - def handle_item_list_to_list_filter(data: list[Item]) -> Any: - if data: - extended_data = data[0].dict() - extended_data.update({"secret_data": "classified", "internal_id": 12345}) - extended_data["sub"].update({"internal_id": 67890}) - return [extended_data, extended_data] - return [] - - -client = TestClient(app) - - -def test_item_to_list(): - response = client.post( - "/item", - json={ - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Test Item", - "size": 100, - "description": "This is a test item", - "sub": {"name": "SubItem1"}, - "multi": [{"name": "Multi1"}, {"name": "Multi2"}], - } - - -def test_item_to_list_filter(): - response = client.post( - "/item-filter", - json={ - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Filtered Item", - "size": 200, - "description": "Test filtering", - "sub": {"name": "SubFiltered"}, - "multi": [], - } - # Verify secret fields are filtered out - assert "secret_data" not in item - assert "internal_id" not in item - assert "internal_id" not in item["sub"] - - -def test_list_to_item(): - response = client.post( - "/item-list", - json=[ - {"title": "First Item", "size": 50, "sub": {"name": "First Sub"}}, - {"title": "Second Item", "size": 75, "sub": {"name": "Second Sub"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "First Item", - "size": 50, - "description": None, - "sub": {"name": "First Sub"}, - "multi": [], - } - - -def test_list_to_item_empty(): - response = client.post( - "/item-list", - json=[], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_list_to_item_filter(): - response = client.post( - "/item-list-filter", - json=[ - { - "title": "First Item", - "size": 100, - "sub": {"name": "First Sub"}, - "multi": [{"name": "Multi1"}], - }, - {"title": "Second Item", "size": 200, "sub": {"name": "Second Sub"}}, - ], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == { - "title": "First Item", - "size": 100, - "description": None, - "sub": {"name": "First Sub"}, - "multi": [{"name": "Multi1"}], - } - # Verify secret fields are filtered out - assert "secret_data" not in result - assert "internal_id" not in result - - -def test_list_to_item_filter_no_data(): - response = client.post("/item-list-filter", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_list_to_list(): - input_items = [ - {"title": "Item 1", "size": 10, "sub": {"name": "Sub1"}}, - { - "title": "Item 2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - {"title": "Item 3", "size": 30, "sub": {"name": "Sub3"}}, - ] - response = client.post( - "/item-list-to-list", - json=input_items, - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 3 - assert result[0] == { - "title": "Item 1", - "size": 10, - "description": None, - "sub": {"name": "Sub1"}, - "multi": [], - } - assert result[1] == { - "title": "Item 2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - } - assert result[2] == { - "title": "Item 3", - "size": 30, - "description": None, - "sub": {"name": "Sub3"}, - "multi": [], - } - - -def test_list_to_list_filter(): - response = client.post( - "/item-list-to-list-filter", - json=[{"title": "Item 1", "size": 100, "sub": {"name": "Sub1"}}], - ) - assert response.status_code == 200, response.text - result = response.json() - assert isinstance(result, list) - assert len(result) == 2 - for item in result: - assert item == { - "title": "Item 1", - "size": 100, - "description": None, - "sub": {"name": "Sub1"}, - "multi": [], - } - # Verify secret fields are filtered out - assert "secret_data" not in item - assert "internal_id" not in item - - -def test_list_to_list_filter_no_data(): - response = client.post( - "/item-list-to-list-filter", - json=[], - ) - assert response.status_code == 200, response.text - assert response.json() == [] - - -def test_list_validation_error(): - response = client.post( - "/item-list", - json=[ - {"title": "Valid Item", "size": 100, "sub": {"name": "Sub1"}}, - { - "title": "Invalid Item" - # Missing required fields: size and sub - }, - ], - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 2 - assert { - "loc": ["body", 1, "size"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - assert { - "loc": ["body", 1, "sub"], - "msg": "field required", - "type": "value_error.missing", - } in error_detail - - -def test_list_nested_validation_error(): - response = client.post( - "/item-list", - json=[ - {"title": "Item with bad sub", "size": 100, "sub": {"wrong_field": "value"}} - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 0, "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_list_type_validation_error(): - response = client.post( - "/item-list", - json=[{"title": "Item", "size": "not_a_number", "sub": {"name": "Sub"}}], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 0, "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_invalid_list_structure(): - response = client.post( - "/item-list", - json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ] - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/item": { - "post": { - "summary": "Handle Item", - "operationId": "handle_item_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item Item Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-filter": { - "post": { - "summary": "Handle Item Filter", - "operationId": "handle_item_filter_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item Filter Item Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list": { - "post": { - "summary": "Handle Item List", - "operationId": "handle_item_list_item_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "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" - } - } - }, - }, - }, - } - }, - "/item-list-filter": { - "post": { - "summary": "Handle Item List Filter", - "operationId": "handle_item_list_filter_item_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "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" - } - } - }, - }, - }, - } - }, - "/item-list-to-list": { - "post": { - "summary": "Handle Item List To List", - "operationId": "handle_item_list_to_list_item_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item List To List Item List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/item-list-to-list-filter": { - "post": { - "summary": "Handle Item List To List Filter", - "operationId": "handle_item_list_to_list_filter_item_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle Item List To List Filter Item List To List Filter Post", - } - } - }, - }, - "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": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "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_pydantic_v1_v2_mixed.py b/tests/test_pydantic_v1_v2_mixed.py deleted file mode 100644 index 895835a4c0..0000000000 --- a/tests/test_pydantic_v1_v2_mixed.py +++ /dev/null @@ -1,1408 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot -from pydantic import BaseModel as NewBaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -class NewSubItem(NewBaseModel): - new_sub_name: str - - -class NewItem(NewBaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: NewSubItem - new_multi: list[NewSubItem] = [] - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/v1-to-v2/item") - def handle_v1_item_to_v2(data: Item) -> NewItem: - return NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - - @app.post("/v1-to-v2/item-filter", response_model=NewItem) - def handle_v1_item_to_v2_filter(data: Item) -> Any: - result = { - "new_title": data.title, - "new_size": data.size, - "new_description": data.description, - "new_sub": { - "new_sub_name": data.sub.name, - "new_sub_secret": "sub_hidden", - }, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} - for s in data.multi - ], - "secret": "hidden_v1_to_v2", - } - return result - - @app.post("/v2-to-v1/item") - def handle_v2_item_to_v1(data: NewItem) -> Item: - return Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - @app.post("/v2-to-v1/item-filter", response_model=Item) - def handle_v2_item_to_v1_filter(data: NewItem) -> Any: - result = { - "title": data.new_title, - "size": data.new_size, - "description": data.new_description, - "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} - for s in data.new_multi - ], - "secret": "hidden_v2_to_v1", - } - return result - - @app.post("/v1-to-v2/item-to-list") - def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]: - converted = NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - return [converted, converted] - - @app.post("/v1-to-v2/list-to-list") - def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]: - result = [] - for item in data: - result.append( - NewItem( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=NewSubItem(new_sub_name=item.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], - ) - ) - return result - - @app.post("/v1-to-v2/list-to-list-filter", response_model=list[NewItem]) - def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any: - result = [] - for item in data: - converted = { - "new_title": item.title, - "new_size": item.size, - "new_description": item.description, - "new_sub": { - "new_sub_name": item.sub.name, - "new_sub_secret": "sub_hidden", - }, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} - for s in item.multi - ], - "secret": "hidden_v2_to_v1", - } - result.append(converted) - return result - - @app.post("/v1-to-v2/list-to-item") - def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem: - if data: - item = data[0] - return NewItem( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=NewSubItem(new_sub_name=item.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi], - ) - return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name="")) - - @app.post("/v2-to-v1/item-to-list") - def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]: - converted = Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - return [converted, converted] - - @app.post("/v2-to-v1/list-to-list") - def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]: - result = [] - for item in data: - result.append( - Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=SubItem(name=item.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - ) - return result - - @app.post("/v2-to-v1/list-to-list-filter", response_model=list[Item]) - def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any: - result = [] - for item in data: - converted = { - "title": item.new_title, - "size": item.new_size, - "description": item.new_description, - "sub": { - "name": item.new_sub.new_sub_name, - "sub_secret": "sub_hidden", - }, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} - for s in item.new_multi - ], - "secret": "hidden_v2_to_v1", - } - result.append(converted) - return result - - @app.post("/v2-to-v1/list-to-item") - def handle_v2_list_to_v1_item(data: list[NewItem]) -> Item: - if data: - item = data[0] - return Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=SubItem(name=item.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - return Item(title="", size=0, sub=SubItem(name="")) - - -client = TestClient(app) - - -def test_v1_to_v2_item(): - response = client.post( - "/v1-to-v2/item", - json={ - "title": "Old Item", - "size": 100, - "description": "V1 description", - "sub": {"name": "V1 Sub"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Old Item", - "new_size": 100, - "new_description": "V1 description", - "new_sub": {"new_sub_name": "V1 Sub"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - } - - -def test_v1_to_v2_item_minimal(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Minimal", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "MinSub"}, - "new_multi": [], - } - - -def test_v1_to_v2_item_filter(): - response = client.post( - "/v1-to-v2/item-filter", - json={ - "title": "Filtered Item", - "size": 50, - "sub": {"name": "Sub"}, - "multi": [{"name": "Multi1"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - { - "new_title": "Filtered Item", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "Sub"}, - "new_multi": [{"new_sub_name": "Multi1"}], - } - ) - # Verify secret fields are filtered out - assert "secret" not in result - assert "new_sub_secret" not in result["new_sub"] - assert "new_sub_secret" not in result["new_multi"][0] - - -def test_v2_to_v1_item(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "New Item", - "new_size": 200, - "new_description": "V2 description", - "new_sub": {"new_sub_name": "V2 Sub"}, - "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "New Item", - "size": 200, - "description": "V2 description", - "sub": {"name": "V2 Sub"}, - "multi": [{"name": "N1"}, {"name": "N2"}], - } - - -def test_v2_to_v1_item_minimal(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "MinimalNew", - "new_size": 75, - "new_sub": {"new_sub_name": "MinNewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "MinimalNew", - "size": 75, - "description": None, - "sub": {"name": "MinNewSub"}, - "multi": [], - } - - -def test_v2_to_v1_item_filter(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Filtered New", - "new_size": 75, - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - { - "title": "Filtered New", - "size": 75, - "description": None, - "sub": {"name": "NewSub"}, - "multi": [], - } - ) - # Verify secret fields are filtered out - assert "secret" not in result - assert "sub_secret" not in result["sub"] - - -def test_v1_item_to_v2_list(): - response = client.post( - "/v1-to-v2/item-to-list", - json={ - "title": "Single to List", - "size": 150, - "description": "Convert to list", - "sub": {"name": "Sub1"}, - "multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == [ - { - "new_title": "Single to List", - "new_size": 150, - "new_description": "Convert to list", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Single to List", - "new_size": 150, - "new_description": "Convert to list", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - ] - - -def test_v1_list_to_v2_list(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Item1", "size": 10, "sub": {"name": "Sub1"}}, - { - "title": "Item2", - "size": 20, - "description": "Second item", - "sub": {"name": "Sub2"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - {"title": "Item3", "size": 30, "sub": {"name": "Sub3"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == [ - { - "new_title": "Item1", - "new_size": 10, - "new_description": None, - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Item2", - "new_size": 20, - "new_description": "Second item", - "new_sub": {"new_sub_name": "Sub2"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - }, - { - "new_title": "Item3", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "Sub3"}, - "new_multi": [], - }, - ] - - -def test_v1_list_to_v2_list_filter(): - response = client.post( - "/v1-to-v2/list-to-list-filter", - json=[{"title": "FilterMe", "size": 30, "sub": {"name": "SubF"}}], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - [ - { - "new_title": "FilterMe", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubF"}, - "new_multi": [], - } - ] - ) - # Verify secret fields are filtered out - assert "secret" not in result[0] - assert "new_sub_secret" not in result[0]["new_sub"] - - -def test_v1_list_to_v2_item(): - response = client.post( - "/v1-to-v2/list-to-item", - json=[ - {"title": "First", "size": 100, "sub": {"name": "FirstSub"}}, - {"title": "Second", "size": 200, "sub": {"name": "SecondSub"}}, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "First", - "new_size": 100, - "new_description": None, - "new_sub": {"new_sub_name": "FirstSub"}, - "new_multi": [], - } - - -def test_v1_list_to_v2_item_empty(): - response = client.post("/v1-to-v2/list-to-item", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "", - "new_size": 0, - "new_description": None, - "new_sub": {"new_sub_name": ""}, - "new_multi": [], - } - - -def test_v2_item_to_v1_list(): - response = client.post( - "/v2-to-v1/item-to-list", - json={ - "new_title": "Single New", - "new_size": 250, - "new_description": "New to list", - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [ - { - "title": "Single New", - "size": 250, - "description": "New to list", - "sub": {"name": "NewSub"}, - "multi": [], - }, - { - "title": "Single New", - "size": 250, - "description": "New to list", - "sub": {"name": "NewSub"}, - "multi": [], - }, - ] - - -def test_v2_list_to_v1_list(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - {"new_title": "New1", "new_size": 15, "new_sub": {"new_sub_name": "NS1"}}, - { - "new_title": "New2", - "new_size": 25, - "new_description": "Second new", - "new_sub": {"new_sub_name": "NS2"}, - "new_multi": [{"new_sub_name": "NM1"}], - }, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == [ - { - "title": "New1", - "size": 15, - "description": None, - "sub": {"name": "NS1"}, - "multi": [], - }, - { - "title": "New2", - "size": 25, - "description": "Second new", - "sub": {"name": "NS2"}, - "multi": [{"name": "NM1"}], - }, - ] - - -def test_v2_list_to_v1_list_filter(): - response = client.post( - "/v2-to-v1/list-to-list-filter", - json=[ - { - "new_title": "FilterNew", - "new_size": 35, - "new_sub": {"new_sub_name": "NSF"}, - } - ], - ) - assert response.status_code == 200, response.text - result = response.json() - assert result == snapshot( - [ - { - "title": "FilterNew", - "size": 35, - "description": None, - "sub": {"name": "NSF"}, - "multi": [], - } - ] - ) - # Verify secret fields are filtered out - assert "secret" not in result[0] - assert "sub_secret" not in result[0]["sub"] - - -def test_v2_list_to_v1_item(): - response = client.post( - "/v2-to-v1/list-to-item", - json=[ - { - "new_title": "FirstNew", - "new_size": 300, - "new_sub": {"new_sub_name": "FNS"}, - }, - { - "new_title": "SecondNew", - "new_size": 400, - "new_sub": {"new_sub_name": "SNS"}, - }, - ], - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "FirstNew", - "size": 300, - "description": None, - "sub": {"name": "FNS"}, - "multi": [], - } - - -def test_v2_list_to_v1_item_empty(): - response = client.post("/v2-to-v1/list-to-item", json=[]) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_v1_to_v2_validation_error(): - response = client.post("/v1-to-v2/item", json={"title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v1_to_v2_nested_validation_error(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "sub", "name"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_v1_to_v2_type_validation_error(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_v2_to_v1_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={"new_title": "Missing fields"}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_size"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - { - "type": "missing", - "loc": ["body", "new_sub"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - ] - } - ) - - -def test_v2_to_v1_nested_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad sub", - "new_size": 200, - "new_sub": {"wrong_field": "value"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "Field required", - "input": {"wrong_field": "value"}, - } - ] - } - ) - - -def test_v1_list_validation_error(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Valid", "size": 10, "sub": {"name": "S"}}, - {"title": "Invalid"}, - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", 1, "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v2_list_validation_error(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - {"new_title": "Valid", "new_size": 10, "new_sub": {"new_sub_name": "NS"}}, - {"new_title": "Invalid"}, - ], - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", 1, "new_size"], - "msg": "Field required", - "input": {"new_title": "Invalid"}, - }, - { - "type": "missing", - "loc": ["body", 1, "new_sub"], - "msg": "Field required", - "input": {"new_title": "Invalid"}, - }, - ] - } - ) - - -def test_invalid_list_structure_v1(): - response = client.post( - "/v1-to-v2/list-to-list", - json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ] - } - ) - - -def test_invalid_list_structure_v2(): - response = client.post( - "/v2-to-v1/list-to-list", - json={ - "new_title": "Not a list", - "new_size": 100, - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "list_type", - "loc": ["body"], - "msg": "Input should be a valid list", - "input": { - "new_title": "Not a list", - "new_size": 100, - "new_sub": {"new_sub_name": "Sub"}, - }, - } - ] - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/item": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-filter": { - "post": { - "summary": "Handle V1 Item To V2 Filter", - "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "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" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-filter": { - "post": { - "summary": "Handle V2 Item To V1 Filter", - "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "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" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-to-list": { - "post": { - "summary": "Handle V1 Item To V2 List", - "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list": { - "post": { - "summary": "Handle V1 List To V2 List", - "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list-filter": { - "post": { - "summary": "Handle V1 List To V2 List Filter", - "operationId": "handle_v1_list_to_v2_list_filter_v1_to_v2_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List Filter V1 To V2 List To List Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-item": { - "post": { - "summary": "Handle V1 List To V2 Item", - "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewItem" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-to-list": { - "post": { - "summary": "Handle V2 Item To V1 List", - "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list": { - "post": { - "summary": "Handle V2 List To V1 List", - "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list-filter": { - "post": { - "summary": "Handle V2 List To V1 List Filter", - "operationId": "handle_v2_list_to_v1_list_filter_v2_to_v1_list_to_list_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List Filter V2 To V1 List To List Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-item": { - "post": { - "summary": "Handle V2 List To V1 Item", - "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/NewItem" - }, - "type": "array", - "title": "Data", - } - } - }, - "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" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "NewItem": { - "properties": { - "new_title": {"type": "string", "title": "New Title"}, - "new_size": {"type": "integer", "title": "New Size"}, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, - "new_multi": { - "items": {"$ref": "#/components/schemas/NewSubItem"}, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "NewItem", - }, - "NewSubItem": { - "properties": { - "new_sub_name": {"type": "string", "title": "New Sub Name"} - }, - "type": "object", - "required": ["new_sub_name"], - "title": "NewSubItem", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "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_pydantic_v1_v2_multifile/__init__.py b/tests/test_pydantic_v1_v2_multifile/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/test_pydantic_v1_v2_multifile/main.py b/tests/test_pydantic_v1_v2_multifile/main.py deleted file mode 100644 index 4180ec3bf5..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/main.py +++ /dev/null @@ -1,137 +0,0 @@ -import warnings - -from fastapi import FastAPI - -from . import modelsv1, modelsv2, modelsv2b - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/v1-to-v2/item") - def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item: - return modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) - - @app.post("/v2-to-v1/item") - def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item: - return modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - @app.post("/v1-to-v2/item-to-list") - def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]: - converted = modelsv2.Item( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=modelsv2.SubItem(new_sub_name=data.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], - ) - return [converted, converted] - - @app.post("/v1-to-v2/list-to-list") - def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]: - result = [] - for item in data: - result.append( - modelsv2.Item( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), - new_multi=[ - modelsv2.SubItem(new_sub_name=s.name) for s in item.multi - ], - ) - ) - return result - - @app.post("/v1-to-v2/list-to-item") - def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item: - if data: - item = data[0] - return modelsv2.Item( - new_title=item.title, - new_size=item.size, - new_description=item.description, - new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), - new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], - ) - return modelsv2.Item( - new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="") - ) - - @app.post("/v2-to-v1/item-to-list") - def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]: - converted = modelsv1.Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=modelsv1.SubItem(name=data.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - return [converted, converted] - - @app.post("/v2-to-v1/list-to-list") - def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]: - result = [] - for item in data: - result.append( - modelsv1.Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), - multi=[ - modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi - ], - ) - ) - return result - - @app.post("/v2-to-v1/list-to-item") - def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item: - if data: - item = data[0] - return modelsv1.Item( - title=item.new_title, - size=item.new_size, - description=item.new_description, - sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], - ) - return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name="")) - - @app.post("/v2-to-v1/same-name") - def handle_v2_same_name_to_v1( - item1: modelsv2.Item, item2: modelsv2b.Item - ) -> modelsv1.Item: - return modelsv1.Item( - title=item1.new_title, - size=item2.dup_size, - description=item1.new_description, - sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name), - multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi], - ) - - @app.post("/v2-to-v1/list-of-items-to-list-of-items") - def handle_v2_items_in_list_to_v1_item_in_list( - data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList] - ) -> list[modelsv1.ItemInList]: - item1 = data1[0] - item2 = data2[0] - return [ - modelsv1.ItemInList(name1=item1.name2), - modelsv1.ItemInList(name1=item2.dup_name2), - ] diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv1.py b/tests/test_pydantic_v1_v2_multifile/modelsv1.py deleted file mode 100644 index 0cc8de4559..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv1.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union - -from fastapi._compat.v1 import BaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -class ItemInList(BaseModel): - name1: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2.py b/tests/test_pydantic_v1_v2_multifile/modelsv2.py deleted file mode 100644 index d80b77e103..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv2.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union - -from pydantic import BaseModel - - -class SubItem(BaseModel): - new_sub_name: str - - -class Item(BaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: SubItem - new_multi: list[SubItem] = [] - - -class ItemInList(BaseModel): - name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py b/tests/test_pydantic_v1_v2_multifile/modelsv2b.py deleted file mode 100644 index e992bea2e1..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union - -from pydantic import BaseModel - - -class SubItem(BaseModel): - dup_sub_name: str - - -class Item(BaseModel): - dup_title: str - dup_size: int - dup_description: Union[str, None] = None - dup_sub: SubItem - dup_multi: list[SubItem] = [] - - -class ItemInList(BaseModel): - dup_name2: str diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py deleted file mode 100644 index 32d9019616..0000000000 --- a/tests/test_pydantic_v1_v2_multifile/test_multifile.py +++ /dev/null @@ -1,951 +0,0 @@ -import sys - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from .main import app - -client = TestClient(app) - - -def test_v1_to_v2_item(): - response = client.post( - "/v1-to-v2/item", - json={"title": "Test", "size": 10, "sub": {"name": "SubTest"}}, - ) - assert response.status_code == 200 - assert response.json() == { - "new_title": "Test", - "new_size": 10, - "new_description": None, - "new_sub": {"new_sub_name": "SubTest"}, - "new_multi": [], - } - - -def test_v2_to_v1_item(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "NewTest", - "new_size": 20, - "new_sub": {"new_sub_name": "NewSubTest"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "title": "NewTest", - "size": 20, - "description": None, - "sub": {"name": "NewSubTest"}, - "multi": [], - } - - -def test_v1_to_v2_item_to_list(): - response = client.post( - "/v1-to-v2/item-to-list", - json={"title": "ListTest", "size": 30, "sub": {"name": "SubListTest"}}, - ) - assert response.status_code == 200 - assert response.json() == [ - { - "new_title": "ListTest", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubListTest"}, - "new_multi": [], - }, - { - "new_title": "ListTest", - "new_size": 30, - "new_description": None, - "new_sub": {"new_sub_name": "SubListTest"}, - "new_multi": [], - }, - ] - - -def test_v1_to_v2_list_to_list(): - response = client.post( - "/v1-to-v2/list-to-list", - json=[ - {"title": "Item1", "size": 40, "sub": {"name": "Sub1"}}, - {"title": "Item2", "size": 50, "sub": {"name": "Sub2"}}, - ], - ) - assert response.status_code == 200 - assert response.json() == [ - { - "new_title": "Item1", - "new_size": 40, - "new_description": None, - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [], - }, - { - "new_title": "Item2", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "Sub2"}, - "new_multi": [], - }, - ] - - -def test_v1_to_v2_list_to_item(): - response = client.post( - "/v1-to-v2/list-to-item", - json=[ - {"title": "FirstItem", "size": 60, "sub": {"name": "FirstSub"}}, - {"title": "SecondItem", "size": 70, "sub": {"name": "SecondSub"}}, - ], - ) - assert response.status_code == 200 - assert response.json() == { - "new_title": "FirstItem", - "new_size": 60, - "new_description": None, - "new_sub": {"new_sub_name": "FirstSub"}, - "new_multi": [], - } - - -def test_v2_to_v1_item_to_list(): - response = client.post( - "/v2-to-v1/item-to-list", - json={ - "new_title": "ListNew", - "new_size": 80, - "new_sub": {"new_sub_name": "SubListNew"}, - }, - ) - assert response.status_code == 200 - assert response.json() == [ - { - "title": "ListNew", - "size": 80, - "description": None, - "sub": {"name": "SubListNew"}, - "multi": [], - }, - { - "title": "ListNew", - "size": 80, - "description": None, - "sub": {"name": "SubListNew"}, - "multi": [], - }, - ] - - -def test_v2_to_v1_list_to_list(): - response = client.post( - "/v2-to-v1/list-to-list", - json=[ - { - "new_title": "New1", - "new_size": 90, - "new_sub": {"new_sub_name": "NewSub1"}, - }, - { - "new_title": "New2", - "new_size": 100, - "new_sub": {"new_sub_name": "NewSub2"}, - }, - ], - ) - assert response.status_code == 200 - assert response.json() == [ - { - "title": "New1", - "size": 90, - "description": None, - "sub": {"name": "NewSub1"}, - "multi": [], - }, - { - "title": "New2", - "size": 100, - "description": None, - "sub": {"name": "NewSub2"}, - "multi": [], - }, - ] - - -def test_v2_to_v1_list_to_item(): - response = client.post( - "/v2-to-v1/list-to-item", - json=[ - { - "new_title": "FirstNew", - "new_size": 110, - "new_sub": {"new_sub_name": "FirstNewSub"}, - }, - { - "new_title": "SecondNew", - "new_size": 120, - "new_sub": {"new_sub_name": "SecondNewSub"}, - }, - ], - ) - assert response.status_code == 200 - assert response.json() == { - "title": "FirstNew", - "size": 110, - "description": None, - "sub": {"name": "FirstNewSub"}, - "multi": [], - } - - -def test_v1_to_v2_list_to_item_empty(): - response = client.post("/v1-to-v2/list-to-item", json=[]) - assert response.status_code == 200 - assert response.json() == { - "new_title": "", - "new_size": 0, - "new_description": None, - "new_sub": {"new_sub_name": ""}, - "new_multi": [], - } - - -def test_v2_to_v1_list_to_item_empty(): - response = client.post("/v2-to-v1/list-to-item", json=[]) - assert response.status_code == 200 - assert response.json() == { - "title": "", - "size": 0, - "description": None, - "sub": {"name": ""}, - "multi": [], - } - - -def test_v2_same_name_to_v1(): - response = client.post( - "/v2-to-v1/same-name", - json={ - "item1": { - "new_title": "Title1", - "new_size": 100, - "new_description": "Description1", - "new_sub": {"new_sub_name": "Sub1"}, - "new_multi": [{"new_sub_name": "Multi1"}], - }, - "item2": { - "dup_title": "Title2", - "dup_size": 200, - "dup_description": "Description2", - "dup_sub": {"dup_sub_name": "Sub2"}, - "dup_multi": [ - {"dup_sub_name": "Multi2a"}, - {"dup_sub_name": "Multi2b"}, - ], - }, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "title": "Title1", - "size": 200, - "description": "Description1", - "sub": {"name": "Sub1"}, - "multi": [{"name": "Multi2a"}, {"name": "Multi2b"}], - } - - -def test_v2_items_in_list_to_v1_item_in_list(): - response = client.post( - "/v2-to-v1/list-of-items-to-list-of-items", - json={ - "data1": [{"name2": "Item1"}, {"name2": "Item2"}], - "data2": [{"dup_name2": "Item3"}, {"dup_name2": "Item4"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [ - {"name1": "Item1"}, - {"name1": "Item3"}, - ] - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/item": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-to-list": { - "post": { - "summary": "Handle V1 Item To V2 List", - "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "type": "array", - "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-list": { - "post": { - "summary": "Handle V1 List To V2 List", - "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - }, - "type": "array", - "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/list-to-item": { - "post": { - "summary": "Handle V1 List To V2 Item", - "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-to-list": { - "post": { - "summary": "Handle V2 Item To V1 List", - "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-list": { - "post": { - "summary": "Handle V2 List To V1 List", - "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - }, - "type": "array", - "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-to-item": { - "post": { - "summary": "Handle V2 List To V1 Item", - "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "type": "array", - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/same-name": { - "post": { - "summary": "Handle V2 Same Name To V1", - "operationId": "handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/list-of-items-to-list-of-items": { - "post": { - "summary": "Handle V2 Items In List To V1 Item In List", - "operationId": "handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList" - }, - "type": "array", - "title": "Response Handle V2 Items In List To V1 Item In List V2 To V1 List Of Items To List Of Items Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": { - "properties": { - "data1": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList" - }, - "type": "array", - "title": "Data1", - }, - "data2": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList" - }, - "type": "array", - "title": "Data2", - }, - }, - "type": "object", - "required": ["data1", "data2"], - "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post", - }, - "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": { - "properties": { - "item1": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" - }, - "item2": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item" - }, - }, - "type": "object", - "required": ["item1", "item2"], - "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - {"type": "string"}, - {"type": "integer"}, - ] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": { - "type": "string", - "title": "Description", - }, - "sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem" - }, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { - "properties": {"name1": {"type": "string", "title": "Name1"}}, - "type": "object", - "required": ["name1"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input": { - "properties": { - "new_title": { - "type": "string", - "title": "New Title", - }, - "new_size": { - "type": "integer", - "title": "New Size", - }, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "new_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" - }, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { - "properties": {"name2": {"type": "string", "title": "Name2"}}, - "type": "object", - "required": ["name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": { - "properties": { - "new_sub_name": { - "type": "string", - "title": "New Sub Name", - } - }, - "type": "object", - "required": ["new_sub_name"], - "title": "SubItem", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": { - "properties": { - "dup_title": { - "type": "string", - "title": "Dup Title", - }, - "dup_size": { - "type": "integer", - "title": "Dup Size", - }, - "dup_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Dup Description", - }, - "dup_sub": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "dup_multi": { - "items": { - "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem" - }, - "type": "array", - "title": "Dup Multi", - "default": [], - }, - }, - "type": "object", - "required": ["dup_title", "dup_size", "dup_sub"], - "title": "Item", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": { - "properties": { - "dup_name2": { - "type": "string", - "title": "Dup Name2", - } - }, - "type": "object", - "required": ["dup_name2"], - "title": "ItemInList", - }, - "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": { - "properties": { - "dup_sub_name": { - "type": "string", - "title": "Dup Sub Name", - } - }, - "type": "object", - "required": ["dup_sub_name"], - "title": "SubItem", - }, - }, - }, - } - ) diff --git a/tests/test_pydantic_v1_v2_noneable.py b/tests/test_pydantic_v1_v2_noneable.py deleted file mode 100644 index ba98b5653c..0000000000 --- a/tests/test_pydantic_v1_v2_noneable.py +++ /dev/null @@ -1,692 +0,0 @@ -import sys -import warnings -from typing import Any, Union - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - -from fastapi import FastAPI -from fastapi._compat.v1 import BaseModel -from fastapi.testclient import TestClient -from inline_snapshot import snapshot -from pydantic import BaseModel as NewBaseModel - - -class SubItem(BaseModel): - name: str - - -class Item(BaseModel): - title: str - size: int - description: Union[str, None] = None - sub: SubItem - multi: list[SubItem] = [] - - -class NewSubItem(NewBaseModel): - new_sub_name: str - - -class NewItem(NewBaseModel): - new_title: str - new_size: int - new_description: Union[str, None] = None - new_sub: NewSubItem - new_multi: list[NewSubItem] = [] - - -app = FastAPI() - -with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/v1-to-v2/") - def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]: - if data.size < 0: - return None - return NewItem( - new_title=data.title, - new_size=data.size, - new_description=data.description, - new_sub=NewSubItem(new_sub_name=data.sub.name), - new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], - ) - - @app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None]) - def handle_v1_item_to_v2_filter(data: Item) -> Any: - if data.size < 0: - return None - result = { - "new_title": data.title, - "new_size": data.size, - "new_description": data.description, - "new_sub": { - "new_sub_name": data.sub.name, - "new_sub_secret": "sub_hidden", - }, - "new_multi": [ - {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} - for s in data.multi - ], - "secret": "hidden_v1_to_v2", - } - return result - - @app.post("/v2-to-v1/item") - def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]: - if data.new_size < 0: - return None - return Item( - title=data.new_title, - size=data.new_size, - description=data.new_description, - sub=SubItem(name=data.new_sub.new_sub_name), - multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], - ) - - @app.post("/v2-to-v1/item-filter", response_model=Union[Item, None]) - def handle_v2_item_to_v1_filter(data: NewItem) -> Any: - if data.new_size < 0: - return None - result = { - "title": data.new_title, - "size": data.new_size, - "description": data.new_description, - "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, - "multi": [ - {"name": s.new_sub_name, "sub_secret": "sub_hidden"} - for s in data.new_multi - ], - "secret": "hidden_v2_to_v1", - } - return result - - -client = TestClient(app) - - -def test_v1_to_v2_item_success(): - response = client.post( - "/v1-to-v2/", - json={ - "title": "Old Item", - "size": 100, - "description": "V1 description", - "sub": {"name": "V1 Sub"}, - "multi": [{"name": "M1"}, {"name": "M2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Old Item", - "new_size": 100, - "new_description": "V1 description", - "new_sub": {"new_sub_name": "V1 Sub"}, - "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}], - } - - -def test_v1_to_v2_item_returns_none(): - response = client.post( - "/v1-to-v2/", - json={"title": "Invalid Item", "size": -10, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v1_to_v2_item_minimal(): - response = client.post( - "/v1-to-v2/", json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "new_title": "Minimal", - "new_size": 50, - "new_description": None, - "new_sub": {"new_sub_name": "MinSub"}, - "new_multi": [], - } - - -def test_v1_to_v2_item_filter_success(): - response = client.post( - "/v1-to-v2/item-filter", - json={ - "title": "Filtered Item", - "size": 50, - "sub": {"name": "Sub"}, - "multi": [{"name": "Multi1"}], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result["new_title"] == "Filtered Item" - assert result["new_size"] == 50 - assert result["new_sub"]["new_sub_name"] == "Sub" - assert result["new_multi"][0]["new_sub_name"] == "Multi1" - # Verify secret fields are filtered out - assert "secret" not in result - assert "new_sub_secret" not in result["new_sub"] - assert "new_sub_secret" not in result["new_multi"][0] - - -def test_v1_to_v2_item_filter_returns_none(): - response = client.post( - "/v1-to-v2/item-filter", - json={"title": "Invalid", "size": -1, "sub": {"name": "Sub"}}, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v2_to_v1_item_success(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "New Item", - "new_size": 200, - "new_description": "V2 description", - "new_sub": {"new_sub_name": "V2 Sub"}, - "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "New Item", - "size": 200, - "description": "V2 description", - "sub": {"name": "V2 Sub"}, - "multi": [{"name": "N1"}, {"name": "N2"}], - } - - -def test_v2_to_v1_item_returns_none(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Invalid New", - "new_size": -5, - "new_sub": {"new_sub_name": "NewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v2_to_v1_item_minimal(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "MinimalNew", - "new_size": 75, - "new_sub": {"new_sub_name": "MinNewSub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "title": "MinimalNew", - "size": 75, - "description": None, - "sub": {"name": "MinNewSub"}, - "multi": [], - } - - -def test_v2_to_v1_item_filter_success(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Filtered New", - "new_size": 75, - "new_sub": {"new_sub_name": "NewSub"}, - "new_multi": [], - }, - ) - assert response.status_code == 200, response.text - result = response.json() - assert result["title"] == "Filtered New" - assert result["size"] == 75 - assert result["sub"]["name"] == "NewSub" - # Verify secret fields are filtered out - assert "secret" not in result - assert "sub_secret" not in result["sub"] - - -def test_v2_to_v1_item_filter_returns_none(): - response = client.post( - "/v2-to-v1/item-filter", - json={ - "new_title": "Invalid Filtered", - "new_size": -100, - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 200, response.text - assert response.json() is None - - -def test_v1_to_v2_validation_error(): - response = client.post("/v1-to-v2/", json={"title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "sub"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_v1_to_v2_nested_validation_error(): - response = client.post( - "/v1-to-v2/", - json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 1 - assert error_detail[0]["loc"] == ["body", "sub", "name"] - - -def test_v1_to_v2_type_validation_error(): - response = client.post( - "/v1-to-v2/", - json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}}, - ) - assert response.status_code == 422, response.text - error_detail = response.json()["detail"] - assert len(error_detail) == 1 - assert error_detail[0]["loc"] == ["body", "size"] - - -def test_v2_to_v1_validation_error(): - response = client.post("/v2-to-v1/item", json={"new_title": "Missing fields"}) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_size"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - { - "type": "missing", - "loc": ["body", "new_sub"], - "msg": "Field required", - "input": {"new_title": "Missing fields"}, - }, - ] - } - ) - - -def test_v2_to_v1_nested_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad sub", - "new_size": 200, - "new_sub": {"wrong_field": "value"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "new_sub", "new_sub_name"], - "msg": "Field required", - "input": {"wrong_field": "value"}, - } - ] - } - ) - - -def test_v2_to_v1_type_validation_error(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Bad type", - "new_size": "not_a_number", - "new_sub": {"new_sub_name": "Sub"}, - }, - ) - assert response.status_code == 422, response.text - assert response.json() == snapshot( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "new_size"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "not_a_number", - } - ] - } - ) - - -def test_v1_to_v2_with_multi_items(): - response = client.post( - "/v1-to-v2/", - json={ - "title": "Complex Item", - "size": 300, - "description": "Item with multiple sub-items", - "sub": {"name": "Main Sub"}, - "multi": [{"name": "Sub1"}, {"name": "Sub2"}, {"name": "Sub3"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "new_title": "Complex Item", - "new_size": 300, - "new_description": "Item with multiple sub-items", - "new_sub": {"new_sub_name": "Main Sub"}, - "new_multi": [ - {"new_sub_name": "Sub1"}, - {"new_sub_name": "Sub2"}, - {"new_sub_name": "Sub3"}, - ], - } - ) - - -def test_v2_to_v1_with_multi_items(): - response = client.post( - "/v2-to-v1/item", - json={ - "new_title": "Complex New Item", - "new_size": 400, - "new_description": "New item with multiple sub-items", - "new_sub": {"new_sub_name": "Main New Sub"}, - "new_multi": [{"new_sub_name": "NewSub1"}, {"new_sub_name": "NewSub2"}], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "title": "Complex New Item", - "size": 400, - "description": "New item with multiple sub-items", - "sub": {"name": "Main New Sub"}, - "multi": [{"name": "NewSub1"}, {"name": "NewSub2"}], - } - ) - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/v1-to-v2/": { - "post": { - "summary": "Handle V1 Item To V2", - "operationId": "handle_v1_item_to_v2_v1_to_v2__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 V1 To V2 Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v1-to-v2/item-filter": { - "post": { - "summary": "Handle V1 Item To V2 Filter", - "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Data", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/NewItem" - }, - {"type": "null"}, - ], - "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post", - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item": { - "post": { - "summary": "Handle V2 Item To V1", - "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "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" - } - } - }, - }, - }, - } - }, - "/v2-to-v1/item-filter": { - "post": { - "summary": "Handle V2 Item To V1 Filter", - "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NewItem"} - } - }, - "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" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "title": {"type": "string", "title": "Title"}, - "size": {"type": "integer", "title": "Size"}, - "description": {"type": "string", "title": "Description"}, - "sub": {"$ref": "#/components/schemas/SubItem"}, - "multi": { - "items": {"$ref": "#/components/schemas/SubItem"}, - "type": "array", - "title": "Multi", - "default": [], - }, - }, - "type": "object", - "required": ["title", "size", "sub"], - "title": "Item", - }, - "NewItem": { - "properties": { - "new_title": {"type": "string", "title": "New Title"}, - "new_size": {"type": "integer", "title": "New Size"}, - "new_description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "New Description", - }, - "new_sub": {"$ref": "#/components/schemas/NewSubItem"}, - "new_multi": { - "items": {"$ref": "#/components/schemas/NewSubItem"}, - "type": "array", - "title": "New Multi", - "default": [], - }, - }, - "type": "object", - "required": ["new_title", "new_size", "new_sub"], - "title": "NewItem", - }, - "NewSubItem": { - "properties": { - "new_sub_name": {"type": "string", "title": "New Sub Name"} - }, - "type": "object", - "required": ["new_sub_name"], - "title": "NewSubItem", - }, - "SubItem": { - "properties": {"name": {"type": "string", "title": "Name"}}, - "type": "object", - "required": ["name"], - "title": "SubItem", - }, - "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_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py index a195634b8a..cd7389252a 100644 --- a/tests/test_read_with_orm_mode.py +++ b/tests/test_read_with_orm_mode.py @@ -1,12 +1,9 @@ -import warnings from typing import Any from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict -from .utils import needs_pydanticv1 - def test_read_with_orm_mode() -> None: class PersonBase(BaseModel): @@ -44,50 +41,3 @@ def test_read_with_orm_mode() -> None: assert data["name"] == person_data["name"] assert data["lastname"] == person_data["lastname"] assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] - - -@needs_pydanticv1 -def test_read_with_orm_mode_pv1() -> None: - from pydantic import v1 - - class PersonBase(v1.BaseModel): - name: str - lastname: str - - class Person(PersonBase): - @property - def full_name(self) -> str: - return f"{self.name} {self.lastname}" - - class Config: - orm_mode = True - read_with_orm_mode = True - - class PersonCreate(PersonBase): - pass - - class PersonRead(PersonBase): - full_name: str - - class Config: - orm_mode = True - - app = FastAPI() - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - @app.post("/people/", response_model=PersonRead) - def create_person(person: PersonCreate) -> Any: - db_person = Person.from_orm(person) - return db_person - - client = TestClient(app) - - person_data = {"name": "Dive", "lastname": "Wilson"} - response = client.post("/people/", json=person_data) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == person_data["name"] - assert data["lastname"] == person_data["lastname"] - assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 9e527d6a01..58fba89f1a 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -1,4 +1,3 @@ -import warnings from typing import Union import pytest @@ -8,8 +7,6 @@ from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient from pydantic import BaseModel -from tests.utils import needs_pydanticv1 - class BaseUser(BaseModel): name: str @@ -512,29 +509,6 @@ def test_invalid_response_model_field(): assert "parameter response_model=None" in e.value.args[0] -# TODO: remove when dropping Pydantic v1 support -@needs_pydanticv1 -def test_invalid_response_model_field_pv1(): - from fastapi._compat import v1 - - app = FastAPI() - - class Model(v1.BaseModel): - foo: str - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - - with pytest.raises(FastAPIError) as e: - - @app.get("/") - def read_root() -> Union[Response, Model, None]: - return Response(content="Foo") # pragma: no cover - - assert "valid Pydantic field type" in e.value.args[0] - assert "parameter response_model=None" in e.value.args[0] - - def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py deleted file mode 100644 index 62b67a98c1..0000000000 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py +++ /dev/null @@ -1,115 +0,0 @@ -import importlib - -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - pytest.param("tutorial007_pv1_py39"), - ], -) -def get_client(request: pytest.FixtureRequest): - mod = importlib.import_module( - f"docs_src.path_operation_advanced_configuration.{request.param}" - ) - - client = TestClient(mod.app) - return client - - -@needs_pydanticv1 -def test_post(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - - x-force - - x-men - - x-avengers - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Deadpoolio", - "tags": ["x-force", "x-men", "x-avengers"], - } - - -@needs_pydanticv1 -def test_post_broken_yaml(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - x - x-force - x - x-men - x - x-avengers - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 422, response.text - assert response.json() == {"detail": "Invalid YAML"} - - -@needs_pydanticv1 -def test_post_invalid(client: TestClient): - yaml_data = """ - name: Deadpoolio - tags: - - x-force - - x-men - - x-avengers - - sneaky: object - """ - response = client.post("/items/", content=yaml_data) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} - ] - } - - -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/x-yaml": { - "schema": { - "title": "Item", - "required": ["name", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - }, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - } diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py b/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py deleted file mode 100644 index 4090eba012..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys -from typing import Any - -import pytest - -from tests.utils import skip_module_if_py_gte_314 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from ...utils import needs_py310 - - -@pytest.fixture( - name="mod", - params=[ - "tutorial001_an_py39", - pytest.param("tutorial001_an_py310", marks=needs_py310), - ], -) -def get_mod(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - return mod - - -def test_model(mod: Any): - item = mod.Item(name="Foo", size=3.4) - assert item.dict() == {"name": "Foo", "description": None, "size": 3.4} 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 deleted file mode 100644 index 9d1baf8530..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py +++ /dev/null @@ -1,143 +0,0 @@ -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 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - "tutorial002_an_py39", - pytest.param("tutorial002_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "size": 3.4}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "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": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "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" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "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_pydantic_v1_in_v2/test_tutorial003.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py deleted file mode 100644 index 23b236888d..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py +++ /dev/null @@ -1,158 +0,0 @@ -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 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - "tutorial003_an_py39", - pytest.param("tutorial003_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "size": 3.4}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "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": { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemV2" - } - } - }, - }, - "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": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "Item", - }, - "ItemV2": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "title": "ItemV2", - }, - "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_pydantic_v1_in_v2/test_tutorial004.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py deleted file mode 100644 index 61c0f63571..0000000000 --- a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py +++ /dev/null @@ -1,156 +0,0 @@ -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 - -if sys.version_info >= (3, 14): - skip_module_if_py_gte_314() - - -import importlib - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture( - name="client", - params=[ - pytest.param("tutorial004_an_py39"), - pytest.param("tutorial004_an_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}") - - c = TestClient(mod.app) - return c - - -def test_call(client: TestClient): - response = client.post("/items/", json={"item": {"name": "Foo", "size": 3.4}}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "size": 3.4, - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "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": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_item_items__post" - } - ], - "title": "Body", - } - } - }, - "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": { - "Body_create_item_items__post": { - "properties": { - "item": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - } - }, - "type": "object", - "required": ["item"], - "title": "Body_create_item_items__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "size": {"type": "number", "title": "Size"}, - }, - "type": "object", - "required": ["name", "size"], - "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_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py deleted file mode 100644 index 50be458962..0000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py +++ /dev/null @@ -1,128 +0,0 @@ -import importlib -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.testclient import TestClient - -from ...utils import needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - "tutorial002_pv1_py39", - "tutorial002_pv1_an_py39", - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - - client = TestClient(mod.app) - return client - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.extra", - "loc": ["body", "extra"], - "msg": "extra fields not permitted", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } 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 deleted file mode 100644 index 83c7176567..0000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py +++ /dev/null @@ -1,152 +0,0 @@ -import importlib -import warnings - -import pytest -from fastapi.exceptions import FastAPIDeprecationWarning -from fastapi.testclient import TestClient -from inline_snapshot import snapshot - -from ...utils import needs_py310, needs_pydanticv1 - - -@pytest.fixture( - name="client", - params=[ - pytest.param("tutorial001_pv1_py39"), - pytest.param("tutorial001_pv1_py310", marks=needs_py310), - ], -) -def get_client(request: pytest.FixtureRequest): - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", - message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", - category=FastAPIDeprecationWarning, - ) - mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") - - client = TestClient(mod.app) - return client - - -@needs_pydanticv1 -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 - - -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == snapshot( - { - "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": [ - { - "required": True, - "schema": {"type": "integer", "title": "Item Id"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "price": {"type": "number", "title": "Price"}, - "tax": {"type": "number", "title": "Tax"}, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } - ], - }, - "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_settings/test_app03.py b/tests/test_tutorial/test_settings/test_app03.py index 06e82398d1..72de497967 100644 --- a/tests/test_tutorial/test_settings/test_app03.py +++ b/tests/test_tutorial/test_settings/test_app03.py @@ -5,8 +5,6 @@ import pytest from fastapi.testclient import TestClient from pytest import MonkeyPatch -from ...utils import needs_pydanticv1 - @pytest.fixture( name="mod_path", @@ -34,16 +32,6 @@ def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch): assert settings.items_per_user == 50 -@needs_pydanticv1 -def test_settings_pv1(mod_path: str, monkeypatch: MonkeyPatch): - monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") - config_mod = importlib.import_module(f"{mod_path}.config_pv1") - settings = config_mod.Settings() - assert settings.app_name == "Awesome API" - assert settings.admin_email == "admin@example.com" - assert settings.items_per_user == 50 - - def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") client = TestClient(main_mod.app) diff --git a/tests/test_tutorial/test_settings/test_tutorial001.py b/tests/test_tutorial/test_settings/test_tutorial001.py index 6a08096989..f4576a0d21 100644 --- a/tests/test_tutorial/test_settings/test_tutorial001.py +++ b/tests/test_tutorial/test_settings/test_tutorial001.py @@ -4,16 +4,8 @@ import pytest from fastapi.testclient import TestClient from pytest import MonkeyPatch -from ...utils import needs_pydanticv1 - -@pytest.fixture( - name="app", - params=[ - pytest.param("tutorial001_py39"), - pytest.param("tutorial001_pv1_py39", marks=needs_pydanticv1), - ], -) +@pytest.fixture(name="app", params=[pytest.param("tutorial001_py39")]) def get_app(request: pytest.FixtureRequest, monkeypatch: MonkeyPatch): monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") mod = importlib.import_module(f"docs_src.settings.{request.param}") diff --git a/tests/utils.py b/tests/utils.py index b896d4527f..efa0bfd52b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,8 +10,6 @@ needs_py_lt_314 = pytest.mark.skipif( sys.version_info >= (3, 14), reason="requires python3.13-" ) -needs_pydanticv1 = needs_py_lt_314 - def skip_module_if_py_gte_314(): """Skip entire module on Python 3.14+ at import time.""" From 4b2cfcfd34d0b2e158a05b377426e0c8376b94de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 12:55:22 +0000 Subject: [PATCH 46/59] =?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 10b262b489..21523b2e2f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Breaking Changes + +* ➖ Drop support for `pydantic.v1`. PR [#14609](https://github.com/fastapi/fastapi/pull/14609) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ✅ Run performance tests only on Pydantic v2. PR [#14608](https://github.com/fastapi/fastapi/pull/14608) by [@tiangolo](https://github.com/tiangolo). From 8322a4445a3b25acd9b26b61192571b2d92f9bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 16:19:50 +0100 Subject: [PATCH 47/59] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.128.?= =?UTF-8?q?0?= 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 21523b2e2f..ef7894037c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.128.0 + ### Breaking Changes * ➖ Drop support for `pydantic.v1`. PR [#14609](https://github.com/fastapi/fastapi/pull/14609) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index dc447c8bfd..6133787b06 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.1" +__version__ = "0.128.0" from starlette import status as status From 44c849c4fc3636f54eae74ab4e586c8ca9f3fb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:19:10 -0800 Subject: [PATCH 48/59] =?UTF-8?q?=F0=9F=94=A5=20Remove=20Pydantic=20v1=20?= =?UTF-8?q?=20specific=20test=20variants=20(#14611)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_additional_properties_bool.py | 21 +- ...onal_responses_custom_model_in_callback.py | 207 ++- tests/test_annotated.py | 48 +- tests/test_application.py | 25 +- tests/test_dependency_duplicates.py | 34 +- tests/test_dependency_overrides.py | 405 ++---- tests/test_extra_routes.py | 13 +- tests/test_filter_pydantic_sub_model_pv2.py | 45 +- tests/test_forms_single_model.py | 84 +- tests/test_infer_param_optionality.py | 57 +- tests/test_multi_body_errors.py | 185 +-- tests/test_multi_query_errors.py | 51 +- tests/test_openapi_examples.py | 114 +- .../test_openapi_query_parameter_extension.py | 21 +- tests/test_openapi_servers.py | 64 +- tests/test_params_repr.py | 43 +- tests/test_path.py | 1182 +++++------------ tests/test_query.py | 364 ++--- tests/test_regex_deprecated_body.py | 196 ++- tests/test_regex_deprecated_params.py | 61 +- .../test_body/test_list.py | 66 +- .../test_body/test_optional_list.py | 285 ++-- .../test_body/test_optional_str.py | 249 ++-- .../test_body/test_required_str.py | 101 +- .../test_cookie/test_optional_str.py | 59 +- .../test_cookie/test_required_str.py | 107 +- .../test_file/test_list.py | 99 +- .../test_file/test_optional.py | 89 +- .../test_file/test_optional_list.py | 103 +- .../test_file/test_required.py | 100 +- .../test_form/test_list.py | 66 +- .../test_form/test_optional_list.py | 153 +-- .../test_form/test_optional_str.py | 117 +- .../test_form/test_required_str.py | 101 +- .../test_header/test_list.py | 66 +- .../test_header/test_optional_list.py | 75 +- .../test_header/test_optional_str.py | 59 +- .../test_header/test_required_str.py | 101 +- .../test_query/test_list.py | 66 +- .../test_query/test_optional_list.py | 75 +- .../test_query/test_optional_str.py | 59 +- .../test_query/test_required_str.py | 107 +- tests/test_schema_extra_examples.py | 221 +-- tests/test_security_oauth2.py | 165 +-- tests/test_security_oauth2_optional.py | 165 +-- ...st_security_oauth2_optional_description.py | 165 +-- tests/test_sub_callbacks.py | 51 +- tests/test_tuples.py | 97 +- .../test_tutorial002.py | 15 +- .../test_tutorial004.py | 15 +- .../test_behind_a_proxy/test_tutorial003.py | 68 +- .../test_behind_a_proxy/test_tutorial004.py | 66 +- .../test_bigger_applications/test_main.py | 315 ++--- .../test_body/test_tutorial001.py | 355 ++--- .../test_body_fields/test_tutorial001.py | 75 +- .../test_tutorial001.py | 96 +- .../test_tutorial003.py | 177 +-- .../test_tutorial009.py | 34 +- .../test_tutorial001.py | 69 +- .../test_tutorial002.py | 52 +- .../test_cookie_params/test_tutorial001.py | 15 +- .../test_tutorial002.py | 45 +- .../test_dataclasses/test_tutorial001.py | 62 +- .../test_dataclasses/test_tutorial002.py | 108 +- .../test_dependencies/test_tutorial006.py | 51 +- .../test_dependencies/test_tutorial012.py | 101 +- .../test_extra_data_types/test_tutorial001.py | 217 ++- .../test_extra_models/test_tutorial003.py | 200 +-- .../test_handling_errors/test_tutorial005.py | 37 +- .../test_handling_errors/test_tutorial006.py | 34 +- .../test_tutorial001.py | 71 +- .../test_tutorial002.py | 79 +- .../test_tutorial003.py | 115 +- .../test_header_params/test_tutorial001.py | 15 +- .../test_header_params/test_tutorial002.py | 15 +- .../test_header_params/test_tutorial003.py | 25 +- .../test_tutorial001.py | 51 +- .../test_path_params/test_tutorial005.py | 58 +- .../test_tutorial001.py | 81 +- .../test_tutorial002.py | 103 +- .../test_query_params/test_tutorial005.py | 34 +- .../test_query_params/test_tutorial006.py | 82 +- .../test_tutorial010.py | 88 +- .../test_tutorial011.py | 25 +- .../test_request_files/test_tutorial001.py | 67 +- .../test_request_files/test_tutorial001_02.py | 268 ++-- .../test_request_files/test_tutorial002.py | 67 +- .../test_tutorial001.py | 167 +-- .../test_request_forms/test_tutorial001.py | 167 +-- .../test_tutorial001.py | 235 ++-- .../test_response_model/test_tutorial003.py | 212 ++- .../test_tutorial003_01.py | 218 ++- .../test_response_model/test_tutorial004.py | 178 ++- .../test_response_model/test_tutorial005.py | 230 ++-- .../test_response_model/test_tutorial006.py | 230 ++-- .../test_tutorial004.py | 85 +- .../test_tutorial005.py | 40 +- .../test_security/test_tutorial003.py | 59 +- .../test_security/test_tutorial005.py | 364 +++-- .../test_sql_databases/test_tutorial001.py | 36 +- .../test_sql_databases/test_tutorial002.py | 87 +- tests/test_union_body.py | 17 +- tests/test_union_body_discriminator.py | 21 +- tests/test_union_inherited_body.py | 29 +- 104 files changed, 4139 insertions(+), 8074 deletions(-) diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py index 3756b7d7c9..063297a3f2 100644 --- a/tests/test_additional_properties_bool.py +++ b/tests/test_additional_properties_bool.py @@ -1,6 +1,5 @@ from typing import Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict @@ -52,19 +51,13 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Foo"}, - {"type": "null"}, - ], - "title": "Foo", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Foo"} - ) + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/Foo"}, + {"type": "null"}, + ], + "title": "Foo", + } } } }, diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index 2ad5754551..376d7714ed 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -1,6 +1,6 @@ -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, HttpUrl from starlette.responses import JSONResponse @@ -32,121 +32,114 @@ client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "post": { - "summary": "Main Route", - "operationId": "main_route__post", - "parameters": [ - { - "required": True, - "schema": IsDict( - { - "title": "Callback Url", - "minLength": 1, - "type": "string", - "format": "uri", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict( - { + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Main Route", + "operationId": "main_route__post", + "parameters": [ + { + "required": True, + "schema": { "title": "Callback Url", "maxLength": 2083, "minLength": 1, "type": "string", "format": "uri", - } - ), - "name": "callback_url", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + }, + "name": "callback_url", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "callbacks": { + "callback_route": { + "{$callback_url}/callback/": { + "get": { + "summary": "Callback Route", + "operationId": "callback_route__callback_url__callback__get", + "responses": { + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomModel" + } + } + }, + "description": "Bad Request", + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + }, } } - }, + } + }, + } + } + }, + "components": { + "schemas": { + "CustomModel": { + "title": "CustomModel", + "required": ["a"], + "type": "object", + "properties": {"a": {"title": "A", "type": "integer"}}, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } }, }, - "callbacks": { - "callback_route": { - "{$callback_url}/callback/": { - "get": { - "summary": "Callback Route", - "operationId": "callback_route__callback_url__callback__get", - "responses": { - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomModel" - } - } - }, - "description": "Bad Request", - }, - "200": { - "description": "Successful Response", - "content": { - "application/json": {"schema": {}} - }, - }, - }, - } - } - } + "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"}, + }, }, } - } - }, - "components": { - "schemas": { - "CustomModel": { - "title": "CustomModel", - "required": ["a"], - "type": "object", - "properties": {"a": {"title": "A", "type": "integer"}}, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } + }, + } + ) diff --git a/tests/test_annotated.py b/tests/test_annotated.py index a9e7c78c99..39f6f83b29 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,7 +1,6 @@ from typing import Annotated import pytest -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI, Query from fastapi.testclient import TestClient @@ -32,44 +31,23 @@ client = TestClient(app) foo_is_missing = { "detail": [ - IsDict( - { - "loc": ["query", "foo"], - "msg": "Field required", - "type": "missing", - "input": None, - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict( - { - "loc": ["query", "foo"], - "msg": "field required", - "type": "value_error.missing", - } - ) + { + "loc": ["query", "foo"], + "msg": "Field required", + "type": "missing", + "input": None, + } ] } foo_is_short = { "detail": [ - IsDict( - { - "ctx": {"min_length": 1}, - "loc": ["query", "foo"], - "msg": "String should have at least 1 character", - "type": "string_too_short", - "input": "", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict( - { - "ctx": {"limit_value": 1}, - "loc": ["query", "foo"], - "msg": "ensure this value has at least 1 characters", - "type": "value_error.any_str.min_length", - } - ) + { + "ctx": {"min_length": 1}, + "loc": ["query", "foo"], + "msg": "String should have at least 1 character", + "type": "string_too_short", + "input": "", + } ] } diff --git a/tests/test_application.py b/tests/test_application.py index 8f1b0a18d3..001586ff78 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,5 +1,4 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -274,14 +273,10 @@ def test_openapi_schema(): "name": "item_id", "in": "path", "required": True, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Item Id", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict({"title": "Item Id", "type": "string"}), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Item Id", + }, } ], } @@ -984,14 +979,10 @@ def test_openapi_schema(): "name": "query", "in": "query", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Query", - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict({"title": "Query", "type": "integer"}), + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Query", + }, } ], } diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 7c6717e2aa..a8658e03bb 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -46,29 +45,16 @@ async def no_duplicates_sub( def test_no_duplicates_invalid(): response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item2"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item2"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "item2"], + "msg": "Field required", + "input": None, + } + ] + } def test_no_duplicates(): diff --git a/tests/test_dependency_overrides.py b/tests/test_dependency_overrides.py index 154937fa0b..e25db624d8 100644 --- a/tests/test_dependency_overrides.py +++ b/tests/test_dependency_overrides.py @@ -1,7 +1,6 @@ from typing import Optional import pytest -from dirty_equals import IsDict from fastapi import APIRouter, Depends, FastAPI from fastapi.testclient import TestClient @@ -54,29 +53,16 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend def test_main_depends(): response = client.get("/main-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_main_depends_q_foo(): @@ -100,29 +86,16 @@ def test_main_depends_q_foo_skip_100_limit_200(): def test_decorator_depends(): response = client.get("/decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_decorator_depends_q_foo(): @@ -140,29 +113,16 @@ def test_decorator_depends_q_foo_skip_100_limit_200(): def test_router_depends(): response = client.get("/router-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_router_depends_q_foo(): @@ -186,29 +146,16 @@ def test_router_depends_q_foo_skip_100_limit_200(): def test_router_decorator_depends(): response = client.get("/router-decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "q"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] + } def test_router_decorator_depends_q_foo(): @@ -272,29 +219,17 @@ def test_override_with_sub_main_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/main-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -302,29 +237,17 @@ def test_override_with_sub__main_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/main-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -340,29 +263,17 @@ def test_override_with_sub_decorator_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -370,29 +281,17 @@ def test_override_with_sub_decorator_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/decorator-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -408,29 +307,17 @@ def test_override_with_sub_router_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -438,29 +325,17 @@ def test_override_with_sub_router_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -476,29 +351,17 @@ def test_override_with_sub_router_decorator_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-decorator-depends/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} @@ -506,29 +369,17 @@ def test_override_with_sub_router_decorator_depends_q_foo(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub response = client.get("/router-decorator-depends/?q=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "k"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + } + ] + } + app.dependency_overrides = {} diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index bd16fe9254..45734ec28a 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -328,14 +327,10 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "price": IsDict( - { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - # TODO: remove when deprecating Pydantic v1 - | IsDict({"title": "Price", "type": "number"}), + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index d70f530435..fc5876410d 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -1,7 +1,7 @@ from typing import Optional import pytest -from dirty_equals import HasRepr, IsDict, IsOneOf +from dirty_equals import HasRepr from fastapi import Depends, FastAPI from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient @@ -63,23 +63,13 @@ def test_validator_is_cloned(client: TestClient): with pytest.raises(ResponseValidationError) as err: client.get("/model/modelX") assert err.value.errors() == [ - IsDict( - { - "type": "value_error", - "loc": ("response", "name"), - "msg": "Value error, name must end in A", - "input": "modelX", - "ctx": {"error": HasRepr("ValueError('name must end in A')")}, - } - ) - | IsDict( - # TODO remove when deprecating Pydantic v1 - { - "loc": ("response", "name"), - "msg": "name must end in A", - "type": "value_error", - } - ) + { + "type": "value_error", + "loc": ("response", "name"), + "msg": "Value error, name must end in A", + "input": "modelX", + "ctx": {"error": HasRepr("ValueError('name must end in A')")}, + } ] @@ -145,23 +135,14 @@ def test_openapi_schema(client: TestClient): }, "ModelA": { "title": "ModelA", - "required": IsOneOf( - ["name", "description", "foo"], - # TODO remove when deprecating Pydantic v1 - ["name", "foo"], - ), + "required": ["name", "foo"], "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | - # TODO remove when deprecating Pydantic v1 - IsDict({"title": "Description", "type": "string"}), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "foo": {"$ref": "#/components/schemas/ModelB"}, "tags": { "additionalProperties": {"type": "string"}, diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py index c401cc9374..7d03d29572 100644 --- a/tests/test_forms_single_model.py +++ b/tests/test_forms_single_model.py @@ -1,6 +1,5 @@ from typing import Annotated, Optional -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -79,68 +78,37 @@ def test_invalid_data(): }, ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "age"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "seventy", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "age"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "age"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "seventy", + } + ] + } def test_no_data(): response = client.post("/form/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"tags": ["foo", "bar"], "with": "nothing"}, - }, - { - "type": "missing", - "loc": ["body", "lastname"], - "msg": "Field required", - "input": {"tags": ["foo", "bar"], "with": "nothing"}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "lastname"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"], "with": "nothing"}, + }, + { + "type": "missing", + "loc": ["body", "lastname"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"], "with": "nothing"}, + }, + ] + } def test_extra_param_single(): diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index e3d57bb428..147018996e 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient @@ -163,16 +162,10 @@ def test_openapi_schema(): "required": False, "name": "user_id", "in": "query", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, } ], "responses": { @@ -208,16 +201,10 @@ def test_openapi_schema(): "required": False, "name": "user_id", "in": "query", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, }, ], "responses": { @@ -247,16 +234,10 @@ def test_openapi_schema(): "required": True, "name": "user_id", "in": "path", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, } ], "responses": { @@ -292,16 +273,10 @@ def test_openapi_schema(): "required": True, "name": "user_id", "in": "path", - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + }, }, ], "responses": { diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 6ea405fe70..4418c77cb0 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -1,8 +1,9 @@ from decimal import Decimal -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot from pydantic import BaseModel, condecimal app = FastAPI() @@ -24,109 +25,65 @@ client = TestClient(app) def test_put_correct_body(): response = client.post("/items/", json=[{"name": "Foo", "age": 5}]) assert response.status_code == 200, response.text - assert response.json() == { - "item": [ - { - "name": "Foo", - "age": IsOneOf( - 5, - # TODO: remove when deprecating Pydantic v1 - "5", - ), - } - ] - } + assert response.json() == snapshot( + { + "item": [ + { + "name": "Foo", + "age": "5", + } + ] + } + ) def test_jsonable_encoder_requiring_error(): response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}]) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", 0, "age"], - "msg": "Input should be greater than 0", - "input": -1.0, - "ctx": {"gt": 0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0.0}, - "loc": ["body", 0, "age"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["body", 0, "age"], + "msg": "Input should be greater than 0", + "input": -1.0, + "ctx": {"gt": 0}, + } + ] + } def test_put_incorrect_body_multiple(): response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}]) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", 0, "name"], - "msg": "Field required", - "input": {"age": "five"}, - }, - { - "type": "decimal_parsing", - "loc": ["body", 0, "age"], - "msg": "Input should be a valid decimal", - "input": "five", - }, - { - "type": "missing", - "loc": ["body", 1, "name"], - "msg": "Field required", - "input": {"age": "six"}, - }, - { - "type": "decimal_parsing", - "loc": ["body", 1, "age"], - "msg": "Input should be a valid decimal", - "input": "six", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", 0, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 0, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - { - "loc": ["body", 1, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", 0, "name"], + "msg": "Field required", + "input": {"age": "five"}, + }, + { + "type": "decimal_parsing", + "loc": ["body", 0, "age"], + "msg": "Input should be a valid decimal", + "input": "five", + }, + { + "type": "missing", + "loc": ["body", 1, "name"], + "msg": "Field required", + "input": {"age": "six"}, + }, + { + "type": "decimal_parsing", + "loc": ["body", 1, "age"], + "msg": "Input should be a valid decimal", + "input": "six", + }, + ] + } def test_openapi_schema(): @@ -179,31 +136,21 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [ - {"exclusiveMinimum": 0.0, "type": "number"}, - IsOneOf( - # pydantic < 2.12.0 - {"type": "string"}, - # pydantic >= 2.12.0 - { - "type": "string", - "pattern": r"^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$", - }, - ), - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Age", - "exclusiveMinimum": 0.0, - "type": "number", - } - ), + "age": { + "title": "Age", + "anyOf": [ + {"exclusiveMinimum": 0.0, "type": "number"}, + IsOneOf( + # pydantic < 2.12.0 + {"type": "string"}, + # pydantic >= 2.12.0 + { + "type": "string", + "pattern": r"^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$", + }, + ), + ], + }, }, }, "ValidationError": { diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index 7387a81ddf..5df51ba185 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient @@ -22,40 +21,22 @@ def test_multi_query(): def test_multi_query_incorrect(): response = client.get("/items/?q=five&q=six") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "q", 0], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "five", - }, - { - "type": "int_parsing", - "loc": ["query", "q", 1], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "six", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "q", 0], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "q", 1], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "q", 0], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "five", + }, + { + "type": "int_parsing", + "loc": ["query", "q", 1], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "six", + }, + ] + } def test_openapi_schema(): diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index b3f83ae237..bd0d55452e 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -1,6 +1,5 @@ from typing import Union -from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient from pydantic import BaseModel @@ -155,26 +154,12 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "Data in Body examples, example1"} - ], - } - ) - | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"} - ], - } - ), + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + }, "examples": { "Example One": { "summary": "Example One Summary", @@ -265,27 +250,14 @@ def test_openapi_schema(): "name": "data", "in": "query", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_query1", - "json_schema_query2", - ], - "title": "Data", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "examples": [ - "json_schema_query1", - "json_schema_query2", - ], - "type": "string", - "title": "Data", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_query1", + "json_schema_query2", + ], + "title": "Data", + }, "examples": { "Query One": { "summary": "Query One Summary", @@ -323,27 +295,14 @@ def test_openapi_schema(): "name": "data", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_header1", - "json_schema_header2", - ], - "title": "Data", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "examples": [ - "json_schema_header1", - "json_schema_header2", - ], - "title": "Data", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_header1", + "json_schema_header2", + ], + "title": "Data", + }, "examples": { "Header One": { "summary": "Header One Summary", @@ -381,27 +340,14 @@ def test_openapi_schema(): "name": "data", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "examples": [ - "json_schema_cookie1", - "json_schema_cookie2", - ], - "title": "Data", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "examples": [ - "json_schema_cookie1", - "json_schema_cookie2", - ], - "title": "Data", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_cookie1", + "json_schema_cookie2", + ], + "title": "Data", + }, "examples": { "Cookie One": { "summary": "Cookie One Summary", diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index dc7147c712..084cb695d4 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -53,21 +52,11 @@ def test_openapi(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "default": 50, - "title": "Standard Query Param", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Standard Query Param", - "type": "integer", - "default": 50, - } - ), + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": 50, + "title": "Standard Query Param", + }, "name": "standard_query_param", "in": "query", }, diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py index 8697c8438b..33079e4b1d 100644 --- a/tests/test_openapi_servers.py +++ b/tests/test_openapi_servers.py @@ -1,6 +1,6 @@ -from dirty_equals import IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient +from inline_snapshot import snapshot app = FastAPI( servers=[ @@ -30,39 +30,31 @@ def test_app(): 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"}, - "servers": [ - {"url": "/", "description": "Default, relative server"}, - { - "url": IsOneOf( - "http://staging.localhost.tiangolo.com:8000/", - # TODO: remove when deprecating Pydantic v1 - "http://staging.localhost.tiangolo.com:8000", - ), - "description": "Staging but actually localhost still", - }, - { - "url": IsOneOf( - "https://prod.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://prod.example.com", - ) - }, - ], - "paths": { - "/foo": { - "get": { - "summary": "Foo", - "operationId": "foo_foo_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/", "description": "Default, relative server"}, + { + "url": "http://staging.localhost.tiangolo.com:8000", + "description": "Staging but actually localhost still", + }, + {"url": "https://prod.example.com"}, + ], + "paths": { + "/foo": { + "get": { + "summary": "Foo", + "operationId": "foo_foo_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index 19c2e8d696..670e4f5ddf 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -1,6 +1,5 @@ from typing import Any -from dirty_equals import IsOneOf from fastapi.params import Body, Cookie, Header, Param, Path, Query test_data: list[Any] = ["teststr", None, ..., 1, []] @@ -19,11 +18,7 @@ def test_param_repr_none(): def test_param_repr_ellipsis(): - assert repr(Param(...)) == IsOneOf( - "Param(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Param(Ellipsis)", - ) + assert repr(Param(...)) == "Param(PydanticUndefined)" def test_param_repr_number(): @@ -35,16 +30,8 @@ def test_param_repr_list(): def test_path_repr(): - assert repr(Path()) == IsOneOf( - "Path(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Path(Ellipsis)", - ) - assert repr(Path(...)) == IsOneOf( - "Path(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Path(Ellipsis)", - ) + assert repr(Path()) == "Path(PydanticUndefined)" + assert repr(Path(...)) == "Path(PydanticUndefined)" def test_query_repr_str(): @@ -56,11 +43,7 @@ def test_query_repr_none(): def test_query_repr_ellipsis(): - assert repr(Query(...)) == IsOneOf( - "Query(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Query(Ellipsis)", - ) + assert repr(Query(...)) == "Query(PydanticUndefined)" def test_query_repr_number(): @@ -80,11 +63,7 @@ def test_header_repr_none(): def test_header_repr_ellipsis(): - assert repr(Header(...)) == IsOneOf( - "Header(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Header(Ellipsis)", - ) + assert repr(Header(...)) == "Header(PydanticUndefined)" def test_header_repr_number(): @@ -104,11 +83,7 @@ def test_cookie_repr_none(): def test_cookie_repr_ellipsis(): - assert repr(Cookie(...)) == IsOneOf( - "Cookie(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Cookie(Ellipsis)", - ) + assert repr(Cookie(...)) == "Cookie(PydanticUndefined)" def test_cookie_repr_number(): @@ -128,11 +103,7 @@ def test_body_repr_none(): def test_body_repr_ellipsis(): - assert repr(Body(...)) == IsOneOf( - "Body(PydanticUndefined)", - # TODO: remove when deprecating Pydantic v1 - "Body(Ellipsis)", - ) + assert repr(Body(...)) == "Body(PydanticUndefined)" def test_body_repr_number(): diff --git a/tests/test_path.py b/tests/test_path.py index 09c1f13fb1..47051b927c 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -45,57 +44,31 @@ def test_path_str_True(): def test_path_int_foobar(): response = client.get("/path/int/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foobar", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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": "foobar", + } + ] + } def test_path_int_True(): response = client.get("/path/int/True") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "True", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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": "True", + } + ] + } def test_path_int_42(): @@ -107,85 +80,46 @@ def test_path_int_42(): def test_path_int_42_5(): response = client.get("/path/int/42.5") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "42.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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": "42.5", + } + ] + } def test_path_float_foobar(): response = client.get("/path/float/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "foobar", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "foobar", + } + ] + } def test_path_float_True(): response = client.get("/path/float/True") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "True", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "True", + } + ] + } def test_path_float_42(): @@ -203,29 +137,16 @@ def test_path_float_42_5(): def test_path_bool_foobar(): response = client.get("/path/bool/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "bool_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid boolean, unable to interpret input", - "input": "foobar", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "foobar", + } + ] + } def test_path_bool_True(): @@ -237,57 +158,31 @@ def test_path_bool_True(): def test_path_bool_42(): response = client.get("/path/bool/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "bool_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid boolean, unable to interpret input", - "input": "42", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42", + } + ] + } def test_path_bool_42_5(): response = client.get("/path/bool/42.5") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "bool_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid boolean, unable to interpret input", - "input": "42.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42.5", + } + ] + } def test_path_bool_1(): @@ -335,31 +230,17 @@ def test_path_param_minlength_foo(): def test_path_param_minlength_fo(): response = client.get("/path/param-minlength/fo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_short", - "loc": ["path", "item_id"], - "msg": "String should have at least 3 characters", - "input": "fo", - "ctx": {"min_length": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 3 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 3 characters", + "input": "fo", + "ctx": {"min_length": 3}, + } + ] + } def test_path_param_maxlength_foo(): @@ -371,31 +252,17 @@ def test_path_param_maxlength_foo(): def test_path_param_maxlength_foobar(): response = client.get("/path/param-maxlength/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_long", - "loc": ["path", "item_id"], - "msg": "String should have at most 3 characters", - "input": "foobar", - "ctx": {"max_length": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at most 3 characters", - "type": "value_error.any_str.max_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + } + ] + } def test_path_param_min_maxlength_foo(): @@ -407,60 +274,33 @@ def test_path_param_min_maxlength_foo(): def test_path_param_min_maxlength_foobar(): response = client.get("/path/param-min_maxlength/foobar") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_long", - "loc": ["path", "item_id"], - "msg": "String should have at most 3 characters", - "input": "foobar", - "ctx": {"max_length": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at most 3 characters", - "type": "value_error.any_str.max_length", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + } + ] + } def test_path_param_min_maxlength_f(): response = client.get("/path/param-min_maxlength/f") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_too_short", - "loc": ["path", "item_id"], - "msg": "String should have at least 2 characters", - "input": "f", - "ctx": {"min_length": 2}, - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 2 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 2}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 2 characters", + "input": "f", + "ctx": {"min_length": 2}, + } + ] + } def test_path_param_gt_42(): @@ -472,31 +312,17 @@ def test_path_param_gt_42(): def test_path_param_gt_2(): response = client.get("/path/param-gt/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 3", - "input": "2", - "ctx": {"gt": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 3", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3.0}, + } + ] + } def test_path_param_gt0_0_05(): @@ -508,31 +334,17 @@ def test_path_param_gt0_0_05(): def test_path_param_gt0_0(): response = client.get("/path/param-gt0/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 0", - "input": "0", - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 0", + "input": "0", + "ctx": {"gt": 0.0}, + } + ] + } def test_path_param_ge_42(): @@ -550,61 +362,33 @@ def test_path_param_ge_3(): def test_path_param_ge_2(): response = client.get("/path/param-ge/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be greater than or equal to 3", - "input": "2", - "ctx": {"ge": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than or equal to 3", - "type": "value_error.number.not_ge", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3.0}, + } + ] + } def test_path_param_lt_42(): response = client.get("/path/param-lt/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "42", - "ctx": {"lt": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3.0}, + } + ] + } def test_path_param_lt_2(): @@ -622,61 +406,33 @@ def test_path_param_lt0__1(): def test_path_param_lt0_0(): response = client.get("/path/param-lt0/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 0", - "input": "0", - "ctx": {"lt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 0", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 0}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 0", + "input": "0", + "ctx": {"lt": 0.0}, + } + ] + } def test_path_param_le_42(): response = client.get("/path/param-le/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "42", - "ctx": {"le": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3.0}, + } + ] + } def test_path_param_le_3(): @@ -700,61 +456,33 @@ def test_path_param_lt_gt_2(): def test_path_param_lt_gt_4(): response = client.get("/path/param-lt-gt/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "4", - "ctx": {"lt": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3.0}, + } + ] + } def test_path_param_lt_gt_0(): response = client.get("/path/param-lt-gt/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 1", - "input": "0", - "ctx": {"gt": 1.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 1", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 1}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1.0}, + } + ] + } def test_path_param_le_ge_2(): @@ -777,31 +505,17 @@ def test_path_param_le_ge_3(): def test_path_param_le_ge_4(): response = client.get("/path/param-le-ge/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "4", - "ctx": {"le": 3.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3.0}, + } + ] + } def test_path_param_lt_int_2(): @@ -813,59 +527,32 @@ def test_path_param_lt_int_2(): def test_path_param_lt_int_42(): response = client.get("/path/param-lt-int/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "42", - "ctx": {"lt": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3}, + } + ] + } def test_path_param_lt_int_2_7(): response = client.get("/path/param-lt-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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": "2.7", + } + ] + } def test_path_param_gt_int_42(): @@ -877,89 +564,48 @@ def test_path_param_gt_int_42(): def test_path_param_gt_int_2(): response = client.get("/path/param-gt-int/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 3", - "input": "2", - "ctx": {"gt": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 3", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3}, + } + ] + } def test_path_param_gt_int_2_7(): response = client.get("/path/param-gt-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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": "2.7", + } + ] + } def test_path_param_le_int_42(): response = client.get("/path/param-le-int/42") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "42", - "ctx": {"le": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3}, + } + ] + } def test_path_param_le_int_3(): @@ -977,29 +623,16 @@ def test_path_param_le_int_2(): def test_path_param_le_int_2_7(): response = client.get("/path/param-le-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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": "2.7", + } + ] + } def test_path_param_ge_int_42(): @@ -1017,59 +650,32 @@ def test_path_param_ge_int_3(): def test_path_param_ge_int_2(): response = client.get("/path/param-ge-int/2") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be greater than or equal to 3", - "input": "2", - "ctx": {"ge": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than or equal to 3", - "type": "value_error.number.not_ge", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3}, + } + ] + } def test_path_param_ge_int_2_7(): response = client.get("/path/param-ge-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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": "2.7", + } + ] + } def test_path_param_lt_gt_int_2(): @@ -1081,89 +687,48 @@ def test_path_param_lt_gt_int_2(): def test_path_param_lt_gt_int_4(): response = client.get("/path/param-lt-gt-int/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than", - "loc": ["path", "item_id"], - "msg": "Input should be less than 3", - "input": "4", - "ctx": {"lt": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3}, + } + ] + } def test_path_param_lt_gt_int_0(): response = client.get("/path/param-lt-gt-int/0") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["path", "item_id"], - "msg": "Input should be greater than 1", - "input": "0", - "ctx": {"gt": 1}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 1", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 1}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1}, + } + ] + } def test_path_param_lt_gt_int_2_7(): response = client.get("/path/param-lt-gt-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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": "2.7", + } + ] + } def test_path_param_le_ge_int_2(): @@ -1187,56 +752,29 @@ def test_path_param_le_ge_int_3(): def test_path_param_le_ge_int_4(): response = client.get("/path/param-le-ge-int/4") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["path", "item_id"], - "msg": "Input should be less than or equal to 3", - "input": "4", - "ctx": {"le": 3}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3}, + } + ] + } def test_path_param_le_ge_int_2_7(): response = client.get("/path/param-le-ge-int/2.7") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "2.7", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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": "2.7", + } + ] + } diff --git a/tests/test_query.py b/tests/test_query.py index 57f551d2ab..c25960caca 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -9,29 +8,16 @@ client = TestClient(app) def test_query(): response = client.get("/query") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_query_baz(): @@ -43,29 +29,16 @@ def test_query_query_baz(): def test_query_not_declared_baz(): response = client.get("/query?not_declared=baz") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_optional(): @@ -89,29 +62,16 @@ def test_query_optional_not_declared_baz(): def test_query_int(): response = client.get("/query/int") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_int_query_42(): @@ -123,85 +83,46 @@ def test_query_int_query_42(): def test_query_int_query_42_5(): response = client.get("/query/int?query=42.5") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "42.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + } + ] + } def test_query_int_query_baz(): response = client.get("/query/int?query=baz") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "baz", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "baz", + } + ] + } def test_query_int_not_declared_baz(): response = client.get("/query/int?not_declared=baz") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_int_optional(): @@ -219,29 +140,16 @@ def test_query_int_optional_query_50(): def test_query_int_optional_query_foo(): response = client.get("/query/int/optional?query=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_query_int_default(): @@ -259,29 +167,16 @@ def test_query_int_default_query_50(): def test_query_int_default_query_foo(): response = client.get("/query/int/default?query=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_query_param(): @@ -299,29 +194,16 @@ def test_query_param_query_50(): def test_query_param_required(): response = client.get("/query/param-required") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_param_required_query_50(): @@ -333,29 +215,16 @@ def test_query_param_required_query_50(): def test_query_param_required_int(): response = client.get("/query/param-required/int") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "query"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + } + ] + } def test_query_param_required_int_query_50(): @@ -367,29 +236,16 @@ def test_query_param_required_int_query_50(): def test_query_param_required_int_query_foo(): response = client.get("/query/param-required/int?query=foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["query", "query"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_query_frozenset_query_1_query_1_query_2(): diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py index 9d58c5dae1..5b4daa450f 100644 --- a/tests/test_regex_deprecated_body.py +++ b/tests/test_regex_deprecated_body.py @@ -1,10 +1,10 @@ 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 inline_snapshot import snapshot from .utils import needs_py310 @@ -47,31 +47,17 @@ def test_query_nonregexquery(): client = get_client() response = client.post("/items/", data={"q": "nonregexquery"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "q"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["body", "q"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } @needs_py310 @@ -79,104 +65,88 @@ def test_openapi_schema(): client = get_client() response = client.get("/openapi.json") assert response.status_code == 200, response.text - # insert_assert(response.json()) - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Read Items", - "operationId": "read_items_items__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Read Items", + "operationId": "read_items_items__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { "$ref": "#/components/schemas/Body_read_items_items__post" } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - }, + } } - } - }, - "components": { - "schemas": { - "Body_read_items_items__post": { - "properties": { - "q": IsDict( - { + }, + "components": { + "schemas": { + "Body_read_items_items__post": { + "properties": { + "q": { "anyOf": [ {"type": "string", "pattern": "^fixedquery$"}, {"type": "null"}, ], "title": "Q", } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"type": "string", "pattern": "^fixedquery$", "title": "Q"} - ) - }, - "type": "object", - "title": "Body_read_items_items__post", - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, + "type": "object", + "title": "Body_read_items_items__post", }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py index 8973657a90..d6eaa45fb1 100644 --- a/tests/test_regex_deprecated_params.py +++ b/tests/test_regex_deprecated_params.py @@ -1,7 +1,6 @@ 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 @@ -47,31 +46,17 @@ def test_query_params_str_validations_item_query_nonregexquery(): client = get_client() response = client.get("/items/", params={"q": "nonregexquery"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["query", "q"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "q"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } @needs_py310 @@ -93,23 +78,13 @@ def test_openapi_schema(): "name": "q", "in": "query", "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "string", "pattern": "^fixedquery$"}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "pattern": "^fixedquery$", - "title": "Q", - } - ), + "schema": { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + }, } ], "responses": { diff --git a/tests/test_request_params/test_body/test_list.py b/tests/test_request_params/test_body/test_list.py index 0048da0f8b..50847335ce 100644 --- a/tests/test_request_params/test_body/test_list.py +++ b/tests/test_request_params/test_body/test_list.py @@ -148,29 +148,16 @@ def test_required_list_alias_missing(path: str, json: Union[dict, None]): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -181,29 +168,16 @@ def test_required_list_alias_by_name(path: str): client = TestClient(app) response = client.post(path, json={"p": ["hello", "world"]}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": ["hello", "world"]}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_body/test_optional_list.py b/tests/test_request_params/test_body/test_optional_list.py index 960e8890f3..ba8ba9092e 100644 --- a/tests/test_request_params/test_body/test_optional_list.py +++ b/tests/test_request_params/test_body/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -38,30 +37,19 @@ def test_optional_list_str_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p": {"items": {"type": "string"}, "type": "array", "title": "P"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_list_str_missing(): @@ -75,29 +63,16 @@ def test_model_optional_list_str_missing(): client = TestClient(app) response = client.post("/model-optional-list-str") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -153,34 +128,19 @@ def test_optional_list_str_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_list_alias_missing(): @@ -194,29 +154,16 @@ def test_model_optional_list_alias_missing(): client = TestClient(app) response = client.post("/model-optional-list-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -289,34 +236,19 @@ def test_optional_list_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Val Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_list_validation_alias_missing(): @@ -330,29 +262,16 @@ def test_model_optional_list_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-list-validation-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -438,34 +357,19 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Val Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_list_alias_and_validation_alias_missing(): @@ -479,29 +383,16 @@ def test_model_optional_list_alias_and_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-list-alias-and-validation-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_body/test_optional_str.py b/tests/test_request_params/test_body/test_optional_str.py index 59732688a2..b9c18034da 100644 --- a/tests/test_request_params/test_body/test_optional_str.py +++ b/tests/test_request_params/test_body/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -36,27 +35,16 @@ def test_optional_str_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p": {"type": "string", "title": "P"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_str_missing(): @@ -70,29 +58,16 @@ def test_model_optional_str_missing(): client = TestClient(app) response = client.post("/model-optional-str") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -148,27 +123,16 @@ def test_optional_str_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_alias": {"type": "string", "title": "P Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_alias_missing(): @@ -182,29 +146,16 @@ def test_model_optional_alias_missing(): client = TestClient(app) response = client.post("/model-optional-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -274,27 +225,16 @@ def test_optional_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": {"type": "string", "title": "P Val Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_validation_alias_missing(): @@ -308,29 +248,16 @@ def test_model_optional_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-validation-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( @@ -413,27 +340,16 @@ def test_optional_alias_and_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": {"type": "string", "title": "P Val Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } def test_optional_alias_and_validation_alias_missing(): @@ -447,29 +363,16 @@ def test_model_optional_alias_and_validation_alias_missing(): client = TestClient(app) response = client.post("/model-optional-alias-and-validation-alias") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "input": None, - "loc": ["body"], - "msg": "Field required", - "type": "missing", - }, - ], - } - ) | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - } - ) + assert response.json() == { + "detail": [ + { + "input": None, + "loc": ["body"], + "msg": "Field required", + "type": "missing", + }, + ], + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_body/test_required_str.py b/tests/test_request_params/test_body/test_required_str.py index 5571ba5d5a..5b434fa1db 100644 --- a/tests/test_request_params/test_body/test_required_str.py +++ b/tests/test_request_params/test_body/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated, Any, Union import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -55,29 +55,16 @@ def test_required_str_missing(path: str, json: Union[dict[str, Any], None]): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": IsOneOf(["body"], ["body", "p"]), - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": IsOneOf(["body"], ["body", "p"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body"], ["body", "p"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -141,29 +128,16 @@ def test_required_alias_missing(path: str, json: Union[dict[str, Any], None]): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p_alias"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -174,29 +148,16 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.post(path, json={"p": "hello"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": "hello"}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": IsOneOf(["body", "p_alias"], ["body"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_cookie/test_optional_str.py b/tests/test_request_params/test_cookie/test_optional_str.py index b2f7f9cef5..6f381c8b86 100644 --- a/tests/test_request_params/test_cookie/test_optional_str.py +++ b/tests/test_request_params/test_cookie/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import Cookie, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -32,26 +31,15 @@ async def read_model_optional_str(p: Annotated[CookieModelOptionalStr, Cookie()] ) def test_optional_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, - "name": "p", - "in": "cookie", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P", "type": "string"}, - "name": "p", - "in": "cookie", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "cookie", + } ] @@ -104,26 +92,15 @@ async def read_model_optional_alias(p: Annotated[CookieModelOptionalAlias, Cooki ) def test_optional_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, - "name": "p_alias", - "in": "cookie", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P Alias", "type": "string"}, - "name": "p_alias", - "in": "cookie", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "cookie", + } ] diff --git a/tests/test_request_params/test_cookie/test_required_str.py b/tests/test_request_params/test_cookie/test_required_str.py index 58bb7af5b9..3e877b3e3d 100644 --- a/tests/test_request_params/test_cookie/test_required_str.py +++ b/tests/test_request_params/test_cookie/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import Cookie, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -49,29 +49,16 @@ def test_required_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["cookie", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -127,29 +114,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["cookie", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -164,32 +138,19 @@ def test_required_alias_by_name(path: str): client.cookies.set("p", "hello") response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "p_alias"], - "msg": "Field required", - "input": IsOneOf( - None, - {"p": "hello"}, - ), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["cookie", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "p_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, + ), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py index 90f36e5f7c..f096532554 100644 --- a/tests/test_request_params/test_file/test_list.py +++ b/tests/test_request_params/test_file/test_list.py @@ -75,29 +75,16 @@ def test_list_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -182,29 +169,16 @@ def test_list_alias_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -218,29 +192,16 @@ def test_list_alias_by_name(path: str): client = TestClient(app) response = client.post(path, files=[("p", b"hello"), ("p", b"world")]) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_file/test_optional.py b/tests/test_request_params/test_file/test_optional.py index 4e9564873c..45ef7bdec4 100644 --- a/tests/test_request_params/test_file/test_optional.py +++ b/tests/test_request_params/test_file/test_optional.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient @@ -36,21 +35,13 @@ def test_optional_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p": ( - IsDict( - { - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - "title": "P", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "P", "type": "string", "format": "binary"} - ) - ), + "p": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P", + } }, "title": body_model_name, "type": "object", @@ -116,21 +107,13 @@ def test_optional_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_alias": ( - IsDict( - { - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - "title": "P Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "P Alias", "type": "string", "format": "binary"} - ) - ), + "p_alias": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Alias", + } }, "title": body_model_name, "type": "object", @@ -215,21 +198,13 @@ def test_optional_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( - { - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - "title": "P Val Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "P Val Alias", "type": "string", "format": "binary"} - ) - ), + "p_val_alias": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Val Alias", + } }, "title": body_model_name, "type": "object", @@ -319,21 +294,13 @@ def test_optional_alias_and_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( - { - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - "title": "P Val Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "P Val Alias", "type": "string", "format": "binary"} - ) - ), + "p_val_alias": { + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + "title": "P Val Alias", + } }, "title": body_model_name, "type": "object", diff --git a/tests/test_request_params/test_file/test_optional_list.py b/tests/test_request_params/test_file/test_optional_list.py index e18f36e152..162fbe08ae 100644 --- a/tests/test_request_params/test_file/test_optional_list.py +++ b/tests/test_request_params/test_file/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient @@ -38,28 +37,16 @@ def test_optional_list_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p": ( - IsDict( + "p": { + "anyOf": [ { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "P", "type": "array", "items": {"type": "string", "format": "binary"}, }, - ) - ), + {"type": "null"}, + ], + "title": "P", + } }, "title": body_model_name, "type": "object", @@ -125,28 +112,16 @@ def test_optional_list_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_alias": ( - IsDict( + "p_alias": { + "anyOf": [ { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "P Alias", "type": "array", "items": {"type": "string", "format": "binary"}, - } - ) - ), + }, + {"type": "null"}, + ], + "title": "P Alias", + } }, "title": body_model_name, "type": "object", @@ -228,28 +203,16 @@ def test_optional_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( + "p_val_alias": { + "anyOf": [ { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Val Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "P Val Alias", "type": "array", "items": {"type": "string", "format": "binary"}, - } - ) - ), + }, + {"type": "null"}, + ], + "title": "P Val Alias", + } }, "title": body_model_name, "type": "object", @@ -336,28 +299,16 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( + "p_val_alias": { + "anyOf": [ { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Val Alias", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "P Val Alias", "type": "array", "items": {"type": "string", "format": "binary"}, - } - ) - ), + }, + {"type": "null"}, + ], + "title": "P Val Alias", + } }, "title": body_model_name, "type": "object", diff --git a/tests/test_request_params/test_file/test_required.py b/tests/test_request_params/test_file/test_required.py index 9783f4bceb..a0f9d23a6b 100644 --- a/tests/test_request_params/test_file/test_required.py +++ b/tests/test_request_params/test_file/test_required.py @@ -1,7 +1,6 @@ from typing import Annotated import pytest -from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient @@ -55,29 +54,16 @@ def test_required_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -142,29 +128,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -178,29 +151,16 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.post(path, files=[("p", b"hello")]) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_form/test_list.py b/tests/test_request_params/test_form/test_list.py index 600dba4ae9..cfc42f523a 100644 --- a/tests/test_request_params/test_form/test_list.py +++ b/tests/test_request_params/test_form/test_list.py @@ -146,29 +146,16 @@ def test_required_list_alias_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -182,29 +169,16 @@ def test_required_list_alias_by_name(path: str): client = TestClient(app) response = client.post(path, data={"p": ["hello", "world"]}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": ["hello", "world"]}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_form/test_optional_list.py b/tests/test_request_params/test_form/test_optional_list.py index 4552623f51..6d1957a18c 100644 --- a/tests/test_request_params/test_form/test_optional_list.py +++ b/tests/test_request_params/test_form/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -38,30 +37,19 @@ def test_optional_list_str_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p": {"items": {"type": "string"}, "type": "array", "title": "P"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -119,34 +107,19 @@ def test_optional_list_str_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -217,34 +190,19 @@ def test_optional_list_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Val Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -326,34 +284,19 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": { - "items": {"type": "string"}, - "type": "array", - "title": "P Val Alias", - }, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_form/test_optional_str.py b/tests/test_request_params/test_form/test_optional_str.py index 1b08299046..810e83caa3 100644 --- a/tests/test_request_params/test_form/test_optional_str.py +++ b/tests/test_request_params/test_form/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -36,27 +35,16 @@ def test_optional_str_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p": {"type": "string", "title": "P"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -112,27 +100,16 @@ def test_optional_str_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_alias": {"type": "string", "title": "P Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -200,27 +177,16 @@ def test_optional_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": {"type": "string", "title": "P Val Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( @@ -303,27 +269,16 @@ def test_optional_alias_and_validation_alias_schema(path: str): openapi = app.openapi() body_model_name = get_body_model_name(openapi, path) - assert app.openapi()["components"]["schemas"][body_model_name] == IsDict( - { - "properties": { - "p_val_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Val Alias", - }, + assert app.openapi()["components"]["schemas"][body_model_name] == { + "properties": { + "p_val_alias": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Val Alias", }, - "title": body_model_name, - "type": "object", - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "properties": { - "p_val_alias": {"type": "string", "title": "P Val Alias"}, - }, - "title": body_model_name, - "type": "object", - } - ) + }, + "title": body_model_name, + "type": "object", + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_form/test_required_str.py b/tests/test_request_params/test_form/test_required_str.py index 1d2431b333..7c9523b308 100644 --- a/tests/test_request_params/test_form/test_required_str.py +++ b/tests/test_request_params/test_form/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -54,29 +54,16 @@ def test_required_str_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -137,29 +124,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -170,29 +144,16 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.post(path, data={"p": "hello"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": "hello"}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": "hello"}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_list.py b/tests/test_request_params/test_header/test_list.py index 2eba17559e..65510094af 100644 --- a/tests/test_request_params/test_header/test_list.py +++ b/tests/test_request_params/test_header/test_list.py @@ -135,29 +135,16 @@ def test_required_list_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p_alias"], - "msg": "Field required", - "input": AnyThing, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": AnyThing, + } + ] + } @pytest.mark.parametrize( @@ -171,29 +158,16 @@ def test_required_list_alias_by_name(path: str): client = TestClient(app) response = client.get(path, headers=[("p", "hello"), ("p", "world")]) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_optional_list.py b/tests/test_request_params/test_header/test_optional_list.py index cd6167a183..5dd4ea9ade 100644 --- a/tests/test_request_params/test_header/test_optional_list.py +++ b/tests/test_request_params/test_header/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -36,29 +35,18 @@ async def read_model_optional_list_str( ) def test_optional_list_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P", - }, - "name": "p", - "in": "header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"items": {"type": "string"}, "type": "array", "title": "P"}, - "name": "p", - "in": "header", - } - ) + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "header", + } ] @@ -112,33 +100,18 @@ async def read_model_optional_list_alias( ) def test_optional_list_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Alias", - }, - "name": "p_alias", - "in": "header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": { - "items": {"type": "string"}, - "type": "array", - "title": "P Alias", - }, - "name": "p_alias", - "in": "header", - } - ) + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } ] diff --git a/tests/test_request_params/test_header/test_optional_str.py b/tests/test_request_params/test_header/test_optional_str.py index d4f25cc1e7..0bd0eddc1b 100644 --- a/tests/test_request_params/test_header/test_optional_str.py +++ b/tests/test_request_params/test_header/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -32,26 +31,15 @@ async def read_model_optional_str(p: Annotated[HeaderModelOptionalStr, Header()] ) def test_optional_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, - "name": "p", - "in": "header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P", "type": "string"}, - "name": "p", - "in": "header", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "header", + } ] @@ -103,26 +91,15 @@ async def read_model_optional_alias(p: Annotated[HeaderModelOptionalAlias, Heade ) def test_optional_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, - "name": "p_alias", - "in": "header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P Alias", "type": "string"}, - "name": "p_alias", - "in": "header", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "header", + } ] diff --git a/tests/test_request_params/test_header/test_required_str.py b/tests/test_request_params/test_header/test_required_str.py index 85bb43d5a8..20dd296570 100644 --- a/tests/test_request_params/test_header/test_required_str.py +++ b/tests/test_request_params/test_header/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict +from dirty_equals import AnyThing, IsOneOf, IsPartialDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -49,29 +49,16 @@ def test_required_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p"], - "msg": "Field required", - "input": AnyThing, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p"], + "msg": "Field required", + "input": AnyThing, + } + ] + } @pytest.mark.parametrize( @@ -126,29 +113,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p_alias"], - "msg": "Field required", - "input": AnyThing, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": AnyThing, + } + ] + } @pytest.mark.parametrize( @@ -162,29 +136,16 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.get(path, headers={"p": "hello"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, IsPartialDict({"p": "hello"})), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, IsPartialDict({"p": "hello"})), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_list.py b/tests/test_request_params/test_query/test_list.py index dc21a85006..ed2ea6c809 100644 --- a/tests/test_request_params/test_query/test_list.py +++ b/tests/test_request_params/test_query/test_list.py @@ -135,29 +135,16 @@ def test_required_list_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -171,29 +158,16 @@ def test_required_list_alias_by_name(path: str): client = TestClient(app) response = client.get(f"{path}?p=hello&p=world") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {"p": ["hello", "world"]}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {"p": ["hello", "world"]}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_optional_list.py b/tests/test_request_params/test_query/test_optional_list.py index 2a8f63a36a..351e03a713 100644 --- a/tests/test_request_params/test_query/test_optional_list.py +++ b/tests/test_request_params/test_query/test_optional_list.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -36,29 +35,18 @@ async def read_model_optional_list_str( ) def test_optional_list_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P", - }, - "name": "p", - "in": "query", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"items": {"type": "string"}, "type": "array", "title": "P"}, - "name": "p", - "in": "query", - } - ) + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P", + }, + "name": "p", + "in": "query", + } ] @@ -112,33 +100,18 @@ async def read_model_optional_list_alias( ) def test_optional_list_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [ - {"items": {"type": "string"}, "type": "array"}, - {"type": "null"}, - ], - "title": "P Alias", - }, - "name": "p_alias", - "in": "query", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": { - "items": {"type": "string"}, - "type": "array", - "title": "P Alias", - }, - "name": "p_alias", - "in": "query", - } - ) + { + "required": False, + "schema": { + "anyOf": [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } ] diff --git a/tests/test_request_params/test_query/test_optional_str.py b/tests/test_request_params/test_query/test_optional_str.py index c6a70bc283..12e1b465a7 100644 --- a/tests/test_request_params/test_query/test_optional_str.py +++ b/tests/test_request_params/test_query/test_optional_str.py @@ -1,7 +1,6 @@ from typing import Annotated, Optional import pytest -from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -32,26 +31,15 @@ async def read_model_optional_str(p: Annotated[QueryModelOptionalStr, Query()]): ) def test_optional_str_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P", - }, - "name": "p", - "in": "query", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P", "type": "string"}, - "name": "p", - "in": "query", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P", + }, + "name": "p", + "in": "query", + } ] @@ -103,26 +91,15 @@ async def read_model_optional_alias(p: Annotated[QueryModelOptionalAlias, Query( ) def test_optional_str_alias_schema(path: str): assert app.openapi()["paths"][path]["get"]["parameters"] == [ - IsDict( - { - "required": False, - "schema": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "P Alias", - }, - "name": "p_alias", - "in": "query", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "required": False, - "schema": {"title": "P Alias", "type": "string"}, - "name": "p_alias", - "in": "query", - } - ) + { + "required": False, + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "P Alias", + }, + "name": "p_alias", + "in": "query", + } ] diff --git a/tests/test_request_params/test_query/test_required_str.py b/tests/test_request_params/test_query/test_required_str.py index 2ef1b0373d..9e7b961453 100644 --- a/tests/test_request_params/test_query/test_required_str.py +++ b/tests/test_request_params/test_query/test_required_str.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -49,29 +49,16 @@ def test_required_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -126,29 +113,16 @@ def test_required_alias_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p_alias"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( @@ -162,32 +136,19 @@ def test_required_alias_by_name(path: str): client = TestClient(app) response = client.get(f"{path}?p=hello") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p_alias"], - "msg": "Field required", - "input": IsOneOf( - None, - {"p": "hello"}, - ), - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "p_alias"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p_alias"], + "msg": "Field required", + "input": IsOneOf( + None, + {"p": "hello"}, + ), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 8f5195ba11..ac8999c90a 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,7 +1,6 @@ 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 @@ -336,28 +335,13 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "Data in Body examples, example1"}, - {"data": "Data in Body examples, example2"}, - ], - } - ) - | IsDict( - # TODO: remove this when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"}, - {"data": "Data in Body examples, example2"}, - ], - } - ) + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } } }, "required": True, @@ -387,28 +371,13 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - {"data": "examples example_examples 1"}, - {"data": "examples example_examples 2"}, - ], - } - ) - | IsDict( - # TODO: remove this when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - {"data": "examples example_examples 1"}, - {"data": "examples example_examples 2"}, - ], - }, - ), + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + }, "example": {"data": "Overridden example"}, } }, @@ -539,16 +508,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - {"title": "Data", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, "example": "query1", "name": "data", "in": "query", @@ -579,21 +542,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["query1", "query2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + }, "name": "data", "in": "query", } @@ -623,21 +576,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["query1", "query2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + }, "example": "query_overridden", "name": "data", "in": "query", @@ -668,16 +611,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - {"title": "Data", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, "example": "header1", "name": "data", "in": "header", @@ -708,21 +645,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["header1", "header2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "type": "string", - "title": "Data", - "examples": ["header1", "header2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + }, "name": "data", "in": "header", } @@ -752,21 +679,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["header1", "header2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "title": "Data", - "type": "string", - "examples": ["header1", "header2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + }, "example": "header_overridden", "name": "data", "in": "header", @@ -797,16 +714,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - {"title": "Data", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + }, "example": "cookie1", "name": "data", "in": "cookie", @@ -837,21 +748,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["cookie1", "cookie2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "title": "Data", - "type": "string", - "examples": ["cookie1", "cookie2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + }, "name": "data", "in": "cookie", } @@ -881,21 +782,11 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Data", - "examples": ["cookie1", "cookie2"], - } - ) - | IsDict( - # TODO: Remove this when deprecating Pydantic v1 - { - "title": "Data", - "type": "string", - "examples": ["cookie1", "cookie2"], - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + }, "example": "cookie_overridden", "name": "data", "in": "cookie", diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 804e4152db..7ad9369956 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -1,5 +1,4 @@ import pytest -from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient @@ -64,79 +63,43 @@ def test_security_oauth2_password_bearer_no_header(): def test_strict_login_no_data(): response = client.post("/login") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_strict_login_no_grant_type(): response = client.post("/login", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -153,31 +116,17 @@ def test_strict_login_incorrect_grant_type(grant_type: str): data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "grant_type"], - "msg": "String should match pattern '^password$'", - "input": grant_type, - "ctx": {"pattern": "^password$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "^password$"', - "type": "value_error.str.regex", - "ctx": {"pattern": "^password$"}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, + } + ] + } def test_strict_login_correct_grant_type(): @@ -264,26 +213,14 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 046ac57637..57c16058af 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -1,7 +1,6 @@ from typing import Optional import pytest -from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient @@ -67,79 +66,43 @@ def test_security_oauth2_password_bearer_no_header(): def test_strict_login_no_data(): response = client.post("/login") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_strict_login_no_grant_type(): response = client.post("/login", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -156,31 +119,17 @@ def test_strict_login_incorrect_grant_type(grant_type: str): data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "grant_type"], - "msg": "String should match pattern '^password$'", - "input": grant_type, - "ctx": {"pattern": "^password$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "^password$"', - "type": "value_error.str.regex", - "ctx": {"pattern": "^password$"}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, + } + ] + } def test_strict_login_correct_data(): @@ -267,26 +216,14 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index 629cddca2f..60c6c242e0 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -1,7 +1,6 @@ from typing import Optional import pytest -from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient @@ -68,79 +67,43 @@ def test_security_oauth2_password_bearer_no_header(): def test_strict_login_None(): response = client.post("/login", data=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_strict_login_no_grant_type(): response = client.post("/login", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "grant_type"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + } + ] + } @pytest.mark.parametrize( @@ -157,31 +120,17 @@ def test_strict_login_incorrect_grant_type(grant_type: str): data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["body", "grant_type"], - "msg": "String should match pattern '^password$'", - "input": grant_type, - "ctx": {"pattern": "^password$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "^password$"', - "type": "value_error.str.regex", - "ctx": {"pattern": "^password$"}, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, + } + ] + } def test_strict_login_correct_correct_grant_type(): @@ -268,26 +217,14 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index ed7f4efe8a..cc7e5f5c6a 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -1,6 +1,5 @@ from typing import Optional -from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, HttpUrl @@ -99,30 +98,18 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": IsDict( - { - "title": "Callback Url", - "anyOf": [ - { - "type": "string", - "format": "uri", - "minLength": 1, - "maxLength": 2083, - }, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - } - ), + "schema": { + "title": "Callback Url", + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + }, "name": "callback_url", "in": "query", } @@ -262,16 +249,10 @@ def test_openapi_schema(): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": IsDict( - { - "title": "Title", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Title", "type": "string"} - ), + "title": { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_tuples.py b/tests/test_tuples.py index fbc69a6145..d3c89045b4 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel @@ -125,31 +124,16 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } - ) + "schema": { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } } }, "required": True, @@ -212,28 +196,16 @@ def test_openapi_schema(): "required": ["values"], "type": "object", "properties": { - "values": IsDict( - { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"type": "integer"}, - {"type": "integer"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "integer"}, {"type": "integer"}], - } - ) + "values": { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "integer"}, + {"type": "integer"}, + ], + } }, }, "Coordinate": { @@ -264,26 +236,15 @@ def test_openapi_schema(): "items": { "title": "Items", "type": "array", - "items": IsDict( - { - "maxItems": 2, - "minItems": 2, - "type": "array", - "prefixItems": [ - {"type": "string"}, - {"type": "string"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "string"}, {"type": "string"}], - } - ), + "items": { + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "string"}, + {"type": "string"}, + ], + }, } }, }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index bbcad8f294..8208605956 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -3,7 +3,6 @@ import os import shutil import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -80,16 +79,10 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "boolean"}, {"type": "null"}], - "title": "Img", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Img", "type": "boolean"} - ), + "schema": { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + }, "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index cbd4fff7d6..c6abf5e466 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -3,7 +3,6 @@ import os import shutil import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -83,16 +82,10 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "boolean"}, {"type": "null"}], - "title": "Img", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Img", "type": "boolean"} - ), + "schema": { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + }, "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index 2d1d1b03c8..a164bb80b5 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -1,5 +1,5 @@ -from dirty_equals import IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.behind_a_proxy.tutorial003_py39 import app @@ -15,40 +15,34 @@ def test_main(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - {"url": "/api/v1"}, - { - "url": IsOneOf( - "https://stag.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://stag.example.com", - ), - "description": "Staging environment", - }, - { - "url": IsOneOf( - "https://prod.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://prod.example.com", - ), - "description": "Production environment", - }, - ], - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/api/v1"}, + { + "url": "https://stag.example.com", + "description": "Staging environment", + }, + { + "url": "https://prod.example.com", + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index e8a03e8112..01bba9fedd 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -1,5 +1,5 @@ -from dirty_equals import IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from docs_src.behind_a_proxy.tutorial004_py39 import app @@ -15,39 +15,33 @@ def test_main(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - { - "url": IsOneOf( - "https://stag.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://stag.example.com", - ), - "description": "Staging environment", - }, - { - "url": IsOneOf( - "https://prod.example.com/", - # TODO: remove when deprecating Pydantic v1 - "https://prod.example.com", - ), - "description": "Production environment", - }, - ], - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + { + "url": "https://stag.example.com", + "description": "Staging environment", + }, + { + "url": "https://prod.example.com", + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index 7493a9e661..f5e243b95a 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -28,29 +27,16 @@ def test_users_token_jessica(client: TestClient): def test_users_with_no_token(client: TestClient): response = client.get("/users") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_users_foo_token_jessica(client: TestClient): @@ -62,29 +48,16 @@ def test_users_foo_token_jessica(client: TestClient): def test_users_foo_with_no_token(client: TestClient): response = client.get("/users/foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_users_me_token_jessica(client: TestClient): @@ -96,29 +69,16 @@ def test_users_me_token_jessica(client: TestClient): def test_users_me_with_no_token(client: TestClient): response = client.get("/users/me") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_users_token_monica_with_no_jessica(client: TestClient): @@ -141,29 +101,16 @@ def test_items_token_jessica(client: TestClient): def test_items_with_no_token_jessica(client: TestClient): response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_items_plumbus_token_jessica(client: TestClient): @@ -187,29 +134,16 @@ def test_items_plumbus_with_no_token(client: TestClient): "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} ) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_items_with_invalid_token(client: TestClient): @@ -227,57 +161,31 @@ def test_items_bar_with_invalid_token(client: TestClient): def test_items_with_missing_x_token_header(client: TestClient): response = client.get("/items?token=jessica") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + } + ] + } def test_items_plumbus_with_missing_x_token_header(client: TestClient): response = client.get("/items/plumbus?token=jessica") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + } + ] + } def test_root_token_jessica(client: TestClient): @@ -289,68 +197,37 @@ def test_root_token_jessica(client: TestClient): def test_root_with_no_token(client: TestClient): response = client.get("/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + } + ] + } def test_put_no_header(client: TestClient): response = client.put("/items/foo") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_put_invalid_header(client: TestClient): diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 6aa9f2593e..5a7cae1603 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -2,7 +2,6 @@ import importlib from unittest.mock import patch import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -74,124 +73,67 @@ def test_post_with_str_float_description_tax(client: TestClient): def test_post_with_only_name(client: TestClient): response = client.post("/items/", json={"name": "Foo"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "price"], - "msg": "Field required", - "input": {"name": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {"name": "Foo"}, + } + ] + } def test_post_with_only_name_price(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["body", "price"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "twenty", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "twenty", + } + ] + } def test_post_with_no_data(client: TestClient): response = client.post("/items/", json={}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "name"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "price"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + }, + ] + } def test_post_with_none(client: TestClient): response = client.post("/items/", json=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_broken_body(client: TestClient): @@ -201,67 +143,32 @@ def test_post_broken_body(client: TestClient): content="{some broken json}", ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "json_invalid", - "loc": ["body", 1], - "msg": "JSON decode error", - "input": {}, - "ctx": { - "error": "Expecting property name enclosed in double quotes" - }, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "json_invalid", + "loc": ["body", 1], + "msg": "JSON decode error", + "input": {}, + "ctx": {"error": "Expecting property name enclosed in double quotes"}, + } + ] + } def test_post_form_for_json(client: TestClient): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": "name=Foo&price=50.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": "name=Foo&price=50.5", + } + ] + } def test_explicit_content_type(client: TestClient): @@ -302,84 +209,46 @@ def test_wrong_headers(client: TestClient): "/items/", content=data, headers={"Content-Type": "text/plain"} ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + } + ] + } response = client.post( "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + } + ] + } + response = client.post( "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + } + ] + } def test_other_exceptions(client: TestClient): @@ -435,26 +304,14 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index d54ec7191a..0ecadbb660 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -59,31 +58,17 @@ def test_items_6(client: TestClient): def test_invalid_price(client: TestClient): response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", "item", "price"], - "msg": "Input should be greater than 0", - "input": -3.0, - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + } + ] + } def test_openapi_schema(client: TestClient): @@ -142,39 +127,23 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "The description of the item", - "anyOf": [ - {"maxLength": 300, "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - } - ), + "description": { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + }, "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 2035cf9448..63c9c16d62 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -53,29 +52,16 @@ def test_post_no_body(client: TestClient): def test_post_id_foo(client: TestClient): response = client.put("/items/foo", json=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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): @@ -119,16 +105,10 @@ def test_openapi_schema(client: TestClient): }, { "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", }, @@ -136,19 +116,13 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Item"}, - {"type": "null"}, - ], - "title": "Item", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Item"} - ) + "schema": { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } } } }, @@ -163,27 +137,15 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index d3e6401af2..76b7ff7099 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -49,101 +48,55 @@ def test_post_body_valid(client: TestClient): def test_post_body_no_data(client: TestClient): response = client.put("/items/5", json=None) assert response.status_code == 422 - assert response.json() == IsDict( - { - "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, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + 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_post_body_empty_list(client: TestClient): response = client.put("/items/5", json=[]) assert response.status_code == 422 - assert response.json() == IsDict( - { - "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, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + 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_openapi_schema(client: TestClient): @@ -202,27 +155,15 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "User": { @@ -231,16 +172,10 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), + "full_name": { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index db9f04546e..f2e56d40fb 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -29,29 +28,16 @@ def test_post_invalid_body(client: TestClient): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "foo", "[key]"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "foo", "[key]"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + } + ] + } def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py index 265dee944e..ac8e7bdae1 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -54,30 +53,16 @@ def test_cookie_param_model_invalid(client: TestClient): response = client.get("/items/") assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["cookie", "session_id"], - "msg": "Field required", - "input": {}, - } - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.missing", - "loc": ["cookie", "session_id"], - "msg": "field required", - } - ] - } - ) + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "session_id"], + "msg": "Field required", + "input": {}, + } + ] + } ) @@ -115,37 +100,19 @@ def test_openapi_schema(client: TestClient): "name": "fatebook_tracker", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Fatebook Tracker", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Fatebook Tracker", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Fatebook Tracker", + }, }, { "name": "googall_tracker", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Googall Tracker", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Googall Tracker", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + }, }, ], "responses": { diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py index 0fbf141e05..d7c3d15f1b 100644 --- a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -72,30 +71,16 @@ def test_cookie_param_model_extra(client: TestClient): response = c.get("/items/") assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "extra_forbidden", - "loc": ["cookie", "extra"], - "msg": "Extra inputs are not permitted", - "input": "track-me-here-too", - } - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.extra", - "loc": ["cookie", "extra"], - "msg": "extra fields not permitted", - } - ] - } - ) + { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["cookie", "extra"], + "msg": "Extra inputs are not permitted", + "input": "track-me-here-too", + } + ] + } ) @@ -134,19 +119,10 @@ def test_openapi_schema(client: TestClient): "name": "googall_tracker", "in": "cookie", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Googall Tracker", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Googall Tracker", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + }, }, ], "responses": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index a65249d657..9b47cbc67a 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -2,7 +2,6 @@ import importlib from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -75,16 +74,10 @@ def test_openapi_schema(mod: ModuleType): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Ads Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Ads Id", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + }, "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py index 643011637e..a9c7ae638b 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py @@ -1,7 +1,7 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -30,34 +30,17 @@ def test_endpoint_works(client: TestClient): def test_exception_handler_body_access(client: TestClient): response = client.post("/", json={"numbers": [1, 2, 3]}) - assert response.json() == IsDict( - { - "detail": { - "errors": [ - { - "type": "list_type", - "loc": ["body"], - "msg": "Input should be a valid list", - "input": {"numbers": [1, 2, 3]}, - } - ], - # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 - "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), - } + assert response.json() == { + "detail": { + "errors": [ + { + "type": "list_type", + "loc": ["body"], + "msg": "Input should be a valid list", + "input": {"numbers": [1, 2, 3]}, + } + ], + # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 + "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": { - # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 - "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), - "errors": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ], - } - } - ) + } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index bc407234a1..4683062f59 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -36,29 +35,16 @@ def test_post_item(client: TestClient): def test_post_invalid_item(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": "invalid price"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["body", "price"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "invalid price", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "invalid price", + } + ] + } def test_openapi_schema(client: TestClient): @@ -119,26 +105,14 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 995d926752..210d743bb8 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from tests.utils import needs_py310 @@ -37,77 +37,53 @@ def test_get_item(client: TestClient): 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/next": { - "get": { - "summary": "Read Next Item", - "operationId": "read_next_item_items_next_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - } - }, + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/next": { + "get": { + "summary": "Read Next Item", + "operationId": "read_next_item_items_next_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + } + }, + } } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "price", "tags", "description", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tags": IsDict( - { + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tags": { "title": "Tags", "type": "array", "items": {"type": "string"}, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - } - ), - "description": IsDict( - { + }, + "description": { "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { + }, + "tax": { "title": "Tax", "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, + }, + }, + } } - } - }, - } + }, + } + ) diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 46f0066f9f..59202df3bf 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -22,40 +21,22 @@ def get_client(request: pytest.FixtureRequest): def test_get_no_headers(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + }, + ] + } def test_get_invalid_one_header(client: TestClient): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index b791ee0aa1..d5599ac73a 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -22,79 +21,43 @@ def get_client(request: pytest.FixtureRequest): def test_get_no_headers_items(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + }, + ] + } def test_get_no_headers_users(client: TestClient): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + }, + ] + } def test_get_invalid_one_header_items(client: TestClient): diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index e11f73fe35..5479e29252 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -47,146 +47,117 @@ def test_extra_types(client: TestClient): 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": {}}}, + assert response.json() == snapshot( + { + "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" + } + } + }, + }, }, - "422": { - "description": "Validation Error", + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "required": True, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" } } }, }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ) - } - }, - }, + } } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": IsDict( - { + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { "title": "Repeat At", "anyOf": [ {"type": "string", "format": "time"}, {"type": "null"}, ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Repeat At", - "type": "string", - "format": "time", - } - ), - "process_after": IsDict( - { + }, + "process_after": { "title": "Process After", "type": "string", "format": "duration", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Process After", - "type": "number", - "format": "time-delta", - } - ), - }, - "required": ["start_datetime", "end_datetime", "process_after"], - }, - "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"}, + "required": ["start_datetime", "end_datetime", "process_after"], }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, }, - }, - } - }, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index 3aa83c0c40..872af53830 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -43,107 +43,115 @@ def test_get_plane(client: TestClient): 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": { - "title": "Response Read Item Items Item Id Get", - "anyOf": [ - {"$ref": "#/components/schemas/PlaneItem"}, - {"$ref": "#/components/schemas/CarItem"}, - ], + assert response.json() == snapshot( + { + "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": { + "title": "Response Read Item Items Item Id Get", + "anyOf": [ + { + "$ref": "#/components/schemas/PlaneItem" + }, + { + "$ref": "#/components/schemas/CarItem" + }, + ], + } } - } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - "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", - } - ], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } } - } - }, - "components": { - "schemas": { - "PlaneItem": { - "title": "PlaneItem", - "required": IsOneOf( - ["description", "type", "size"], - # TODO: remove when deprecating Pydantic v1 - ["description", "size"], - ), - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "plane"}, - "size": {"title": "Size", "type": "integer"}, + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": ["description", "size"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": { + "title": "Type", + "type": "string", + "default": "plane", + }, + "size": {"title": "Size", "type": "integer"}, + }, }, - }, - "CarItem": { - "title": "CarItem", - "required": IsOneOf( - ["description", "type"], - # TODO: remove when deprecating Pydantic v1 - ["description"], - ), - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "car"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] + "CarItem": { + "title": "CarItem", + "required": ["description"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": { + "title": "Type", + "type": "string", + "default": "car", }, }, - "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"}, - } + "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_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index d713c5d876..7bd947f194 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.handling_errors.tutorial005_py39 import app @@ -9,31 +8,17 @@ client = TestClient(app) def test_post_validation_error(): response = client.post("/items/", json={"title": "towel", "size": "XL"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "size"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "XL", - } - ], - "body": {"title": "towel", "size": "XL"}, - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ], - "body": {"title": "towel", "size": "XL"}, - } - ) + assert response.json() == { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "size"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "XL", + } + ], + "body": {"title": "towel", "size": "XL"}, + } def test_post(): diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 491e461b3d..e95e53d5ed 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.handling_errors.tutorial006_py39 import app @@ -9,29 +8,16 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) + 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_get_http_error(): diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial001.py b/tests/test_tutorial/test_header_param_models/test_tutorial001.py index f59d50762c..1fa8aee461 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -63,29 +62,19 @@ def test_header_param_model_invalid(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": { - "x_tag": [], - "host": "testserver", - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "user-agent": "testclient", - }, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "x_tag": [], + "host": "testserver", + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + }, + } ] } ) @@ -136,37 +125,19 @@ def test_openapi_schema(client: TestClient): "name": "if-modified-since", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "If Modified Since", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "If Modified Since", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + }, }, { "name": "traceparent", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Traceparent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Traceparent", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + }, }, { "name": "x-tag", diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial002.py b/tests/test_tutorial/test_header_param_models/test_tutorial002.py index ed4743ebf9..079a8f5402 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -64,22 +63,12 @@ def test_header_param_model_invalid(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": {"x_tag": [], "host": "testserver"}, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": {"x_tag": [], "host": "testserver"}, + } ] } ) @@ -93,22 +82,12 @@ def test_header_param_model_extra(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "extra_forbidden", - "loc": ["header", "tool"], - "msg": "Extra inputs are not permitted", - "input": "plumbus", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.extra", - "loc": ["header", "tool"], - "msg": "extra fields not permitted", - } - ) + { + "type": "extra_forbidden", + "loc": ["header", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } ] } ) @@ -143,37 +122,19 @@ def test_openapi_schema(client: TestClient): "name": "if-modified-since", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "If Modified Since", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "If Modified Since", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + }, }, { "name": "traceparent", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Traceparent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Traceparent", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + }, }, { "name": "x-tag", diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial003.py b/tests/test_tutorial/test_header_param_models/test_tutorial003.py index 947587504f..4c89d80ee2 100644 --- a/tests/test_tutorial/test_header_param_models/test_tutorial003.py +++ b/tests/test_tutorial/test_header_param_models/test_tutorial003.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -60,33 +59,23 @@ def test_header_param_model_no_underscore(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": { - "host": "testserver", - "traceparent": "123", - "x_tag": [], - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "user-agent": "testclient", - "save-data": "true", - "if-modified-since": "yesterday", - "x-tag": ["one", "two"], - }, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "host": "testserver", + "traceparent": "123", + "x_tag": [], + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + "save-data": "true", + "if-modified-since": "yesterday", + "x-tag": ["one", "two"], + }, + } ] } ) @@ -110,29 +99,19 @@ def test_header_param_model_invalid(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "missing", - "loc": ["header", "save_data"], - "msg": "Field required", - "input": { - "x_tag": [], - "host": "testserver", - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "user-agent": "testclient", - }, - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.missing", - "loc": ["header", "save_data"], - "msg": "field required", - } - ) + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "x_tag": [], + "host": "testserver", + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + }, + } ] } ) @@ -183,37 +162,19 @@ def test_openapi_schema(client: TestClient): "name": "if_modified_since", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "If Modified Since", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "If Modified Since", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + }, }, { "name": "traceparent", "in": "header", "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Traceparent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Traceparent", - } - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + }, }, { "name": "x_tag", diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index beaf917f92..88591b8225 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -67,16 +66,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User-Agent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User-Agent", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + }, "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index b892ff905f..229f96c1f8 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -78,16 +77,10 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Strange Header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Strange Header", "type": "string"} - ), + "schema": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + }, "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index ef76244159..cf067ccf9e 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -56,23 +55,13 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "title": "X-Token", - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - } - ), + "schema": { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + }, "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index 975e07cbdd..6fde96cb5b 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -2,7 +2,6 @@ import importlib from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from tests.utils import needs_py310 @@ -55,30 +54,18 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [ - { - "type": "string", - "format": "uri", - "minLength": 1, - "maxLength": 2083, - }, - {"type": "null"}, - ], - "title": "Callback Url", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - } - ), + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + "title": "Callback Url", + }, "name": "callback_url", "in": "query", } @@ -171,16 +158,10 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": IsDict( - { - "title": "Title", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Title", "type": "string"} - ), + "title": { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index b3be70471a..86ccce7b6d 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_params.tutorial005_py39 import app @@ -27,31 +26,17 @@ def test_get_enums_resnet(): def test_get_enums_invalid(): response = client.get("/models/foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "enum", - "loc": ["path", "model_name"], - "msg": "Input should be 'alexnet', 'resnet' or 'lenet'", - "input": "foo", - "ctx": {"expected": "'alexnet', 'resnet' or 'lenet'"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]}, - "loc": ["path", "model_name"], - "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'", - "type": "type_error.enum", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "enum", + "loc": ["path", "model_name"], + "msg": "Input should be 'alexnet', 'resnet' or 'lenet'", + "input": "foo", + "ctx": {"expected": "'alexnet', 'resnet' or 'lenet'"}, + } + ] + } def test_openapi_schema(): @@ -106,22 +91,11 @@ def test_openapi_schema(): } }, }, - "ModelName": IsDict( - { - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - } - ) - | IsDict( - { - # TODO: remove when deprecating Pydantic v1 - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - "description": "An enumeration.", - } - ), + "ModelName": { + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial001.py b/tests/test_tutorial/test_query_param_models/test_tutorial001.py index 86830b9341..d3ce57121d 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial001.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -65,61 +64,31 @@ def test_query_param_model_invalid(client: TestClient): ) assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["query", "limit"], - "msg": "Input should be less than or equal to 100", - "input": "150", - "ctx": {"le": 100}, - }, - { - "type": "greater_than_equal", - "loc": ["query", "offset"], - "msg": "Input should be greater than or equal to 0", - "input": "-1", - "ctx": {"ge": 0}, - }, - { - "type": "literal_error", - "loc": ["query", "order_by"], - "msg": "Input should be 'created_at' or 'updated_at'", - "input": "invalid", - "ctx": {"expected": "'created_at' or 'updated_at'"}, - }, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.number.not_le", - "loc": ["query", "limit"], - "msg": "ensure this value is less than or equal to 100", - "ctx": {"limit_value": 100}, - }, - { - "type": "value_error.number.not_ge", - "loc": ["query", "offset"], - "msg": "ensure this value is greater than or equal to 0", - "ctx": {"limit_value": 0}, - }, - { - "type": "value_error.const", - "loc": ["query", "order_by"], - "msg": "unexpected value; permitted: 'created_at', 'updated_at'", - "ctx": { - "given": "invalid", - "permitted": ["created_at", "updated_at"], - }, - }, - ] - } - ) + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } ) diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial002.py b/tests/test_tutorial/test_query_param_models/test_tutorial002.py index 0e9c3351a0..96abce6ab9 100644 --- a/tests/test_tutorial/test_query_param_models/test_tutorial002.py +++ b/tests/test_tutorial/test_query_param_models/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -65,61 +64,31 @@ def test_query_param_model_invalid(client: TestClient): ) assert response.status_code == 422 assert response.json() == snapshot( - IsDict( - { - "detail": [ - { - "type": "less_than_equal", - "loc": ["query", "limit"], - "msg": "Input should be less than or equal to 100", - "input": "150", - "ctx": {"le": 100}, - }, - { - "type": "greater_than_equal", - "loc": ["query", "offset"], - "msg": "Input should be greater than or equal to 0", - "input": "-1", - "ctx": {"ge": 0}, - }, - { - "type": "literal_error", - "loc": ["query", "order_by"], - "msg": "Input should be 'created_at' or 'updated_at'", - "input": "invalid", - "ctx": {"expected": "'created_at' or 'updated_at'"}, - }, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "type": "value_error.number.not_le", - "loc": ["query", "limit"], - "msg": "ensure this value is less than or equal to 100", - "ctx": {"limit_value": 100}, - }, - { - "type": "value_error.number.not_ge", - "loc": ["query", "offset"], - "msg": "ensure this value is greater than or equal to 0", - "ctx": {"limit_value": 0}, - }, - { - "type": "value_error.const", - "loc": ["query", "order_by"], - "msg": "unexpected value; permitted: 'created_at', 'updated_at'", - "ctx": { - "given": "invalid", - "permitted": ["created_at", "updated_at"], - }, - }, - ] - } - ) + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } ) @@ -138,22 +107,12 @@ def test_query_param_model_extra(client: TestClient): assert response.json() == snapshot( { "detail": [ - IsDict( - { - "type": "extra_forbidden", - "loc": ["query", "tool"], - "msg": "Extra inputs are not permitted", - "input": "plumbus", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "value_error.extra", - "loc": ["query", "tool"], - "msg": "extra fields not permitted", - } - ) + { + "type": "extra_forbidden", + "loc": ["query", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } ] } ) diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index ad4e4efa6b..1030781472 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -1,4 +1,3 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.query_params.tutorial005_py39 import app @@ -15,29 +14,16 @@ def test_foo_needy_very(): def test_foo_no_needy(): response = client.get("/items/foo") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "needy"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + } + ] + } def test_openapi_schema(): diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 349f8dd223..157322c7e3 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -35,51 +34,28 @@ def test_foo_needy_very(client: TestClient): def test_foo_no_needy(client: TestClient): response = client.get("/items/foo?skip=a&limit=b") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "needy"], - "msg": "Field required", - "input": None, - }, - { - "type": "int_parsing", - "loc": ["query", "skip"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "a", - }, - { - "type": "int_parsing", - "loc": ["query", "limit"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "b", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + }, + { + "type": "int_parsing", + "loc": ["query", "skip"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + }, + { + "type": "int_parsing", + "loc": ["query", "limit"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "b", + }, + ] + } def test_openapi_schema(client: TestClient): @@ -134,16 +110,10 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Limit", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Limit", "type": "integer"} - ), + "schema": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + }, "name": "limit", "in": "query", }, diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py index de5dbbb2ee..00889c5bf7 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE from fastapi.testclient import TestClient @@ -50,31 +49,17 @@ def test_query_params_str_validations_q_fixedquery(client: TestClient): def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): response = client.get("/items/", params={"item-query": "nonregexquery"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "string_pattern_mismatch", - "loc": ["query", "item-query"], - "msg": "String should match pattern '^fixedquery$'", - "input": "nonregexquery", - "ctx": {"pattern": "^fixedquery$"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + } + ] + } def test_openapi_schema(client: TestClient): @@ -109,38 +94,25 @@ def test_openapi_schema(client: TestClient): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": IsDict( - { - "anyOf": [ - { - "type": "string", - "minLength": 3, - "maxLength": 50, - "pattern": "^fixedquery$", - }, - {"type": "null"}, - ], - "title": "Query string", - "description": "Query string for the items to search in the database that have a good match", - # See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. - **( - {"deprecated": True} - if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) - else {} - ), - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - } - ), + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + # See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. + **( + {"deprecated": True} + if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) + else {} + ), + }, "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index 50b3c5683c..11de33ae14 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -69,23 +68,13 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": IsDict( - { - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - } - ), + "schema": { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + }, "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index a16d951dc6..e0e1bbe639 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -22,57 +21,31 @@ def get_client(request: pytest.FixtureRequest): def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_file(tmp_path, client: TestClient): diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index caea0d2e8f..18948c5444 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -2,8 +2,8 @@ import importlib from pathlib import Path import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -59,166 +59,132 @@ def test_post_upload_file(tmp_path: Path, client: TestClient): 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": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { "$ref": "#/components/schemas/Body_create_file_files__post" } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, }, }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, }, } }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } + } + ) diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 34dbbb985d..03772419ad 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -28,57 +27,31 @@ def get_client(app: FastAPI): def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "files"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "files"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_files(tmp_path, app: FastAPI): diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py index a54df8536c..0c43dd7b21 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -28,135 +27,73 @@ def test_post_body_form(client: TestClient): def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index da20535cf8..4276414fc2 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient @@ -28,135 +27,73 @@ def test_post_body_form(client: TestClient): def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + } + ] + } def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + }, + ] + } def test_openapi_schema(client: TestClient): diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index f37ffad443..7fa4c3de57 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -28,140 +27,76 @@ def get_client(app: FastAPI): def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_file_no_token(tmp_path, app: FastAPI): @@ -172,40 +107,22 @@ def test_post_file_no_token(tmp_path, app: FastAPI): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + }, + ] + } def test_post_files_and_token(tmp_path, app: FastAPI): diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 0f9eac890b..35ed5572dd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -42,125 +42,115 @@ def test_post_user(client: TestClient): 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"} - } + assert response.json() == snapshot( + { + "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" + } + } + }, }, }, - "422": { - "description": "Validation Error", + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {"$ref": "#/components/schemas/UserIn"} } }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "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"}], + }, }, }, - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} + "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" + }, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "UserOut": { - "title": "UserOut", - "required": IsOneOf( - ["username", "email", "full_name"], - # TODO: remove when deprecating Pydantic v1 - ["username", "email"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "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": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "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"}, - }, - }, - "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_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index 1a7ce4c7a1..fa1eb62770 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -42,125 +42,115 @@ def test_post_user(client: TestClient): 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": { - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseUser" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "BaseUser": { + "title": "BaseUser", + "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"}], + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, } }, - "required": True, }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/BaseUser"} - } + "UserIn": { + "title": "UserIn", + "required": ["username", "email", "password"], + "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"}], + }, + "password": {"title": "Password", "type": "string"}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + }, + "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"}, }, }, } - } - }, - "components": { - "schemas": { - "BaseUser": { - "title": "BaseUser", - "required": IsOneOf( - ["username", "email", "full_name"], - # TODO: remove when deprecating Pydantic v1 - ["username", "email"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "UserIn": { - "title": "UserIn", - "required": ["username", "email", "password"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "password": {"title": "Password", "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_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 19f6998f70..9c0d95ebd0 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -50,104 +50,98 @@ def test_get(url, data, client: TestClient): 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" + assert response.json() == snapshot( + { + "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", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax", "tags"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "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"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "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"}] + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], }, }, - "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"}, - } + "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_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index 47d77dc498..63e8535db0 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -40,132 +40,126 @@ def test_read_item_public_data(client: TestClient): 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}/name": { - "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" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} } - } + }, }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "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" + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "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 Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "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"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, - "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"}, - } + "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_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index a03aa41e8c..08ab659527 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -1,8 +1,8 @@ import importlib import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -40,132 +40,126 @@ def test_read_item_public_data(client: TestClient): 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}/name": { - "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" + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} } - } + }, }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "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" + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "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 Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "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"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, - "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"}, - } + "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_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index 47ecb9ba73..9326e06290 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_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 @@ -58,46 +57,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) + "schema": { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } } }, "required": True, @@ -140,27 +115,15 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py index 1c964f3d15..2d0dee48ca 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -58,16 +57,7 @@ def test_openapi_schema(client: TestClient) -> None: "requestBody": { "content": { "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), + "schema": {"$ref": "#/components/schemas/Item"}, "examples": { "normal": { "summary": "A normal example", @@ -134,27 +124,15 @@ def test_openapi_schema(client: TestClient) -> None: "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 000c8b2ac4..6a786348cf 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -1,7 +1,6 @@ import importlib import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -144,23 +143,13 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "^password$", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "^password$", - "type": "string", - } - ), + "grant_type": { + "title": "Grant Type", + "anyOf": [ + {"pattern": "^password$", "type": "string"}, + {"type": "null"}, + ], + }, "username": {"title": "Username", "type": "string"}, "password": { "title": "Password", @@ -168,31 +157,15 @@ def test_openapi_schema(client: TestClient): "format": "password", }, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - "format": "password", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Client Secret", - "type": "string", - "format": "password", - } - ), + "client_id": { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "client_secret": { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + "format": "password", + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 7953e8e3f6..25b47f0adc 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -2,8 +2,8 @@ import importlib from types import ModuleType import pytest -from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient +from inline_snapshot import snapshot from ...utils import needs_py310 @@ -215,240 +215,200 @@ 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"} - } + assert response.json() == snapshot( + { + "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" + } + } + }, }, }, - "422": { - "description": "Validation Error", + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" } } }, + "required": True, }, - }, - "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" - } + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, } }, - "required": True, - }, - } + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/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": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, }, - "/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": ["me"]}], - } - }, - "/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": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { "title": "Email", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { + }, + "full_name": { "title": "Full Name", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { + }, + "disabled": { "title": "Disabled", "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), + }, + }, }, - }, - "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"}, + "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": IsDict( - { + "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"}, ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "^password$", + }, + "username": {"title": "Username", "type": "string"}, + "password": { + "title": "Password", "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": { - "title": "Password", - "type": "string", - "format": "password", - }, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { + "format": "password", + }, + "scope": { + "title": "Scope", + "type": "string", + "default": "", + }, + "client_id": { "title": "Client Id", "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { + }, + "client_secret": { "title": "Client Secret", "anyOf": [{"type": "string"}, {"type": "null"}], "format": "password", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Client Secret", - "type": "string", - "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": { - "me": "Read information about the current user.", - "items": "Read items.", + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, - "tokenUrl": "token", - } + "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": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, }, - }, - } + } + ) diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py index e3e6bac128..2c628f5257 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py @@ -2,7 +2,7 @@ import importlib import warnings import pytest -from dirty_equals import IsDict, IsInt +from dirty_equals import IsInt from fastapi.testclient import TestClient from inline_snapshot import snapshot from sqlalchemy import StaticPool @@ -318,33 +318,15 @@ def test_openapi_schema(client: TestClient): }, "Hero": { "properties": { - "id": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Id", - } - ), + "id": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Id", + }, "name": {"type": "string", "title": "Name"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "secret_name": {"type": "string", "title": "Secret Name"}, }, "type": "object", diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py index e3b8c7f9e4..c72c16e9ae 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py @@ -2,7 +2,7 @@ import importlib import warnings import pytest -from dirty_equals import IsDict, IsInt +from dirty_equals import IsInt from fastapi.testclient import TestClient from inline_snapshot import Is, snapshot from sqlalchemy import StaticPool @@ -373,19 +373,10 @@ def test_openapi_schema(client: TestClient): "HeroCreate": { "properties": { "name": {"type": "string", "title": "Name"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "secret_name": {"type": "string", "title": "Secret Name"}, }, "type": "object", @@ -395,19 +386,10 @@ def test_openapi_schema(client: TestClient): "HeroPublic": { "properties": { "name": {"type": "string", "title": "Name"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "id": {"type": "integer", "title": "Id"}, }, "type": "object", @@ -416,45 +398,18 @@ def test_openapi_schema(client: TestClient): }, "HeroUpdate": { "properties": { - "name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Name", - } - ), - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "integer", - "title": "Age", - } - ), - "secret_name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Secret Name", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "type": "string", - "title": "Secret Name", - } - ), + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, + "secret_name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Secret Name", + }, }, "type": "object", "title": "HeroUpdate", diff --git a/tests/test_union_body.py b/tests/test_union_body.py index c15acacd18..ee7fcc4231 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -1,6 +1,5 @@ from typing import Optional, Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -91,18 +90,12 @@ def test_openapi_schema(): "Item": { "title": "Item", "type": "object", - "properties": IsDict( - { - "name": { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"name": {"title": "Name", "type": "string"}} - ), + }, }, "ValidationError": { "title": "ValidationError", diff --git a/tests/test_union_body_discriminator.py b/tests/test_union_body_discriminator.py index 40fd0065a9..6c31649bcc 100644 --- a/tests/test_union_body_discriminator.py +++ b/tests/test_union_body_discriminator.py @@ -1,6 +1,5 @@ from typing import Annotated, Any, Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -90,21 +89,11 @@ def test_discriminator_pydantic_v2() -> None: "description": "Successful Response", "content": { "application/json": { - "schema": IsDict( - { - # Pydantic 2.10, in Python 3.8 - # TODO: remove when dropping support for Python 3.8 - "type": "object", - "title": "Response Save Union Body Discriminator Items Post", - } - ) - | IsDict( - { - "type": "object", - "additionalProperties": True, - "title": "Response Save Union Body Discriminator Items Post", - } - ) + "schema": { + "type": "object", + "additionalProperties": True, + "title": "Response Save Union Body Discriminator Items Post", + } } }, }, diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index ef75d459ea..3c062e7f5a 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -1,6 +1,5 @@ from typing import Optional, Union -from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -86,16 +85,10 @@ def test_openapi_schema(): "title": "Item", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ) + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } }, }, "ExtendedItem": { @@ -103,16 +96,10 @@ def test_openapi_schema(): "required": ["age"], "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, "age": {"title": "Age", "type": "integer"}, }, }, From ded035a421a9eb11d23b51016b626bbe2433493f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:19:33 +0000 Subject: [PATCH 49/59] =?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 ef7894037c..8d080d6c56 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 🔥 Remove Pydantic v1 specific test variants. PR [#14611](https://github.com/fastapi/fastapi/pull/14611) by [@tiangolo](https://github.com/tiangolo). + ## 0.128.0 ### Breaking Changes From 1b42639296b47f1344e24c1789f649caabbee67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:31:34 -0800 Subject: [PATCH 50/59] =?UTF-8?q?=F0=9F=94=A5=20Remove=20test=20variants?= =?UTF-8?q?=20for=20Pydantic=20v1=20in=20test=5Frequest=5Fparams=20(#14612?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_body/test_list.py | 34 ++---- .../test_file/test_list.py | 105 ++++-------------- .../test_form/test_list.py | 34 ++---- .../test_header/test_list.py | 34 ++---- .../test_query/test_list.py | 34 ++---- 5 files changed, 64 insertions(+), 177 deletions(-) diff --git a/tests/test_request_params/test_body/test_list.py b/tests/test_request_params/test_body/test_list.py index 50847335ce..970e6a6607 100644 --- a/tests/test_request_params/test_body/test_list.py +++ b/tests/test_request_params/test_body/test_list.py @@ -1,7 +1,7 @@ from typing import Annotated, Union import pytest -from dirty_equals import IsDict, IsOneOf, IsPartialDict +from dirty_equals import IsOneOf, IsPartialDict from fastapi import Body, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -59,28 +59,16 @@ def test_required_list_str_missing(path: str, json: Union[dict, None]): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": IsOneOf(["body", "p"], ["body"]), - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": IsOneOf(["body", "p"], ["body"]), - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": IsOneOf(["body", "p"], ["body"]), + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py index f096532554..68280fcf32 100644 --- a/tests/test_request_params/test_file/test_list.py +++ b/tests/test_request_params/test_file/test_list.py @@ -1,7 +1,6 @@ from typing import Annotated import pytest -from dirty_equals import IsDict from fastapi import FastAPI, File, UploadFile from fastapi.testclient import TestClient @@ -36,27 +35,11 @@ def test_list_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p": ( - IsDict( - { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P", - }, - ) - | IsDict( - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - "title": "P", - }, - ) - ) + "p": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P", + }, }, "required": ["p"], "title": body_model_name, @@ -130,27 +113,11 @@ def test_list_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_alias": ( - IsDict( - { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Alias", - }, - ) - | IsDict( - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - "title": "P Alias", - }, - ) - ) + "p_alias": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Alias", + }, }, "required": ["p_alias"], "title": body_model_name, @@ -252,27 +219,11 @@ def test_list_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( - { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Val Alias", - }, - ) - | IsDict( - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - "title": "P Val Alias", - }, - ) - ) + "p_val_alias": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Val Alias", + }, }, "required": ["p_val_alias"], "title": body_model_name, @@ -385,27 +336,11 @@ def test_list_alias_and_validation_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_val_alias": ( - IsDict( - { - "anyOf": [ - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - }, - {"type": "null"}, - ], - "title": "P Val Alias", - }, - ) - | IsDict( - { - "type": "array", - "items": {"type": "string", "format": "binary"}, - "title": "P Val Alias", - }, - ) - ) + "p_val_alias": { + "type": "array", + "items": {"type": "string", "format": "binary"}, + "title": "P Val Alias", + }, }, "required": ["p_val_alias"], "title": body_model_name, diff --git a/tests/test_request_params/test_form/test_list.py b/tests/test_request_params/test_form/test_list.py index cfc42f523a..abe781c945 100644 --- a/tests/test_request_params/test_form/test_list.py +++ b/tests/test_request_params/test_form/test_list.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf, IsPartialDict +from dirty_equals import IsOneOf, IsPartialDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -58,28 +58,16 @@ def test_required_list_str_missing(path: str): client = TestClient(app) response = client.post(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": ["body", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_header/test_list.py b/tests/test_request_params/test_header/test_list.py index 65510094af..489a6b3e7d 100644 --- a/tests/test_request_params/test_header/test_list.py +++ b/tests/test_request_params/test_header/test_list.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict +from dirty_equals import AnyThing, IsOneOf, IsPartialDict from fastapi import FastAPI, Header from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -53,28 +53,16 @@ def test_required_list_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "p"], - "msg": "Field required", - "input": AnyThing, - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": ["header", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["header", "p"], + "msg": "Field required", + "input": AnyThing, + } + ] + } @pytest.mark.parametrize( diff --git a/tests/test_request_params/test_query/test_list.py b/tests/test_request_params/test_query/test_list.py index ed2ea6c809..e933da214d 100644 --- a/tests/test_request_params/test_query/test_list.py +++ b/tests/test_request_params/test_query/test_list.py @@ -1,7 +1,7 @@ from typing import Annotated import pytest -from dirty_equals import IsDict, IsOneOf +from dirty_equals import IsOneOf from fastapi import FastAPI, Query from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -53,28 +53,16 @@ def test_required_list_str_missing(path: str): client = TestClient(app) response = client.get(path) assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "p"], - "msg": "Field required", - "input": IsOneOf(None, {}), - } - ] - } - ) | IsDict( - { - "detail": [ - { - "loc": ["query", "p"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "p"], + "msg": "Field required", + "input": IsOneOf(None, {}), + } + ] + } @pytest.mark.parametrize( From a1735d6d119a7f8a33dd0f20d0cf0bb1b1f631c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:31:59 +0000 Subject: [PATCH 51/59] =?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 8d080d6c56..45a6d31d66 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 🔥 Remove test variants for Pydantic v1 in test_request_params. PR [#14612](https://github.com/fastapi/fastapi/pull/14612) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Pydantic v1 specific test variants. PR [#14611](https://github.com/fastapi/fastapi/pull/14611) by [@tiangolo](https://github.com/tiangolo). ## 0.128.0 From 4d4fb28f9f7331d758a0da908804348b5a0ba719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:48:45 -0800 Subject: [PATCH 52/59] =?UTF-8?q?=F0=9F=91=B7=20Do=20not=20run=20translati?= =?UTF-8?q?ons=20on=20cron=20while=20finishing=20updating=20existing=20lan?= =?UTF-8?q?guages=20(#14613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/translate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index e681762ca3..f1267d21f5 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -1,8 +1,8 @@ name: Translate on: - schedule: - - cron: "0 5 15 * *" # Run at 05:00 on the 15 of every month + # schedule: + # - cron: "0 5 15 * *" # Run at 05:00 on the 15 of every month workflow_dispatch: inputs: From 52842fb8d371ad1517dbac48950d55e9cfa5f64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:49:08 -0800 Subject: [PATCH 53/59] =?UTF-8?q?=F0=9F=94=A7=20Add=20LLM=20prompt=20file?= =?UTF-8?q?=20for=20Simplified=20Chinese,=20generated=20from=20the=20exist?= =?UTF-8?q?ing=20translations=20(#14549)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 史雲昔 (Vincy SHI) --- docs/zh/llm-prompt.md | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/zh/llm-prompt.md diff --git a/docs/zh/llm-prompt.md b/docs/zh/llm-prompt.md new file mode 100644 index 0000000000..1dfbe59162 --- /dev/null +++ b/docs/zh/llm-prompt.md @@ -0,0 +1,46 @@ +### Target language + +Translate to Simplified Chinese (简体中文). + +Language code: zh. + +### Grammar and tone + +1) Use clear, concise technical Chinese consistent with existing docs. +2) Address the reader naturally (commonly using “你/你的”). + +### Headings + +1) Follow existing Simplified Chinese heading style (short and descriptive). +2) Do not add trailing punctuation to headings. +3) If a heading contains only the name of a FastAPI feature, do not translate it. + +### Quotes and punctuation + +1) Keep punctuation style consistent with existing Simplified Chinese docs (they often mix English terms like “FastAPI” with Chinese text). +2) Never change punctuation inside inline code, code blocks, URLs, or file paths. + +### Ellipsis + +1) Keep ellipsis style consistent within each document, prefer `...` over `……`. +2) Never change ellipsis in code, URLs, or CLI examples. + +### Preferred translations / glossary + +Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): 请求 +- response (HTTP): 响应 +- path operation: 路径操作 +- path operation function: 路径操作函数 + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) If a title is present, prefer these canonical titles: + +- `/// tip | 提示` +- `/// note | 注意` +- `/// warning | 警告` +- `/// info | 信息` +- `/// danger | 危险` From 13743e115a2a842e0c0076041f750660305c6b77 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:49:09 +0000 Subject: [PATCH 54/59] =?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 45a6d31d66..50a83da2ea 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Do not run translations on cron while finishing updating existing languages. PR [#14613](https://github.com/fastapi/fastapi/pull/14613) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove test variants for Pydantic v1 in test_request_params. PR [#14612](https://github.com/fastapi/fastapi/pull/14612) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Pydantic v1 specific test variants. PR [#14611](https://github.com/fastapi/fastapi/pull/14611) by [@tiangolo](https://github.com/tiangolo). From dbe83f3919c6de3438b572b8395e8bed9f0060d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 10:49:46 -0800 Subject: [PATCH 55/59] =?UTF-8?q?=F0=9F=94=A7=20Add=20LLM=20prompt=20file?= =?UTF-8?q?=20for=20Traditional=20Chinese,=20generated=20from=20the=20exis?= =?UTF-8?q?ting=20translations=20(#14550)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: W. H. Wang --- docs/zh-hant/llm-prompt.md | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/zh-hant/llm-prompt.md diff --git a/docs/zh-hant/llm-prompt.md b/docs/zh-hant/llm-prompt.md new file mode 100644 index 0000000000..043501162c --- /dev/null +++ b/docs/zh-hant/llm-prompt.md @@ -0,0 +1,54 @@ +### Target language + +Translate to Traditional Chinese (繁體中文). + +Language code: zh-hant. + +### Grammar and tone + +1) Use clear, concise technical Traditional Chinese consistent with existing docs. +2) Address the reader naturally (commonly using “你/你的”). + +### Headings + +1) Follow existing Traditional Chinese heading style (short and descriptive). +2) Do not add trailing punctuation to headings. + +### Quotes and punctuation + +1) Keep punctuation style consistent with existing Traditional Chinese docs (they often mix English terms like “FastAPI” with Chinese text). +2) Never change punctuation inside inline code, code blocks, URLs, or file paths. +3) For more details, please follow the [Chinese Copywriting Guidelines](https://github.com/sparanoid/chinese-copywriting-guidelines). + +### Ellipsis + +1) Keep ellipsis style consistent within each document, prefer `...` over `……`. +2) Never change ellipsis in code, URLs, or CLI examples. + +### Preferred translations / glossary + +1. Should avoid using simplified Chinese characters and terms. Always examine if the translation can be easily comprehended by the Traditional Chinese readers. +2. For some Python-specific terms like "pickle", "list", "dict" etc, we don't have to translate them. +3. Use the following preferred translations when they apply in documentation prose: + +- request (HTTP): 請求 +- response (HTTP): 回應 +- path operation: 路徑操作 +- path operation function: 路徑操作函式 + +The translation can optionally include the original English text only in the first occurrence of each page (e.g. "路徑操作 (path operation)") if the translation is hard to be comprehended by most of the Chinese readers. + +### `///` admonitions + +1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.). +2) Many Traditional Chinese docs currently omit titles in `///` blocks; that is OK. +3) If a generic title is present, prefer these canonical titles: + +- `/// note | 注意` + +Notes: + +- `details` blocks exist; keep `/// details` as-is and translate only the title after `|`. +- Example canonical titles used in existing docs: + - `/// details | 上述指令的含義` + - `/// details | 關於 `requirements.txt`` From 4ce34686d98eae741f858f93f239c96baba83e01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:51:18 +0000 Subject: [PATCH 56/59] =?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 50a83da2ea..c4b88a4732 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🔧 Add LLM prompt file for Simplified Chinese, generated from the existing translations. PR [#14549](https://github.com/fastapi/fastapi/pull/14549) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 👷 Do not run translations on cron while finishing updating existing languages. PR [#14613](https://github.com/fastapi/fastapi/pull/14613) by [@tiangolo](https://github.com/tiangolo). From f362fdc2346d4c373c714263b9467a5edc1c420f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 18:51:39 +0000 Subject: [PATCH 57/59] =?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 c4b88a4732..584137f5b3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🔧 Add LLM prompt file for Traditional Chinese, generated from the existing translations. PR [#14550](https://github.com/fastapi/fastapi/pull/14550) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add LLM prompt file for Simplified Chinese, generated from the existing translations. PR [#14549](https://github.com/fastapi/fastapi/pull/14549) by [@tiangolo](https://github.com/tiangolo). ### Internal From 3b1b4f034bebddb873d4bd8c1bb5f52aaae17240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Dec 2025 11:05:53 -0800 Subject: [PATCH 58/59] =?UTF-8?q?=F0=9F=94=A8=20Update=20LLM=20translation?= =?UTF-8?q?=20script=20to=20guide=20reviewers=20to=20change=20the=20prompt?= =?UTF-8?q?=20(#14614)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/translate.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/translate.py b/scripts/translate.py index 6ebd24f547..66da46a083 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -1036,9 +1036,13 @@ def make_pr( print("Creating PR") g = Github(github_token) gh_repo = g.get_repo(github_repository) - pr = gh_repo.create_pull( - title=message, body=message, base="master", head=branch_name + body = ( + message + + "\n\nThis PR was created automatically using LLMs." + + f"\n\nIt uses the prompt file https://github.com/fastapi/fastapi/blob/master/docs/{language}/llm-prompt.md." + + "\n\nIn most cases, it's better to make PRs updating that file so that the LLM can do a better job generating the translations than suggesting changes in this PR." ) + pr = gh_repo.create_pull(title=message, body=body, base="master", head=branch_name) print(f"Created PR: {pr.number}") print("Finished") From 47391ea8fbdfcedf6ff3f4a56666ccf928ca10c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 19:06:15 +0000 Subject: [PATCH 59/59] =?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 584137f5b3..1380d7ff6d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,7 @@ hide: ### Internal +* 🔨 Update LLM translation script to guide reviewers to change the prompt. PR [#14614](https://github.com/fastapi/fastapi/pull/14614) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not run translations on cron while finishing updating existing languages. PR [#14613](https://github.com/fastapi/fastapi/pull/14613) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove test variants for Pydantic v1 in test_request_params. PR [#14612](https://github.com/fastapi/fastapi/pull/14612) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Pydantic v1 specific test variants. PR [#14611](https://github.com/fastapi/fastapi/pull/14611) by [@tiangolo](https://github.com/tiangolo).