From 90afc72e64b0e294f21bb823bdafb62d3352d5d0 Mon Sep 17 00:00:00 2001 From: Toan Vuong Date: Mon, 30 Mar 2020 12:44:43 -0700 Subject: [PATCH] :bug: Fix automatic embedding with dependencies and sub-dependencies (#1079) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Handle automatic embedding with Depends * :bug: Fix body embeds for sub-dependencies and simplify implementation * :white_check_mark: Add/update tests for body embeds in dependencies * :construction_worker: Trigger Travis Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 8 +- tests/test_dependency_duplicates.py | 232 ++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 tests/test_dependency_duplicates.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 543479be8..43ab4a098 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -704,8 +704,14 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: first_param = flat_dependant.body_params[0] field_info = get_field_info(first_param) embed = getattr(field_info, "embed", None) - if len(flat_dependant.body_params) == 1 and not embed: + body_param_names_set = set([param.name for param in flat_dependant.body_params]) + if len(body_param_names_set) == 1 and not embed: return get_schema_compatible_field(field=first_param) + # If one field requires to embed, all have to be embedded + # in case a sub-dependency is evaluated with a single unique body field + # That is combined (embedded) with other body fields + for param in flat_dependant.body_params: + setattr(get_field_info(param), "embed", True) model_name = "Body_" + name BodyModel = create_model(model_name) for f in flat_dependant.body_params: diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py new file mode 100644 index 000000000..0462b43c8 --- /dev/null +++ b/tests/test_dependency_duplicates.py @@ -0,0 +1,232 @@ +from typing import List + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + +client = TestClient(app) + + +class Item(BaseModel): + data: str + + +def duplicate_dependency(item: Item): + return item + + +def dependency(item2: Item): + return item2 + + +def sub_duplicate_dependency( + item: Item, sub_item: Item = Depends(duplicate_dependency) +): + return [item, sub_item] + + +@app.post("/with-duplicates") +async def with_duplicates(item: Item, item2: Item = Depends(duplicate_dependency)): + return [item, item2] + + +@app.post("/no-duplicates") +async def no_duplicates(item: Item, item2: Item = Depends(dependency)): + return [item, item2] + + +@app.post("/with-duplicates-sub") +async def no_duplicates_sub( + item: Item, sub_items: List[Item] = Depends(sub_duplicate_dependency) +): + return [item, sub_items] + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/with-duplicates": { + "post": { + "summary": "With Duplicates", + "operationId": "with_duplicates_with_duplicates_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$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" + } + } + }, + }, + }, + } + }, + "/no-duplicates": { + "post": { + "summary": "No Duplicates", + "operationId": "no_duplicates_no_duplicates_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_no_duplicates_no_duplicates_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/with-duplicates-sub": { + "post": { + "summary": "No Duplicates Sub", + "operationId": "no_duplicates_sub_with_duplicates_sub_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$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": { + "Body_no_duplicates_no_duplicates_post": { + "title": "Body_no_duplicates_no_duplicates_post", + "required": ["item", "item2"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["data"], + "type": "object", + "properties": {"data": {"title": "Data", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_no_duplicates_invalid(): + response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["body", "item2"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + + +def test_no_duplicates(): + response = client.post( + "/no-duplicates", + json={"item": {"data": "myitem"}, "item2": {"data": "myitem2"}}, + ) + assert response.status_code == 200 + assert response.json() == [{"data": "myitem"}, {"data": "myitem2"}] + + +def test_duplicates(): + response = client.post("/with-duplicates", json={"data": "myitem"}) + assert response.status_code == 200 + assert response.json() == [{"data": "myitem"}, {"data": "myitem"}] + + +def test_sub_duplicates(): + response = client.post("/with-duplicates-sub", json={"data": "myitem"}) + assert response.status_code == 200 + assert response.json() == [ + {"data": "myitem"}, + [{"data": "myitem"}, {"data": "myitem"}], + ]