Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ dist: xenial
language: python

python:
- 3.5
- 3.6
- 3.7
- 3.8
Expand Down
9 changes: 6 additions & 3 deletions aiohttp_apispec/aiohttp_apispec.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

VALID_RESPONSE_FIELDS = {"description", "headers", "examples"}

DEFAULT_RESPONSE_LOCATION = "json"

NAME_SWAGGER_SPEC = "swagger.spec"
NAME_SWAGGER_DOCS = "swagger.docs"
NAME_SWAGGER_STATIC = "swagger.static"
Expand Down Expand Up @@ -160,7 +162,7 @@ def _update_paths(self, data: dict, method: str, url_path: str):
return None
for schema in data.pop("schemas", []):
parameters = self.plugin.converter.schema2parameters(
schema["schema"], **schema["options"]
schema["schema"], location=schema["location"], **schema["options"]
)
self._add_examples(schema["schema"], parameters, schema["example"])
data["parameters"].extend(parameters)
Expand All @@ -178,6 +180,7 @@ def _update_paths(self, data: dict, method: str, url_path: str):
if "schema" in actual_params:
raw_parameters = self.plugin.converter.schema2parameters(
actual_params["schema"],
location=DEFAULT_RESPONSE_LOCATION,
required=actual_params.get("required", False),
)[0]
updated_params = {
Expand All @@ -200,7 +203,7 @@ def _update_paths(self, data: dict, method: str, url_path: str):
def _add_examples(self, ref_schema, endpoint_schema, example):
def add_to_endpoint_or_ref():
if add_to_refs:
self.spec.components._schemas[name]["example"] = example
self.spec.components.schemas[name]["example"] = example
else:
endpoint_schema[0]['schema']['allOf'] = [endpoint_schema[0]['schema'].pop('$ref')]
endpoint_schema[0]['schema']["example"] = example
Expand All @@ -210,7 +213,7 @@ def add_to_endpoint_or_ref():
name = self.plugin.converter.schema_name_resolver(schema_instance)
add_to_refs = example.pop('add_to_refs')
if self.spec.components.openapi_version.major < 3:
if name and name in self.spec.components._schemas:
if name and name in self.spec.components.schemas:
add_to_endpoint_or_ref()
else:
add_to_endpoint_or_ref()
Expand Down
48 changes: 30 additions & 18 deletions aiohttp_apispec/decorators/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@
import copy


def request_schema(schema, location=None, put_into=None, example=None, add_to_refs=False, **kwargs):
# locations supported by both openapi and webargs.aiohttpparser
VALID_SCHEMA_LOCATIONS = (
"cookies",
"files",
"form",
"headers",
"json",
"match_info",
"path",
"query",
"querystring",
)


def request_schema(schema, location="json", 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 All @@ -29,7 +43,7 @@ async def index(request):
'id': data['id']})

:param schema: :class:`Schema <marshmallow.Schema>` class or instance
:param locations: Default request locations to parse
:param location: Default request locations to parse
:param put_into: name of the key in Request object
where validated data will be placed.
If None (by default) default key will be used
Expand All @@ -39,19 +53,14 @@ async def index(request):
Otherwise add example to endpoint.
Default False
"""

if location not in VALID_SCHEMA_LOCATIONS:
raise ValueError(f"Invalid location argument: {location}")

if callable(schema):
schema = schema()

# 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)}
# 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 @@ -61,14 +70,17 @@ def wrapper(func):
_example = copy.copy(example) or {}
if _example:
_example['add_to_refs'] = add_to_refs
func.__apispec__["schemas"].append({"schema": schema, "options": options, "example": _example})
func.__apispec__["schemas"].append(
{"schema": schema, "location": location, "options": options, "example": _example}
)

# TODO: Remove this block?
if location and "body" in location:
body_schema_exists = (
"body" in func_schema["location"] for func_schema in func.__schemas__
)
if any(body_schema_exists):
raise RuntimeError("Multiple body parameters are not allowed")
# "body" location was replaced by "json" location
if (
location == "json" and
any(func_schema["location"] == "json" for func_schema in func.__schemas__)
):
raise RuntimeError("Multiple json locations are not allowed")

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

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
apispec>=5.1.1
webargs>=8.0.1
jinja2<3.0
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ def read(file_name):
url='https://github.com/maximdanilchenko/aiohttp-apispec',
zip_safe=False,
keywords='aiohttp marshmallow apispec swagger',
python_requires='>=3.5',
python_requires='>=3.6',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Natural Language :: English',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
Expand Down
8 changes: 7 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,29 @@ def pytest_report_header(config):
' ' '
"""


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


class RequestSchema(Schema):
id = fields.Int()
name = fields.Str(description="name")
name = fields.Str(metadata={"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 {
Expand All @@ -68,6 +73,7 @@ def example_for_request_schema():
'nested_field': {'i': 12}
}


@pytest.fixture(
# since multiple locations are no longer supported
# in a single call, location should always expect string
Expand Down
14 changes: 7 additions & 7 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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

Expand All @@ -25,7 +25,7 @@ def aiohttp_view_all(self):
summary="Test method summary",
description="Test method description",
)
@request_schema(RequestSchema, location=["querystring"])
@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, location=["querystring"])
@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__ == [
{"location": ["querystring"], '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,10 +161,10 @@ def test_all(self, aiohttp_view_all):
def test_view_multiple_body_parameters(self):
with pytest.raises(RuntimeError) as ex:

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

assert isinstance(ex.value, RuntimeError)
assert str(ex.value) == "Multiple body parameters are not allowed"
assert str(ex.value) == "Multiple json locations are not allowed"
15 changes: 7 additions & 8 deletions tests/test_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
"type": "boolean",
},
{
"format": "int32",
"in": "query",
"name": "id",
"required": False,
Expand All @@ -47,7 +46,7 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
{
"collectionFormat": "multi",
"in": "query",
"items": {"format": "int32", "type": "integer"},
"items": {"type": "integer"},
"name": "list_field",
"required": False,
"type": "array",
Expand Down Expand Up @@ -97,7 +96,6 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
"type": "boolean",
},
{
"format": "int32",
"in": "query",
"name": "id",
"required": False,
Expand All @@ -106,7 +104,7 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
{
"collectionFormat": "multi",
"in": "query",
"items": {"format": "int32", "type": "integer"},
"items": {"type": "integer"},
"name": "list_field",
"required": False,
"type": "array",
Expand Down Expand Up @@ -140,7 +138,7 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
'name': 'body',
'schema': {
'allOf': [
'#/definitions/Request'
{'$ref': '#/definitions/#/definitions/Request'}
],
'example': example_for_request_schema
}
Expand All @@ -150,9 +148,9 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
_request_properties = {
"properties": {
"bool_field": {"type": "boolean"},
"id": {"format": "int32", "type": "integer"},
"id": {"type": "integer"},
"list_field": {
"items": {"format": "int32", "type": "integer"},
"items": {"type": "integer"},
"type": "array",
},
"name": {"description": "name", "type": "string"},
Expand All @@ -163,7 +161,7 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
assert json.dumps(docs["definitions"], sort_keys=True) == json.dumps(
{
"MyNested": {
"properties": {"i": {"format": "int32", "type": "integer"}},
"properties": {"i": {"type": "integer"}},
"type": "object",
},
"Request": {**_request_properties, 'example': example_for_request_schema},
Expand All @@ -176,6 +174,7 @@ async def test_app_swagger_json(aiohttp_app, example_for_request_schema):
sort_keys=True,
)


async def test_not_register_route_for_none_url():
app = web.Application()
routes_count = len(app.router.routes())
Expand Down