Skip to content

Commit

Permalink
Merge pull request #250 from sloria/flask-attach-schema-to-error
Browse files Browse the repository at this point in the history
Pass schema instance to error handlers
  • Loading branch information
sloria committed Jul 15, 2018
2 parents ee5ee8c + 3ead80a commit 90b822f
Show file tree
Hide file tree
Showing 13 changed files with 59 additions and 27 deletions.
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

0 comments on commit 90b822f

Please sign in to comment.