diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7c044b80..10bc6030 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,12 @@ Features: provide compatibility with ``falcon.testing`` (:pr:`477`). Thanks :user:`suola` for the PR. +* *Backwards-incompatible*: Factorize the ``use_args`` / ``use_kwargs`` branch + in all parsers. When ``as_kwargs`` is ``False``, arguments are now + consistently appended to the arguments list by the ``use_args`` decorator. + Before this change, the ``PyramidParser`` would prepend the argument list on + each call to ``use_args``. Pyramid view functions must reverse the order of + their arguments. (:pr:`478`) 6.0.0b8 (2020-02-16) ******************** diff --git a/src/webargs/aiohttpparser.py b/src/webargs/aiohttpparser.py index be79aa88..2242c026 100644 --- a/src/webargs/aiohttpparser.py +++ b/src/webargs/aiohttpparser.py @@ -101,13 +101,12 @@ async def load_json(self, req: Request, schema: Schema) -> typing.Dict: return core.missing try: return await req.json(loads=json.loads) - except json.JSONDecodeError as e: - if e.doc == "": + except json.JSONDecodeError as exc: + if exc.doc == "": return core.missing - else: - return self._handle_invalid_json_error(e, req) - except UnicodeDecodeError as e: - return self._handle_invalid_json_error(e, req) + return self._handle_invalid_json_error(exc, req) + except UnicodeDecodeError as exc: + return self._handle_invalid_json_error(exc, req) def load_headers(self, req: Request, schema: Schema) -> MultiDictProxy: """Return headers from the request as a MultiDictProxy.""" @@ -138,7 +137,7 @@ def get_request_from_view_args( if isinstance(arg, web.Request): req = arg break - elif isinstance(arg, web.View): + if isinstance(arg, web.View): req = arg.request break if not isinstance(req, web.Request): diff --git a/src/webargs/asyncparser.py b/src/webargs/asyncparser.py index 4eae668f..1ba77c70 100644 --- a/src/webargs/asyncparser.py +++ b/src/webargs/asyncparser.py @@ -147,13 +147,10 @@ async def wrapper(*args, **kwargs): error_status_code=error_status_code, error_headers=error_headers, ) - if as_kwargs: - kwargs.update(parsed_args or {}) - return await func(*args, **kwargs) - else: - # Add parsed_args after other positional arguments - new_args = args + (parsed_args,) - return await func(*new_args, **kwargs) + args, kwargs = self._update_args_kwargs( + args, kwargs, parsed_args, as_kwargs + ) + return await func(*args, **kwargs) else: @@ -172,22 +169,11 @@ def wrapper(*args, **kwargs): error_status_code=error_status_code, error_headers=error_headers, ) - if as_kwargs: - kwargs.update(parsed_args) - return func(*args, **kwargs) # noqa: B901 - else: - # Add parsed_args after other positional arguments - new_args = args + (parsed_args,) - return func(*new_args, **kwargs) + args, kwargs = self._update_args_kwargs( + args, kwargs, parsed_args, as_kwargs + ) + return func(*args, **kwargs) return wrapper return decorator - - def use_kwargs(self, *args, **kwargs) -> typing.Callable: - """Decorator that injects parsed arguments into a view function or method. - - Receives the same arguments as `webargs.core.Parser.use_kwargs`. - - """ - return super().use_kwargs(*args, **kwargs) diff --git a/src/webargs/bottleparser.py b/src/webargs/bottleparser.py index 54b9e771..3cfd2993 100644 --- a/src/webargs/bottleparser.py +++ b/src/webargs/bottleparser.py @@ -45,8 +45,7 @@ def _raw_load_json(self, req): # see: https://github.com/bottlepy/bottle/issues/1160 if data is None: return core.missing - else: - return data + return data def load_querystring(self, req, schema): """Return query params from the request as a MultiDictProxy.""" diff --git a/src/webargs/core.py b/src/webargs/core.py index a7b0df1e..46dbb1db 100644 --- a/src/webargs/core.py +++ b/src/webargs/core.py @@ -1,5 +1,6 @@ import functools import inspect +import typing import logging import warnings from collections.abc import Mapping @@ -35,8 +36,7 @@ def _callable_or_raise(obj): """ if obj and not callable(obj): raise ValueError("{!r} is not callable.".format(obj)) - else: - return obj + return obj def is_multiple(field): @@ -66,17 +66,17 @@ def is_json(mimetype): return False -def parse_json(s, *, encoding="utf-8"): - if isinstance(s, bytes): +def parse_json(string, *, encoding="utf-8"): + if isinstance(string, bytes): try: - s = s.decode(encoding) - except UnicodeDecodeError as e: + string = string.decode(encoding) + except UnicodeDecodeError as exc: raise json.JSONDecodeError( - "Bytes decoding error : {}".format(e.reason), - doc=str(e.object), - pos=e.start, + "Bytes decoding error : {}".format(exc.reason), + doc=str(exc.object), + pos=exc.start, ) - return json.loads(s) + return json.loads(string) def _ensure_list_of_callables(obj): @@ -291,6 +291,16 @@ def get_request_from_view_args(self, view, args, kwargs): """ return None + @staticmethod + def _update_args_kwargs(args, kwargs, parsed_args, as_kwargs): + """Update args or kwargs with parsed_args depending on as_kwargs""" + if as_kwargs: + kwargs.update(parsed_args) + else: + # Add parsed_args after other positional arguments + args += (parsed_args,) + return args, kwargs + def use_args( self, argmap, @@ -350,20 +360,17 @@ def wrapper(*args, **kwargs): error_status_code=error_status_code, error_headers=error_headers, ) - if as_kwargs: - kwargs.update(parsed_args) - return func(*args, **kwargs) - else: - # Add parsed_args after other positional arguments - new_args = args + (parsed_args,) - return func(*new_args, **kwargs) + args, kwargs = self._update_args_kwargs( + args, kwargs, parsed_args, as_kwargs + ) + return func(*args, **kwargs) wrapper.__wrapped__ = func return wrapper return decorator - def use_kwargs(self, *args, **kwargs): + def use_kwargs(self, *args, **kwargs) -> typing.Callable: """Decorator that injects parsed arguments into a view function or method as keyword arguments. @@ -456,12 +463,12 @@ def load_json(self, req, schema): # code sharing amongst the built-in webargs parsers try: return self._raw_load_json(req) - except json.JSONDecodeError as e: - if e.doc == "": + except json.JSONDecodeError as exc: + if exc.doc == "": return missing - return self._handle_invalid_json_error(e, req) - except UnicodeDecodeError as e: - return self._handle_invalid_json_error(e, req) + return self._handle_invalid_json_error(exc, req) + except UnicodeDecodeError as exc: + return self._handle_invalid_json_error(exc, req) def load_json_or_form(self, req, schema): """Load data from a request, accepting either JSON or form-encoded diff --git a/src/webargs/falconparser.py b/src/webargs/falconparser.py index 4307f452..85c7f1d1 100644 --- a/src/webargs/falconparser.py +++ b/src/webargs/falconparser.py @@ -106,8 +106,7 @@ def _raw_load_json(self, req): body = req.stream.read(req.content_length) if body: return core.parse_json(body) - else: - return core.missing + return core.missing def load_headers(self, req, schema): """Return headers from the request.""" diff --git a/src/webargs/multidictproxy.py b/src/webargs/multidictproxy.py index 8279b914..569e4f12 100644 --- a/src/webargs/multidictproxy.py +++ b/src/webargs/multidictproxy.py @@ -1,8 +1,8 @@ +from collections.abc import Mapping + from webargs.compat import MARSHMALLOW_VERSION_INFO from webargs.core import missing, is_multiple -from collections.abc import Mapping - class MultiDictProxy(Mapping): """ @@ -18,7 +18,8 @@ def __init__(self, multidict, schema): self.data = multidict self.multiple_keys = self._collect_multiple_keys(schema) - def _collect_multiple_keys(self, schema): + @staticmethod + def _collect_multiple_keys(schema): result = set() for name, field in schema.fields.items(): if not is_multiple(field): @@ -35,9 +36,9 @@ def __getitem__(self, key): return val if hasattr(self.data, "getlist"): return self.data.getlist(key) - elif hasattr(self.data, "getall"): + if hasattr(self.data, "getall"): return self.data.getall(key) - elif isinstance(val, (list, tuple)): + if isinstance(val, (list, tuple)): return val if val is None: return None diff --git a/src/webargs/pyramidparser.py b/src/webargs/pyramidparser.py index ef7f4e30..7f9d5c01 100644 --- a/src/webargs/pyramidparser.py +++ b/src/webargs/pyramidparser.py @@ -159,11 +159,10 @@ def wrapper(obj, *args, **kwargs): error_status_code=error_status_code, error_headers=error_headers, ) - if as_kwargs: - kwargs.update(parsed_args) - return func(obj, *args, **kwargs) - else: - return func(obj, parsed_args, *args, **kwargs) + args, kwargs = self._update_args_kwargs( + args, kwargs, parsed_args, as_kwargs + ) + return func(obj, *args, **kwargs) wrapper.__wrapped__ = func return wrapper diff --git a/src/webargs/tornadoparser.py b/src/webargs/tornadoparser.py index c09691f2..4c919a02 100644 --- a/src/webargs/tornadoparser.py +++ b/src/webargs/tornadoparser.py @@ -46,17 +46,16 @@ def __getitem__(self, key): value = self.data.get(key, core.missing) if value is core.missing: return core.missing - elif key in self.multiple_keys: + if key in self.multiple_keys: return [ _unicode(v) if isinstance(v, (str, bytes)) else v for v in value ] - elif value and isinstance(value, (list, tuple)): + if value and isinstance(value, (list, tuple)): value = value[0] if isinstance(value, (str, bytes)): return _unicode(value) - else: - return value + return value # based on tornado.web.RequestHandler.decode_argument except UnicodeDecodeError: raise HTTPError(400, "Invalid unicode in {}: {!r}".format(key, value[:40])) @@ -73,10 +72,9 @@ def __getitem__(self, key): cookie = self.data.get(key, core.missing) if cookie is core.missing: return core.missing - elif key in self.multiple_keys: + if key in self.multiple_keys: return [cookie.value] - else: - return cookie.value + return cookie.value class TornadoParser(core.Parser): diff --git a/tests/apps/aiohttp_app.py b/tests/apps/aiohttp_app.py index c6933920..a0b38070 100644 --- a/tests/apps/aiohttp_app.py +++ b/tests/apps/aiohttp_app.py @@ -1,9 +1,9 @@ import asyncio import aiohttp -import marshmallow as ma -from aiohttp import web from aiohttp.web import json_response +import marshmallow as ma + from webargs import fields from webargs.aiohttpparser import parser, use_args, use_kwargs from webargs.core import MARSHMALLOW_VERSION_INFO, json @@ -48,7 +48,7 @@ async def echo_json(request): try: parsed = await parser.parse(hello_args, request, location="json") except json.JSONDecodeError: - raise web.HTTPBadRequest( + raise aiohttp.web.HTTPBadRequest( body=json.dumps(["Invalid JSON."]).encode("utf-8"), content_type="application/json", ) @@ -59,7 +59,7 @@ async def echo_json_or_form(request): try: parsed = await parser.parse(hello_args, request, location="json_or_form") except json.JSONDecodeError: - raise web.HTTPBadRequest( + raise aiohttp.web.HTTPBadRequest( body=json.dumps(["Invalid JSON."]).encode("utf-8"), content_type="application/json", ) @@ -184,7 +184,7 @@ async def get(self, request, args): return json_response(args) -class EchoHandlerView(web.View): +class EchoHandlerView(aiohttp.web.View): @asyncio.coroutine @use_args(hello_args, location="query") def get(self, args): diff --git a/tests/apps/bottle_app.py b/tests/apps/bottle_app.py index 2dcc46fa..f03ef9c8 100644 --- a/tests/apps/bottle_app.py +++ b/tests/apps/bottle_app.py @@ -1,10 +1,9 @@ -from webargs.core import json from bottle import Bottle, HTTPResponse, debug, request, response import marshmallow as ma from webargs import fields from webargs.bottleparser import parser, use_args, use_kwargs -from webargs.core import MARSHMALLOW_VERSION_INFO +from webargs.core import json, MARSHMALLOW_VERSION_INFO hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} diff --git a/tests/apps/django_app/echo/views.py b/tests/apps/django_app/echo/views.py index 7666c4cc..363dbc9d 100644 --- a/tests/apps/django_app/echo/views.py +++ b/tests/apps/django_app/echo/views.py @@ -1,11 +1,10 @@ -from webargs.core import json from django.http import HttpResponse from django.views.generic import View - import marshmallow as ma + from webargs import fields from webargs.djangoparser import parser, use_args, use_kwargs -from webargs.core import MARSHMALLOW_VERSION_INFO +from webargs.core import json, MARSHMALLOW_VERSION_INFO hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} diff --git a/tests/apps/falcon_app.py b/tests/apps/falcon_app.py index 05efe549..36430190 100644 --- a/tests/apps/falcon_app.py +++ b/tests/apps/falcon_app.py @@ -1,5 +1,6 @@ import falcon import marshmallow as ma + from webargs import fields from webargs.core import MARSHMALLOW_VERSION_INFO, json from webargs.falconparser import parser, use_args, use_kwargs diff --git a/tests/apps/flask_app.py b/tests/apps/flask_app.py index cfa6d127..232e715e 100644 --- a/tests/apps/flask_app.py +++ b/tests/apps/flask_app.py @@ -1,11 +1,10 @@ -from webargs.core import json from flask import Flask, jsonify as J, Response, request from flask.views import MethodView - import marshmallow as ma + from webargs import fields from webargs.flaskparser import parser, use_args, use_kwargs -from webargs.core import MARSHMALLOW_VERSION_INFO +from webargs.core import json, MARSHMALLOW_VERSION_INFO class TestAppConfig: diff --git a/tests/apps/pyramid_app.py b/tests/apps/pyramid_app.py index 49c80455..6404b46a 100644 --- a/tests/apps/pyramid_app.py +++ b/tests/apps/pyramid_app.py @@ -1,12 +1,10 @@ -from webargs.core import json - from pyramid.config import Configurator from pyramid.httpexceptions import HTTPBadRequest import marshmallow as ma from webargs import fields from webargs.pyramidparser import parser, use_args, use_kwargs -from webargs.core import MARSHMALLOW_VERSION_INFO +from webargs.core import json, MARSHMALLOW_VERSION_INFO hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} hello_multiple = {"name": fields.List(fields.Str())}