Skip to content

Commit f935e09

Browse files
kam193sloria
authored andcommitted
Support apispec >= 4 (jmcarp#206)
* 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 <[email protected]>
1 parent ad434c0 commit f935e09

File tree

6 files changed

+89
-35
lines changed

6 files changed

+89
-35
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ env:
55
- MARSHMALLOW_VERSION="==3.0.0"
66
- MARSHMALLOW_VERSION=""
77
python:
8-
- '3.5'
98
- '3.6'
9+
- '3.8'
1010
before_install:
1111
- travis_retry pip install codecov
1212
install:
@@ -21,7 +21,7 @@ jobs:
2121
include:
2222
- stage: PyPI Release
2323
if: tag IS present
24-
python: "3.6"
24+
python: "3.8"
2525
env: []
2626
# Override install, and script to no-ops
2727
before_install: true

CHANGELOG.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
Changelog
22
---------
33

4+
0.11.0 (unreleased)
5+
*******************
6+
7+
Features:
8+
9+
* Support apispec>=4.0.0 (:issue:`202`). Thanks :user:`kam193`.
10+
*Backwards-incompatible*: apispec<4.0.0 is no longer supported.
11+
12+
Other changes:
13+
14+
* *Backwards-incompatible*: Drop Python 3.5 compatibility. Only Python>=3.6 is supported.
15+
416
0.10.1 (2020-10-25)
517
*******************
618

flask_apispec/apidoc.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import functools
23

34
import apispec
45
from apispec.core import VALID_METHODS
@@ -15,7 +16,6 @@
1516
)
1617

1718
class Converter:
18-
1919
def __init__(self, app, spec, document_options=True):
2020
self.app = app
2121
self.spec = spec
@@ -72,31 +72,24 @@ def get_parent(self, view):
7272
return None
7373

7474
def get_parameters(self, rule, view, docs, parent=None):
75-
if APISPEC_VERSION_INFO[0] < 3:
76-
openapi = self.marshmallow_plugin.openapi
77-
else:
78-
openapi = self.marshmallow_plugin.converter
75+
openapi = self.marshmallow_plugin.converter
7976
annotation = resolve_annotations(view, 'args', parent)
8077
extra_params = []
8178
for args in annotation.options:
8279
schema = args.get('args', {})
83-
if is_instance_or_subclass(schema, Schema):
84-
converter = openapi.schema2parameters
85-
elif callable(schema):
86-
schema = schema(request=None)
87-
if is_instance_or_subclass(schema, Schema):
88-
converter = openapi.schema2parameters
80+
openapi_converter = openapi.schema2parameters
81+
if not is_instance_or_subclass(schema, Schema):
82+
if callable(schema):
83+
schema = schema(request=None)
8984
else:
90-
converter = openapi.fields2parameters
91-
else:
92-
converter = openapi.fields2parameters
85+
schema = Schema.from_dict(schema)
86+
openapi_converter = functools.partial(
87+
self._convert_dict_schema, openapi_converter)
88+
9389
options = copy.copy(args.get('kwargs', {}))
94-
location = options.pop('location', None)
95-
if location:
96-
options['default_in'] = location
97-
elif 'default_in' not in options:
98-
options['default_in'] = 'body'
99-
extra_params += converter(schema, **options) if args else []
90+
if not options.get('location'):
91+
options['location'] = 'body'
92+
extra_params += openapi_converter(schema, **options) if args else []
10093

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

@@ -125,6 +118,33 @@ def get_responses(self, view, parent=None):
125118
options.append(exploded)
126119
return merge_recursive(options)
127120

121+
def _convert_dict_schema(self, openapi_converter, schema, location, **options):
122+
"""When location is 'body' and OpenApi is 2, return one param for body fields.
123+
124+
Otherwise return fields exactly as converted by apispec."""
125+
if self.spec.openapi_version.major < 3 and location == 'body':
126+
params = openapi_converter(schema, location=None, **options)
127+
body_parameter = {
128+
"in": "body",
129+
"name": "body",
130+
"required": False,
131+
"schema": {
132+
"type": "object",
133+
"properties": {},
134+
},
135+
}
136+
for param in params:
137+
name = param["name"]
138+
body_parameter["schema"]["properties"].update({name: param})
139+
if param.get("required", False):
140+
body_parameter["schema"].setdefault("required", []).append(name)
141+
del param["name"]
142+
del param["in"]
143+
del param["required"]
144+
return [body_parameter]
145+
146+
return openapi_converter(schema, location=location, **options)
147+
128148
class ViewConverter(Converter):
129149

