Skip to content

Converged on webargs behavior for @use_kwargs and @use_args decorators #152

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ Quickstart
def get(self, pet_id):
return Pet.query.filter(Pet.id == pet_id).one()

@use_kwargs(PetSchema)
@use_args(PetSchema)
@marshal_with(PetSchema, code=201)
def post(self, **kwargs):
return Pet(**kwargs)
def post(self, data):
return Pet(**data)

@use_kwargs(PetSchema)
@marshal_with(PetSchema)
Expand Down
12 changes: 9 additions & 3 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@ Usage
Decorators
----------

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.
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.

.. code-block:: python

import flask
from webargs import fields
from flask_apispec import use_kwargs, marshal_with
from flask_apispec import use_args, use_kwargs, marshal_with

from .models import Pet
from .schemas import PetSchema

app = flask.Flask(__name__)

@app.route('/pets')
@app.route('/pets', methods=['POST'])
@use_args(PetSchema)
@marshal_with(PetSchema)
def create_pet(data):
return Pet(**data)

@app.route('/pets', methods=['GET'])
@use_kwargs({'species': fields.Str()})
@marshal_with(PetSchema(many=True))
def list_pets(**kwargs):
Expand Down
3 changes: 2 additions & 1 deletion flask_apispec/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
from flask_apispec.views import ResourceMeta, MethodResource
from flask_apispec.annotations import doc, wrap_with, use_kwargs, marshal_with
from flask_apispec.annotations import doc, wrap_with, use_args, use_kwargs, marshal_with
from flask_apispec.extension import FlaskApiSpec
from flask_apispec.utils import Ref

