Skip to content

Commit

Permalink
Support apispec >= 4 (#206)
Browse files Browse the repository at this point in the history
* Compatibility with apispec 4

1. Rename 'default_in'
(marshmallow-code/apispec#526)
2. Dict schema: convert it to object and handle special case 'body',
since prior used method no longer exists
(marshmallow-code/apispec#581)

* Drop support for apispec < 4, Python < 3.6

Supporting different apispec version requires
different logic for each of them. New apispec
requires Python >= 3.6

* Remove unused imports

* Update tox.ini

* Update changelog

* Drop Python 3.5 support

apispec no longer supports 3.5

Co-authored-by: Steven Loria <sloria1@gmail.com>
  • Loading branch information
kam193 and sloria committed Oct 25, 2020
1 parent 891a39a commit 3f65753
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 35 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ env:
- MARSHMALLOW_VERSION="==3.0.0"
- MARSHMALLOW_VERSION=""
python:
- '3.5'
- '3.6'
- '3.8'
before_install:
- travis_retry pip install codecov
install:
Expand All @@ -21,7 +21,7 @@ jobs:
include:
- stage: PyPI Release
if: tag IS present
python: "3.6"
python: "3.8"
env: []
# Override install, and script to no-ops
before_install: true
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Changelog
---------

0.11.0 (unreleased)
*******************

Features:

* Support apispec>=4.0.0 (:issue:`202`). Thanks :user:`kam193`.
*Backwards-incompatible*: apispec<4.0.0 is no longer supported.

Other changes:

* *Backwards-incompatible*: Drop Python 3.5 compatibility. Only Python>=3.6 is supported.

0.10.1 (2020-10-25)
*******************

Expand Down
60 changes: 40 additions & 20 deletions flask_apispec/apidoc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import copy
import functools

import apispec
from apispec.core import VALID_METHODS
Expand All @@ -15,7 +16,6 @@
)

class Converter:

def __init__(self, app, spec, document_options=True):
self.app = app
self.spec = spec
Expand Down Expand Up @@ -72,31 +72,24 @@ def get_parent(self, view):
return None

def get_parameters(self, rule, view, docs, parent=None):
if APISPEC_VERSION_INFO[0] < 3:
openapi = self.marshmallow_plugin.openapi
else:
openapi = self.marshmallow_plugin.converter
openapi = self.marshmallow_plugin.converter
annotation = resolve_annotations(view, 'args', parent)
extra_params = []
for args in annotation.options:
schema = args.get('args', {})
if is_instance_or_subclass(schema, Schema):
converter = openapi.schema2parameters
elif callable(schema):
schema = schema(request=None)
if is_instance_or_subclass(schema, Schema):
converter = openapi.schema2parameters
openapi_converter = openapi.schema2parameters
if not is_instance_or_subclass(schema, Schema):
if callable(schema):
schema = schema(request=None)
else:
converter = openapi.fields2parameters
else:
converter = openapi.fields2parameters
schema = Schema.from_dict(schema)
openapi_converter = functools.partial(
self._convert_dict_schema, openapi_converter)

options = copy.copy(args.get('kwargs', {}))
location = options.pop('location', None)
if location:
options['default_in'] = location
elif 'default_in' not in options:
options['default_in'] = 'body'
extra_params += converter(schema, **options) if args else []
if not options.get('location'):
options['location'] = 'body'
extra_params += openapi_converter(schema, **options) if args else []

rule_params = rule_to_params(rule, docs.get('params')) or []

Expand All @@ -106,6 +99,33 @@ def get_responses(self, view, parent=None):
annotation = resolve_annotations(view, 'schemas', parent)
return merge_recursive(annotation.options)

def _convert_dict_schema(self, openapi_converter, schema, location, **options):
"""When location is 'body' and OpenApi is 2, return one param for body fields.
Otherwise return fields exactly as converted by apispec."""
if self.spec.openapi_version.major < 3 and location == 'body':
params = openapi_converter(schema, location=None, **options)
body_parameter = {
"in": "body",
"name": "body",
"required": False,
"schema": {
"type": "object",
"properties": {},
},
}
for param in params:
name = param["name"]
body_parameter["schema"]["properties"].update({name: param})
if param.get("required", False):
body_parameter["schema"].setdefault("required", []).append(name)
del param["name"]
del param["in"]
del param["required"]
return [body_parameter]

return openapi_converter(schema, location=location, **options)

class ViewConverter(Converter):

def get_operations(self, rule, view):
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
'flask>=0.10.1',
'marshmallow>=3.0.0',
'webargs>=6.0.0',
'apispec>=1.0.0,<4.0.0',
'apispec>=4.0.0',
]


Expand Down Expand Up @@ -48,7 +48,7 @@ def read(fname):
license='MIT',
zip_safe=False,
keywords='flask marshmallow webargs apispec',
python_requires=">=3.5",
python_requires=">=3.6",
test_suite='tests',
project_urls={
'Bug Reports': 'https://github.com/jmcarp/flask-apispec/issues',
Expand Down
42 changes: 32 additions & 10 deletions tests/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from flask_apispec.paths import rule_to_params
from flask_apispec.views import MethodResource
from flask_apispec import doc, use_kwargs, marshal_with
from flask_apispec.apidoc import APISPEC_VERSION_INFO, ViewConverter, ResourceConverter
from flask_apispec.apidoc import ViewConverter, ResourceConverter

@pytest.fixture()
def marshmallow_plugin():
Expand All @@ -24,10 +24,7 @@ def spec(marshmallow_plugin):

@pytest.fixture()
def openapi(marshmallow_plugin):
if APISPEC_VERSION_INFO[0] < 3:
return marshmallow_plugin.openapi
else:
return marshmallow_plugin.converter
return marshmallow_plugin.converter

def ref_path(spec):
if spec.openapi_version.version[0] < 3:
Expand Down Expand Up @@ -113,8 +110,8 @@ def test_params(self, app, path, openapi):
params = path['get']['parameters']
rule = app.url_map._rules_by_endpoint['get_band'][0]
expected = (
openapi.fields2parameters(
{'name': fields.Str()}, default_in='query') +
openapi.schema2parameters(
Schema.from_dict({'name': fields.Str()}), location='query') +
rule_to_params(rule)
)
assert params == expected
Expand Down Expand Up @@ -184,8 +181,7 @@ def test_params(self, app, path, openapi):
params = path['get']['parameters']
rule = app.url_map._rules_by_endpoint['band'][0]
expected = (
openapi.fields2parameters(
{'name': fields.Str()}, default_in='query') +
[{'in': 'query', 'name': 'name', 'required': False, 'type': 'string'}] +
rule_to_params(rule)
)
assert params == expected
Expand Down Expand Up @@ -242,7 +238,6 @@ def test_params(self, app, path):
)
assert params == expected


class TestGetFieldsNoLocationProvided:

@pytest.fixture
Expand Down Expand Up @@ -277,6 +272,33 @@ def test_params(self, app, path):
},
} in params

class TestGetFieldsBodyLocation(TestGetFieldsNoLocationProvided):

@pytest.fixture
def function_view(self, app):
@app.route('/bands/<int:band_id>/')
@use_kwargs({'name': fields.Str(required=True), 'address': fields.Str(), 'email': fields.Str(required=True)})
def get_band(**kwargs):
return kwargs

return get_band

def test_params(self, app, path):
params = path['get']['parameters']
assert {
'in': 'body',
'name': 'body',
'required': False,
'schema': {
'properties': {
'address': {'type': 'string'},
'name': {'type': 'string'},
'email': {'type': 'string'},
},
'required': ["name", "email"],
'type': 'object',
},
} in params

class TestSchemaNoLocationProvided:

Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist=py27,py35,py36,pypy
envlist=py35,py36,py37,py38,py39,pypy
[testenv]
deps=
-rdev-requirements.txt
Expand Down

0 comments on commit 3f65753

Please sign in to comment.