Browse Source

🐛 Fix path and query parameters receiving dict as valid (#287)

* 🐛 Fix path and query parameters accepting dict

*  Add several tests to ensure invalid types are not accepted

* 📝 Document (to include tested source) using query params with list

* 🐛 Fix OpenAPI schema in query with list tutorial
pull/288/head
Sebastián Ramírez 6 years ago
committed by GitHub
parent
commit
c7db2ff858
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      docs/src/query_params_str_validations/tutorial013.py
  2. 13
      docs/tutorial/query-params-str-validations.md
  3. 7
      fastapi/dependencies/utils.py
  4. 77
      tests/test_invalid_path_param.py
  5. 26
      tests/test_invalid_sequence_param.py
  6. 7
      tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py
  7. 91
      tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py

9
docs/src/query_params_str_validations/tutorial013.py

@ -0,0 +1,9 @@
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: list = Query(None)):
query_items = {"q": q}
return query_items

13
docs/tutorial/query-params-str-validations.md

@ -183,6 +183,19 @@ the default of `q` will be: `["foo", "bar"]` and your response will be:
}
```
#### Using `list`
You can also use `list` directly instead of `List[str]`:
```Python hl_lines="7"
{!./src/query_params_str_validations/tutorial013.py!}
```
!!! note
Have in mind that in this case, FastAPI won't check the contents of the list.
For example, `List[int]` would check (and document) that the contents of the list are integers. But `list` alone wouldn't.
## Declare more metadata
You can add more information about the parameter.

7
fastapi/dependencies/utils.py

@ -127,12 +127,13 @@ def is_scalar_field(field: Field) -> bool:
return (
field.shape == Shape.SINGLETON
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, sequence_types + (dict,))
and not isinstance(field.schema, params.Body)
)
def is_scalar_sequence_field(field: Field) -> bool:
if field.shape in sequence_shapes and not lenient_issubclass(
if (field.shape in sequence_shapes) and not lenient_issubclass(
field.type_, BaseModel
):
if field.sub_fields is not None:
@ -140,6 +141,8 @@ def is_scalar_sequence_field(field: Field) -> bool:
if not is_scalar_field(sub_field):
return False
return True
if lenient_issubclass(field.type_, sequence_types):
return True
return False
@ -346,7 +349,7 @@ def request_params_to_args(
values = {}
errors = []
for field in required_params:
if field.shape in sequence_shapes and isinstance(
if is_scalar_sequence_field(field) and isinstance(
received_params, (QueryParams, Headers)
):
value = received_params.getlist(field.alias) or field.default

77
tests/test_invalid_path_param.py

@ -0,0 +1,77 @@
from typing import Dict, List, Tuple
import pytest
from fastapi import FastAPI
from pydantic import BaseModel
def test_invalid_sequence():
with pytest.raises(AssertionError):
app = FastAPI()
class Item(BaseModel):
title: str
@app.get("/items/{id}")
def read_items(id: List[Item]):
pass # pragma: no cover
def test_invalid_tuple():
with pytest.raises(AssertionError):
app = FastAPI()
class Item(BaseModel):
title: str
@app.get("/items/{id}")
def read_items(id: Tuple[Item, Item]):
pass # pragma: no cover
def test_invalid_dict():
with pytest.raises(AssertionError):
app = FastAPI()
class Item(BaseModel):
title: str
@app.get("/items/{id}")
def read_items(id: Dict[str, Item]):
pass # pragma: no cover
def test_invalid_simple_list():
with pytest.raises(AssertionError):
app = FastAPI()
@app.get("/items/{id}")
def read_items(id: list):
pass # pragma: no cover
def test_invalid_simple_tuple():
with pytest.raises(AssertionError):
app = FastAPI()
@app.get("/items/{id}")
def read_items(id: tuple):
pass # pragma: no cover
def test_invalid_simple_set():
with pytest.raises(AssertionError):
app = FastAPI()
@app.get("/items/{id}")
def read_items(id: set):
pass # pragma: no cover
def test_invalid_simple_dict():
with pytest.raises(AssertionError):
app = FastAPI()
@app.get("/items/{id}")
def read_items(id: dict):
pass # pragma: no cover

26
tests/test_invalid_sequence_param.py

@ -1,4 +1,4 @@
from typing import List, Tuple
from typing import Dict, List, Tuple
import pytest
from fastapi import FastAPI, Query
@ -27,3 +27,27 @@ def test_invalid_tuple():
@app.get("/items/")
def read_items(q: Tuple[Item, Item] = Query(None)):
pass # pragma: no cover
def test_invalid_dict():
with pytest.raises(AssertionError):
app = FastAPI()
class Item(BaseModel):
title: str
@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: dict = Query(None)):
pass # pragma: no cover

7
tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py

@ -86,3 +86,10 @@ def test_multi_query_values():
response = client.get(url)
assert response.status_code == 200
assert response.json() == {"q": ["foo", "bar"]}
def test_query_no_values():
url = "/items/"
response = client.get(url)
assert response.status_code == 200
assert response.json() == {"q": None}

91
tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py

@ -0,0 +1,91 @@
from starlette.testclient import TestClient
from query_params_str_validations.tutorial013 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"required": False,
"schema": {"title": "Q", "type": "array"},
"name": "q",
"in": "query",
}
],
}
}
},
"components": {
"schemas": {
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_multi_query_values():
url = "/items/?q=foo&q=bar"
response = client.get(url)
assert response.status_code == 200
assert response.json() == {"q": ["foo", "bar"]}
def test_query_no_values():
url = "/items/"
response = client.get(url)
assert response.status_code == 200
assert response.json() == {"q": None}
Loading…
Cancel
Save