Skip to content

Commit 4d1a8bf

Browse files
committed
Converged on webargs behavior for @use_kwargs and added @use_args decorator
1 parent b51962a commit 4d1a8bf

File tree

8 files changed

+240
-29
lines changed

8 files changed

+240
-29
lines changed

README.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ Quickstart
6464
def get(self, pet_id):
6565
return Pet.query.filter(Pet.id == pet_id).one()
6666
67-
@use_kwargs(PetSchema)
67+
@use_args(PetSchema)
6868
@marshal_with(PetSchema, code=201)
69-
def post(self, **kwargs):
70-
return Pet(**kwargs)
69+
def post(self, data):
70+
return Pet(**data)
7171
7272
@use_kwargs(PetSchema)
7373
@marshal_with(PetSchema)

docs/usage.rst

+9-3
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,26 @@ Usage
66
Decorators
77
----------
88

9-
Use the :func:`use_kwargs <flask_apispec.annotations.use_kwargs>` and :func:`marshal_with <flask_apispec.annotations.marshal_with>` decorators on functions, methods, or classes to declare request parsing and response marshalling behavior, respectively.
9+
Use the :func:`use_args <flask_apispec.annotations.use_args>`, :func:`use_kwargs <flask_apispec.annotations.use_kwargs>` and :func:`marshal_with <flask_apispec.annotations.marshal_with>` decorators on functions, methods, or classes to declare request parsing and response marshalling behavior, respectively.
1010

1111
.. code-block:: python
1212
1313
import flask
1414
from webargs import fields
15-
from flask_apispec import use_kwargs, marshal_with
15+
from flask_apispec import use_args, use_kwargs, marshal_with
1616
1717
from .models import Pet
1818
from .schemas import PetSchema
1919
2020
app = flask.Flask(__name__)
2121
22-
@app.route('/pets')
22+
@app.route('/pets', methods=['POST'])
23+
@use_args(PetSchema)
24+
@marshal_with(PetSchema)
25+
def create_pet(data):
26+
return Pet(**data)
27+
28+
@app.route('/pets', methods=['GET'])
2329
@use_kwargs({'species': fields.Str()})
2430
@marshal_with(PetSchema(many=True))
2531
def list_pets(**kwargs):

flask_apispec/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# -*- coding: utf-8 -*-
22
from flask_apispec.views import ResourceMeta, MethodResource
3-
from flask_apispec.annotations import doc, wrap_with, use_kwargs, marshal_with
3+
from flask_apispec.annotations import doc, wrap_with, use_args, use_kwargs, marshal_with
44
from flask_apispec.extension import FlaskApiSpec
55
from flask_apispec.utils import Ref
66

