mirror of https://github.com/tiangolo/fastapi.git
Merge branch 'master-mj' into task/add-freeform-queries-110
This commit is contained in:
commit
04ba0b302b
Binary file not shown.
|
After Width: | Height: | Size: 350 KiB |
|
|
@ -225,3 +225,45 @@ In this case, there are 3 query parameters:
|
|||
|
||||
!!! tip
|
||||
You could also use `Enum`s the same way as with [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}.
|
||||
|
||||
## Free Form Query Parameters
|
||||
|
||||
Sometimes you want to receive some query parameters, but you don't know in advance what they are called. **FastAPI** provides support for this use case.
|
||||
|
||||
=== "Python 3.10+"
|
||||
|
||||
```Python hl_lines="8"
|
||||
{!> ../../../docs_src/query_params/tutorial007_py310.py!}
|
||||
```
|
||||
|
||||
And when you open your browser at <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>, you will that OpenAPI supports this format of query parameter:
|
||||
|
||||
<img src="/img/tutorial/path-params/image01.png">
|
||||
|
||||
However, since the query parameters are declared in the request as
|
||||
|
||||
```
|
||||
http://127.0.0.1:8000/query/mixed-type-params?query=1&foo=bar&foo=baz
|
||||
```
|
||||
|
||||
**FastAPI** greedily adds all the query parameters to every `Query` argument for which it is valid. The above request will be parsed as:
|
||||
|
||||
```Python
|
||||
{
|
||||
"query": 1,
|
||||
"string_mapping": {
|
||||
"query": "1",
|
||||
"foo": "baz"
|
||||
},
|
||||
"mapping_query_int": {
|
||||
"query": 1
|
||||
},
|
||||
"sequence_mapping_queries": {
|
||||
"query": [
|
||||
"1"
|
||||
],
|
||||
"foo": []
|
||||
}
|
||||
}
|
||||
```
|
||||
As you can see the `query` parameter is added to every `Query` argument for which it is valid. This is because **FastAPI** does not know which `Query` argument you want to add the `query` parameter to, and `1` validates as both an `int` and a `str`. `List[str]`. `foo` is only added to the `string_mapping` and `sequence_mapping_queries` arguments because it is not a valid `int`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
from typing import List, Mapping
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/query/mixed-type-params")
|
||||
def get_mixed_mapping_mixed_type_query_params(
|
||||
query: int = Query(),
|
||||
mapping_query_str: Mapping[str, str] = Query({}),
|
||||
mapping_query_int: Mapping[str, int] = Query({}),
|
||||
sequence_mapping_queries: Mapping[str, List[int]] = Query({}),
|
||||
):
|
||||
return {
|
||||
"query": query,
|
||||
"string_mapping": mapping_query_str,
|
||||
"mapping_query_int": mapping_query_int,
|
||||
"sequence_mapping_queries": sequence_mapping_queries,
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ from typing import (
|
|||
)
|
||||
|
||||
from fastapi.exceptions import RequestErrorModel
|
||||
from fastapi.types import IncEx, ModelNameMap, UnionType
|
||||
from fastapi.types import IncEx, ModelNameMap, UnionType, FFQuery
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.version import VERSION as PYDANTIC_VERSION
|
||||
from starlette.datastructures import UploadFile
|
||||
|
|
@ -43,6 +43,13 @@ sequence_annotation_to_type = {
|
|||
|
||||
sequence_types = tuple(sequence_annotation_to_type.keys())
|
||||
|
||||
mapping_annotation_to_type = {
|
||||
FFQuery: list,
|
||||
}
|
||||
|
||||
mapping_types = tuple(mapping_annotation_to_type.keys())
|
||||
|
||||
|
||||
if PYDANTIC_V2:
|
||||
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
|
||||
from pydantic import TypeAdapter
|
||||
|
|
@ -242,6 +249,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_mapping(field.field_info.annotation)
|
||||
|
||||
def is_scalar_mapping_field(field: ModelField) -> bool:
|
||||
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_)
|
||||
|
||||
|
|
@ -294,6 +307,7 @@ else:
|
|||
from pydantic.fields import ( # type: ignore[attr-defined]
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_LIST,
|
||||
SHAPE_MAPPING,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_SET,
|
||||
SHAPE_SINGLETON,
|
||||
|
|
@ -341,6 +355,7 @@ else:
|
|||
SHAPE_SEQUENCE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
}
|
||||
|
||||
sequence_shape_to_type = {
|
||||
SHAPE_LIST: list,
|
||||
SHAPE_SET: set,
|
||||
|
|
@ -349,6 +364,11 @@ else:
|
|||
SHAPE_TUPLE_ELLIPSIS: list,
|
||||
}
|
||||
|
||||
mapping_shapes = {
|
||||
SHAPE_MAPPING,
|
||||
}
|
||||
mapping_shapes_to_type = {SHAPE_MAPPING: FFQuery}
|
||||
|
||||
@dataclass
|
||||
class GenerateJsonSchema: # type: ignore[no-redef]
|
||||
ref_template: str
|
||||
|
|
@ -416,6 +436,28 @@ else:
|
|||
return True
|
||||
return False
|
||||
|
||||
def is_pv1_scalar_mapping_field(field: ModelField) -> bool:
|
||||
if (field.shape in mapping_shapes) and not lenient_issubclass( # type: ignore[attr-defined]
|
||||
field.type_, BaseModel
|
||||
):
|
||||
if field.sub_fields is not None: # type: ignore[attr-defined]
|
||||
for sub_field in field.sub_fields: # type: ignore[attr-defined]
|
||||
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( # type: ignore[attr-defined]
|
||||
field.type_, BaseModel
|
||||
):
|
||||
if field.sub_fields is not None: # type: ignore[attr-defined]
|
||||
for sub_field in field.sub_fields: # type: ignore[attr-defined]
|
||||
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] = []
|
||||
for error in errors:
|
||||
|
|
@ -486,6 +528,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_mapping_field(field)
|
||||
|
||||
def is_scalar_mapping_field(field: ModelField) -> bool:
|
||||
return is_pv1_scalar_mapping_field(field)
|
||||
|
||||
def is_bytes_field(field: ModelField) -> bool:
|
||||
return lenient_issubclass(field.type_, bytes)
|
||||
|
||||
|
|
@ -535,14 +583,27 @@ def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
|
|||
)
|
||||
|
||||
|
||||
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_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]
|
||||
|
||||
|
||||
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 _annotation_is_mapping(annotation)
|
||||
or is_dataclass(annotation)
|
||||
)
|
||||
|
||||
|
|
@ -573,8 +634,6 @@ def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> b
|
|||
if field_annotation_is_scalar_sequence(arg):
|
||||
at_least_one_scalar_sequence = True
|
||||
continue
|
||||
elif not field_annotation_is_scalar(arg):
|
||||
return False
|
||||
return at_least_one_scalar_sequence
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
field_annotation_is_scalar(sub_annotation)
|
||||
|
|
@ -582,6 +641,22 @@ def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> b
|
|||
)
|
||||
|
||||
|
||||
def field_annotation_is_scalar_mapping(annotation: Union[Type[Any], None]) -> bool:
|
||||
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:
|
||||
return field_annotation_is_mapping(annotation) and all(
|
||||
field_annotation_is_scalar_sequence(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)[1:]
|
||||
)
|
||||
|
||||
|
||||
def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
|
||||
if lenient_issubclass(annotation, bytes):
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import inspect
|
||||
from collections import defaultdict
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from copy import deepcopy
|
||||
from typing import (
|
||||
|
|
@ -35,7 +36,9 @@ from fastapi._compat import (
|
|||
is_bytes_field,
|
||||
is_bytes_sequence_field,
|
||||
is_scalar_field,
|
||||
is_scalar_mapping_field,
|
||||
is_scalar_sequence_field,
|
||||
is_scalar_sequence_mapping_field,
|
||||
is_sequence_field,
|
||||
is_uploadfile_or_nonable_uploadfile_annotation,
|
||||
is_uploadfile_sequence_annotation,
|
||||
|
|
@ -463,6 +466,11 @@ 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) and (
|
||||
is_scalar_sequence_mapping_field(param_field)
|
||||
or is_scalar_mapping_field(param_field)
|
||||
):
|
||||
return False
|
||||
else:
|
||||
assert isinstance(
|
||||
param_field.field_info, params.Body
|
||||
|
|
@ -647,6 +655,10 @@ async def solve_dependencies(
|
|||
return values, errors, background_tasks, response, dependency_cache
|
||||
|
||||
|
||||
class Marker:
|
||||
pass
|
||||
|
||||
|
||||
def request_params_to_args(
|
||||
required_params: Sequence[ModelField],
|
||||
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
||||
|
|
@ -658,6 +670,16 @@ 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 = dict(received_params.multi_items()) or field.default
|
||||
elif is_scalar_sequence_mapping_field(field) and isinstance(
|
||||
received_params, QueryParams
|
||||
):
|
||||
value = defaultdict(list)
|
||||
for k, v in received_params.multi_items():
|
||||
value[k].append(v)
|
||||
else:
|
||||
value = received_params.get(field.alias)
|
||||
field_info = field.field_info
|
||||
|
|
@ -674,6 +696,31 @@ def request_params_to_args(
|
|||
v_, errors_ = field.validate(value, values, loc=loc)
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif (
|
||||
isinstance(errors_, list)
|
||||
and is_scalar_sequence_mapping_field(field)
|
||||
and isinstance(received_params, QueryParams)
|
||||
):
|
||||
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
||||
# remove all invalid parameters
|
||||
marker = Marker()
|
||||
for _, _, key, index in [error["loc"] for error in new_errors]:
|
||||
value[key][index] = marker
|
||||
for key in value:
|
||||
value[key] = [x for x in value[key] if x != marker]
|
||||
v_, _ = field.validate(value, values, loc=loc)
|
||||
values[field.name] = v_
|
||||
elif (
|
||||
isinstance(errors_, list)
|
||||
and is_scalar_mapping_field(field)
|
||||
and isinstance(received_params, QueryParams)
|
||||
):
|
||||
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
||||
# remove all invalid parameters
|
||||
for _, _, key in [error["loc"] for error in new_errors]:
|
||||
value.pop(key)
|
||||
v_, _ = field.validate(value, values, loc=loc)
|
||||
values[field.name] = v_
|
||||
elif isinstance(errors_, list):
|
||||
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
||||
errors.extend(new_errors)
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
|
|||
UnionType = getattr(types, "UnionType", Union)
|
||||
ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
|
||||
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
|
||||
FFQuery = Dict[str, Union[str, IncEx]]
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import http
|
||||
from typing import FrozenSet, Optional
|
||||
from typing import FrozenSet, List, Mapping, Optional, Union
|
||||
|
||||
from fastapi import FastAPI, Path, Query
|
||||
from fastapi.types import FFQuery
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
|
@ -184,6 +185,38 @@ def get_query_param_required_type(query: int = Query()):
|
|||
return f"foo bar {query}"
|
||||
|
||||
|
||||
@app.get("/query/mapping-params")
|
||||
def get_mapping_query_params(queries: FFQuery[str, str] = Query({})):
|
||||
return f"foo bar {queries['foo']} {queries['bar']}"
|
||||
|
||||
|
||||
@app.get("/query/mapping-sequence-params")
|
||||
def get_sequence_mapping_query_params(queries: FFQuery[str, List[int]] = Query({})):
|
||||
return f"foo bar {dict(queries)}"
|
||||
|
||||
|
||||
@app.get("/query/mixed-params")
|
||||
def get_mixed_mapping_query_params(
|
||||
sequence_mapping_queries: FFQuery[str, List[Union[str, int]]] = Query({}),
|
||||
mapping_query: FFQuery[str, str] = Query(),
|
||||
query: str = Query(),
|
||||
):
|
||||
return (
|
||||
f"foo bar {sequence_mapping_queries['foo'][0]} {sequence_mapping_queries['foo'][1]} "
|
||||
f"{mapping_query['foo']} {mapping_query['bar']} {query}"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/query/mixed-type-params")
|
||||
def get_mixed_mapping_mixed_type_query_params(
|
||||
sequence_mapping_queries: FFQuery[str, List[int]] = Query({}),
|
||||
mapping_query_str: FFQuery[str, str] = Query({}),
|
||||
mapping_query_int: FFQuery[str, int] = Query({}),
|
||||
query: int = Query(),
|
||||
):
|
||||
return f"foo bar {query} {mapping_query_str} {mapping_query_int} {dict(sequence_mapping_queries)}"
|
||||
|
||||
|
||||
@app.get("/enum-status-code", status_code=http.HTTPStatus.CREATED)
|
||||
def get_enum_status_code():
|
||||
return "foo bar"
|
||||
|
|
|
|||
|
|
@ -1163,6 +1163,204 @@ def test_openapi_schema():
|
|||
},
|
||||
}
|
||||
},
|
||||
"/query/mapping-params": {
|
||||
"get": {
|
||||
"summary": "Get Mapping Query Params",
|
||||
"operationId": "get_mapping_query_params_query_mapping_params_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"additionalProperties": {"type": "string"},
|
||||
"type": "object",
|
||||
"title": "Queries",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/query/mapping-sequence-params": {
|
||||
"get": {
|
||||
"summary": "Get Sequence Mapping Query Params",
|
||||
"operationId": "get_sequence_mapping_query_params_query_mapping_sequence_params_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"additionalProperties": {
|
||||
"items": {"type": "integer"},
|
||||
"type": "array",
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Queries",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/query/mixed-params": {
|
||||
"get": {
|
||||
"summary": "Get Mixed Mapping Query Params",
|
||||
"operationId": "get_mixed_mapping_query_params_query_mixed_params_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer"},
|
||||
]
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Sequence Mapping Queries",
|
||||
"default": {},
|
||||
},
|
||||
"name": "sequence_mapping_queries",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"additionalProperties": {"type": "string"},
|
||||
"type": "object",
|
||||
"title": "Mapping Query",
|
||||
},
|
||||
"name": "mapping_query",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"type": "string", "title": "Query"},
|
||||
"name": "query",
|
||||
"in": "query",
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/query/mixed-type-params": {
|
||||
"get": {
|
||||
"summary": "Get Mixed Mapping Mixed Type Query Params",
|
||||
"operationId": "get_mixed_mapping_mixed_type_query_params_query_mixed_type_params_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"additionalProperties": {
|
||||
"items": {"type": "integer"},
|
||||
"type": "array",
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Sequence Mapping Queries",
|
||||
"default": {},
|
||||
},
|
||||
"name": "sequence_mapping_queries",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"additionalProperties": {"type": "string"},
|
||||
"type": "object",
|
||||
"title": "Mapping Query Str",
|
||||
"default": {},
|
||||
},
|
||||
"name": "mapping_query_str",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"additionalProperties": {"type": "integer"},
|
||||
"type": "object",
|
||||
"title": "Mapping Query Int",
|
||||
"default": {},
|
||||
},
|
||||
"name": "mapping_query_int",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"type": "integer", "title": "Query"},
|
||||
"name": "query",
|
||||
"in": "query",
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
from typing import List, Mapping
|
||||
import pytest
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.types import FFQuery
|
||||
|
||||
|
||||
def test_invalid_sequence():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: FFQuery[str, List[List[str]]] = Query(default=None)):
|
||||
pass # pragma: no cover
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -408,3 +408,36 @@ 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"
|
||||
|
||||
|
||||
def test_mapping_with_non_mapping_query():
|
||||
response = client.get("/query/mixed-params/?foo=fuzz&foo=baz&bar=buzz&query=fizz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar fuzz baz baz buzz fizz"
|
||||
|
||||
|
||||
def test_mapping_with_non_mapping_query_mixed_types():
|
||||
response = client.get("/query/mixed-type-params/?foo=fuzz&foo=baz&bar=buzz&query=1")
|
||||
assert response.status_code == 200
|
||||
assert (
|
||||
response.json()
|
||||
== "foo bar 1 {'foo': 'baz', 'bar': 'buzz', 'query': '1'} {'query': 1} {'foo': [], 'bar': [], 'query': [1]}"
|
||||
)
|
||||
|
||||
|
||||
def test_sequence_mapping_query():
|
||||
response = client.get("/query/mapping-sequence-params/?foo=1&foo=2")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar {'foo': [1, 2]}"
|
||||
|
||||
|
||||
def test_sequence_mapping_query_drops_invalid():
|
||||
response = client.get("/query/mapping-sequence-params/?foo=fuzz&foo=buzz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar {'foo': []}"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.query_params.tutorial007_py310 import app
|
||||
|
||||
c = TestClient(app)
|
||||
return c
|
||||
|
||||
|
||||
def test_foo_needy_very(client: TestClient):
|
||||
response = client.get("/query/mixed-type-params?query=1&query=2&foo=bar&foo=baz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"query": 2,
|
||||
"string_mapping": {"query": "2", "foo": "baz"},
|
||||
"mapping_query_int": {"query": 2},
|
||||
"sequence_mapping_queries": {"query": [1, 2], "foo": []},
|
||||
}
|
||||
Loading…
Reference in New Issue