Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Pass schema instance to error handlers #250

Merged
merged 1 commit into from Jul 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -4,6 +4,30 @@ Changelog
4.0.0 (unreleased)
******************

Features:

* *Backwards-incompatible*: Custom error handlers receive the
`marshmallow.Schema` instance as the third argument. Update any
functions decorated with `Parser.error_handler` to take a ``schema``
argument, like so:

.. code-block:: python

# 3.x
@parser.error_handler
def handle_error(error, req):
raise CustomError(error.messages)


# 4.x
@parser.error_handler
def handle_error(error, req, schema):
raise CustomError(error.messages)


See `marshmallow-code/marshmallow#840 (comment) <https://github.com/marshmallow-code/marshmallow/issues/840#issuecomment-403481686>`_
for more information about this change.

Bug fixes:

* *Backwards-incompatible*: Rename ``webargs.async`` to
Expand Down
3 changes: 2 additions & 1 deletion docs/framework_support.rst
Expand Up @@ -31,7 +31,8 @@ When using the :meth:`use_args <webargs.flaskparser.FlaskParser.use_args>` decor
Error Handling
++++++++++++++

Webargs uses Flask's ``abort`` function to raise an ``HTTPException`` when a validation error occurs. If you use the ``Flask.errorhandler`` method to handle errors, you can access validation messages from the ``data`` attribute of an error.
Webargs uses Flask's ``abort`` function to raise an ``HTTPException`` when a validation error occurs.
If you use the ``Flask.errorhandler`` method to handle errors, you can access validation messages from the ``data`` attribute of an error.

Here is an example error handler that returns validation messages to the client as JSON.

Expand Down
8 changes: 4 additions & 4 deletions docs/quickstart.rst
Expand Up @@ -185,22 +185,22 @@ Error Handling
--------------

Each parser has a default error handling method. To override the error handling callback, write a function that
receives an error and the request and handles the error.
receives an error, the request, and the `marshmallow.Schema` instance.
Then decorate that function with :func:`Parser.error_handler <webargs.core.Parser.error_handler>`.

.. code-block:: python

from webargs import core
from webargs import flaskparser

parser = core.Parser()
parser = flaskparser.FlaskParser()


class CustomError(Exception):
pass


@parser.error_handler
def handle_error(error, req):
def handle_error(error, req, schema):
raise CustomError(error.messages)

