Browse Source

Merge branch 'master-mj' into task/add-freeform-queries-110

pull/9837/head
JONEMI19 2 years ago
parent
commit
04ba0b302b
  1. BIN
      docs/en/docs/img/tutorial/query-params/image01.png
  2. 42
      docs/en/docs/tutorial/query-params.md
  3. 20
      docs_src/query_params/tutorial007_py310.py
  4. 83
      fastapi/_compat.py
  5. 47
      fastapi/dependencies/utils.py
  6. 1
      fastapi/types.py
  7. 35
      tests/main.py
  8. 198
      tests/test_application.py
  9. 13
      tests/test_invalid_mapping_param.py
  10. 14
      tests/test_invalid_sequence_param.py
  11. 33
      tests/test_query.py
  12. 21
      tests/test_tutorial/test_query_params/test_tutorial007_py310.py

BIN
docs/en/docs/img/tutorial/query-params/image01.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

42
docs/en/docs/tutorial/query-params.md

@ -225,3 +225,45 @@ In this case, there are 3 query parameters:
!!! tip !!! tip
You could also use `Enum`s the same way as with [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}. 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`.

20
docs_src/query_params/tutorial007_py310.py

@ -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,
}

83
fastapi/_compat.py

@ -18,7 +18,7 @@ from typing import (
) )
from fastapi.exceptions import RequestErrorModel 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 import BaseModel, create_model
from pydantic.version import VERSION as PYDANTIC_VERSION from pydantic.version import VERSION as PYDANTIC_VERSION
from starlette.datastructures import UploadFile from starlette.datastructures import UploadFile
@ -43,6 +43,13 @@ sequence_annotation_to_type = {
sequence_types = tuple(sequence_annotation_to_type.keys()) 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: if PYDANTIC_V2:
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import TypeAdapter from pydantic import TypeAdapter
@ -242,6 +249,12 @@ if PYDANTIC_V2:
def is_scalar_sequence_field(field: ModelField) -> bool: def is_scalar_sequence_field(field: ModelField) -> bool:
return field_annotation_is_scalar_sequence(field.field_info.annotation) 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: def is_bytes_field(field: ModelField) -> bool:
return is_bytes_or_nonable_bytes_annotation(field.type_) return is_bytes_or_nonable_bytes_annotation(field.type_)
@ -294,6 +307,7 @@ else:
from pydantic.fields import ( # type: ignore[attr-defined] from pydantic.fields import ( # type: ignore[attr-defined]
SHAPE_FROZENSET, SHAPE_FROZENSET,
SHAPE_LIST, SHAPE_LIST,
SHAPE_MAPPING,
SHAPE_SEQUENCE, SHAPE_SEQUENCE,
SHAPE_SET, SHAPE_SET,
SHAPE_SINGLETON, SHAPE_SINGLETON,
@ -341,6 +355,7 @@ else:
SHAPE_SEQUENCE, SHAPE_SEQUENCE,
SHAPE_TUPLE_ELLIPSIS, SHAPE_TUPLE_ELLIPSIS,
} }
sequence_shape_to_type = { sequence_shape_to_type = {
SHAPE_LIST: list, SHAPE_LIST: list,
SHAPE_SET: set, SHAPE_SET: set,
@ -349,6 +364,11 @@ else:
SHAPE_TUPLE_ELLIPSIS: list, SHAPE_TUPLE_ELLIPSIS: list,
} }
mapping_shapes = {
SHAPE_MAPPING,
}
mapping_shapes_to_type = {SHAPE_MAPPING: FFQuery}
@dataclass @dataclass
class GenerateJsonSchema: # type: ignore[no-redef] class GenerateJsonSchema: # type: ignore[no-redef]
ref_template: str ref_template: str
@ -416,6 +436,28 @@ else:
return True return True
return False 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]]: def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
use_errors: List[Any] = [] use_errors: List[Any] = []
for error in errors: for error in errors:
@ -486,6 +528,12 @@ else:
def is_scalar_sequence_field(field: ModelField) -> bool: def is_scalar_sequence_field(field: ModelField) -> bool:
return is_pv1_scalar_sequence_field(field) 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: def is_bytes_field(field: ModelField) -> bool:
return lenient_issubclass(field.type_, bytes) 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: def value_is_sequence(value: Any) -> bool:
return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] 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: def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
return ( return (
lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) lenient_issubclass(annotation, (BaseModel, UploadFile))
or _annotation_is_sequence(annotation) or _annotation_is_sequence(annotation)
or _annotation_is_mapping(annotation)
or is_dataclass(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): if field_annotation_is_scalar_sequence(arg):
at_least_one_scalar_sequence = True at_least_one_scalar_sequence = True
continue continue
elif not field_annotation_is_scalar(arg):
return False
return at_least_one_scalar_sequence return at_least_one_scalar_sequence
return field_annotation_is_sequence(annotation) and all( return field_annotation_is_sequence(annotation) and all(
field_annotation_is_scalar(sub_annotation) 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: def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
if lenient_issubclass(annotation, bytes): if lenient_issubclass(annotation, bytes):
return True return True

47
fastapi/dependencies/utils.py

@ -1,4 +1,5 @@
import inspect import inspect
from collections import defaultdict
from contextlib import AsyncExitStack, contextmanager from contextlib import AsyncExitStack, contextmanager
from copy import deepcopy from copy import deepcopy
from typing import ( from typing import (
@ -35,7 +36,9 @@ from fastapi._compat import (
is_bytes_field, is_bytes_field,
is_bytes_sequence_field, is_bytes_sequence_field,
is_scalar_field, is_scalar_field,
is_scalar_mapping_field,
is_scalar_sequence_field, is_scalar_sequence_field,
is_scalar_sequence_mapping_field,
is_sequence_field, is_sequence_field,
is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_or_nonable_uploadfile_annotation,
is_uploadfile_sequence_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) param_field.field_info, (params.Query, params.Header)
) and is_scalar_sequence_field(param_field): ) and is_scalar_sequence_field(param_field):
return False 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: else:
assert isinstance( assert isinstance(
param_field.field_info, params.Body param_field.field_info, params.Body
@ -647,6 +655,10 @@ async def solve_dependencies(
return values, errors, background_tasks, response, dependency_cache return values, errors, background_tasks, response, dependency_cache
class Marker:
pass
def request_params_to_args( def request_params_to_args(
required_params: Sequence[ModelField], required_params: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers], received_params: Union[Mapping[str, Any], QueryParams, Headers],
@ -658,6 +670,16 @@ def request_params_to_args(
received_params, (QueryParams, Headers) received_params, (QueryParams, Headers)
): ):
value = received_params.getlist(field.alias) or field.default 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: else:
value = received_params.get(field.alias) value = received_params.get(field.alias)
field_info = field.field_info field_info = field.field_info
@ -674,6 +696,31 @@ def request_params_to_args(
v_, errors_ = field.validate(value, values, loc=loc) v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, ErrorWrapper): if isinstance(errors_, ErrorWrapper):
errors.append(errors_) 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): elif isinstance(errors_, list):
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
errors.extend(new_errors) errors.extend(new_errors)

1
fastapi/types.py

@ -8,3 +8,4 @@ DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
UnionType = getattr(types, "UnionType", Union) UnionType = getattr(types, "UnionType", Union)
ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str] ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]] IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
FFQuery = Dict[str, Union[str, IncEx]]

35
tests/main.py

@ -1,7 +1,8 @@
import http import http
from typing import FrozenSet, Optional from typing import FrozenSet, List, Mapping, Optional, Union
from fastapi import FastAPI, Path, Query from fastapi import FastAPI, Path, Query
from fastapi.types import FFQuery
app = FastAPI() app = FastAPI()
@ -184,6 +185,38 @@ def get_query_param_required_type(query: int = Query()):
return f"foo bar {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) @app.get("/enum-status-code", status_code=http.HTTPStatus.CREATED)
def get_enum_status_code(): def get_enum_status_code():
return "foo bar" return "foo bar"

198
tests/test_application.py

@ -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": { "components": {
"schemas": { "schemas": {

13
tests/test_invalid_mapping_param.py

@ -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

14
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 import pytest
from fastapi import FastAPI, Query from fastapi import FastAPI, Query
@ -39,15 +39,3 @@ def test_invalid_dict():
@app.get("/items/") @app.get("/items/")
def read_items(q: Dict[str, Item] = Query(default=None)): def read_items(q: Dict[str, Item] = Query(default=None)):
pass # pragma: no cover 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

33
tests/test_query.py

@ -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") response = client.get("/query/frozenset/?query=1&query=1&query=2")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == "1,2" 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': []}"

21
tests/test_tutorial/test_query_params/test_tutorial007_py310.py

@ -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…
Cancel
Save