From baa5cd2ca6c67c760387c1bf47ef56037c94343c Mon Sep 17 00:00:00 2001 From: Daniyar Yeralin Date: Wed, 12 Aug 2020 14:37:07 -0400 Subject: [PATCH 01/24] Introduce mapping shapes --- fastapi/dependencies/utils.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 7c9f7e847..d09ccde8f 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -41,7 +41,7 @@ from pydantic.fields import ( SHAPE_TUPLE_ELLIPSIS, FieldInfo, ModelField, - Required, + Required, SHAPE_MAPPING, ) from pydantic.schema import get_annotation_from_field_info from pydantic.typing import ForwardRef, evaluate_forwardref @@ -69,6 +69,13 @@ sequence_shape_to_type = { SHAPE_TUPLE_ELLIPSIS: list, } +mapping_shapes = { + SHAPE_MAPPING +} +mapping_types = (dict) +mapping_shapes_to_type = { + SHAPE_MAPPING: dict +} multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -240,6 +247,20 @@ def is_scalar_sequence_field(field: ModelField) -> bool: return False +def is_scalar_mapping_field(field: ModelField) -> bool: + if (field.shape in mapping_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_scalar_field(sub_field): + return False + return True + if lenient_issubclass(field.type_, mapping_types): + return True + return False + + def get_typed_signature(call: Callable) -> inspect.Signature: signature = inspect.signature(call) globalns = getattr(call, "__globals__", {}) @@ -324,7 +345,8 @@ def get_dependant( add_param_to_fields(field=param_field, dependant=dependant) elif isinstance( param.default, (params.Query, params.Header) - ) and is_scalar_sequence_field(param_field): + ) and (is_scalar_sequence_field(param_field) + or is_scalar_mapping_field(param_field)): add_param_to_fields(field=param_field, dependant=dependant) else: field_info = param_field.field_info @@ -610,6 +632,10 @@ def request_params_to_args( received_params, (QueryParams, Headers) ): value = received_params.getlist(field.alias) or field.default + elif is_scalar_mapping_field(field) and isinstance( + received_params, (QueryParams,) + ): + value = received_params._dict else: value = received_params.get(field.alias) field_info = field.field_info From e8c83f100e35f012ec3716d9341aee56fb8ddb1b Mon Sep 17 00:00:00 2001 From: Daniyar Yeralin Date: Wed, 12 Aug 2020 14:37:24 -0400 Subject: [PATCH 02/24] Cover mapping shapes with tests --- tests/main.py | 7 ++++++- tests/test_query.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/main.py b/tests/main.py index d5603d0e6..fdd16f2d2 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,5 +1,5 @@ import http -from typing import Optional +from typing import Optional, Dict from fastapi import FastAPI, Path, Query @@ -184,6 +184,11 @@ def get_query_param_required(query=Query(...)): return f"foo bar {query}" +@app.get("/query/params") +def get_query_params(queries: Dict[str, int] = Query({})): + return f"foo bar {queries}" + + @app.get("/query/param-required/int") def get_query_param_required_type(query: int = Query(...)): return f"foo bar {query}" diff --git a/tests/test_query.py b/tests/test_query.py index cdbdd1ccd..6c4f4b0e8 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -53,6 +53,8 @@ response_not_valid_int = { ("/query/param-required/int", 422, response_missing), ("/query/param-required/int?query=50", 200, "foo bar 50"), ("/query/param-required/int?query=foo", 422, response_not_valid_int), + ("/query/params?first-query=1&second-query=2", 200, "foo bar {'first-query': 1, " + "'second-query': 2}") ], ) def test_get_path(path, expected_status, expected_response): From ec624a27be2bc9abc9063263fd7a6173ab47f323 Mon Sep 17 00:00:00 2001 From: Daniyar Yeralin Date: Wed, 12 Aug 2020 14:40:38 -0400 Subject: [PATCH 03/24] Format proposed code --- fastapi/dependencies/utils.py | 21 +++++++++------------ tests/main.py | 2 +- tests/test_query.py | 7 +++++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index d09ccde8f..843f59bf6 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -34,6 +34,7 @@ from pydantic.error_wrappers import ErrorWrapper from pydantic.errors import MissingError from pydantic.fields import ( SHAPE_LIST, + SHAPE_MAPPING, SHAPE_SEQUENCE, SHAPE_SET, SHAPE_SINGLETON, @@ -41,7 +42,7 @@ from pydantic.fields import ( SHAPE_TUPLE_ELLIPSIS, FieldInfo, ModelField, - Required, SHAPE_MAPPING, + Required, ) from pydantic.schema import get_annotation_from_field_info from pydantic.typing import ForwardRef, evaluate_forwardref @@ -69,13 +70,9 @@ sequence_shape_to_type = { SHAPE_TUPLE_ELLIPSIS: list, } -mapping_shapes = { - SHAPE_MAPPING -} -mapping_types = (dict) -mapping_shapes_to_type = { - SHAPE_MAPPING: dict -} +mapping_shapes = {SHAPE_MAPPING} +mapping_types = dict +mapping_shapes_to_type = {SHAPE_MAPPING: dict} multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -343,10 +340,10 @@ def get_dependant( add_param_to_fields(field=param_field, dependant=dependant) elif is_scalar_field(field=param_field): add_param_to_fields(field=param_field, dependant=dependant) - elif isinstance( - param.default, (params.Query, params.Header) - ) and (is_scalar_sequence_field(param_field) - or is_scalar_mapping_field(param_field)): + elif isinstance(param.default, (params.Query, params.Header)) and ( + is_scalar_sequence_field(param_field) + or is_scalar_mapping_field(param_field) + ): add_param_to_fields(field=param_field, dependant=dependant) else: field_info = param_field.field_info diff --git a/tests/main.py b/tests/main.py index fdd16f2d2..17b380f6d 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,5 +1,5 @@ import http -from typing import Optional, Dict +from typing import Dict, Optional from fastapi import FastAPI, Path, Query diff --git a/tests/test_query.py b/tests/test_query.py index 6c4f4b0e8..934d34136 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -53,8 +53,11 @@ response_not_valid_int = { ("/query/param-required/int", 422, response_missing), ("/query/param-required/int?query=50", 200, "foo bar 50"), ("/query/param-required/int?query=foo", 422, response_not_valid_int), - ("/query/params?first-query=1&second-query=2", 200, "foo bar {'first-query': 1, " - "'second-query': 2}") + ( + "/query/params?first-query=1&second-query=2", + 200, + "foo bar {'first-query': 1, " "'second-query': 2}", + ), ], ) def test_get_path(path, expected_status, expected_response): From 23740cf5248a1a2e3ad448ce631915f0da0724a4 Mon Sep 17 00:00:00 2001 From: Daniyar Yeralin Date: Wed, 12 Aug 2020 15:02:49 -0400 Subject: [PATCH 04/24] Adapt tests to a new endpoint Remove unused import Add entry to openapi.json Reformat openapi.json --- tests/main.py | 10 ++++---- tests/test_application.py | 35 ++++++++++++++++++++++++++++ tests/test_invalid_sequence_param.py | 14 +---------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/tests/main.py b/tests/main.py index 17b380f6d..7700b4dbc 100644 --- a/tests/main.py +++ b/tests/main.py @@ -184,16 +184,16 @@ def get_query_param_required(query=Query(...)): return f"foo bar {query}" -@app.get("/query/params") -def get_query_params(queries: Dict[str, int] = Query({})): - return f"foo bar {queries}" - - @app.get("/query/param-required/int") def get_query_param_required_type(query: int = Query(...)): return f"foo bar {query}" +@app.get("/query/params") +def get_query_params(queries: Dict[str, int] = Query({})): + return f"foo bar {queries}" + + @app.get("/enum-status-code", status_code=http.HTTPStatus.CREATED) def get_enum_status_code(): return "foo bar" diff --git a/tests/test_application.py b/tests/test_application.py index 5ba737307..dea2524a2 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1078,6 +1078,41 @@ openapi_schema = { ], } }, + "/query/params": { + "get": { + "summary": "Get Query Params", + "operationId": "get_query_params_query_params_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Queries", + "type": "object", + "additionalProperties": {"type": "integer"}, + "default": {}, + }, + "name": "queries", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, "/enum-status-code": { "get": { "responses": { diff --git a/tests/test_invalid_sequence_param.py b/tests/test_invalid_sequence_param.py index f00dd7b93..836b5f947 100644 --- a/tests/test_invalid_sequence_param.py +++ b/tests/test_invalid_sequence_param.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Tuple import pytest from fastapi import FastAPI, Query @@ -39,15 +39,3 @@ def test_invalid_dict(): @app.get("/items/") def read_items(q: Dict[str, Item] = Query(None)): pass # pragma: no cover - - -def test_invalid_simple_dict(): - with pytest.raises(AssertionError): - app = FastAPI() - - class Item(BaseModel): - title: str - - @app.get("/items/") - def read_items(q: Optional[dict] = Query(None)): - pass # pragma: no cover From 279285e4ec5e6ea296f3545bac1d7e4abaa63ca0 Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Mon, 13 Mar 2023 17:09:43 +0000 Subject: [PATCH 05/24] use mapping type --- tests/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/main.py b/tests/main.py index 3bc4150cc..842a0e745 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,5 +1,5 @@ import http -from typing import FrozenSet, Optional +from typing import FrozenSet, Mapping, Optional from fastapi import FastAPI, Path, Query @@ -190,7 +190,7 @@ def get_query_param_required_type(query: int = Query()): @app.get("/query/params") -def get_query_params(queries: Dict[str, int] = Query({})): +def get_query_params(queries: Mapping[str, int] = Query({})): return f"foo bar {queries}" From 2b0e941acb035ba880a44c8b407a794a3b5d8403 Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Mon, 13 Mar 2023 18:01:40 +0000 Subject: [PATCH 06/24] change parsing of return values to account for multiple copies of same param --- fastapi/dependencies/utils.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index a881312d7..479a740c6 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -18,6 +18,8 @@ from typing import ( cast, ) +from collections import defaultdict + import anyio from fastapi import params from fastapi.concurrency import ( @@ -253,10 +255,11 @@ def is_scalar_mapping_field(field: ModelField) -> bool: if (field.shape in mapping_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_scalar_field(sub_field): - return False + if field.sub_fields is None: + return True + for sub_field in field.sub_fields: + if (not is_scalar_field(sub_field)) and (not is_scalar_sequence_field(sub_field)): + return False return True if lenient_issubclass(field.type_, mapping_types): return True @@ -625,7 +628,9 @@ def request_params_to_args( elif is_scalar_mapping_field(field) and isinstance( received_params, (QueryParams,) ): - value = received_params._dict + value = defaultdict(list) + for key, field_value in received_params.multi_items(): + value[key].append(field_value) else: value = received_params.get(field.alias) field_info = field.field_info From 668f96029dbfca983b5fd813a994f5f821446eae Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Mon, 13 Mar 2023 18:01:49 +0000 Subject: [PATCH 07/24] linting --- fastapi/dependencies/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 479a740c6..804a2940a 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -17,7 +17,6 @@ from typing import ( Union, cast, ) - from collections import defaultdict import anyio From ee70593999a92d8e8170367870de9d1a86b14d1c Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Mon, 13 Mar 2023 18:14:18 +0000 Subject: [PATCH 08/24] fix test --- tests/test_invalid_sequence_param.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_invalid_sequence_param.py b/tests/test_invalid_sequence_param.py index f03d18fb5..475786adb 100644 --- a/tests/test_invalid_sequence_param.py +++ b/tests/test_invalid_sequence_param.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple import pytest from fastapi import FastAPI, Query From fb8a2d5fb2e0b083f03eb65142e210bd1db1761b Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Mon, 13 Mar 2023 18:18:06 +0000 Subject: [PATCH 09/24] import order --- fastapi/dependencies/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 804a2940a..01fe85c2b 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,5 +1,6 @@ import dataclasses import inspect +from collections import defaultdict from contextlib import contextmanager from copy import deepcopy from typing import ( @@ -17,7 +18,6 @@ from typing import ( Union, cast, ) -from collections import defaultdict import anyio from fastapi import params @@ -257,7 +257,9 @@ def is_scalar_mapping_field(field: ModelField) -> bool: if field.sub_fields is None: return True for sub_field in field.sub_fields: - if (not is_scalar_field(sub_field)) and (not is_scalar_sequence_field(sub_field)): + if (not is_scalar_field(sub_field)) and ( + not is_scalar_sequence_field(sub_field) + ): return False return True if lenient_issubclass(field.type_, mapping_types): From 758590f66c5a4810429d869e385f42f0b15ce15f Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Tue, 14 Mar 2023 07:12:38 +0000 Subject: [PATCH 10/24] added sequence params to test cases --- tests/main.py | 7 ++++++- tests/test_application.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/tests/main.py b/tests/main.py index 842a0e745..21f0035f5 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,5 +1,5 @@ import http -from typing import FrozenSet, Mapping, Optional +from typing import FrozenSet, List, Mapping, Optional from fastapi import FastAPI, Path, Query @@ -194,6 +194,11 @@ def get_query_params(queries: Mapping[str, int] = Query({})): return f"foo bar {queries}" +@app.get("/query/sequence-params") +def get_sequence_query_params(queries: Mapping[str, List[int]] = Query({})): + return f"foo bar {queries}" + + @app.get("/enum-status-code", status_code=http.HTTPStatus.CREATED) def get_enum_status_code(): return "foo bar" diff --git a/tests/test_application.py b/tests/test_application.py index c412515db..7bd54f0c4 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1113,6 +1113,44 @@ openapi_schema = { }, } }, + "/query/sequence-params": { + "get": { + "operationId": "get_sequence_query_params_query_sequence_params_get", + "parameters": [ + { + "in": "query", + "name": "queries", + "required": False, + "schema": { + "additionalProperties": { + "items": {"type": "integer"}, + "type": "array", + }, + "default": {}, + "title": "Queries", + "type": "object", + }, + } + ], + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful " "Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation " "Error", + }, + }, + "summary": "Get Sequence Query " "Params", + } + }, "/enum-status-code": { "get": { "responses": { From 378d30d18b36c20ac39b6e920caeb078e538b9c1 Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Tue, 14 Mar 2023 07:13:26 +0000 Subject: [PATCH 11/24] handle Map[scalar, List[scalar]] and Map[scalar, scalar] sepaeratly --- fastapi/dependencies/utils.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 01fe85c2b..5002e1a05 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -257,9 +257,22 @@ def is_scalar_mapping_field(field: ModelField) -> bool: if field.sub_fields is None: return True for sub_field in field.sub_fields: - if (not is_scalar_field(sub_field)) and ( - not is_scalar_sequence_field(sub_field) - ): + if not is_scalar_field(sub_field): + return False + return True + if lenient_issubclass(field.type_, mapping_types): + return True + return False + + +def is_scalar_sequence_mapping_field(field: ModelField) -> bool: + if (field.shape in mapping_shapes) and not lenient_issubclass( + field.type_, BaseModel + ): + if field.sub_fields is None: + return True + for sub_field in field.sub_fields: + if not is_scalar_sequence_field(sub_field): return False return True if lenient_issubclass(field.type_, mapping_types): @@ -349,6 +362,7 @@ def get_dependant( elif isinstance(param.default, (params.Query, params.Header)) and ( is_scalar_sequence_field(param_field) or is_scalar_mapping_field(param_field) + or is_scalar_sequence_mapping_field(param_field) ): add_param_to_fields(field=param_field, dependant=dependant) else: @@ -628,10 +642,15 @@ def request_params_to_args( value = received_params.getlist(field.alias) or field.default elif is_scalar_mapping_field(field) and isinstance( received_params, (QueryParams,) + ): + value = dict(received_params.multi_items()) + elif is_scalar_sequence_mapping_field(field) and isinstance( + received_params, (QueryParams,) ): value = defaultdict(list) for key, field_value in received_params.multi_items(): value[key].append(field_value) + value = dict(value) else: value = received_params.get(field.alias) field_info = field.field_info From cfa97c5884dce8183af95a3d1fdf27255a31ec1a Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Tue, 14 Mar 2023 07:13:50 +0000 Subject: [PATCH 12/24] remove check for now-valid sequence param --- tests/test_invalid_sequence_param.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/test_invalid_sequence_param.py b/tests/test_invalid_sequence_param.py index 475786adb..cb61cfdce 100644 --- a/tests/test_invalid_sequence_param.py +++ b/tests/test_invalid_sequence_param.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Tuple import pytest from fastapi import FastAPI, Query @@ -39,15 +39,3 @@ def test_invalid_dict(): @app.get("/items/") def read_items(q: Dict[str, Item] = Query(default=None)): pass # pragma: no cover - - -def test_invalid_simple_dict(): - with pytest.raises(AssertionError): - app = FastAPI() - - class Item(BaseModel): - title: str - - @app.get("/items/") - def read_items(q: Optional[dict] = Query(default=None)): - pass # pragma: no cover From 6d411b53cf69b1e47a115d5ed8e66202b3081ef3 Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Tue, 14 Mar 2023 07:14:12 +0000 Subject: [PATCH 13/24] add test queries for params and sequence params --- tests/test_query.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_query.py b/tests/test_query.py index b44e188cd..6fbca4d2c 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -57,7 +57,12 @@ response_not_valid_int = { ( "/query/params?first-query=1&second-query=2", 200, - "foo bar {'first-query': 1, " "'second-query': 2}", + "foo bar {'first-query': 1, 'second-query': 2}", + ), + ( + "/query/sequence-params?first-query=1&first-query=2", + 200, + "foo bar {'first-query': [1, 2]}", ), ], ) From 4e15aa1d124a808a422772b9ffa8e20690c8ce98 Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Tue, 14 Mar 2023 09:08:45 +0000 Subject: [PATCH 14/24] adding tests and restricting mappings to only Mapping type --- fastapi/dependencies/utils.py | 12 ++---------- tests/main.py | 8 ++++---- tests/test_application.py | 8 ++++---- tests/test_query.py | 2 +- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 5002e1a05..ee8b003cc 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -77,8 +77,8 @@ sequence_shape_to_type = { } mapping_shapes = {SHAPE_MAPPING} -mapping_types = dict -mapping_shapes_to_type = {SHAPE_MAPPING: dict} +mapping_types = (Mapping) +mapping_shapes_to_type = {SHAPE_MAPPING: Mapping} multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -254,14 +254,10 @@ def is_scalar_mapping_field(field: ModelField) -> bool: if (field.shape in mapping_shapes) and not lenient_issubclass( field.type_, BaseModel ): - if field.sub_fields is None: - return True for sub_field in field.sub_fields: if not is_scalar_field(sub_field): return False return True - if lenient_issubclass(field.type_, mapping_types): - return True return False @@ -269,14 +265,10 @@ def is_scalar_sequence_mapping_field(field: ModelField) -> bool: if (field.shape in mapping_shapes) and not lenient_issubclass( field.type_, BaseModel ): - if field.sub_fields is None: - return True for sub_field in field.sub_fields: if not is_scalar_sequence_field(sub_field): return False return True - if lenient_issubclass(field.type_, mapping_types): - return True return False diff --git a/tests/main.py b/tests/main.py index 21f0035f5..6040a53b5 100644 --- a/tests/main.py +++ b/tests/main.py @@ -190,13 +190,13 @@ def get_query_param_required_type(query: int = Query()): @app.get("/query/params") -def get_query_params(queries: Mapping[str, int] = Query({})): - return f"foo bar {queries}" +def get_query_params(query: Mapping[str, int] = Query({})): + return f"foo bar {query}" @app.get("/query/sequence-params") -def get_sequence_query_params(queries: Mapping[str, List[int]] = Query({})): - return f"foo bar {queries}" +def get_sequence_query_params(query: Mapping[str, List[int]] = Query({})): + return f"foo bar {query}" @app.get("/enum-status-code", status_code=http.HTTPStatus.CREATED) diff --git a/tests/test_application.py b/tests/test_application.py index 7bd54f0c4..9363e1280 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1086,12 +1086,12 @@ openapi_schema = { { "required": False, "schema": { - "title": "Queries", + "title": "Query", "type": "object", "additionalProperties": {"type": "integer"}, "default": {}, }, - "name": "queries", + "name": "query", "in": "query", } ], @@ -1119,7 +1119,7 @@ openapi_schema = { "parameters": [ { "in": "query", - "name": "queries", + "name": "query", "required": False, "schema": { "additionalProperties": { @@ -1127,7 +1127,7 @@ openapi_schema = { "type": "array", }, "default": {}, - "title": "Queries", + "title": "Query", "type": "object", }, } diff --git a/tests/test_query.py b/tests/test_query.py index 6fbca4d2c..3e0544bb2 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -63,7 +63,7 @@ response_not_valid_int = { "/query/sequence-params?first-query=1&first-query=2", 200, "foo bar {'first-query': [1, 2]}", - ), + ) ], ) def test_get_path(path, expected_status, expected_response): From 846ae007c8bcaadf5c59cd252dd74af5502fce7e Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Tue, 14 Mar 2023 09:10:43 +0000 Subject: [PATCH 15/24] linting --- fastapi/dependencies/utils.py | 2 +- tests/test_query.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index ee8b003cc..dad7e6562 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -77,7 +77,7 @@ sequence_shape_to_type = { } mapping_shapes = {SHAPE_MAPPING} -mapping_types = (Mapping) +mapping_types = Mapping mapping_shapes_to_type = {SHAPE_MAPPING: Mapping} multipart_not_installed_error = ( diff --git a/tests/test_query.py b/tests/test_query.py index 3e0544bb2..6fbca4d2c 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -63,7 +63,7 @@ response_not_valid_int = { "/query/sequence-params?first-query=1&first-query=2", 200, "foo bar {'first-query': [1, 2]}", - ) + ), ], ) def test_get_path(path, expected_status, expected_response): From 2251c78e146752a876340ea9361206d636bb13c2 Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Tue, 14 Mar 2023 09:14:49 +0000 Subject: [PATCH 16/24] add none check back in --- fastapi/dependencies/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index dad7e6562..97c1f7eaf 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -254,6 +254,8 @@ def is_scalar_mapping_field(field: ModelField) -> bool: if (field.shape in mapping_shapes) and not lenient_issubclass( field.type_, BaseModel ): + if field.sub_fields is None: + return False for sub_field in field.sub_fields: if not is_scalar_field(sub_field): return False @@ -265,6 +267,8 @@ def is_scalar_sequence_mapping_field(field: ModelField) -> bool: if (field.shape in mapping_shapes) and not lenient_issubclass( field.type_, BaseModel ): + if field.sub_fields is None: + return False for sub_field in field.sub_fields: if not is_scalar_sequence_field(sub_field): return False From b0109cd3995317816fa767cb8c5b256e90bc720a Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Tue, 14 Mar 2023 11:10:54 +0000 Subject: [PATCH 17/24] add test for invalid mapping --- fastapi/dependencies/utils.py | 16 ++++++++-------- tests/test_invalid_mapping_param.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 tests/test_invalid_mapping_param.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 97c1f7eaf..3d969ed10 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -251,11 +251,11 @@ def is_scalar_sequence_field(field: ModelField) -> bool: def is_scalar_mapping_field(field: ModelField) -> bool: - if (field.shape in mapping_shapes) and not lenient_issubclass( - field.type_, BaseModel + if ( + (field.shape in mapping_shapes) + and not lenient_issubclass(field.type_, BaseModel) + and field.sub_fields is not None ): - if field.sub_fields is None: - return False for sub_field in field.sub_fields: if not is_scalar_field(sub_field): return False @@ -264,11 +264,11 @@ def is_scalar_mapping_field(field: ModelField) -> bool: def is_scalar_sequence_mapping_field(field: ModelField) -> bool: - if (field.shape in mapping_shapes) and not lenient_issubclass( - field.type_, BaseModel + if ( + (field.shape in mapping_shapes) + and not lenient_issubclass(field.type_, BaseModel) + and field.sub_fields is not None ): - if field.sub_fields is None: - return False for sub_field in field.sub_fields: if not is_scalar_sequence_field(sub_field): return False diff --git a/tests/test_invalid_mapping_param.py b/tests/test_invalid_mapping_param.py new file mode 100644 index 000000000..ef49a3218 --- /dev/null +++ b/tests/test_invalid_mapping_param.py @@ -0,0 +1,15 @@ +from typing import Mapping, List + +import pytest +from fastapi import FastAPI, Query +from pydantic import BaseModel + + +def test_invalid_sequence(): + with pytest.raises(AssertionError): + app = FastAPI() + + + @app.get("/items/") + def read_items(q: Mapping[str, List[List[str]]] = Query(default=None)): + pass # pragma: no cover From cc9d9d3f13c71b84636b42086dd067896dca966b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 11:53:42 +0000 Subject: [PATCH 18/24] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 12 +++++++----- tests/test_invalid_mapping_param.py | 4 +--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index b3d013241..aa9ed56be 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -40,7 +40,6 @@ from pydantic.fields import ( SHAPE_LIST, SHAPE_MAPPING, SHAPE_SEQUENCE, - SHAPE_MAPPING, SHAPE_SET, SHAPE_SINGLETON, SHAPE_TUPLE, @@ -251,6 +250,7 @@ def is_scalar_sequence_field(field: ModelField) -> bool: return True return False + def is_scalar_mapping_field(field: ModelField) -> bool: if (field.shape in mapping_shapes) and not lenient_issubclass( field.type_, BaseModel @@ -276,6 +276,7 @@ def is_scalar_sequence_mapping_field(field: ModelField) -> bool: return True return False + def is_scalar_mapping_field(field: ModelField) -> bool: if ( (field.shape in mapping_shapes) @@ -544,9 +545,10 @@ def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool: param_field.field_info, (params.Query, params.Header) ) and is_scalar_sequence_field(param_field): return False - elif isinstance( - param_field.field_info, (params.Query, params.Header) - ) and (is_scalar_sequence_mapping_field(param_field) or is_scalar_mapping_field(param_field)): + elif isinstance(param_field.field_info, (params.Query, params.Header)) and ( + is_scalar_sequence_mapping_field(param_field) + or is_scalar_mapping_field(param_field) + ): return False else: assert isinstance( @@ -751,7 +753,7 @@ def request_params_to_args( ): if not len(received_params.multi_items()): value = field.default - else: + else: value = defaultdict(list) for k, v in received_params.multi_items(): value[k].append(v) diff --git a/tests/test_invalid_mapping_param.py b/tests/test_invalid_mapping_param.py index ef49a3218..249a759d5 100644 --- a/tests/test_invalid_mapping_param.py +++ b/tests/test_invalid_mapping_param.py @@ -1,15 +1,13 @@ -from typing import Mapping, List +from typing import List, Mapping import pytest from fastapi import FastAPI, Query -from pydantic import BaseModel def test_invalid_sequence(): with pytest.raises(AssertionError): app = FastAPI() - @app.get("/items/") def read_items(q: Mapping[str, List[List[str]]] = Query(default=None)): pass # pragma: no cover From 405fe23735dd5779684a65b3d6d20aea93e933ff Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Fri, 7 Jul 2023 19:18:50 +0000 Subject: [PATCH 19/24] add is_scaler_mapping back --- .gitignore | 3 +++ .pre-commit-config.yaml | 2 +- fastapi/_compat.py | 12 ++++++++++++ fastapi/dependencies/utils.py | 2 ++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d380d16b7..c8247162f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ archive.zip *~ .*.sw? .cache + +main.py +.devcontainer diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f7085f72..f97947a8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks default_language_version: - python: python3.10 + python: python3.11 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 2233fe33c..8c76a8011 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -227,6 +227,12 @@ if PYDANTIC_V2: def is_scalar_sequence_field(field: ModelField) -> bool: return field_annotation_is_scalar_sequence(field.field_info.annotation) + + def is_scalar_sequence_mapping_field(field: ModelField) -> bool: + return field_annotation_is_scalar_sequence(field.field_info.annotation) + + def is_scalar_mapping_field(field: ModelField) -> bool: + return field_annotation_is_scalar_sequence(field.field_info.annotation) def is_bytes_field(field: ModelField) -> bool: return is_bytes_or_nonable_bytes_annotation(field.type_) @@ -467,6 +473,12 @@ else: def is_scalar_sequence_field(field: ModelField) -> bool: return is_pv1_scalar_sequence_field(field) + + def is_scalar_sequence_mapping_field(field: ModelField) -> bool: + return is_pv1_scalar_sequence_field(field) + + def is_scalar_mapping_field(field: ModelField) -> bool: + return is_pv1_scalar_sequence_field(field) def is_bytes_field(field: ModelField) -> bool: return lenient_issubclass(field.type_, bytes) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 0556325b9..255f58a22 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -37,6 +37,8 @@ from fastapi._compat import ( is_bytes_sequence_field, is_scalar_field, is_scalar_sequence_field, + is_scalar_mapping_field, + is_scalar_sequence_mapping_field, is_sequence_field, is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_sequence_annotation, From 3e85d2cda27e48858a57400cea4dfb0ac12eae3c Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Fri, 7 Jul 2023 19:24:24 +0000 Subject: [PATCH 20/24] add pydantic v1 support --- fastapi/_compat.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 8c76a8011..1d9a3738e 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -331,6 +331,10 @@ else: SHAPE_SEQUENCE, SHAPE_TUPLE_ELLIPSIS, } + + mapping_shapes = { + SHAPE_MAPPING, + } sequence_shape_to_type = { SHAPE_LIST: list, SHAPE_SET: set, @@ -405,6 +409,32 @@ else: if _annotation_is_sequence(field.type_): return True return False + + def is_pv1_scalar_mapping_field(field: ModelField) -> bool: + if (field.shape in mapping_shapes) and not lenient_issubclass( + field.type_, BaseModel + ): + if field.sub_fields is None: + return False + for sub_field in field.sub_fields: + if not is_scalar_field(sub_field): + return False + return True + return False + + + def is_pv1_scalar_sequence_mapping_field(field: ModelField) -> bool: + if (field.shape in mapping_shapes) and not lenient_issubclass( + field.type_, BaseModel + ): + if field.sub_fields is None: + return False + for sub_field in field.sub_fields: + if not is_scalar_sequence_field(sub_field): + return False + return True + return False + def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: use_errors: List[Any] = [] From b11f63c052c004538a000c2e56e52a3740cf4a8c Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Fri, 7 Jul 2023 19:28:45 +0000 Subject: [PATCH 21/24] add mapping --- fastapi/_compat.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 1d9a3738e..7a4622f4b 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -286,6 +286,7 @@ else: SHAPE_SINGLETON, SHAPE_TUPLE, SHAPE_TUPLE_ELLIPSIS, + SHAPE_MAPPING, ) from pydantic.fields import FieldInfo as FieldInfo from pydantic.fields import ( # type: ignore[no-redef,attr-defined] @@ -332,9 +333,6 @@ else: SHAPE_TUPLE_ELLIPSIS, } - mapping_shapes = { - SHAPE_MAPPING, - } sequence_shape_to_type = { SHAPE_LIST: list, SHAPE_SET: set, @@ -343,6 +341,11 @@ else: SHAPE_TUPLE_ELLIPSIS: list, } + mapping_shapes = { + SHAPE_MAPPING, + } + mapping_shapes_to_type = {SHAPE_MAPPING: Mapping} + @dataclass class GenerateJsonSchema: # type: ignore[no-redef] ref_template: str @@ -505,10 +508,10 @@ else: return is_pv1_scalar_sequence_field(field) def is_scalar_sequence_mapping_field(field: ModelField) -> bool: - return is_pv1_scalar_sequence_field(field) + return is_pv1_scalar_sequence_mapping_field(field) def is_scalar_mapping_field(field: ModelField) -> bool: - return is_pv1_scalar_sequence_field(field) + return is_pv1_scalar_mapping_field(field) def is_bytes_field(field: ModelField) -> bool: return lenient_issubclass(field.type_, bytes) From 168d839114e69d8cf9d9f99626e2c15687c0d851 Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Fri, 7 Jul 2023 19:29:17 +0000 Subject: [PATCH 22/24] fmt --- fastapi/_compat.py | 14 ++++++-------- fastapi/dependencies/utils.py | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 7a4622f4b..8fadd6ed3 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -227,10 +227,10 @@ if PYDANTIC_V2: def is_scalar_sequence_field(field: ModelField) -> bool: return field_annotation_is_scalar_sequence(field.field_info.annotation) - + def is_scalar_sequence_mapping_field(field: ModelField) -> bool: return field_annotation_is_scalar_sequence(field.field_info.annotation) - + def is_scalar_mapping_field(field: ModelField) -> bool: return field_annotation_is_scalar_sequence(field.field_info.annotation) @@ -281,12 +281,12 @@ else: from pydantic.fields import ( # type: ignore[attr-defined] SHAPE_FROZENSET, SHAPE_LIST, + SHAPE_MAPPING, SHAPE_SEQUENCE, SHAPE_SET, SHAPE_SINGLETON, SHAPE_TUPLE, SHAPE_TUPLE_ELLIPSIS, - SHAPE_MAPPING, ) from pydantic.fields import FieldInfo as FieldInfo from pydantic.fields import ( # type: ignore[no-redef,attr-defined] @@ -412,7 +412,7 @@ else: if _annotation_is_sequence(field.type_): return True return False - + def is_pv1_scalar_mapping_field(field: ModelField) -> bool: if (field.shape in mapping_shapes) and not lenient_issubclass( field.type_, BaseModel @@ -425,7 +425,6 @@ else: return True return False - def is_pv1_scalar_sequence_mapping_field(field: ModelField) -> bool: if (field.shape in mapping_shapes) and not lenient_issubclass( field.type_, BaseModel @@ -438,7 +437,6 @@ else: return True return False - def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: use_errors: List[Any] = [] for error in errors: @@ -506,10 +504,10 @@ else: def is_scalar_sequence_field(field: ModelField) -> bool: return is_pv1_scalar_sequence_field(field) - + def is_scalar_sequence_mapping_field(field: ModelField) -> bool: return is_pv1_scalar_sequence_mapping_field(field) - + def is_scalar_mapping_field(field: ModelField) -> bool: return is_pv1_scalar_mapping_field(field) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 255f58a22..2b6e7cf7a 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -36,8 +36,8 @@ from fastapi._compat import ( is_bytes_field, is_bytes_sequence_field, is_scalar_field, - is_scalar_sequence_field, is_scalar_mapping_field, + is_scalar_sequence_field, is_scalar_sequence_mapping_field, is_sequence_field, is_uploadfile_or_nonable_uploadfile_annotation, From 0e835695f1d89f72f8e04ec896590e4898b9a006 Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Fri, 7 Jul 2023 20:07:03 +0000 Subject: [PATCH 23/24] add mapping types --- fastapi/_compat.py | 54 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 8fadd6ed3..2fbef2d15 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -43,6 +43,14 @@ sequence_annotation_to_type = { sequence_types = tuple(sequence_annotation_to_type.keys()) +mapping_annotation_to_type = { + Mapping: list, + List: list, +} + +mapping_types = tuple(mapping_annotation_to_type.keys()) + + if PYDANTIC_V2: from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import TypeAdapter @@ -229,10 +237,10 @@ if PYDANTIC_V2: return field_annotation_is_scalar_sequence(field.field_info.annotation) def is_scalar_sequence_mapping_field(field: ModelField) -> bool: - return field_annotation_is_scalar_sequence(field.field_info.annotation) + return field_annotation_is_scalar_sequence_mapping(field.field_info.annotation) def is_scalar_mapping_field(field: ModelField) -> bool: - return field_annotation_is_scalar_sequence(field.field_info.annotation) + return field_annotation_is_scalar_mapping(field.field_info.annotation) def is_bytes_field(field: ModelField) -> bool: return is_bytes_or_nonable_bytes_annotation(field.type_) @@ -559,6 +567,15 @@ def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: get_origin(annotation) ) +def _annotation_is_mapping(annotation: Union[Type[Any], None]) -> bool: + if lenient_issubclass(annotation, (str, bytes)): + return False + return lenient_issubclass(annotation, mapping_types) + +def field_annotation_is_mapping(annotation: Union[Type[Any], None]) -> bool: + return _annotation_is_mapping(annotation) or _annotation_is_sequence( + get_origin(annotation) + ) def value_is_sequence(value: Any) -> bool: return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] @@ -566,7 +583,7 @@ def value_is_sequence(value: Any) -> bool: def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: return ( - lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) + lenient_issubclass(annotation, (BaseModel, UploadFile)) or _annotation_is_sequence(annotation) or is_dataclass(annotation) ) @@ -606,6 +623,37 @@ def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> b for sub_annotation in get_args(annotation) ) +def field_annotation_is_scalar_mapping(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one_scalar_mapping = False + for arg in get_args(annotation): + if field_annotation_is_scalar_mapping(arg): + at_least_one_scalar_mapping = True + continue + elif not field_annotation_is_scalar(arg): + return False + return at_least_one_scalar_mapping + return field_annotation_is_mapping(annotation) and all( + field_annotation_is_scalar(sub_annotation) + for sub_annotation in get_args(annotation) + ) + +def field_annotation_is_scalar_sequence_mapping(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one_scalar_mapping = False + for arg in get_args(annotation): + if field_annotation_is_scalar_mapping(arg): + at_least_one_scalar_mapping = True + continue + elif not field_annotation_is_scalar(arg): + return False + return at_least_one_scalar_mapping + return field_annotation_is_mapping(annotation) and all( + (field_annotation_is_scalar_sequence(sub_annotation) or field_annotation_is_scalar(sub_annotation)) + for sub_annotation in get_args(annotation) + ) def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: if lenient_issubclass(annotation, bytes): From 32f2db3fac4a737b37481c55b9e7f3063323348c Mon Sep 17 00:00:00 2001 From: JONEMI19 Date: Fri, 7 Jul 2023 21:01:55 +0000 Subject: [PATCH 24/24] tests passing for freeform queries --- fastapi/_compat.py | 19 +- fastapi/dependencies/utils.py | 2 +- test_openapi.json | 1472 +++++++++++++++++++++++++++++++++ tests/main.py | 15 +- tests/test_application.py | 1394 ++++++++++++++++--------------- tests/test_query.py | 6 + 6 files changed, 2252 insertions(+), 656 deletions(-) create mode 100644 test_openapi.json diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 2fbef2d15..bdf40371b 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -45,7 +45,6 @@ sequence_types = tuple(sequence_annotation_to_type.keys()) mapping_annotation_to_type = { Mapping: list, - List: list, } mapping_types = tuple(mapping_annotation_to_type.keys()) @@ -567,16 +566,19 @@ def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: get_origin(annotation) ) + def _annotation_is_mapping(annotation: Union[Type[Any], None]) -> bool: if lenient_issubclass(annotation, (str, bytes)): return False return lenient_issubclass(annotation, mapping_types) + def field_annotation_is_mapping(annotation: Union[Type[Any], None]) -> bool: - return _annotation_is_mapping(annotation) or _annotation_is_sequence( + return _annotation_is_mapping(annotation) or _annotation_is_mapping( get_origin(annotation) ) + def value_is_sequence(value: Any) -> bool: return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] @@ -585,6 +587,7 @@ def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: return ( lenient_issubclass(annotation, (BaseModel, UploadFile)) or _annotation_is_sequence(annotation) + or _annotation_is_mapping(annotation) or is_dataclass(annotation) ) @@ -623,6 +626,7 @@ def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> b for sub_annotation in get_args(annotation) ) + def field_annotation_is_scalar_mapping(annotation: Union[Type[Any], None]) -> bool: origin = get_origin(annotation) if origin is Union or origin is UnionType: @@ -639,7 +643,10 @@ def field_annotation_is_scalar_mapping(annotation: Union[Type[Any], None]) -> bo for sub_annotation in get_args(annotation) ) -def field_annotation_is_scalar_sequence_mapping(annotation: Union[Type[Any], None]) -> bool: + +def field_annotation_is_scalar_sequence_mapping( + annotation: Union[Type[Any], None] +) -> bool: origin = get_origin(annotation) if origin is Union or origin is UnionType: at_least_one_scalar_mapping = False @@ -651,10 +658,14 @@ def field_annotation_is_scalar_sequence_mapping(annotation: Union[Type[Any], Non return False return at_least_one_scalar_mapping return field_annotation_is_mapping(annotation) and all( - (field_annotation_is_scalar_sequence(sub_annotation) or field_annotation_is_scalar(sub_annotation)) + ( + field_annotation_is_scalar_sequence(sub_annotation) + or field_annotation_is_scalar(sub_annotation) + ) for sub_annotation in get_args(annotation) ) + def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: if lenient_issubclass(annotation, bytes): return True diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 2b6e7cf7a..3184664ca 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -652,7 +652,7 @@ def request_params_to_args( received_params, (QueryParams, Headers) ): value = received_params.getlist(field.alias) or field.default - if is_scalar_mapping_field(field) and isinstance( + elif is_scalar_mapping_field(field) and isinstance( received_params, (QueryParams, Headers) ): value = dict(received_params.multi_items()) or field.default diff --git a/test_openapi.json b/test_openapi.json new file mode 100644 index 000000000..05df1c120 --- /dev/null +++ b/test_openapi.json @@ -0,0 +1,1472 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/api_route": { + "get": { + "summary": "Non Operation", + "operationId": "non_operation_api_route_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/non_decorated_route": { + "get": { + "summary": "Non Decorated Route", + "operationId": "non_decorated_route_non_decorated_route_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/text": { + "get": { + "summary": "Get Text", + "operationId": "get_text_text_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/path/{item_id}": { + "get": { + "summary": "Get Id", + "operationId": "get_id_path__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/str/{item_id}": { + "get": { + "summary": "Get Str Id", + "operationId": "get_str_id_path_str__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/int/{item_id}": { + "get": { + "summary": "Get Int Id", + "operationId": "get_int_id_path_int__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/float/{item_id}": { + "get": { + "summary": "Get Float Id", + "operationId": "get_float_id_path_float__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "number", + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/bool/{item_id}": { + "get": { + "summary": "Get Bool Id", + "operationId": "get_bool_id_path_bool__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "boolean", + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param/{item_id}": { + "get": { + "summary": "Get Path Param Id", + "operationId": "get_path_param_id_path_param__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-minlength/{item_id}": { + "get": { + "summary": "Get Path Param Min Length", + "operationId": "get_path_param_min_length_path_param_minlength__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-maxlength/{item_id}": { + "get": { + "summary": "Get Path Param Max Length", + "operationId": "get_path_param_max_length_path_param_maxlength__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "maxLength": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-min_maxlength/{item_id}": { + "get": { + "summary": "Get Path Param Min Max Length", + "operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 2, + "maxLength": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-gt/{item_id}": { + "get": { + "summary": "Get Path Param Gt", + "operationId": "get_path_param_gt_path_param_gt__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "number", + "exclusiveMinimum": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-gt0/{item_id}": { + "get": { + "summary": "Get Path Param Gt0", + "operationId": "get_path_param_gt0_path_param_gt0__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "number", + "exclusiveMinimum": 0, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-ge/{item_id}": { + "get": { + "summary": "Get Path Param Ge", + "operationId": "get_path_param_ge_path_param_ge__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "number", + "minimum": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-lt/{item_id}": { + "get": { + "summary": "Get Path Param Lt", + "operationId": "get_path_param_lt_path_param_lt__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "number", + "exclusiveMaximum": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-lt0/{item_id}": { + "get": { + "summary": "Get Path Param Lt0", + "operationId": "get_path_param_lt0_path_param_lt0__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "number", + "exclusiveMaximum": 0, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-le/{item_id}": { + "get": { + "summary": "Get Path Param Le", + "operationId": "get_path_param_le_path_param_le__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "number", + "maximum": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-lt-gt/{item_id}": { + "get": { + "summary": "Get Path Param Lt Gt", + "operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "number", + "exclusiveMaximum": 3, + "exclusiveMinimum": 1, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-le-ge/{item_id}": { + "get": { + "summary": "Get Path Param Le Ge", + "operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "number", + "maximum": 3, + "minimum": 1, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-lt-int/{item_id}": { + "get": { + "summary": "Get Path Param Lt Int", + "operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "exclusiveMaximum": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-gt-int/{item_id}": { + "get": { + "summary": "Get Path Param Gt Int", + "operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "exclusiveMinimum": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-le-int/{item_id}": { + "get": { + "summary": "Get Path Param Le Int", + "operationId": "get_path_param_le_int_path_param_le_int__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "maximum": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-ge-int/{item_id}": { + "get": { + "summary": "Get Path Param Ge Int", + "operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 3, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-lt-gt-int/{item_id}": { + "get": { + "summary": "Get Path Param Lt Gt Int", + "operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "exclusiveMaximum": 3, + "exclusiveMinimum": 1, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/path/param-le-ge-int/{item_id}": { + "get": { + "summary": "Get Path Param Le Ge Int", + "operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "maximum": 3, + "minimum": 1, + "title": "Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query": { + "get": { + "summary": "Get Query", + "operationId": "get_query_query_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": true, + "schema": { + "title": "Query" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/optional": { + "get": { + "summary": "Get Query Optional", + "operationId": "get_query_optional_query_optional_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": false, + "schema": { + "title": "Query" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/int": { + "get": { + "summary": "Get Query Type", + "operationId": "get_query_type_query_int_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Query" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/int/optional": { + "get": { + "summary": "Get Query Type Optional", + "operationId": "get_query_type_optional_query_int_optional_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Query" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/int/default": { + "get": { + "summary": "Get Query Type Int Default", + "operationId": "get_query_type_int_default_query_int_default_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 10, + "title": "Query" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/param": { + "get": { + "summary": "Get Query Param", + "operationId": "get_query_param_query_param_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": false, + "schema": { + "title": "Query" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/param-required": { + "get": { + "summary": "Get Query Param Required", + "operationId": "get_query_param_required_query_param_required_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": true, + "schema": { + "title": "Query" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/param-required/int": { + "get": { + "summary": "Get Query Param Required Type", + "operationId": "get_query_param_required_type_query_param_required_int_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Query" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/sequence-params": { + "get": { + "summary": "Get Sequence Query Params", + "operationId": "get_sequence_query_params_query_sequence_params_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": false, + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "integer" + } + }, + "default": {}, + "title": "Query" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/mapping-params": { + "get": { + "summary": "Get Mapping Query Params", + "operationId": "get_mapping_query_params_query_mapping_params_get", + "parameters": [ + { + "name": "queries", + "in": "query", + "required": false, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": {}, + "title": "Queries" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/query/mapping-sequence-params": { + "get": { + "summary": "Get Sequence Mapping Query Params", + "operationId": "get_sequence_mapping_query_params_query_mapping_sequence_params_get", + "parameters": [ + { + "name": "queries", + "in": "query", + "required": false, + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "integer" + } + }, + "default": {}, + "title": "Queries" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/enum-status-code": { + "get": { + "summary": "Get Enum Status Code", + "operationId": "get_enum_status_code_enum_status_code_get", + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/query/frozenset": { + "get": { + "summary": "Get Query Type Frozenset", + "operationId": "get_query_type_frozenset_query_frozenset_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": true, + "schema": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "integer" + }, + "title": "Query" + } + } + ], + "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" + }, + "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" + } + } + } +} \ No newline at end of file diff --git a/tests/main.py b/tests/main.py index 935ff270f..1a590add8 100644 --- a/tests/main.py +++ b/tests/main.py @@ -184,16 +184,21 @@ def get_query_param_required_type(query: int = Query()): return f"foo bar {query}" -@app.get("/query/params") -def get_query_params(query: Mapping[str, int] = Query({})): - return f"foo bar {query}" - - @app.get("/query/sequence-params") def get_sequence_query_params(query: Mapping[str, List[int]] = Query({})): return f"foo bar {query}" +@app.get("/query/mapping-params") +def get_mapping_query_params(queries: Mapping[str, str] = Query({})): + return f"foo bar {queries['foo']} {queries['bar']}" + + +@app.get("/query/mapping-sequence-params") +def get_sequence_mapping_query_params(queries: Mapping[str, List[int]] = Query({})): + return f"foo bar {queries}" + + @app.get("/enum-status-code", status_code=http.HTTPStatus.CREATED) def get_enum_status_code(): return "foo bar" diff --git a/tests/test_application.py b/tests/test_application.py index 49e4711ed..b54a97281 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 @@ -1256,192 +1255,172 @@ def test_openapi_schema(): "paths": { "/api_route": { "get": { + "summary": "Non Operation", + "operationId": "non_operation_api_route_get", "responses": { "200": { "description": "Successful Response", "content": {"application/json": {"schema": {}}}, } }, - "summary": "Non Operation", - "operationId": "non_operation_api_route_get", } }, "/non_decorated_route": { "get": { + "summary": "Non Decorated Route", + "operationId": "non_decorated_route_non_decorated_route_get", "responses": { "200": { "description": "Successful Response", "content": {"application/json": {"schema": {}}}, } }, - "summary": "Non Decorated Route", - "operationId": "non_decorated_route_non_decorated_route_get", } }, "/text": { "get": { + "summary": "Get Text", + "operationId": "get_text_text_get", "responses": { "200": { "description": "Successful Response", "content": {"application/json": {"schema": {}}}, } }, - "summary": "Get Text", - "operationId": "get_text_text_get", } }, "/path/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Id", "operationId": "get_id_path__item_id__get", "parameters": [ { - "required": True, - "schema": {"title": "Item Id"}, "name": "item_id", "in": "path", + "required": True, + "schema": {"title": "Item Id"}, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/str/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Str Id", "operationId": "get_str_id_path_str__item_id__get", "parameters": [ { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, "name": "item_id", "in": "path", + "required": True, + "schema": {"type": "string", "title": "Item Id"}, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/int/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Int Id", "operationId": "get_int_id_path_int__item_id__get", "parameters": [ { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, "name": "item_id", "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/float/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Float Id", "operationId": "get_float_id_path_float__item_id__get", "parameters": [ { - "required": True, - "schema": {"title": "Item Id", "type": "number"}, "name": "item_id", "in": "path", + "required": True, + "schema": {"type": "number", "title": "Item Id"}, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/bool/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Bool Id", "operationId": "get_bool_id_path_bool__item_id__get", "parameters": [ { - "required": True, - "schema": {"title": "Item Id", "type": "boolean"}, "name": "item_id", "in": "path", + "required": True, + "schema": {"type": "boolean", "title": "Item Id"}, } ], - } - }, - "/path/param/{item_id}": { - "get": { "responses": { "200": { "description": "Successful Response", @@ -1458,6 +1437,10 @@ def test_openapi_schema(): }, }, }, + } + }, + "/path/param/{item_id}": { + "get": { "summary": "Get Path Param Id", "operationId": "get_path_param_id_path_param__item_id__get", "parameters": [ @@ -1465,693 +1448,685 @@ 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", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-minlength/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Min Length", "operationId": "get_path_param_min_length_path_param_minlength__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "minLength": 3, - "type": "string", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "string", + "minLength": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-maxlength/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Max Length", "operationId": "get_path_param_max_length_path_param_maxlength__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "maxLength": 3, - "type": "string", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "string", + "maxLength": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-min_maxlength/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Min Max Length", "operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "maxLength": 3, - "minLength": 2, - "type": "string", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "string", + "minLength": 2, + "maxLength": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-gt/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Gt", "operationId": "get_path_param_gt_path_param_gt__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 3.0, - "type": "number", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "number", + "exclusiveMinimum": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-gt0/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Gt0", "operationId": "get_path_param_gt0_path_param_gt0__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 0.0, - "type": "number", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "number", + "exclusiveMinimum": 0, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-ge/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Ge", "operationId": "get_path_param_ge_path_param_ge__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "minimum": 3.0, - "type": "number", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "number", + "minimum": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-lt/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Lt", "operationId": "get_path_param_lt_path_param_lt__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "type": "number", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "number", + "exclusiveMaximum": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-lt0/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Lt0", "operationId": "get_path_param_lt0_path_param_lt0__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 0.0, - "type": "number", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "number", + "exclusiveMaximum": 0, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-le/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Le", "operationId": "get_path_param_le_path_param_le__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "type": "number", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "number", + "maximum": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-lt-gt/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Lt Gt", "operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "exclusiveMinimum": 1.0, - "type": "number", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "number", + "exclusiveMaximum": 3, + "exclusiveMinimum": 1, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-le-ge/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Le Ge", "operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "minimum": 1.0, - "type": "number", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "number", + "maximum": 3, + "minimum": 1, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-lt-int/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Lt Int", "operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "type": "integer", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "integer", + "exclusiveMaximum": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-gt-int/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Gt Int", "operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 3.0, - "type": "integer", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "integer", + "exclusiveMinimum": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-le-int/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Le Int", "operationId": "get_path_param_le_int_path_param_le_int__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "type": "integer", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "integer", + "maximum": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-ge-int/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Ge Int", "operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "minimum": 3.0, - "type": "integer", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "integer", + "minimum": 3, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-lt-gt-int/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Lt Gt Int", "operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "exclusiveMinimum": 1.0, - "type": "integer", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "integer", + "exclusiveMaximum": 3, + "exclusiveMinimum": 1, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/path/param-le-ge-int/{item_id}": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Path Param Le Ge Int", "operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get", "parameters": [ { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "minimum": 1.0, - "type": "integer", - }, "name": "item_id", "in": "path", + "required": True, + "schema": { + "type": "integer", + "maximum": 3, + "minimum": 1, + "title": "Item Id", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/query": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Query", "operationId": "get_query_query_get", "parameters": [ { - "required": True, - "schema": {"title": "Query"}, "name": "query", "in": "query", + "required": True, + "schema": {"title": "Query"}, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/query/optional": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Query Optional", "operationId": "get_query_optional_query_optional_get", "parameters": [ { - "required": False, - "schema": {"title": "Query"}, "name": "query", "in": "query", + "required": False, + "schema": {"title": "Query"}, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/query/int": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Query Type", "operationId": "get_query_type_query_int_get", "parameters": [ { - "required": True, - "schema": {"title": "Query", "type": "integer"}, "name": "query", "in": "query", + "required": True, + "schema": {"type": "integer", "title": "Query"}, } ], - } - }, - "/query/int/optional": { - "get": { "responses": { "200": { "description": "Successful Response", @@ -2168,6 +2143,10 @@ def test_openapi_schema(): }, }, }, + } + }, + "/query/int/optional": { + "get": { "summary": "Get Query Type Optional", "operationId": "get_query_type_optional_query_int_optional_get", "parameters": [ @@ -2175,114 +2154,106 @@ 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", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/query/int/default": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Query Type Int Default", "operationId": "get_query_type_int_default_query_int_default_get", "parameters": [ { - "required": False, - "schema": { - "title": "Query", - "type": "integer", - "default": 10, - }, "name": "query", "in": "query", + "required": False, + "schema": { + "type": "integer", + "default": 10, + "title": "Query", + }, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/query/param": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Query Param", "operationId": "get_query_param_query_param_get", "parameters": [ { - "required": False, - "schema": {"title": "Query"}, "name": "query", "in": "query", + "required": False, + "schema": {"title": "Query"}, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/query/param-required": { "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, "summary": "Get Query Param Required", "operationId": "get_query_param_required_query_param_required_get", "parameters": [ { - "required": True, - "schema": {"title": "Query"}, "name": "query", "in": "query", + "required": True, + "schema": {"title": "Query"}, } ], - } - }, - "/query/param-required/int": { - "get": { "responses": { "200": { "description": "Successful Response", @@ -2299,28 +2270,159 @@ def test_openapi_schema(): }, }, }, + } + }, + "/query/param-required/int": { + "get": { "summary": "Get Query Param Required Type", "operationId": "get_query_param_required_type_query_param_required_int_get", "parameters": [ { - "required": True, - "schema": {"title": "Query", "type": "integer"}, "name": "query", "in": "query", + "required": True, + "schema": {"type": "integer", "title": "Query"}, } ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query/sequence-params": { + "get": { + "summary": "Get Sequence Query Params", + "operationId": "get_sequence_query_params_query_sequence_params_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": False, + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": {"type": "integer"}, + }, + "default": {}, + "title": "Query", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query/mapping-params": { + "get": { + "summary": "Get Mapping Query Params", + "operationId": "get_mapping_query_params_query_mapping_params_get", + "parameters": [ + { + "name": "queries", + "in": "query", + "required": False, + "schema": { + "type": "object", + "additionalProperties": {"type": "string"}, + "default": {}, + "title": "Queries", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query/mapping-sequence-params": { + "get": { + "summary": "Get Sequence Mapping Query Params", + "operationId": "get_sequence_mapping_query_params_query_mapping_sequence_params_get", + "parameters": [ + { + "name": "queries", + "in": "query", + "required": False, + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": {"type": "integer"}, + }, + "default": {}, + "title": "Queries", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } }, "/enum-status-code": { "get": { + "summary": "Get Enum Status Code", + "operationId": "get_enum_status_code_enum_status_code_get", "responses": { "201": { "description": "Successful Response", "content": {"application/json": {"schema": {}}}, - }, + } }, - "summary": "Get Enum Status Code", - "operationId": "get_enum_status_code_enum_status_code_get", } }, "/query/frozenset": { @@ -2329,15 +2431,15 @@ def test_openapi_schema(): "operationId": "get_query_type_frozenset_query_frozenset_get", "parameters": [ { - "required": True, - "schema": { - "title": "Query", - "uniqueItems": True, - "type": "array", - "items": {"type": "integer"}, - }, "name": "query", "in": "query", + "required": True, + "schema": { + "type": "array", + "uniqueItems": True, + "items": {"type": "integer"}, + "title": "Query", + }, } ], "responses": { @@ -2361,32 +2463,32 @@ def test_openapi_schema(): }, "components": { "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { "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"}, - } + "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_query.py b/tests/test_query.py index 5bb9995d6..43f0a8038 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -408,3 +408,9 @@ def test_query_frozenset_query_1_query_1_query_2(): response = client.get("/query/frozenset/?query=1&query=1&query=2") assert response.status_code == 200 assert response.json() == "1,2" + + +def test_mapping_query(): + response = client.get("/query/mapping-params/?foo=fuzz&bar=buzz") + assert response.status_code == 200 + assert response.json() == "foo bar fuzz buzz"