Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webargs 8 #112

Merged
merged 20 commits into from
Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7d14d4a
add nested marshmallow schemas, data fixture and handler for nested_u…
pzhang65 Aug 28, 2021
896332f
add test case for unknown field validation
pzhang65 Aug 28, 2021
a508f3b
add negative test for unknown nested fields
pzhang65 Aug 28, 2021
f22cba8
add /test_unknown_field route for unknown top-level and nested field …
pzhang65 Aug 28, 2021
e846009
bump webargs to 8.0.1
pzhang65 Aug 29, 2021
6a7fa73
change all locations keys to location in conftest
pzhang65 Aug 29, 2021
f380f3b
add new validation error namespace response layer to negative tests
pzhang65 Aug 29, 2021
4cd4dcf
exclude unknown fields for HeadersSchema
pzhang65 Aug 29, 2021
141aab6
drop old location kwarg compatability, no longer convert to locations
pzhang65 Aug 29, 2021
cc7d471
drop old location kwarg compatability, no longer convert to locations
pzhang65 Aug 29, 2021
7c1ea43
switch locations arg to location and pass unknown=None to use schema …
pzhang65 Aug 29, 2021
ef76cd4
complete validation middleware minimum tests
pzhang65 Aug 29, 2021
6f21823
discontinue use of locations kwarg in decorators, use querystring ins…
pzhang65 Aug 29, 2021
fe109d0
remove extra endpoint, add nested_field to MyRequest schema instead
pzhang65 Aug 29, 2021
fbd886e
add default_in option fallback to body to reflect default behavior in…
pzhang65 Aug 29, 2021
3b4d08e
update unit tests
pzhang65 Aug 29, 2021
3152e1c
add TODO to support apispec >=, would need to rename default_in optio…
pzhang65 Aug 29, 2021
047ab02
add nested_field example for request schema
pzhang65 Aug 29, 2021
0ff8d1b
update documentation unit tests, add nested definitions and params, f…
pzhang65 Aug 29, 2021
5e975a5
remove TODO comment, being caught by Codefactor
pzhang65 Aug 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions aiohttp_apispec/decorators/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import copy


def request_schema(schema, locations=None, put_into=None, example=None, add_to_refs=False, **kwargs):
def request_schema(schema, location=None, put_into=None, example=None, add_to_refs=False, **kwargs):
"""
Add request info into the swagger spec and
prepare injection keyword arguments from the specified
Expand Down Expand Up @@ -41,18 +41,17 @@ async def index(request):
"""
if callable(schema):
schema = schema()
# location kwarg added for compatibility with old versions
locations = locations or []
if not locations:
locations = kwargs.pop("location", None)
if locations:
locations = [locations]
else:
locations = None

# Compatability with old versions should be dropped,
# multiple locations are no longer supported by a single call
# so therefore **locations should never be used

options = {"required": kwargs.pop("required", False)}
if locations:
options["default_in"] = locations[0]
# to support apispec >=4 need to rename default_in
if location:
options["default_in"] = location
elif "default_in" not in options:
options["default_in"] = "body"

def wrapper(func):
if not hasattr(func, "__apispec__"):
Expand All @@ -64,14 +63,14 @@ def wrapper(func):
_example['add_to_refs'] = add_to_refs
func.__apispec__["schemas"].append({"schema": schema, "options": options, "example": _example})
# TODO: Remove this block?
if locations and "body" in locations:
if location and "body" in location:
body_schema_exists = (
"body" in func_schema["locations"] for func_schema in func.__schemas__
"body" in func_schema["location"] for func_schema in func.__schemas__
)
if any(body_schema_exists):
raise RuntimeError("Multiple body parameters are not allowed")

func.__schemas__.append({"schema": schema, "locations": locations, "put_into": put_into})
func.__schemas__.append({"schema": schema, "location": location, "put_into": put_into})

return func

