From e5594e860f34c939d530640b1c240ed7f64003b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 28 Jun 2020 13:58:21 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Update=20response=5Fmodel=5Fby=5Fal?= =?UTF-8?q?ias=20(#1642)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make openapi models honor response_model_by_alias * Add test for response_model_by_alias working with openapi models * ⏪ Revert changes * ✅ Update and extend tests for response_model_by_alias * ⏪ Revert test name change * 📌 Pin Pytest and Pytest-Cov Co-authored-by: Martin Zaťko --- fastapi/routing.py | 6 +- pyproject.toml | 4 +- tests/test_duplicate_models_openapi.py | 87 +++++-- tests/test_response_by_alias.py | 323 +++++++++++++++++++++++++ 4 files changed, 400 insertions(+), 20 deletions(-) create mode 100644 tests/test_response_by_alias.py diff --git a/fastapi/routing.py b/fastapi/routing.py index 38216823a..9a4456550 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -52,7 +52,6 @@ except ImportError: # pragma: nocover def _prepare_response_content( res: Any, *, - by_alias: bool = True, exclude_unset: bool, exclude_defaults: bool = False, exclude_none: bool = False, @@ -60,14 +59,14 @@ def _prepare_response_content( if isinstance(res, BaseModel): if PYDANTIC_1: return res.dict( - by_alias=by_alias, + by_alias=True, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) else: return res.dict( - by_alias=by_alias, skip_defaults=exclude_unset, + by_alias=True, skip_defaults=exclude_unset, ) # pragma: nocover elif isinstance(res, list): return [ @@ -108,7 +107,6 @@ async def serialize_response( errors = [] response_content = _prepare_response_content( response_content, - by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, diff --git a/pyproject.toml b/pyproject.toml index 3dab7da91..a6f0030a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,8 +43,8 @@ Documentation = "https://fastapi.tiangolo.com/" [tool.flit.metadata.requires-extra] test = [ - "pytest >=5.4.3", - "pytest-cov", + "pytest==5.4.3", + "pytest-cov==2.10.0", "mypy", "black", "isort", diff --git a/tests/test_duplicate_models_openapi.py b/tests/test_duplicate_models_openapi.py index ad4f1e784..f077dfea0 100644 --- a/tests/test_duplicate_models_openapi.py +++ b/tests/test_duplicate_models_openapi.py @@ -1,23 +1,82 @@ from fastapi import FastAPI +from fastapi.testclient import TestClient from pydantic import BaseModel +app = FastAPI() -def test_get_openapi(): - app = FastAPI() - class Model(BaseModel): - pass +class Model(BaseModel): + pass - class Model2(BaseModel): - a: Model - class Model3(BaseModel): - c: Model - d: Model2 +class Model2(BaseModel): + a: Model - @app.get("/", response_model=Model3) - def f(): - pass # pragma: no cover - openapi = app.openapi() - assert isinstance(openapi, dict) +class Model3(BaseModel): + c: Model + d: Model2 + + +@app.get("/", response_model=Model3) +def f(): + return {"c": {}, "d": {"a": {}}} + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "F", + "operationId": "f__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model3"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Model": {"title": "Model", "type": "object", "properties": {}}, + "Model2": { + "title": "Model2", + "required": ["a"], + "type": "object", + "properties": {"a": {"$ref": "#/components/schemas/Model"}}, + }, + "Model3": { + "title": "Model3", + "required": ["c", "d"], + "type": "object", + "properties": { + "c": {"$ref": "#/components/schemas/Model"}, + "d": {"$ref": "#/components/schemas/Model2"}, + }, + }, + } + }, +} + + +client = TestClient(app) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_api_route(): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"c": {}, "d": {"a": {}}} diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py new file mode 100644 index 000000000..8121d7b88 --- /dev/null +++ b/tests/test_response_by_alias.py @@ -0,0 +1,323 @@ +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Model(BaseModel): + name: str = Field(alias="alias") + + +class ModelNoAlias(BaseModel): + name: str + + class Config: + schema_extra = { + "description": ( + "response_model_by_alias=False is basically a quick hack, to support " + "proper OpenAPI use another model with the correct field names" + ) + } + + +@app.get("/dict", response_model=Model, response_model_by_alias=False) +def read_dict(): + return {"alias": "Foo"} + + +@app.get("/model", response_model=Model, response_model_by_alias=False) +def read_model(): + return Model(alias="Foo") + + +@app.get("/list", response_model=List[Model], response_model_by_alias=False) +def read_list(): + return [{"alias": "Foo"}, {"alias": "Bar"}] + + +@app.get("/by-alias/dict", response_model=Model) +def by_alias_dict(): + return {"alias": "Foo"} + + +@app.get("/by-alias/model", response_model=Model) +def by_alias_model(): + return Model(alias="Foo") + + +@app.get("/by-alias/list", response_model=List[Model]) +def by_alias_list(): + return [{"alias": "Foo"}, {"alias": "Bar"}] + + +@app.get("/no-alias/dict", response_model=ModelNoAlias) +def by_alias_dict(): + return {"name": "Foo"} + + +@app.get("/no-alias/model", response_model=ModelNoAlias) +def by_alias_model(): + return ModelNoAlias(name="Foo") + + +@app.get("/no-alias/list", response_model=List[ModelNoAlias]) +def by_alias_list(): + return [{"name": "Foo"}, {"name": "Bar"}] + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/dict": { + "get": { + "summary": "Read Dict", + "operationId": "read_dict_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/model": { + "get": { + "summary": "Read Model", + "operationId": "read_model_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/list": { + "get": { + "summary": "Read List", + "operationId": "read_list_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read List List Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Model"}, + } + } + }, + } + }, + } + }, + "/by-alias/dict": { + "get": { + "summary": "By Alias Dict", + "operationId": "by_alias_dict_by_alias_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/by-alias/model": { + "get": { + "summary": "By Alias Model", + "operationId": "by_alias_model_by_alias_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/by-alias/list": { + "get": { + "summary": "By Alias List", + "operationId": "by_alias_list_by_alias_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response By Alias List By Alias List Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Model"}, + } + } + }, + } + }, + } + }, + "/no-alias/dict": { + "get": { + "summary": "By Alias Dict", + "operationId": "by_alias_dict_no_alias_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ModelNoAlias"} + } + }, + } + }, + } + }, + "/no-alias/model": { + "get": { + "summary": "By Alias Model", + "operationId": "by_alias_model_no_alias_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ModelNoAlias"} + } + }, + } + }, + } + }, + "/no-alias/list": { + "get": { + "summary": "By Alias List", + "operationId": "by_alias_list_no_alias_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response By Alias List No Alias List Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelNoAlias" + }, + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Model": { + "title": "Model", + "required": ["alias"], + "type": "object", + "properties": {"alias": {"title": "Alias", "type": "string"}}, + }, + "ModelNoAlias": { + "title": "ModelNoAlias", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "Name", "type": "string"}}, + "description": "response_model_by_alias=False is basically a quick hack, to support proper OpenAPI use another model with the correct field names", + }, + } + }, +} + + +client = TestClient(app) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_read_dict(): + response = client.get("/dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo"} + + +def test_read_model(): + response = client.get("/model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo"} + + +def test_read_list(): + response = client.get("/list") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Foo"}, + {"name": "Bar"}, + ] + + +def test_read_dict_by_alias(): + response = client.get("/by-alias/dict") + assert response.status_code == 200, response.text + assert response.json() == {"alias": "Foo"} + + +def test_read_model_by_alias(): + response = client.get("/by-alias/model") + assert response.status_code == 200, response.text + assert response.json() == {"alias": "Foo"} + + +def test_read_list_by_alias(): + response = client.get("/by-alias/list") + assert response.status_code == 200, response.text + assert response.json() == [ + {"alias": "Foo"}, + {"alias": "Bar"}, + ] + + +def test_read_dict_no_alias(): + response = client.get("/no-alias/dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo"} + + +def test_read_model_no_alias(): + response = client.get("/no-alias/model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo"} + + +def test_read_list_no_alias(): + response = client.get("/no-alias/list") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Foo"}, + {"name": "Bar"}, + ]