__version__ = '0.8.1'
__all__ = [
'doc',
'wrap_with',
'use_args',
'use_kwargs',
'marshal_with',
'ResourceMeta',
Expand Down
47 changes: 42 additions & 5 deletions flask_apispec/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,45 @@
from flask_apispec import utils
from flask_apispec.wrapper import Wrapper

def use_kwargs(args, locations=None, inherit=None, apply=None, **kwargs):

def use_args(argmap, locations=None, inherit=None, apply=None, **kwargs):
"""Inject positional arguments from the specified webargs arguments into the
decorated view function.

Usage:

.. code-block:: python

from marshmallow import fields, Schema

class PetSchema(Schema):
name = fields.Str()

@use_args(PetSchema)
def create_pet(data):
pet = Pet(**data)
return session.add(pet)

:param argmap: Mapping of argument names to :class:`Field <marshmallow.fields.Field>`
objects, :class:`Schema <marshmallow.Schema>`, or a callable which accepts a
request and returns a :class:`Schema <marshmallow.Schema>`
:param locations: Default request locations to parse
:param inherit: Inherit args from parent classes
:param apply: Parse request with specified args
"""
kwargs.update({'locations': locations})

def wrapper(func):
options = {
'argmap': argmap,
'kwargs': kwargs
}
annotate(func, 'args', [options], inherit=inherit, apply=apply)
return activate(func)

return wrapper

def use_kwargs(argmap, locations=None, inherit=None, apply=None, **kwargs):
"""Inject keyword arguments from the specified webargs arguments into the
decorated view function.

Expand All @@ -19,7 +57,7 @@ def use_kwargs(args, locations=None, inherit=None, apply=None, **kwargs):
def get_pets(**kwargs):
return Pet.query.filter_by(**kwargs).all()

:param args: Mapping of argument names to :class:`Field <marshmallow.fields.Field>`
:param argmap: Mapping of argument names to :class:`Field <marshmallow.fields.Field>`
objects, :class:`Schema <marshmallow.Schema>`, or a callable which accepts a
request and returns a :class:`Schema <marshmallow.Schema>`
:param locations: Default request locations to parse
Expand All @@ -30,14 +68,13 @@ def get_pets(**kwargs):

def wrapper(func):
options = {
'args': args,
'argmap': argmap,
'kwargs': kwargs,
}
annotate(func, 'args', [options], inherit=inherit, apply=apply)
annotate(func, 'kwargs', [options], inherit=inherit, apply=apply)
return activate(func)
return wrapper


def marshal_with(schema, code='default', description='', inherit=None, apply=None):
"""Marshal the return value of the decorated view function using the
specified schema.
Expand Down
4 changes: 2 additions & 2 deletions flask_apispec/apidoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ def get_parent(self, view):

def get_parameters(self, rule, view, docs, parent=None):
openapi = self.marshmallow_plugin.openapi
annotation = resolve_annotations(view, 'args', parent)
annotation = resolve_annotations(view, 'kwargs', parent)
args = merge_recursive(annotation.options)
schema = args.get('args', {})
schema = args.get('argmap', {})
if is_instance_or_subclass(schema, Schema):
converter = openapi.schema2parameters
elif callable(schema):
Expand Down
2 changes: 1 addition & 1 deletion flask_apispec/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

def inherit(child, parents):
child.__apispec__ = child.__dict__.get('__apispec__', {})
for key in ['args', 'schemas', 'docs']:
for key in ['kwargs', 'schemas', 'docs']:
child.__apispec__.setdefault(key, []).extend(
annotation
for parent in parents
Expand Down
73 changes: 64 additions & 9 deletions flask_apispec/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
except ImportError: # Python 2
from collections import Mapping

from types import MethodType

import flask
import marshmallow as ma
Expand Down Expand Up @@ -37,21 +38,32 @@ def __call__(self, *args, **kwargs):
return self.marshal_result(unpacked, status_code)

def call_view(self, *args, **kwargs):
view_fn = self.func
config = flask.current_app.config
parser = config.get('APISPEC_WEBARGS_PARSER', flaskparser.parser)
# Delegate webargs.use_args annotations
annotation = utils.resolve_annotations(self.func, 'args', self.instance)
if annotation.apply is not False:
for option in annotation.options:
schema = utils.resolve_schema(option['args'], request=flask.request)
parsed = parser.parse(schema, locations=option['kwargs']['locations'])
schema = utils.resolve_schema(option['argmap'], request=flask.request)
view_fn = parser.use_args(schema, **option['kwargs'])(view_fn)
# Delegate webargs.use_kwargs annotations
annotation = utils.resolve_annotations(self.func, 'kwargs', self.instance)
if annotation.apply is not False:
for option in annotation.options:
schema = utils.resolve_schema(option['argmap'], request=flask.request)
if getattr(schema, 'many', False):
args += tuple(parsed)
elif isinstance(parsed, Mapping):
kwargs.update(parsed)
else:
args += (parsed, )

return self.func(*args, **kwargs)
raise Exception("@use_kwargs cannot be used with a with a "
"'many=True' schema, as it must deserialize "
"to a dict")
elif isinstance(schema, ma.Schema):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: What follows here is entirely to provide more informative error output, since this now shares the same failure cases as webargs which may break some existing code that uses flask-apispec. My thought here was to be as helpful as possible in transition, but knowing that this adds nothing functional, I'm not sure if the value add is worth it.

At the judgement of the maintainer, I'd be okay doing one of 3 things:

  1. Leaving this code as is
  2. Rewriting to only run on failure (i.e. - except on the expected failures and just provide a better error message)
  3. Remove this block entirely

# Spy the post_load to provide a more informative error
# if it doesn't return a Mapping
post_load_fns = post_load_fn_names(schema)
for post_load_fn_name in post_load_fns:
spy_post_load(schema, post_load_fn_name)
view_fn = parser.use_kwargs(schema, **option['kwargs'])(view_fn)
return view_fn(*args, **kwargs)

def marshal_result(self, unpacked, status_code):
config = flask.current_app.config
Expand All @@ -78,3 +90,46 @@ def format_output(values):
while values[-1] is None:
values = values[:-1]
return values if len(values) > 1 else values[0]

def post_load_fn_names(schema):
fn_names = []
if hasattr(schema, '_hooks'):
# Marshmallow >=3
hooks = getattr(schema, '_hooks')
for key in ((ma.decorators.POST_LOAD, True),
(ma.decorators.POST_LOAD, False)):
if key in hooks:
fn_names.append(*hooks[key])
else:
# Marshmallow <= 2
processors = getattr(schema, '__processors__')
for key in ((ma.decorators.POST_LOAD, True),
(ma.decorators.POST_LOAD, False)):
if key in processors:
fn_names.append(*processors[key])
return fn_names

def spy_post_load(schema, post_load_fn_name):
processor = getattr(schema, post_load_fn_name)

def _spy_processor(_self, *args, **kwargs):
rv = processor(*args, **kwargs)
if not isinstance(rv, Mapping):
raise Exception("The @use_kwargs decorator can only use Schemas that "
"return dicts, but the @post_load-annotated method "
"'{schema_type}.{post_load_fn_name}' returned: {rv}"
.format(schema_type=type(schema),
post_load_fn_name=post_load_fn_name,
rv=rv))
return rv

for attr in (
# Marshmallow <= 2.x
'__marshmallow_tags__',
'__marshmallow_kwargs__',
# Marshmallow >= 3.x
'__marshmallow_hook__'
):
if hasattr(processor, attr):
setattr(_spy_processor, attr, getattr(processor, attr))
setattr(schema, post_load_fn_name, MethodType(_spy_processor, schema))
Loading