Expand All @@ -84,15 +83,15 @@ def wrapper(func):
# Decorators for specific request data validations (shortenings)
match_info_schema = partial(
request_schema,
locations=["match_info"],
location="match_info",
put_into="match_info"
)
querystring_schema = partial(
request_schema,
locations=["querystring"],
location="querystring",
put_into="querystring"
)
form_schema = partial(request_schema, locations=["form"], put_into="form")
json_schema = partial(request_schema, locations=["json"], put_into="json")
headers_schema = partial(request_schema, locations=["headers"], put_into="headers")
cookies_schema = partial(request_schema, locations=["cookies"], put_into="cookies")
form_schema = partial(request_schema, location="form", put_into="form")
json_schema = partial(request_schema, location="json", put_into="json")
headers_schema = partial(request_schema, location="headers", put_into="headers")
cookies_schema = partial(request_schema, location="cookies", put_into="cookies")
5 changes: 4 additions & 1 deletion aiohttp_apispec/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ async def validation_middleware(request: web.Request, handler) -> web.Response:
result = {}
for schema in schemas:
data = await request.app["_apispec_parser"].parse(
schema["schema"], request, locations=schema["locations"]
schema["schema"],
request,
location=schema["location"],
unknown=None # Pass None to use the schema’s setting instead.
)
if schema["put_into"]:
request[schema["put_into"]] = data
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
aiohttp>=3.0.1,<4.0
apispec>=3.0.0,<4.0
webargs<6.0
webargs>=8.0.1
jinja2<3.0
33 changes: 19 additions & 14 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest
from aiohttp import web
from marshmallow import Schema, fields
from marshmallow import Schema, fields, EXCLUDE, INCLUDE