130150
def get_operations(self, rule, view):

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
'flask>=0.10.1',
77
'marshmallow>=3.0.0',
88
'webargs>=6.0.0',
9-
'apispec>=1.0.0,<4.0.0',
9+
'apispec>=4.0.0',
1010
]
1111

1212

@@ -48,7 +48,7 @@ def read(fname):
4848
license='MIT',
4949
zip_safe=False,
5050
keywords='flask marshmallow webargs apispec',
51-
python_requires=">=3.5",
51+
python_requires=">=3.6",
5252
test_suite='tests',
5353
project_urls={
5454
'Bug Reports': 'https://github.com/jmcarp/flask-apispec/issues',

tests/test_openapi.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from flask_apispec.paths import rule_to_params
88
from flask_apispec.views import MethodResource
99
from flask_apispec import doc, use_kwargs, marshal_with
10-
from flask_apispec.apidoc import APISPEC_VERSION_INFO, ViewConverter, ResourceConverter
10+
from flask_apispec.apidoc import ViewConverter, ResourceConverter
1111

1212
@pytest.fixture()
1313
def marshmallow_plugin():
@@ -33,10 +33,7 @@ def spec_oapi3(marshmallow_plugin):
3333

3434
@pytest.fixture()
3535
def openapi(marshmallow_plugin):
36-
if APISPEC_VERSION_INFO[0] < 3:
37-
return marshmallow_plugin.openapi
38-
else:
39-
return marshmallow_plugin.converter
36+
return marshmallow_plugin.converter
4037

4138
def ref_path(spec):
4239
if spec.openapi_version.version[0] < 3:
@@ -167,8 +164,8 @@ def test_params(self, app, path, openapi):
167164
params = path['get']['parameters']
168165
rule = app.url_map._rules_by_endpoint['get_band'][0]
169166
expected = (
170-
openapi.fields2parameters(
171-
{'name': fields.Str()}, default_in='query') +
167+
openapi.schema2parameters(
168+
Schema.from_dict({'name': fields.Str()}), location='query') +
172169
rule_to_params(rule)
173170
)
174171
assert params == expected
@@ -238,8 +235,7 @@ def test_params(self, app, path, openapi):
238235
params = path['get']['parameters']
239236
rule = app.url_map._rules_by_endpoint['band'][0]
240237
expected = (
241-
openapi.fields2parameters(
242-
{'name': fields.Str()}, default_in='query') +
238+
[{'in': 'query', 'name': 'name', 'required': False, 'type': 'string'}] +
243239
rule_to_params(rule)
244240
)
245241
assert params == expected
@@ -296,7 +292,6 @@ def test_params(self, app, path):
296292
)
297293
assert params == expected
298294

299-
300295
class TestGetFieldsNoLocationProvided:
301296

302297
@pytest.fixture
@@ -331,6 +326,33 @@ def test_params(self, app, path):
331326
},
332327
} in params
333328

329+
class TestGetFieldsBodyLocation(TestGetFieldsNoLocationProvided):
330+
331+
@pytest.fixture
332+
def function_view(self, app):
333+
@app.route('/bands/<int:band_id>/')
334+
@use_kwargs({'name': fields.Str(required=True), 'address': fields.Str(), 'email': fields.Str(required=True)})
335+
def get_band(**kwargs):
336+
return kwargs
337+
338+
return get_band
339+
340+
def test_params(self, app, path):
341+
params = path['get']['parameters']
342+
assert {
343+
'in': 'body',
344+
'name': 'body',
345+
'required': False,
346+
'schema': {
347+
'properties': {
348+
'address': {'type': 'string'},
349+
'name': {'type': 'string'},
350+
'email': {'type': 'string'},
351+
},
352+
'required': ["name", "email"],
353+
'type': 'object',
354+
},
355+
} in params
334356

335357
class TestSchemaNoLocationProvided:
336358

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist=py27,py35,py36,pypy
2+
envlist=py35,py36,py37,py38,py39,pypy
33
[testenv]
44
deps=
55
-rdev-requirements.txt

0 commit comments

Comments
 (0)