77
__version__ = '0.8.1'
88
__all__ = [
99
'doc',
1010
'wrap_with',
11+
'use_args',
1112
'use_kwargs',
1213
'marshal_with',
1314
'ResourceMeta',

flask_apispec/annotations.py

+42-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,45 @@
55
from flask_apispec import utils
66
from flask_apispec.wrapper import Wrapper
77

8-
def use_kwargs(args, locations=None, inherit=None, apply=None, **kwargs):
8+
9+
def use_args(argmap, locations=None, inherit=None, apply=None, **kwargs):
10+
"""Inject positional arguments from the specified webargs arguments into the
11+
decorated view function.
12+
13+
Usage:
14+
15+
.. code-block:: python
16+
17+
from marshmallow import fields, Schema
18+
19+
class PetSchema(Schema):
20+
name = fields.Str()
21+
22+
@use_args(PetSchema)
23+
def create_pet(data):
24+
pet = Pet(**data)
25+
return session.add(pet)
26+
27+
:param argmap: Mapping of argument names to :class:`Field <marshmallow.fields.Field>`
28+
objects, :class:`Schema <marshmallow.Schema>`, or a callable which accepts a
29+
request and returns a :class:`Schema <marshmallow.Schema>`
30+
:param locations: Default request locations to parse
31+
:param inherit: Inherit args from parent classes
32+
:param apply: Parse request with specified args
33+
"""
34+
kwargs.update({'locations': locations})
35+
36+
def wrapper(func):
37+
options = {
38+
'argmap': argmap,
39+
'kwargs': kwargs
40+
}
41+
annotate(func, 'args', [options], inherit=inherit, apply=apply)
42+
return activate(func)
43+
44+
return wrapper
45+
46+
def use_kwargs(argmap, locations=None, inherit=None, apply=None, **kwargs):
947
"""Inject keyword arguments from the specified webargs arguments into the
1048
decorated view function.
1149
@@ -19,7 +57,7 @@ def use_kwargs(args, locations=None, inherit=None, apply=None, **kwargs):
1957
def get_pets(**kwargs):
2058
return Pet.query.filter_by(**kwargs).all()
2159
22-
:param args: Mapping of argument names to :class:`Field <marshmallow.fields.Field>`
60+
:param argmap: Mapping of argument names to :class:`Field <marshmallow.fields.Field>`
2361
objects, :class:`Schema <marshmallow.Schema>`, or a callable which accepts a
2462
request and returns a :class:`Schema <marshmallow.Schema>`
2563
:param locations: Default request locations to parse
@@ -30,14 +68,13 @@ def get_pets(**kwargs):
3068

3169
def wrapper(func):
3270
options = {
33-
'args': args,
71+
'argmap': argmap,
3472
'kwargs': kwargs,
3573
}
36-
annotate(func, 'args', [options], inherit=inherit, apply=apply)
74+
annotate(func, 'kwargs', [options], inherit=inherit, apply=apply)
3775
return activate(func)
3876
return wrapper
3977

40-
4178
def marshal_with(schema, code='default', description='', inherit=None, apply=None):
4279
"""Marshal the return value of the decorated view function using the
4380
specified schema.

flask_apispec/apidoc.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ def get_parent(self, view):
6868

6969
def get_parameters(self, rule, view, docs, parent=None):
7070
openapi = self.marshmallow_plugin.openapi
71-
annotation = resolve_annotations(view, 'args', parent)
71+
annotation = resolve_annotations(view, 'kwargs', parent)
7272
args = merge_recursive(annotation.options)
73-
schema = args.get('args', {})
73+
schema = args.get('argmap', {})
7474
if is_instance_or_subclass(schema, Schema):
7575
converter = openapi.schema2parameters
7676
elif callable(schema):

flask_apispec/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
def inherit(child, parents):
99
child.__apispec__ = child.__dict__.get('__apispec__', {})
10-
for key in ['args', 'schemas', 'docs']:
10+
for key in ['kwargs', 'schemas', 'docs']:
1111
child.__apispec__.setdefault(key, []).extend(
1212
annotation
1313
for parent in parents

flask_apispec/wrapper.py

+64-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
except ImportError: # Python 2
55
from collections import Mapping
66

7+
from types import MethodType
78

89
import flask
910
import marshmallow as ma
@@ -37,21 +38,32 @@ def __call__(self, *args, **kwargs):
3738
return self.marshal_result(unpacked, status_code)
3839

3940
def call_view(self, *args, **kwargs):
41+
view_fn = self.func
4042
config = flask.current_app.config
4143
parser = config.get('APISPEC_WEBARGS_PARSER', flaskparser.parser)
44+
# Delegate webargs.use_args annotations
4245
annotation = utils.resolve_annotations(self.func, 'args', self.instance)
4346
if annotation.apply is not False:
4447
for option in annotation.options:
45-
schema = utils.resolve_schema(option['args'], request=flask.request)
46-
parsed = parser.parse(schema, locations=option['kwargs']['locations'])
48+
schema = utils.resolve_schema(option['argmap'], request=flask.request)
49+
view_fn = parser.use_args(schema, **option['kwargs'])(view_fn)
50+
# Delegate webargs.use_kwargs annotations
51+
annotation = utils.resolve_annotations(self.func, 'kwargs', self.instance)
52+
if annotation.apply is not False:
53+
for option in annotation.options:
54+
schema = utils.resolve_schema(option['argmap'], request=flask.request)
4755
if getattr(schema, 'many', False):
48-
args += tuple(parsed)
49-
elif isinstance(parsed, Mapping):
50-
kwargs.update(parsed)
51-
else:
52-
args += (parsed, )
53-
54-
return self.func(*args, **kwargs)
56+
raise Exception("@use_kwargs cannot be used with a with a "
57+
"'many=True' schema, as it must deserialize "
58+
"to a dict")
59+
elif isinstance(schema, ma.Schema):
60+
# Spy the post_load to provide a more informative error
61+
# if it doesn't return a Mapping
62+
post_load_fns = post_load_fn_names(schema)
63+
for post_load_fn_name in post_load_fns:
64+
spy_post_load(schema, post_load_fn_name)
65+
view_fn = parser.use_kwargs(schema, **option['kwargs'])(view_fn)
66+
return view_fn(*args, **kwargs)
5567

5668
def marshal_result(self, unpacked, status_code):
5769
config = flask.current_app.config
@@ -78,3 +90,46 @@ def format_output(values):
7890
while values[-1] is None:
7991
values = values[:-1]
8092
return values if len(values) > 1 else values[0]
93+
94+
def post_load_fn_names(schema):
95+
fn_names = []
96+
if hasattr(schema, '_hooks'):
97+
# Marshmallow >=3
98+
hooks = getattr(schema, '_hooks')
99+
for key in ((ma.decorators.POST_LOAD, True),
100+
(ma.decorators.POST_LOAD, False)):
101+
if key in hooks:
102+
fn_names.append(*hooks[key])
103+
else:
104+
# Marshmallow <= 2
105+
processors = getattr(schema, '__processors__')
106+
for key in ((ma.decorators.POST_LOAD, True),
107+
(ma.decorators.POST_LOAD, False)):
108+
if key in processors:
109+
fn_names.append(*processors[key])
110+
return fn_names
111+
112+
def spy_post_load(schema, post_load_fn_name):
113+
processor = getattr(schema, post_load_fn_name)
114+
115+
def _spy_processor(_self, *args, **kwargs):
116+
rv = processor(*args, **kwargs)
117+
if not isinstance(rv, Mapping):
118+
raise Exception("The @use_kwargs decorator can only use Schemas that "
119+
"return dicts, but the @post_load-annotated method "
120+
"'{schema_type}.{post_load_fn_name}' returned: {rv}"
121+
.format(schema_type=type(schema),
122+
post_load_fn_name=post_load_fn_name,
123+
rv=rv))
124+
return rv
125+
126+
for attr in (
127+
# Marshmallow <= 2.x
128+
'__marshmallow_tags__',
129+
'__marshmallow_kwargs__',
130+
# Marshmallow >= 3.x
131+
'__marshmallow_hook__'
132+
):
133+
if hasattr(processor, attr):
134+
setattr(_spy_processor, attr, getattr(processor, attr))
135+
setattr(schema, post_load_fn_name, MethodType(_spy_processor, schema))

0 commit comments

Comments
 (0)