from aiohttp_apispec import (
docs,
Expand All @@ -17,6 +17,8 @@


class HeaderSchema(Schema):
class Meta:
unknown = EXCLUDE
some_header = fields.String()


Expand All @@ -38,51 +40,54 @@ def pytest_report_header(config):
' ' '
"""

class MyNestedSchema(Schema):
i = fields.Int()

class RequestSchema(Schema):
id = fields.Int()
name = fields.Str(description="name")
bool_field = fields.Bool()
list_field = fields.List(fields.Int())

nested_field = fields.Nested(MyNestedSchema)

class ResponseSchema(Schema):
msg = fields.Str()
data = fields.Dict()


class MyException(Exception):
def __init__(self, message):
self.message = message


@pytest.fixture
def example_for_request_schema():
return {
'id': 1,
'name': 'test',
'bool_field': True,
'list_field': [1, 2, 3]
'list_field': [1, 2, 3],
'nested_field': {'i': 12}
}

@pytest.fixture(
# since multiple locations are no longer supported
# in a single call, location should always expect string
params=[
({"locations": ["query"]}, True),
({"location": "query"}, True),
({"locations": ["query"]}, False),
({"location": "query"}, False),
({"location": "querystring"}, True),
({"location": "querystring"}, True),
({"location": "querystring"}, False),
({"location": "querystring"}, False),
]
)
def aiohttp_app(loop, aiohttp_client, request, example_for_request_schema):
locations, nested = request.param
location, nested = request.param

@docs(
tags=["mytag"],
summary="Test method summary",
description="Test method description",
responses={404: {"description": "Not Found"}},
)
@request_schema(RequestSchema, **locations)
@request_schema(RequestSchema, **location)
@response_schema(ResponseSchema, 200, description="Success response")
async def handler_get(request):
return web.json_response({"msg": "done", "data": {}})
Expand Down Expand Up @@ -111,7 +116,7 @@ async def handler_post_callable_schema(request):
async def handler_post_echo(request):
return web.json_response(request["data"])

@request_schema(RequestSchema, **locations)
@request_schema(RequestSchema, **location)
async def handler_get_echo(request):
return web.json_response(request["data"])

Expand All @@ -133,7 +138,7 @@ class ViewClass(web.View):
summary="View method summary",
description="View method description",
)
@request_schema(RequestSchema, **locations)
@request_schema(RequestSchema, **location)
async def get(self):
return web.json_response(self.request["data"])

Expand All @@ -143,7 +148,7 @@ async def delete(self):
async def other(request):
return web.Response()

def my_error_handler(error, req, schema, error_status_code, error_headers):
def my_error_handler(error, req, schema, *args, error_status_code, error_headers):
raise MyException({"errors": error.messages, "text": "Oops"})

@web.middleware
Expand Down
10 changes: 5 additions & 5 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def aiohttp_view_all(self):
summary="Test method summary",
description="Test method description",
)
@request_schema(RequestSchema, locations=["query"])
@request_schema(RequestSchema, location=["querystring"])
@response_schema(ResponseSchema, 200)
async def index(request, **data):
return web.json_response({"msg": "done", "data": {}})
Expand All @@ -46,7 +46,7 @@ async def index(request, **data):

@pytest.fixture
def aiohttp_view_kwargs(self):
@request_schema(RequestSchema, locations=["query"])
@request_schema(RequestSchema, location=["querystring"])
async def index(request, **data):
return web.json_response({"msg": "done", "data": {}})

Expand Down Expand Up @@ -91,7 +91,7 @@ def test_request_schema_view(self, aiohttp_view_kwargs):
aiohttp_view_kwargs.__schemas__[0].pop("schema"), RequestSchema
)
assert aiohttp_view_kwargs.__schemas__ == [
{"locations": ["query"], 'put_into': None}
{"location": ["querystring"], 'put_into': None}
]
for param in ("parameters", "responses"):
assert param in aiohttp_view_kwargs.__apispec__
Expand Down Expand Up @@ -161,8 +161,8 @@ def test_all(self, aiohttp_view_all):
def test_view_multiple_body_parameters(self):
with pytest.raises(RuntimeError) as ex:

@request_schema(RequestSchema, locations=["body"])
@request_schema(RequestSchema, locations=["body"])
@request_schema(RequestSchema, location=["body"])
@request_schema(RequestSchema, location=["body"])
async def index(request, **data):
return web.json_response({"msg": "done", "data": {}})

Expand Down
25 changes: 23 additions & 2 deletions tests/test_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
"required": False,
"type": "string",
},
{
# default schema_name_resolver, resolved based on schema __name__
# drops trailing "Schema so, MyNestedSchema resolves to MyNested
"$ref": "#/definitions/MyNested",
"in": "query",
"name": "nested_field",
"required": False,
},
],
"responses": {
"200": {
Expand Down Expand Up @@ -110,6 +118,12 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
"required": False,
"type": "string",
},
{
"$ref": "#/definitions/MyNested",
"in": "query",
"name": "nested_field",
"required": False,
},
],
"responses": {},
"tags": ["mytag"],
Expand Down Expand Up @@ -142,12 +156,16 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
"type": "array",
},
"name": {"description": "name", "type": "string"},
"nested_field": {"$ref": "#/definitions/MyNested"}
},
"type": "object",
}

assert json.dumps(docs["definitions"], sort_keys=True) == json.dumps(
{
"MyNested": {
"properties": {"i": {"format": "int32", "type": "integer"}},
"type": "object",
},
"Request": {**_request_properties, 'example': example_for_request_schema},
"Partial-Request": _request_properties,
"Response": {
Expand All @@ -169,6 +187,9 @@ async def test_not_register_route_for_none_url():
async def test_register_route_for_relative_url():
app = web.Application()
routes_count = len(app.router.routes())
assert routes_count == 0
setup_aiohttp_apispec(app=app, url="api/swagger")
# new route should be registered according to AiohttpApispec.register() method?
routes_count_after_setup_apispec = len(app.router.routes())
assert routes_count == routes_count_after_setup_apispec
# not sure why there was a comparison between the old rount_count vs new_route_count
assert routes_count_after_setup_apispec == 1
27 changes: 24 additions & 3 deletions tests/test_web_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ async def test_response_400_get(aiohttp_app):
res = await aiohttp_app.get("/v1/test", params={"id": "string", "name": "max"})
assert res.status == 400
assert await res.json() == {
'errors': {'id': ['Not a valid integer.']},
'errors': {'querystring': {'id': ['Not a valid integer.']}},
'text': 'Oops',
}

Expand All @@ -26,10 +26,32 @@ async def test_response_400_post(aiohttp_app):
res = await aiohttp_app.post("/v1/test", json={"id": "string", "name": "max"})
assert res.status == 400
assert await res.json() == {
'errors': {'id': ['Not a valid integer.']},
'errors': {'json': {'id': ['Not a valid integer.']}},
'text': 'Oops',
}

async def test_response_400_post_unknown_toplevel_field(aiohttp_app):
# unknown_field is not a field in RequestSchema, default behavior is RAISE exception
res = await aiohttp_app.post("/v1/test", json={"id": 1, "name": "max", "unknown_field": "string"})
assert res.status == 400
assert await res.json() == {
'errors': {'json': {'unknown_field': ['Unknown field.']}},
'text': 'Oops',
}

async def test_response_400_post_nested_fields(aiohttp_app):
payload = {
'nested_field': {
'i': 12,
'j': 12, # unknown nested field
}
}
res = await aiohttp_app.post("/v1/test", json=payload)
assert res.status == 400
assert await res.json() == {
'errors': {'json': {'nested_field': {'j': ['Unknown field.']}}},
'text': 'Oops',
}

async def test_response_not_docked(aiohttp_app):
res = await aiohttp_app.get("/v1/other", params={"id": 1, "name": "max"})
Expand Down Expand Up @@ -153,7 +175,6 @@ async def test_validators(aiohttp_app):
"match_info": {"uuid": 123456},
}


async def test_swagger_path(aiohttp_app):
res = await aiohttp_app.get("/v1/api/docs")
assert res.status == 200
Expand Down