diff --git a/CHANGES.rst b/CHANGES.rst index 5a6be80..58d9d69 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 3.2 (unreleased) ---------------- +- Implement basic subset of Response features in HTTPException class. 3.1 (2016-07-22) ---------------- diff --git a/src/zExceptions/__init__.py b/src/zExceptions/__init__.py index f1055ae..a383425 100644 --- a/src/zExceptions/__init__.py +++ b/src/zExceptions/__init__.py @@ -36,39 +36,143 @@ from ._compat import string_types +status_reasons = { + # Informational + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + + # Successful + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 226: 'IM Used', + + # Redirection + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', + + # Client Error + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Time-out', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request-URI Too Large', + 415: 'Unsupported Media Type', + 416: 'Requested Range Not Satisfiable', + 417: 'Expectation Failed', + 418: "I'm a teapot", + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 451: 'Unavailable for Legal Reasons', + 431: 'Request Header Fields Too Large', + + # Server Error + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Time-out', + 505: 'HTTP Version not supported', + 507: 'Insufficient Storage', + 510: 'Not Extended', + 511: 'Network Authentication Required', +} + + @implementer(IHTTPException) class HTTPException(Exception): - pass + body = None + errmsg = 'Internal Server Error' + status = 500 + + def setBody(self, body): + self.body = body + + def getStatus(self): + return self.status + + def setStatus(self, status, reason=None): + self.status = status + if reason is None: + reason = status_reasons.get(status, 'Unknown') + else: + reason = reason + self.errmsg = reason + + def __call__(self, environ, start_response): + headers = [('content-type', 'text/html;charset=utf-8')] + if self.errmsg is not None: + reason = self.errmsg + reason = status_reasons[self.getStatus()] + start_response( + '%d %s' % (self.getStatus(), reason), + headers) + body = self.body + if body is None: + body = str(self) + return [body] @implementer(IBadRequest) class BadRequest(HTTPException): - pass + errmsg = 'Bad Request' + status = 400 @implementer(IException) class InternalError(HTTPException): - pass + errmsg = 'Internal Server Error' + status = 500 @implementer(INotFound) class NotFound(HTTPException): - pass + errmsg = 'Not Found' + status = 404 @implementer(IForbidden) class Forbidden(HTTPException): - pass + errmsg = 'Forbidden' + status = 403 @implementer(IMethodNotAllowed) class MethodNotAllowed(HTTPException): - pass + errmsg = 'Method Not Allowed' + status = 405 @implementer(IRedirect) class Redirect(HTTPException): - pass + errmsg = 'Found' + status = 302 def convertExceptionType(name): diff --git a/src/zExceptions/tests/test___init__.py b/src/zExceptions/tests/test___init__.py index 14a0069..e6b243b 100644 --- a/src/zExceptions/tests/test___init__.py +++ b/src/zExceptions/tests/test___init__.py @@ -1,6 +1,61 @@ import unittest +class TestHTTPException(unittest.TestCase): + + def _getTargetClass(self): + from zExceptions import HTTPException + return HTTPException + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def test_body(self): + exc = self._makeOne() + self.assertEqual(exc.body, None) + exc.setBody('Foo') + self.assertEqual(exc.body, 'Foo') + + def test_status(self): + exc = self._makeOne() + self.assertEqual(exc.getStatus(), 500) + self.assertEqual(exc.errmsg, 'Internal Server Error') + exc.setStatus(503) + self.assertEqual(exc.getStatus(), 503) + self.assertEqual(exc.errmsg, 'Service Unavailable') + + def test_call(self): + exc = self._makeOne('Foo Error') + called = [] + + def start_response(status, headers): + called.append((status, headers)) + + response = exc({'Foo': 1}, start_response) + self.assertEqual(called, [( + '500 Internal Server Error', + [('content-type', 'text/html;charset=utf-8')] + )]) + self.assertEqual(response, ['Foo Error']) + + def test_call_custom(self): + exc = self._makeOne('Foo Error') + exc.setBody('Foo') + exc.setStatus(503) + + called = [] + + def start_response(status, headers): + called.append((status, headers)) + + response = exc({'Foo': 1}, start_response) + self.assertEqual(called, [( + '503 Service Unavailable', + [('content-type', 'text/html;charset=utf-8')] + )]) + self.assertEqual(response, ['Foo']) + + class TestConvertExceptionType(unittest.TestCase): def _callFUT(self, name):