Skip to content

Commit

Permalink
Deprecate status and headers params of ValidationErrors
Browse files Browse the repository at this point in the history
* `status` and `headers` kwargs are deprecated (close #336)
* Add `error_status_code` and `error_headers`:
  See #327 (comment)

```python
def validate_foo(val):  # Before
    ...
    raise ValidationError("Error!", status_code=400)

@use_args({"foo": fields.Str(validate=validate_foo)})
def my_view(args):
    ...

def validate_foo(val):  # After
    ...
    raise ValidationError("Error!")

@use_args(
    {"foo": fields.Str(validate=validate_foo)},
    error_status_code=400
)
def my_view(args):
    ...
```

* error_handler functions and handle_error methods now take
  error_status_code and error_headers arguments. Backwards
  compatibility was maintained by checking the the number of
  arguments in the signatures.

```python
@parser.error_handler
def handle_error(error, req):  # Before
    raise CustomError(error.messages)

@parser.error_handler
def handle_error(error, req, status_code, headers):  # After
    raise CustomError(error.messages)

@parser.error_handler
def handle_error(error, **kwargs):  # Also works
    raise CustomError(error.messages)
```
  • Loading branch information
sloria committed Dec 25, 2018
1 parent 94e2591 commit cdac38e
Show file tree
Hide file tree
Showing 13 changed files with 223 additions and 38 deletions.
47 changes: 47 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,53 @@ Features:

* Add ``force_all`` argument to ``use_args`` and ``use_kwargs``
(:issue:`252`, :issue:`307`). Thanks :user:`piroux` for reporting.
* The ``status_code`` and ``headers`` arguments to ``ValidationError``
are deprecated. Pass ``error_status_code`` and ``error_headers`` to
`Parser.parse`, `Parser.use_args`, and `Parser.use_kwargs` instead.
(:issue:`327`, :issue:`336`).
* Custom error handlers receive ``error_status_code`` and ``error_headers`` arguments.
(:issue:`327`).

.. code-block:: python
# <4.2.0
@parser.error_handler
def handle_error(error, req, schema):
raise CustomError(error.messages)
class MyParser(FlaskParser):
def handle_error(self, error, req, schema):
# ...
raise CustomError(error.messages)
# >=4.2.0
@parser.error_handler
def handle_error(error, req, schema, status_code, headers):
raise CustomError(error.messages)
# OR
@parser.error_handler
def handle_error(error, **kwargs):
raise CustomError(error.messages)
class MyParser(FlaskParser):
def handle_error(self, error, req, schema, status_code, headers):
# ...
raise CustomError(error.messages)
# OR
def handle_error(self, error, req, **kwargs):
# ...
raise CustomError(error.messages)
Legacy error handlers will be supported until version 5.0.0.

4.1.3 (2018-12-02)
******************
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ Then decorate that function with :func:`Parser.error_handler <webargs.core.Parse
@parser.error_handler
def handle_error(error, req, schema):
def handle_error(error, req, schema, status_code, headers):
raise CustomError(error.messages)
Nesting Fields
Expand Down
3 changes: 2 additions & 1 deletion examples/flask_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def index(args):


@app.route("/add", methods=["POST"])
@use_kwargs(add_args)
@use_kwargs(add_args, error_status_code=400)
def add(x, y):
"""An addition endpoint."""
return jsonify({"result": x + y})
Expand Down Expand Up @@ -63,6 +63,7 @@ def dateadd(value, addend, unit):

# Return validation errors as JSON
@app.errorhandler(422)
@app.errorhandler(400)
def handle_validation_error(err):
exc = getattr(err, "exc", None)
if exc:
Expand Down
2 changes: 1 addition & 1 deletion tests/apps/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,4 @@ def echo_use_kwargs_missing(username, password):
@app.errorhandler(422)
def handle_validation_error(err):
assert isinstance(err.data["schema"], ma.Schema)
return J({"errors": err.exc.messages}), 422
return J({"errors": err.exc.messages}), err.code
39 changes: 35 additions & 4 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}


class MockHTTPError(Exception):
def __init__(self, status_code, headers):
self.status_code = status_code
self.headers = headers
super(MockHTTPError, self).__init__(self, "HTTP Error occurred")


class MockRequestParser(Parser):
"""A minimal parser implementation that parses mock requests."""

Expand Down Expand Up @@ -999,12 +1006,14 @@ def validate(value):


class TestValidationError:
def test_can_store_status_code(self):
err = ValidationError("foo", status_code=401)
def test_status_code_is_deprecated(self):
with pytest.warns(DeprecationWarning):
err = ValidationError("foo", status_code=401)
assert err.status_code == 401

def test_can_store_headers(self):
err = ValidationError("foo", headers={"X-Food-Header": "pizza"})
def test_headers_is_deprecated(self):
with pytest.warns(DeprecationWarning):
err = ValidationError("foo", headers={"X-Food-Header": "pizza"})
assert err.headers == {"X-Food-Header": "pizza"}

def test_str(self):
Expand Down Expand Up @@ -1101,3 +1110,25 @@ def test_get_mimetype():
assert get_mimetype("application/json") == "application/json"
assert get_mimetype("application/json;charset=utf8") == "application/json"
assert get_mimetype(None) is None


class MockRequestParserWithErrorHandler(MockRequestParser):
def handle_error(
self, error, req, schema, error_status_code=None, error_headers=None
):
assert isinstance(error, ValidationError)
assert isinstance(schema, Schema)
raise MockHTTPError(error_status_code, error_headers)


def test_parse_with_error_status_code_and_headers(web_request):
parser = MockRequestParserWithErrorHandler()
web_request.json = {"foo": 42}
args = {"foo": fields.Field(validate=lambda x: False)}
with pytest.raises(MockHTTPError) as excinfo:
parser.parse(
args, web_request, error_status_code=418, error_headers={"X-Foo": "bar"}
)
error = excinfo.value
assert error.status_code == 418
assert error.headers == {"X-Foo": "bar"}
4 changes: 2 additions & 2 deletions webargs/aiohttpparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ 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, schema):
def handle_error(self, error, req, schema, error_status_code, error_headers):
"""Handle ValidationErrors and return a JSON response of error messages to the client."""
error_class = exception_map.get(error.status_code)
error_class = exception_map.get(error_status_code or error.status_code)
if not error_class:
raise LookupError("No exception for {0}".format(error.status_code))
raise error_class(
Expand Down
19 changes: 17 additions & 2 deletions webargs/asyncparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ async def _parse_request(self, schema, req, locations):

# TODO: Lots of duplication from core.Parser here. Rethink.
async def parse(
self, argmap, req=None, locations=None, validate=None, force_all=False
self,
argmap,
req=None,
locations=None,
validate=None,
force_all=False,
error_status_code=None,
error_headers=None,
):
"""Coroutine variant of `webargs.core.Parser`.
Expand All @@ -76,7 +83,9 @@ 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, schema)
self._on_validation_error(
error, req, schema, error_status_code, error_headers
)
finally:
self.clear_cache()
if force_all:
Expand All @@ -91,6 +100,8 @@ def use_args(
as_kwargs=False,
validate=None,
force_all=None,
error_status_code=None,
error_headers=None,
):
"""Decorator that injects parsed arguments into a view function or method.
Expand Down Expand Up @@ -122,6 +133,8 @@ async def wrapper(*args, **kwargs):
locations=locations,
validate=validate,
force_all=force_all_,
error_status_code=error_status_code,
error_headers=error_headers,
)
if as_kwargs:
kwargs.update(parsed_args)
Expand All @@ -146,6 +159,8 @@ def wrapper(*args, **kwargs):
locations=locations,
validate=validate,
force_all=force_all_,
error_status_code=error_status_code,
error_headers=error_headers,
)
if as_kwargs:
kwargs.update(parsed_args)
Expand Down
8 changes: 5 additions & 3 deletions webargs/bottleparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ 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, schema):
def handle_error(self, error, req, schema, error_status_code, error_headers):
"""Handles errors during parsing. Aborts the current request with a
400 error.
"""
status_code = getattr(error, "status_code", self.DEFAULT_VALIDATION_STATUS)
headers = getattr(error, "headers", {})
status_code = error_status_code or getattr(
error, "status_code", self.DEFAULT_VALIDATION_STATUS
)
headers = error_headers or getattr(error, "headers", {})
raise bottle.HTTPError(
status=status_code, body=error.messages, headers=headers, exception=error
)
Expand Down
Loading

0 comments on commit cdac38e

Please sign in to comment.