From ca9e0b535b0b79b497cc8db17bcfccaec29d9da4 Mon Sep 17 00:00:00 2001 From: AWeberChrisMcGuire Date: Fri, 14 Nov 2014 15:27:13 -0500 Subject: [PATCH 1/4] Add JsonErrorMixin: This commit adds the JsonErrorMixin for formatting error message from a given RequestHandler as JSON. --- README.rst | 24 +++++++- sprockets/mixins/json_error/__init__.py | 39 ++++++++++++- test-requirements.txt | 1 + tests.py | 77 ++++++++++++++++++++++--- 4 files changed, 130 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 3ec9044..b19020b 100644 --- a/README.rst +++ b/README.rst @@ -24,12 +24,30 @@ Requirements Example ------- -This examples demonstrates how to use ``sprockets.mixins.json_error`` by ... +This examples demonstrates how to use ``sprockets.mixins.json_error`` to format +errors as JSON. + .. code:: python from sprockets import mixins.json_error + from tornado import web + + class MyRequestHandler(statsd.RequestMetricsMixin, + web.RequestHandler): + + def get(self, *args, **kwargs): + raise web.HTTPError(404, log_message='My reason') + + +The response from the handler will automatically be formatted as: + +.. code:: json + + { + "message": "My reason", + "type": "Not Found" + } - # Example here Version History --------------- @@ -48,4 +66,4 @@ Available at https://sprocketsmixinsjson_error.readthedocs.org/en/latest/history :target: https://pypi.python.org/pypi/sprockets.mixins.json_error .. |License| image:: https://pypip.in/license/sprockets.mixins.json_error/badge.svg? - :target: https://sprocketsmixinsjson_error.readthedocs.org \ No newline at end of file + :target: https://sprocketsmixinsjson_error.readthedocs.org diff --git a/sprockets/mixins/json_error/__init__.py b/sprockets/mixins/json_error/__init__.py index 07d04bc..87ba0d7 100644 --- a/sprockets/mixins/json_error/__init__.py +++ b/sprockets/mixins/json_error/__init__.py @@ -4,5 +4,42 @@ mixins.json_error Handler mixin for writing JSON errors """ -version_info = (0, 0, 0) +version_info = (1, 0, 0) __version__ = '.'.join(str(v) for v in version_info) + + +class JsonErrorMixin(object): + + def write_error(self, status_code, **kwargs): + """Suppress the automatic rendering of HTML code upon an error. + + :param int status_code: + The HTTP status code the :class:`HTTPError` raised. + + :param dict kwargs: + Automatically filled with exception information including + the error that was raised, the class of error raised, and an + object. + + """ + if kwargs.get('error'): + raised_error = kwargs.get('error') + else: + _, raised_error, _ = kwargs['exc_info'] + + error_message = getattr( + raised_error, 'log_message', 'Unexpected Error') + error_type = getattr(raised_error, 'error_type', self._reason) + + self.error = { + 'message': error_message, + 'type': error_type, + } + if hasattr(raised_error, 'documentation_url'): + self.error['documentation_url'] = raised_error.documentation_url + + error_status_code = getattr(raised_error, 'status_code', status_code) + self.set_status(error_status_code) + + self.set_header('Content-Type', 'application/json; charset=UTF-8') + self.finish(self.error) diff --git a/test-requirements.txt b/test-requirements.txt index 20b57db..90ca759 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ coverage>=3.7,<4 coveralls>=0.4,<1 nose>=1.3,<2 +tornado>=4.0,<5 diff --git a/tests.py b/tests.py index 5b467be..1924476 100644 --- a/tests.py +++ b/tests.py @@ -2,12 +2,75 @@ Tests for the sprockets.mixins.json_error package """ -import mock -try: - import unittest2 as unittest -except ImportError: - import unittest +import json + +from sprockets.mixins import json_error +from tornado import testing, web -class MyTest(unittest.TestCase): - pass +class HTTPErrorRequestHandler( + json_error.JsonErrorMixin, web.RequestHandler): + + def get(self): + raise web.HTTPError(400, 'Error Reason') + + +class CustomExceptionRequestHandler( + json_error.JsonErrorMixin, web.RequestHandler): + + class FailureError(Exception): + + status_code = 400 + log_message = 'Too much Foo' + error_type = 'FailureError' + documentation_url = 'http://www.example.com' + + def get(self): + raise self.FailureError() + + +class UnexpectedErrorRequestHandler( + json_error.JsonErrorMixin, web.RequestHandler): + + def get(self): + raise Exception() + + +class TestHTTPError(testing.AsyncHTTPTestCase): + + def get_app(self): + return web.Application([('/', HTTPErrorRequestHandler)]) + + def test_tornado_thrown_exception(self): + response = self.fetch('/') + expected = {'message': 'Error Reason', 'type': 'Bad Request'} + self.assertEqual(json.loads(response.body), expected) + + +class TestCustomExceptions(testing.AsyncHTTPTestCase): + + def get_app(self): + return web.Application([('/', CustomExceptionRequestHandler)]) + + def test_tornado_custom_exception(self): + response = self.fetch('/') + expected = { + 'message': 'Too much Foo', + 'type': 'FailureError', + 'documentation_url': 'http://www.example.com', + } + self.assertEqual(json.loads(response.body), expected) + + +class TestUnexpectedError(testing.AsyncHTTPTestCase): + + def get_app(self): + return web.Application([('/', UnexpectedErrorRequestHandler)]) + + def test_unexpected_exception(self): + response = self.fetch('/') + expected = { + 'message': 'Unexpected Error', + 'type': 'Internal Server Error' + } + self.assertEqual(json.loads(response.body), expected) From 8dfbd864a394bce742d3f1e934a7ad16e4842c0d Mon Sep 17 00:00:00 2001 From: AWeberChrisMcGuire Date: Tue, 18 Nov 2014 09:54:46 -0500 Subject: [PATCH 2/4] Fix typo in README. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b19020b..6206a4a 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ errors as JSON. from sprockets import mixins.json_error from tornado import web - class MyRequestHandler(statsd.RequestMetricsMixin, + class MyRequestHandler(json_error.JsonErrorMixin, web.RequestHandler): def get(self, *args, **kwargs): From f21a6a583f0fc67d23a5cf1cfe014b8e22416633 Mon Sep 17 00:00:00 2001 From: AWeberChrisMcGuire Date: Tue, 18 Nov 2014 10:03:19 -0500 Subject: [PATCH 3/4] Clean up write_error. This commit alters write_error to pull the message from a get_message function of the error, else it using a default Unexpected Error string. --- sprockets/mixins/json_error/__init__.py | 16 +++++++--------- tests.py | 6 ++++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sprockets/mixins/json_error/__init__.py b/sprockets/mixins/json_error/__init__.py index 87ba0d7..97fc5f6 100644 --- a/sprockets/mixins/json_error/__init__.py +++ b/sprockets/mixins/json_error/__init__.py @@ -9,6 +9,7 @@ __version__ = '.'.join(str(v) for v in version_info) class JsonErrorMixin(object): + """Mixin to write errors as JSON.""" def write_error(self, status_code, **kwargs): """Suppress the automatic rendering of HTML code upon an error. @@ -22,15 +23,15 @@ class JsonErrorMixin(object): object. """ - if kwargs.get('error'): - raised_error = kwargs.get('error') - else: - _, raised_error, _ = kwargs['exc_info'] + _, raised_error, _ = kwargs.get('exc_info', (None, None)) - error_message = getattr( - raised_error, 'log_message', 'Unexpected Error') error_type = getattr(raised_error, 'error_type', self._reason) + try: + error_message = raised_error.get_message() + except AttributeError: + error_message = 'Unexpected Error' + self.error = { 'message': error_message, 'type': error_type, @@ -38,8 +39,5 @@ class JsonErrorMixin(object): if hasattr(raised_error, 'documentation_url'): self.error['documentation_url'] = raised_error.documentation_url - error_status_code = getattr(raised_error, 'status_code', status_code) - self.set_status(error_status_code) - self.set_header('Content-Type', 'application/json; charset=UTF-8') self.finish(self.error) diff --git a/tests.py b/tests.py index 1924476..164a00d 100644 --- a/tests.py +++ b/tests.py @@ -21,10 +21,12 @@ class CustomExceptionRequestHandler( class FailureError(Exception): status_code = 400 - log_message = 'Too much Foo' error_type = 'FailureError' documentation_url = 'http://www.example.com' + def get_message(self): + return 'Too much Foo' + def get(self): raise self.FailureError() @@ -43,7 +45,7 @@ class TestHTTPError(testing.AsyncHTTPTestCase): def test_tornado_thrown_exception(self): response = self.fetch('/') - expected = {'message': 'Error Reason', 'type': 'Bad Request'} + expected = {'message': 'Unexpected Error', 'type': 'Bad Request'} self.assertEqual(json.loads(response.body), expected) From dcec231bac41c0158b9e1aa1f7a3e0780a3ea9ba Mon Sep 17 00:00:00 2001 From: AWeberChrisMcGuire Date: Tue, 18 Nov 2014 11:00:20 -0500 Subject: [PATCH 4/4] write_error: get exc_info should return a triple. --- sprockets/mixins/json_error/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sprockets/mixins/json_error/__init__.py b/sprockets/mixins/json_error/__init__.py index 97fc5f6..a56bae3 100644 --- a/sprockets/mixins/json_error/__init__.py +++ b/sprockets/mixins/json_error/__init__.py @@ -23,7 +23,7 @@ class JsonErrorMixin(object): object. """ - _, raised_error, _ = kwargs.get('exc_info', (None, None)) + _, raised_error, _ = kwargs.get('exc_info', (None, None, None)) error_type = getattr(raised_error, 'error_type', self._reason)