Skip to content

Commit 3f65753

Browse files
kam193sloria
andauthored
Support apispec >= 4 (#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 891a39a commit 3f65753

File tree

6 files changed

+89
-35
lines changed

6 files changed

+89
-35
lines changed

.travis.yml

+2-2
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

+12
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

+40-20
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

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

102+
def _convert_dict_schema(self, openapi_converter, schema, location, **options):
103+
"""When location is 'body' and OpenApi is 2, return one param for body fields.
104+
105+
Otherwise return fields exactly as converted by apispec."""
106+
if self.spec.openapi_version.major < 3 and location == 'body':
107+
params = openapi_converter(schema, location=None, **options)
108+
body_parameter = {
109+
"in": "body",
110+
"name": "body",
111+
"required": False,
112+
"schema": {
113+
"type": "object",
114+
"properties": {},
115+
},
116+
}
117+
for param in params:
118+
name = param["name"]
119+
body_parameter["schema"]["properties"].update({name: param})
120+
if param.get("required", False):
121+
body_parameter["schema"].setdefault("required", []).append(name)
122+
del param["name"]
123+
del param["in"]
124+
del param["required"]
125+
return [body_parameter]
126+
127+
return openapi_converter(schema, location=location, **options)
128+
109129
class ViewConverter(Converter):
110130

111131
def get_operations(self, rule, view):

setup.py

+2-2
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

+32-10
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():
@@ -24,10 +24,7 @@ def spec(marshmallow_plugin):
2424

2525
@pytest.fixture()
2626
def openapi(marshmallow_plugin):
27-
if APISPEC_VERSION_INFO[0] < 3:
28-
return marshmallow_plugin.openapi
29-
else:
30-
return marshmallow_plugin.converter
27+
return marshmallow_plugin.converter
3128

3229
def ref_path(spec):
3330
if spec.openapi_version.version[0] < 3:
@@ -113,8 +110,8 @@ def test_params(self, app, path, openapi):
113110
params = path['get']['parameters']
114111
rule = app.url_map._rules_by_endpoint['get_band'][0]
115112
expected = (
116-
openapi.fields2parameters(
117-
{'name': fields.Str()}, default_in='query') +
113+
openapi.schema2parameters(
114+
Schema.from_dict({'name': fields.Str()}), location='query') +
118115
rule_to_params(rule)
119116
)
120117
assert params == expected
@@ -184,8 +181,7 @@ def test_params(self, app, path, openapi):
184181
params = path['get']['parameters']
185182
rule = app.url_map._rules_by_endpoint['band'][0]
186183
expected = (
187-
openapi.fields2parameters(
188-
{'name': fields.Str()}, default_in='query') +
184+
[{'in': 'query', 'name': 'name', 'required': False, 'type': 'string'}] +
189185
rule_to_params(rule)
190186
)
191187
assert params == expected
@@ -242,7 +238,6 @@ def test_params(self, app, path):
242238
)
243239
assert params == expected
244240

245-
246241
class TestGetFieldsNoLocationProvided:
247242

248243
@pytest.fixture
@@ -277,6 +272,33 @@ def test_params(self, app, path):
277272
},
278273
} in params
279274

275+
class TestGetFieldsBodyLocation(TestGetFieldsNoLocationProvided):
276+
277+
@pytest.fixture
278+
def function_view(self, app):
279+
@app.route('/bands/<int:band_id>/')
280+
@use_kwargs({'name': fields.Str(required=True), 'address': fields.Str(), 'email': fields.Str(required=True)})
281+
def get_band(**kwargs):
282+
return kwargs
283+
284+
return get_band
285+
286+
def test_params(self, app, path):
287+
params = path['get']['parameters']
288+
assert {
289+
'in': 'body',
290+
'name': 'body',
291+
'required': False,
292+
'schema': {
293+
'properties': {
294+
'address': {'type': 'string'},
295+
'name': {'type': 'string'},
296+
'email': {'type': 'string'},
297+
},
298+
'required': ["name", "email"],
299+
'type': 'object',
300+
},
301+
} in params
280302

281303
class TestSchemaNoLocationProvided:
282304

tox.ini

+1-1
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)