From 19a2c3bb54ecef5fab936b45e4c262b283e100f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Aug 2023 22:47:07 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Enable=20Pydantic's=20serialization?= =?UTF-8?q?=20mode=20for=20responses,=20add=20support=20for=20Pydantic's?= =?UTF-8?q?=20`computed=5Ffield`,=20better=20OpenAPI=20for=20response=20mo?= =?UTF-8?q?dels,=20proper=20required=20attributes,=20better=20generated=20?= =?UTF-8?q?clients=20(#10011)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Enable Pydantic's serialization mode for responses * ✅ Update tests with new Pydantic v2 serialization mode * ✅ Add a test for Pydantic v2's computed_field --- fastapi/routing.py | 4 +- tests/test_computed_fields.py | 77 +++++++ tests/test_filter_pydantic_sub_model_pv2.py | 8 +- .../test_body_updates/test_tutorial001.py | 210 ++++++++++++++--- .../test_tutorial001_py310.py | 211 +++++++++++++++--- .../test_tutorial001_py39.py | 211 +++++++++++++++--- .../test_dataclasses/test_tutorial002.py | 12 +- .../test_dataclasses/test_tutorial003.py | 185 ++++++++++++--- .../test_extra_models/test_tutorial003.py | 13 +- .../test_tutorial003_py310.py | 13 +- .../test_tutorial004.py | 155 +++++++++++-- .../test_tutorial005.py | 155 +++++++++++-- .../test_tutorial005_py310.py | 156 +++++++++++-- .../test_tutorial005_py39.py | 156 +++++++++++-- .../test_response_model/test_tutorial003.py | 8 +- .../test_tutorial003_01.py | 8 +- .../test_tutorial003_01_py310.py | 8 +- .../test_tutorial003_py310.py | 8 +- .../test_response_model/test_tutorial004.py | 8 +- .../test_tutorial004_py310.py | 8 +- .../test_tutorial004_py39.py | 8 +- .../test_response_model/test_tutorial005.py | 8 +- .../test_tutorial005_py310.py | 8 +- .../test_response_model/test_tutorial006.py | 8 +- .../test_tutorial006_py310.py | 8 +- .../test_security/test_tutorial005.py | 8 +- .../test_security/test_tutorial005_an.py | 8 +- .../test_tutorial005_an_py310.py | 8 +- .../test_security/test_tutorial005_an_py39.py | 8 +- .../test_security/test_tutorial005_py310.py | 8 +- .../test_security/test_tutorial005_py39.py | 8 +- 31 files changed, 1446 insertions(+), 256 deletions(-) create mode 100644 tests/test_computed_fields.py diff --git a/fastapi/routing.py b/fastapi/routing.py index d8ff0579cb813..6efd40ff3bc9f 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -448,9 +448,7 @@ def __init__( self.response_field = create_response_field( name=response_name, type_=self.response_model, - # TODO: This should actually set mode='serialization', just, that changes the schemas - # mode="serialization", - mode="validation", + mode="serialization", ) # Create a clone of the field, so that a Pydantic submodel is not returned # as is just because it's an instance of a subclass of a more limited class diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py new file mode 100644 index 0000000000000..5286507b2d348 --- /dev/null +++ b/tests/test_computed_fields.py @@ -0,0 +1,77 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + app = FastAPI() + + from pydantic import BaseModel, computed_field + + class Rectangle(BaseModel): + width: int + length: int + + @computed_field + @property + def area(self) -> int: + return self.width * self.length + + @app.get("/") + def read_root() -> Rectangle: + return Rectangle(width=3, length=4) + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_get(client: TestClient): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"width": 3, "length": 4, "area": 12} + + +@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": { + "/": { + "get": { + "summary": "Read Root", + "operationId": "read_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Rectangle"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Rectangle": { + "properties": { + "width": {"type": "integer", "title": "Width"}, + "length": {"type": "integer", "title": "Length"}, + "area": {"type": "integer", "title": "Area", "readOnly": True}, + }, + "type": "object", + "required": ["width", "length", "area"], + "title": "Rectangle", + } + } + }, + } diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index ae12179bdf314..9f5e6b08fb667 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 +from dirty_equals import HasRepr, IsDict, IsOneOf from fastapi import Depends, FastAPI from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient @@ -139,7 +139,11 @@ def test_openapi_schema(client: TestClient): }, "ModelA": { "title": "ModelA", - "required": ["name", "foo"], + "required": IsOneOf( + ["name", "description", "foo"], + # TODO remove when deprecating Pydantic v1 + ["name", "foo"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index b02f7c81c3f86..f1a46210aadea 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -1,7 +1,8 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_pydanticv1, needs_pydanticv2 + @pytest.fixture(name="client") def get_client(): @@ -36,7 +37,181 @@ 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 + 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/ItemOutput" + } + } + }, + }, + "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/ItemOutput" + } + } + }, + }, + "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/ItemInput"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "type": "object", + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "type": "object", + "required": ["name", "description", "price", "tax", "tags"], + "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"}, + } + }, + }, + } + }, + } + + +# 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() == { @@ -124,36 +299,9 @@ def test_openapi_schema(client: TestClient): "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"} - ), - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": IsDict( - { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Price", "type": "number"} - ), + "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", diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index 4af2652a78e36..ab696e4c8dfec 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -1,8 +1,7 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -41,7 +40,182 @@ def test_put(client: TestClient): @needs_py310 +@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": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "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/ItemOutput" + } + } + }, + }, + "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/ItemInput"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "type": "object", + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "type": "object", + "required": ["name", "description", "price", "tax", "tags"], + "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"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py310 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -129,36 +303,9 @@ def test_openapi_schema(client: TestClient): "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"} - ), - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": IsDict( - { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Price", "type": "number"} - ), + "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", diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index 832f453884439..2ee6a5cb4c695 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -1,8 +1,7 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -41,7 +40,182 @@ def test_put(client: TestClient): @needs_py39 +@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": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "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/ItemOutput" + } + } + }, + }, + "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/ItemInput"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "type": "object", + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "type": "object", + "required": ["name", "description", "price", "tax", "tags"], + "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"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py39 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -129,36 +303,9 @@ def test_openapi_schema(client: TestClient): "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"} - ), - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": IsDict( - { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Price", "type": "number"} - ), + "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", diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 7d88e286168c4..4146f4cd65dfe 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial002 import app @@ -21,8 +21,7 @@ def test_get_item(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - data = response.json() - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -47,7 +46,11 @@ def test_openapi_schema(): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "price", "tags", "description", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, @@ -57,7 +60,6 @@ def test_openapi_schema(): "title": "Tags", "type": "array", "items": {"type": "string"}, - "default": [], } ) | IsDict( diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index 597757e0931c9..2e5809914f850 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -1,8 +1,9 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial003 import app +from ...utils import needs_pydanticv1, needs_pydanticv2 + client = TestClient(app) @@ -52,6 +53,7 @@ def test_get_authors(): ] +@needs_pydanticv2 def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 @@ -77,7 +79,7 @@ def test_openapi_schema(): "schema": { "title": "Items", "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, + "items": {"$ref": "#/components/schemas/ItemInput"}, } } }, @@ -132,26 +134,164 @@ def test_openapi_schema(): "schemas": { "Author": { "title": "Author", + "required": ["name", "items"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/ItemOutput"}, + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ItemInput": { + "title": "Item", "required": ["name"], "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "items": IsDict( - { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "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"}, + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(): + 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": { @@ -171,16 +311,7 @@ def test_openapi_schema(): "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", "type": "string"}, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index 21192b7db752e..0ccb99948f381 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from docs_src.extra_models.tutorial003 import app @@ -76,7 +77,11 @@ def test_openapi_schema(): "schemas": { "PlaneItem": { "title": "PlaneItem", - "required": ["description", "size"], + "required": IsOneOf( + ["description", "type", "size"], + # TODO: remove when deprecating Pydantic v1 + ["description", "size"], + ), "type": "object", "properties": { "description": {"title": "Description", "type": "string"}, @@ -86,7 +91,11 @@ def test_openapi_schema(): }, "CarItem": { "title": "CarItem", - "required": ["description"], + "required": IsOneOf( + ["description", "type"], + # TODO: remove when deprecating Pydantic v1 + ["description"], + ), "type": "object", "properties": { "description": {"title": "Description", "type": "string"}, diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py index c17ddbbe1d550..b2fe65fd9ffb6 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -86,7 +87,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "PlaneItem": { "title": "PlaneItem", - "required": ["description", "size"], + "required": IsOneOf( + ["description", "type", "size"], + # TODO: remove when deprecating Pydantic v1 + ["description", "size"], + ), "type": "object", "properties": { "description": {"title": "Description", "type": "string"}, @@ -96,7 +101,11 @@ def test_openapi_schema(client: TestClient): }, "CarItem": { "title": "CarItem", - "required": ["description"], + "required": IsOneOf( + ["description", "type"], + # TODO: remove when deprecating Pydantic v1 + ["description"], + ), "type": "object", "properties": { "description": {"title": "Description", "type": "string"}, 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 dd123f48d6e8c..3ffc0bca7ce62 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 @@ -1,8 +1,9 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_operation_advanced_configuration.tutorial004 import app +from ...utils import needs_pydanticv1, needs_pydanticv2 + client = TestClient(app) @@ -18,7 +19,137 @@ def test_query_params_str_validations(): } +@needs_pydanticv2 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/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "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/ItemInput"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ItemInput": { + "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"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description", "price", "tax", "tags"], + "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"}], + }, + "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"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -69,27 +200,9 @@ def test_openapi_schema(): "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", "type": "string"}, "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", "type": "number"}, "tags": { "title": "Tags", "uniqueItems": True, 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 e7e9a982e1f33..ff98295a60a44 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -1,8 +1,9 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_operation_configuration.tutorial005 import app +from ...utils import needs_pydanticv1, needs_pydanticv2 + client = TestClient(app) @@ -18,7 +19,137 @@ def test_query_params_str_validations(): } +@needs_pydanticv2 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/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "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/ItemInput"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ItemInput": { + "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"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description", "price", "tax", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "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"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -69,27 +200,9 @@ def test_openapi_schema(): "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", "type": "string"}, "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", "type": "number"}, "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index ebfeb809cc3a1..ad1c09eaec508 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -1,8 +1,7 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -27,7 +26,138 @@ def test_query_params_str_validations(client: TestClient): @needs_py310 +@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": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "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/ItemInput"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ItemInput": { + "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"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description", "price", "tax", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "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"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py310 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -78,27 +208,9 @@ 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", "type": "string"}, "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", "type": "number"}, "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index 8e79afe968843..045d1d402c949 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -1,8 +1,7 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -27,7 +26,138 @@ def test_query_params_str_validations(client: TestClient): @needs_py39 +@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": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "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/ItemInput"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ItemInput": { + "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"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description", "price", "tax", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "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"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py39 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -78,27 +208,9 @@ 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", "type": "string"}, "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", "type": "number"}, "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 20221399b14fd..384c8e0f146d0 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial003 import app @@ -70,7 +70,11 @@ def test_openapi_schema(): "schemas": { "UserOut": { "title": "UserOut", - "required": ["username", "email"], + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, 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 e8f0658f4c38b..3a6a0b20d9d20 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial003_01 import app @@ -70,7 +70,11 @@ def test_openapi_schema(): "schemas": { "BaseUser": { "title": "BaseUser", - "required": ["username", "email"], + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py index a69f8cc8debed..6985b9de6af74 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -79,7 +79,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "BaseUser": { "title": "BaseUser", - "required": ["username", "email"], + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py index 64dcd6cbdf39c..3a3aee38aaffa 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -79,7 +79,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "UserOut": { "title": "UserOut", - "required": ["username", "email"], + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), "type": "object", "properties": { "username": {"title": "Username", "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 8beb847d1df7d..e9bde18dd58c3 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial004 import app @@ -79,7 +79,11 @@ def test_openapi_schema(): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax", "tags"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py index 28eb88c3478fd..6f8a3cbea8f09 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -87,7 +87,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax", "tags"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py index 9e1a21f8db6ae..cfaa1eba2152f 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -87,7 +87,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax", "tags"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index 06e5d0fd153a3..b20864c07ab44 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial005 import app @@ -102,7 +102,11 @@ def test_openapi_schema(): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py index 0f15662439b01..de552c8f22de5 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -112,7 +112,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 6e6152b9f16cb..1e47e2eadb05e 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial006 import app @@ -102,7 +102,11 @@ def test_openapi_schema(): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py index 9a980ab5b0ae5..40058b12dea45 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -112,7 +112,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 22ae76f428f4a..c669c306ddb01 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.security.tutorial005 import ( @@ -267,7 +267,11 @@ def test_openapi_schema(): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py index 07239cc890051..aaab04f78fb4e 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.security.tutorial005_an import ( @@ -267,7 +267,11 @@ def test_openapi_schema(): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py index 1ab836639e6f9..243d0773c266c 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -295,7 +295,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py index 6aabbe04acc79..17a3f9aa2a548 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -295,7 +295,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_py310.py b/tests/test_tutorial/test_security/test_tutorial005_py310.py index c21884df83a98..06455cd632f09 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -295,7 +295,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_py39.py b/tests/test_tutorial/test_security/test_tutorial005_py39.py index 170c5d60b867d..9455bfb4ef6b9 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py39.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -295,7 +295,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"},