Nesting Fields
Expand Down
1 change: 1 addition & 0 deletions tests/apps/flask_app.py
Expand Up @@ -183,4 +183,5 @@ def echo_use_kwargs_missing(username, password):
# Return validation errors as JSON
@app.errorhandler(422)
def handle_validation_error(err):
assert isinstance(err.data["schema"], ma.Schema)
return J({"errors": err.exc.messages}), 422
8 changes: 5 additions & 3 deletions tests/test_core.py
Expand Up @@ -265,7 +265,7 @@ def test_handle_error_called_when_parsing_raises_error(
def test_handle_error_reraises_errors(web_request):
p = Parser()
with pytest.raises(ValidationError):
p.handle_error(ValidationError("error raised"), web_request)
p.handle_error(ValidationError("error raised"), web_request, Schema())


@mock.patch("webargs.core.Parser.parse_headers")
Expand All @@ -287,7 +287,8 @@ def test_custom_error_handler(parse_json, web_request):
class CustomError(Exception):
pass

def error_handler(error, req):
def error_handler(error, req, schema):
assert isinstance(schema, Schema)
raise CustomError(error)

parse_json.side_effect = ValidationError("parse_json failed")
Expand All @@ -306,7 +307,8 @@ class CustomError(Exception):
parser = Parser()

@parser.error_handler
def handle_error(error, req):
def handle_error(error, req, schema):
assert isinstance(schema, Schema)
raise CustomError(error)

with pytest.raises(CustomError):
Expand Down
2 changes: 1 addition & 1 deletion webargs/aiohttpparser.py
Expand Up @@ -132,7 +132,7 @@ def get_request_from_view_args(self, view, args, kwargs):
assert isinstance(req, web.Request), "Request argument not found for handler"
return req

def handle_error(self, error, req):
def handle_error(self, error, req, schema):
"""Handle ValidationErrors and return a JSON response of error messages to the client."""
error_class = exception_map.get(error.status_code)
if not error_class:
Expand Down
2 changes: 1 addition & 1 deletion webargs/asyncparser.py
Expand Up @@ -76,7 +76,7 @@ async def parse(
data = result.data if core.MARSHMALLOW_VERSION_INFO[0] < 3 else result
self._validate_arguments(data, validators)
except ma.exceptions.ValidationError as error:
self._on_validation_error(error, req)
self._on_validation_error(error, req, schema)
finally:
self.clear_cache()
if force_all:
Expand Down
2 changes: 1 addition & 1 deletion webargs/bottleparser.py
Expand Up @@ -57,7 +57,7 @@ def parse_files(self, req, name, field):
"""Pull a file from the request."""
return core.get_value(req.files, name, field)

def handle_error(self, error, req):
def handle_error(self, error, req, schema):
"""Handles errors during parsing. Aborts the current request with a
400 error.
"""
Expand Down
26 changes: 15 additions & 11 deletions webargs/core.py
Expand Up @@ -317,7 +317,7 @@ def _parse_request(self, schema, req, locations):
parsed[argname] = parsed_value
return parsed

def _on_validation_error(self, error, req):
def _on_validation_error(self, error, req, schema):
if isinstance(error, ma.exceptions.ValidationError) and not isinstance(
error, ValidationError
):
Expand All @@ -331,9 +331,9 @@ def _on_validation_error(self, error, req):
kwargs["status_code"] = self.DEFAULT_VALIDATION_STATUS
error = ValidationError(error.messages, **kwargs)
if self.error_callback:
self.error_callback(error, req)
self.error_callback(error, req, schema)
else:
self.handle_error(error, req)
self.handle_error(error, req, schema)

def _validate_arguments(self, data, validators):
for validator in validators:
Expand Down Expand Up @@ -393,7 +393,7 @@ def parse(self, argmap, req=None, locations=None, validate=None, force_all=False
data = result.data if MARSHMALLOW_VERSION_INFO[0] < 3 else result
self._validate_arguments(data, validators)
except ma.exceptions.ValidationError as error:
self._on_validation_error(error, req)
self._on_validation_error(error, req, schema)
finally:
self.clear_cache()
if force_all:
Expand Down Expand Up @@ -514,7 +514,7 @@ def location_handler(self, name):
from webargs import core
parser = core.Parser()

@parser.location_handler('name')
@parser.location_handler("name")
def parse_data(request, name, field):
return request.data.get(name)

Expand All @@ -529,20 +529,24 @@ def decorator(func):

def error_handler(self, func):
"""Decorator that registers a custom error handling function. The
function should received the raised error and the request object. Overrides
function should received the raised error, request object, and the
`marshmallow.Schema` instance used to parse the request. Overrides
the parser's ``handle_error`` method.

Example: ::

from webargs import core
parser = core.Parser()
from webargs import flaskparser

parser = flaskparser.FlaskParser()


class CustomError(Exception):
pass


@parser.error_handler
def handle_error(error, request):
raise CustomError(error)
def handle_error(error, req, schema):
raise CustomError(error.messages)

:param callable func: The error callback to register.
"""
Expand Down Expand Up @@ -587,7 +591,7 @@ def parse_files(self, req, name, arg):
"""
return missing

def handle_error(self, error, req):
def handle_error(self, error, req, schema):
"""Called if an error occurs while parsing args. By default, just logs and
raises ``error``.
"""
Expand Down
2 changes: 1 addition & 1 deletion webargs/falconparser.py
Expand Up @@ -140,7 +140,7 @@ def parse_files(self, req, name, field):
"Parsing files not yet supported by {0}".format(self.__class__.__name__)
)

def handle_error(self, error, req):
def handle_error(self, error, req, schema):
"""Handles errors during parsing."""
status = status_map.get(error.status_code)
if status is None:
Expand Down
4 changes: 2 additions & 2 deletions webargs/flaskparser.py
Expand Up @@ -93,12 +93,12 @@ def parse_files(self, req, name, field):
"""Pull a file from the request."""
return core.get_value(req.files, name, field)

def handle_error(self, error, req):
def handle_error(self, error, req, schema):
"""Handles errors during parsing. Aborts the current HTTP request and
responds with a 422 error.
"""
status_code = getattr(error, "status_code", self.DEFAULT_VALIDATION_STATUS)
abort(status_code, messages=error.messages, exc=error)
abort(status_code, exc=error, messages=error.messages, schema=schema)

def get_default_request(self):
"""Override to use Flask's thread-local request objec by default"""
Expand Down
2 changes: 1 addition & 1 deletion webargs/pyramidparser.py
Expand Up @@ -73,7 +73,7 @@ def parse_matchdict(self, req, name, field):
"""Pull a value from the request's `matchdict`."""
return core.get_value(req.matchdict, name, field)

def handle_error(self, error, req):
def handle_error(self, error, req, schema):
"""Handles errors during parsing. Aborts the current HTTP request and
responds with a 400 error.
"""
Expand Down
2 changes: 1 addition & 1 deletion webargs/tornadoparser.py
Expand Up @@ -113,7 +113,7 @@ def parse_files(self, req, name, field):
"""Pull a file from the request."""
return get_value(req.files, name, field)

def handle_error(self, error, req):
def handle_error(self, error, req, schema):
"""Handles errors during parsing. Raises a `tornado.web.HTTPError`
with a 400 error.
"""
Expand Down