From 4184d956514ada7dccf2f99ced09cbf07a721cc3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 19:22:13 -0400 Subject: bite the bullet and replace all webob.exc classes with ones of our own --- pyramid/exceptions.py | 1144 +++++++++++++++++++++++++++++++++----- pyramid/tests/test_exceptions.py | 299 +++++++++- 2 files changed, 1308 insertions(+), 135 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index e9718c6ab..53cb0e5a8 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,139 +1,1033 @@ +""" +HTTP Exceptions +--------------- + +This module contains Python exceptions that relate to HTTP status codes by +defining a set of classes, all subclasses of HTTPException. Each +exception, in addition to being a Python exception that can be raised and +caught, is also a ``Response`` object. + +This module defines exceptions according to RFC 2068 [1]_ : codes with +100-300 are not really errors; 400's are client errors, and 500's are +server errors. + +Exception + HTTPException + HTTPOk + * 200 - HTTPOk + * 201 - HTTPCreated + * 202 - HTTPAccepted + * 203 - HTTPNonAuthoritativeInformation + * 204 - HTTPNoContent + * 205 - HTTPResetContent + * 206 - HTTPPartialContent + HTTPRedirection + * 300 - HTTPMultipleChoices + * 301 - HTTPMovedPermanently + * 302 - HTTPFound + * 303 - HTTPSeeOther + * 304 - HTTPNotModified + * 305 - HTTPUseProxy + * 306 - Unused (not implemented, obviously) + * 307 - HTTPTemporaryRedirect + HTTPError + HTTPClientError + * 400 - HTTPBadRequest + * 401 - HTTPUnauthorized + * 402 - HTTPPaymentRequired + * 403 - HTTPForbidden + * 404 - HTTPNotFound + * 405 - HTTPMethodNotAllowed + * 406 - HTTPNotAcceptable + * 407 - HTTPProxyAuthenticationRequired + * 408 - HTTPRequestTimeout + * 409 - HTTPConflict + * 410 - HTTPGone + * 411 - HTTPLengthRequired + * 412 - HTTPPreconditionFailed + * 413 - HTTPRequestEntityTooLarge + * 414 - HTTPRequestURITooLong + * 415 - HTTPUnsupportedMediaType + * 416 - HTTPRequestRangeNotSatisfiable + * 417 - HTTPExpectationFailed + HTTPServerError + * 500 - HTTPInternalServerError + * 501 - HTTPNotImplemented + * 502 - HTTPBadGateway + * 503 - HTTPServiceUnavailable + * 504 - HTTPGatewayTimeout + * 505 - HTTPVersionNotSupported + +Each HTTP exception has the following attributes: + + ``code`` + the HTTP status code for the exception + + ``title`` + remainder of the status line (stuff after the code) + + ``explanation`` + a plain-text explanation of the error message that is + not subject to environment or header substitutions; + it is accessible in the template via %(explanation)s + + ``detail`` + a plain-text message customization that is not subject + to environment or header substitutions; accessible in + the template via %(detail)s + + ``body_template`` + a content fragment (in HTML) used for environment and + header substitution; the default template includes both + the explanation and further detail provided in the + message + +Each HTTP exception accepts the following parameters: + + ``detail`` + a plain-text override of the default ``detail`` + + ``headers`` + a list of (k,v) header pairs + + ``comment`` + a plain-text additional information which is + usually stripped/hidden for end-users + + ``body_template`` + a string.Template object containing a content fragment in HTML + that frames the explanation and further detail + +Substitution of environment variables and headers into template values is +performed if a ``request`` is passed to the exception constructor. + +The subclasses of :class:`~_HTTPMove` +(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, +:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and +:class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` +field. Reflecting this, these subclasses have one additional keyword argument: +``location``, which indicates the location to which to redirect. + +References: + +.. [1] http://www.python.org/peps/pep-0333.html#error-handling +""" + +import types +from string import Template +from webob import Response +from webob import html_escape + from zope.configuration.exceptions import ConfigurationError as ZCE -from zope.interface import classImplements +from zope.interface import implements from pyramid.interfaces import IExceptionResponse -from webob.response import Response - -# Documentation proxy import -from webob.exc import __doc__ - -# API: status_map -from webob.exc import status_map -status_map = status_map.copy() # we mutate it - -# API: parent classes -from webob.exc import HTTPException -from webob.exc import WSGIHTTPException -from webob.exc import HTTPOk -from webob.exc import HTTPRedirection -from webob.exc import HTTPError -from webob.exc import HTTPClientError -from webob.exc import HTTPServerError - -# slightly nasty import-time side effect to provide WSGIHTTPException -# with IExceptionResponse interface (used during config.py exception view -# registration) -classImplements(WSGIHTTPException, IExceptionResponse) - -# API: Child classes -from webob.exc import HTTPCreated -from webob.exc import HTTPAccepted -from webob.exc import HTTPNonAuthoritativeInformation -from webob.exc import HTTPNoContent -from webob.exc import HTTPResetContent -from webob.exc import HTTPPartialContent -from webob.exc import HTTPMultipleChoices -from webob.exc import HTTPMovedPermanently -from webob.exc import HTTPFound -from webob.exc import HTTPSeeOther -from webob.exc import HTTPNotModified -from webob.exc import HTTPUseProxy -from webob.exc import HTTPTemporaryRedirect -from webob.exc import HTTPBadRequest -from webob.exc import HTTPUnauthorized -from webob.exc import HTTPPaymentRequired -from webob.exc import HTTPMethodNotAllowed -from webob.exc import HTTPNotAcceptable -from webob.exc import HTTPProxyAuthenticationRequired -from webob.exc import HTTPRequestTimeout -from webob.exc import HTTPConflict -from webob.exc import HTTPGone -from webob.exc import HTTPLengthRequired -from webob.exc import HTTPPreconditionFailed -from webob.exc import HTTPRequestEntityTooLarge -from webob.exc import HTTPRequestURITooLong -from webob.exc import HTTPUnsupportedMediaType -from webob.exc import HTTPRequestRangeNotSatisfiable -from webob.exc import HTTPExpectationFailed -from webob.exc import HTTPInternalServerError -from webob.exc import HTTPNotImplemented -from webob.exc import HTTPBadGateway -from webob.exc import HTTPServiceUnavailable -from webob.exc import HTTPGatewayTimeout -from webob.exc import HTTPVersionNotSupported - -# API: HTTPNotFound and HTTPForbidden (redefined for bw compat) - -from webob.exc import HTTPForbidden as _HTTPForbidden -from webob.exc import HTTPNotFound as _HTTPNotFound - -class HTTPNotFound(_HTTPNotFound): - """ - Raise this exception within :term:`view` code to immediately - return the :term:`Not Found view` to the invoking user. Usually - this is a basic ``404`` page, but the Not Found view can be - customized as necessary. See :ref:`changing_the_notfound_view`. - This exception's constructor accepts a single positional argument, which - should be a string. The value of this string will be available as the - ``message`` attribute of this exception, for availability to the - :term:`Not Found View`. +newstyle_exceptions = issubclass(Exception, object) + +def no_escape(value): + if value is None: + return '' + if not isinstance(value, basestring): + if hasattr(value, '__unicode__'): + value = unicode(value) + else: + value = str(value) + return value + +class HTTPException(Exception): + implements(IExceptionResponse) """ + Exception used on pre-Python-2.5, where new-style classes cannot be used as + an exception. + """ + + def __init__(self, message, wsgi_response): + self.message = message + Exception.__init__(self, message) + self.__dict__['wsgi_response'] = wsgi_response + + def exception(self): + return self + + exception = property(exception) + + # for old style exceptions + if not newstyle_exceptions: #pragma NO COVERAGE + def __getattr__(self, attr): + if not attr.startswith('_'): + return getattr(self.wsgi_response, attr) + else: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + if attr.startswith('_') or attr in ('args',): + self.__dict__[attr] = value + else: + setattr(self.wsgi_response, attr, value) + +class WSGIHTTPException(Response, HTTPException): + + ## You should set in subclasses: + # code = 200 + # title = 'OK' + # explanation = 'why this happens' + # body_template_obj = Template('response template') + + # differences from webob.exc.WSGIHTTPException: + # - not a WSGI application (just a response) + # + # as a result: + # + # - bases plaintext vs. html result on self.content_type rather than + # on request environ + # + # - doesn't add request.environ keys to template substitutions unless + # 'request' is passed as a keyword argument. + # + # - doesn't use "strip_tags" (${br} placeholder for
, no other html + # in default body template) + # + # - sets a default app_iter if no body, app_iter, or unicode_body is + # passed + # + # - explicitly sets self.message = detail to prevent whining by Python + # 2.6.5+ Exception.message + # + code = None + title = None + explanation = '' + body_template_obj = Template('''\ +${explanation}${br}${br} +${detail} +${html_comment} +''') + + plain_template_obj = Template('''\ +${status} + +${body}''') + + html_template_obj = Template('''\ + + + ${status} + + +

${status}

+ ${body} + +''') + + ## Set this to True for responses that should have no request body + empty_body = False + _default_called = False + def __init__(self, detail=None, headers=None, comment=None, body_template=None, **kw): - self.message = detail # prevent 2.6.X whining - _HTTPNotFound.__init__(self, detail=detail, headers=headers, - comment=comment, body_template=body_template, - **kw) - if not ('body' in kw or 'app_iter' in kw): - if not self.empty_body: - body = self.html_body(self.environ) - if isinstance(body, unicode): - body = body.encode(self.charset) - self.body = body - -class HTTPForbidden(_HTTPForbidden): + status = '%s %s' % (self.code, self.title) + Response.__init__(self, status=status, **kw) + Exception.__init__(self, detail) + self.detail = self.message = detail + if headers: + self.headers.extend(headers) + self.comment = comment + if body_template is not None: + self.body_template = body_template + self.body_template_obj = Template(body_template) + + if self.empty_body: + del self.content_type + del self.content_length + elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): + self.app_iter = iter(self._default_app_iter, None) + + def __str__(self): + return self.detail or self.explanation + + def _default_app_iter(self): + if self._default_called: + return None + html_comment = '' + comment = self.comment or '' + if 'html' in self.content_type or '': + escape = html_escape + page_template = self.html_template_obj + br = '
' + if comment: + html_comment = '' % escape(comment) + else: + escape = no_escape + page_template = self.plain_template_obj + br = '\n' + if comment: + html_comment = escape(comment) + args = { + 'br':br, + 'explanation': escape(self.explanation), + 'detail': escape(self.detail or ''), + 'comment': escape(comment), + 'html_comment':html_comment, + } + body_tmpl = self.body_template_obj + if WSGIHTTPException.body_template_obj is not body_tmpl: + # Custom template; add headers to args + environ = self.environ + if environ is not None: + for k, v in environ.items(): + args[k] = escape(v) + for k, v in self.headers.items(): + args[k.lower()] = escape(v) + body = body_tmpl.substitute(args) + page = page_template.substitute(status=self.status, body=body) + if isinstance(page, unicode): + page = page.encode(self.charset) + self._default_called = True + return page + + def wsgi_response(self): + return self + + wsgi_response = property(wsgi_response) + + def exception(self): + if newstyle_exceptions: + return self + else: + return HTTPException(self.detail, self) + + exception = property(exception) + +class HTTPError(WSGIHTTPException): + """ + base class for status codes in the 400's and 500's + + This is an exception which indicates that an error has occurred, + and that any work in progress should not be committed. These are + typically results in the 400's and 500's. + """ + +class HTTPRedirection(WSGIHTTPException): + """ + base class for 300's status code (redirections) + + This is an abstract base class for 3xx redirection. It indicates + that further action needs to be taken by the user agent in order + to fulfill the request. It does not necessarly signal an error + condition. + """ + +class HTTPOk(WSGIHTTPException): + """ + Base class for the 200's status code (successful responses) + + code: 200, title: OK + """ + code = 200 + title = 'OK' + +############################################################ +## 2xx success +############################################################ + +class HTTPCreated(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that request has been fulfilled and resulted in a new + resource being created. + + code: 201, title: Created + """ + code = 201 + title = 'Created' + +class HTTPAccepted(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the request has been accepted for processing, but the + processing has not been completed. + + code: 202, title: Accepted + """ + code = 202 + title = 'Accepted' + explanation = 'The request is accepted for processing.' + +class HTTPNonAuthoritativeInformation(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the returned metainformation in the entity-header is + not the definitive set as available from the origin server, but is + gathered from a local or a third-party copy. + + code: 203, title: Non-Authoritative Information + """ + code = 203 + title = 'Non-Authoritative Information' + +class HTTPNoContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the request but does + not need to return an entity-body, and might want to return updated + metainformation. + + code: 204, title: No Content + """ + code = 204 + title = 'No Content' + empty_body = True + +class HTTPResetContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the the server has fulfilled the request and + the user agent SHOULD reset the document view which caused the + request to be sent. + + code: 205, title: Reset Content + """ + code = 205 + title = 'Reset Content' + empty_body = True + +class HTTPPartialContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the partial GET + request for the resource. + + code: 206, title: Partial Content + """ + code = 206 + title = 'Partial Content' + +## FIXME: add 207 Multi-Status (but it's complicated) + +############################################################ +## 3xx redirection +############################################################ + +class _HTTPMove(HTTPRedirection): + """ + redirections which require a Location field + + Since a 'Location' header is a required attribute of 301, 302, 303, + 305 and 307 (but not 304), this base class provides the mechanics to + make this easy. + + You must provide a ``location`` keyword argument. + """ + # differences from webob.exc._HTTPMove: + # + # - not a wsgi app + # + # - ${location} isn't wrapped in an tag in body + # + # - location keyword arg defaults to '' + # + # - ``add_slash`` argument is no longer accepted: code that passes + # add_slash argument will receive an exception. + explanation = 'The resource has been moved to' + body_template_obj = Template('''\ +${explanation} ${location}; +you should be redirected automatically. +${detail} +${html_comment}''') + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, location='', **kw): + super(_HTTPMove, self).__init__( + detail=detail, headers=headers, comment=comment, + body_template=body_template, location=location, **kw) + +class HTTPMultipleChoices(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource corresponds to any one + of a set of representations, each with its own specific location, + and agent-driven negotiation information is being provided so that + the user can select a preferred representation and redirect its + request to that location. + + code: 300, title: Multiple Choices + """ + code = 300 + title = 'Multiple Choices' + +class HTTPMovedPermanently(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource has been assigned a new + permanent URI and any future references to this resource SHOULD use + one of the returned URIs. + + code: 301, title: Moved Permanently + """ + code = 301 + title = 'Moved Permanently' + +class HTTPFound(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily under + a different URI. + + code: 302, title: Found + """ + code = 302 + title = 'Found' + explanation = 'The resource was found at' + +# This one is safe after a POST (the redirected location will be +# retrieved with GET): +class HTTPSeeOther(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the response to the request can be found under + a different URI and SHOULD be retrieved using a GET method on that + resource. + + code: 303, title: See Other + """ + code = 303 + title = 'See Other' + +class HTTPNotModified(HTTPRedirection): + """ + subclass of :class:`~HTTPRedirection` + + This indicates that if the client has performed a conditional GET + request and access is allowed, but the document has not been + modified, the server SHOULD respond with this status code. + + code: 304, title: Not Modified + """ + # FIXME: this should include a date or etag header + code = 304 + title = 'Not Modified' + empty_body = True + +class HTTPUseProxy(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource MUST be accessed through + the proxy given by the Location field. + + code: 305, title: Use Proxy + """ + # Not a move, but looks a little like one + code = 305 + title = 'Use Proxy' + explanation = ( + 'The resource must be accessed through a proxy located at') + +class HTTPTemporaryRedirect(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily + under a different URI. + + code: 307, title: Temporary Redirect + """ + code = 307 + title = 'Temporary Redirect' + +############################################################ +## 4xx client error +############################################################ + +class HTTPClientError(HTTPError): + """ + base class for the 400's, where the client is in error + + This is an error condition in which the client is presumed to be + in-error. This is an expected problem, and thus is not considered + a bug. A server-side traceback is not warranted. Unless specialized, + this is a '400 Bad Request' + """ + code = 400 + title = 'Bad Request' + explanation = ('The server could not comply with the request since\r\n' + 'it is either malformed or otherwise incorrect.\r\n') + +class HTTPBadRequest(HTTPClientError): + pass + +class HTTPUnauthorized(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request requires user authentication. + + code: 401, title: Unauthorized """ + code = 401 + title = 'Unauthorized' + explanation = ( + 'This server could not verify that you are authorized to\r\n' + 'access the document you requested. Either you supplied the\r\n' + 'wrong credentials (e.g., bad password), or your browser\r\n' + 'does not understand how to supply the credentials required.\r\n') + +class HTTPPaymentRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + code: 402, title: Payment Required + """ + code = 402 + title = 'Payment Required' + explanation = ('Access was denied for financial reasons.') + +class HTTPForbidden(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server understood the request, but is + refusing to fulfill it. + + code: 403, title: Forbidden + Raise this exception within :term:`view` code to immediately return the :term:`forbidden view` to the invoking user. Usually this is a basic ``403`` page, but the forbidden view can be customized as necessary. See :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be the ``context`` of a :term:`Forbidden View`. - This exception's constructor accepts two arguments. The first argument, - ``message``, should be a string. The value of this string will be used - as the ``message`` attribute of the exception object. The second - argument, ``result`` is usually an instance of + This exception's constructor treats two arguments specially. The first + argument, ``detail``, should be a string. The value of this string will + be used as the ``message`` attribute of the exception object. The second + special keyword argument, ``result`` is usually an instance of :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` each of which indicates a reason for the forbidden error. However, ``result`` is also permitted to be just a plain boolean ``False`` object. The ``result`` value will be used as the ``result`` attribute of the - exception object. + exception object. It defaults to ``None``. The :term:`Forbidden View` can use the attributes of a Forbidden exception as necessary to provide extended information in an error report shown to a user. """ + # differences from webob.exc.HTTPForbidden: + # + # - accepts a ``result`` keyword argument + # + # - overrides constructor to set ``self.result`` + # + # differences from older pyramid.exceptions.Forbidden: + # + # - ``result`` must be passed as a keyword argument. + # + code = 403 + title = 'Forbidden' + explanation = ('Access was denied to this resource.') def __init__(self, detail=None, headers=None, comment=None, body_template=None, result=None, **kw): - self.message = detail # prevent 2.6.X whining - self.result = result # bw compat - _HTTPForbidden.__init__(self, detail=detail, headers=headers, - comment=comment, body_template=body_template, - **kw) - if not ('body' in kw or 'app_iter' in kw): - if not self.empty_body: - body = self.html_body(self.environ) - if isinstance(body, unicode): - body = body.encode(self.charset) - self.body = body + HTTPClientError.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) + self.result = result + +class HTTPNotFound(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find anything matching the + Request-URI. + + code: 404, title: Not Found + + Raise this exception within :term:`view` code to immediately + return the :term:`Not Found view` to the invoking user. Usually + this is a basic ``404`` page, but the Not Found view can be + customized as necessary. See :ref:`changing_the_notfound_view`. + + This exception's constructor accepts a ``detail`` argument + (the first argument), which should be a string. The value of this + string will be available as the ``message`` attribute of this exception, + for availability to the :term:`Not Found View`. + """ + code = 404 + title = 'Not Found' + explanation = ('The resource could not be found.') + +class HTTPMethodNotAllowed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method specified in the Request-Line is + not allowed for the resource identified by the Request-URI. + + code: 405, title: Method Not Allowed + """ + # differences from webob.exc.HTTPMethodNotAllowed: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 405 + title = 'Method Not Allowed' + +class HTTPNotAcceptable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates the resource identified by the request is only + capable of generating response entities which have content + characteristics not acceptable according to the accept headers + sent in the request. + + code: 406, title: Not Acceptable + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # HTTP_ACCEPT) + code = 406 + title = 'Not Acceptable' + +class HTTPProxyAuthenticationRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This is similar to 401, but indicates that the client must first + authenticate itself with the proxy. + + code: 407, title: Proxy Authentication Required + """ + code = 407 + title = 'Proxy Authentication Required' + explanation = ('Authentication with a local proxy is needed.') + +class HTTPRequestTimeout(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the client did not produce a request within + the time that the server was prepared to wait. + + code: 408, title: Request Timeout + """ + code = 408 + title = 'Request Timeout' + explanation = ('The server has waited too long for the request to ' + 'be sent by the client.') + +class HTTPConflict(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request could not be completed due to a + conflict with the current state of the resource. + + code: 409, title: Conflict + """ + code = 409 + title = 'Conflict' + explanation = ('There was a conflict when trying to complete ' + 'your request.') + +class HTTPGone(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the requested resource is no longer available + at the server and no forwarding address is known. + + code: 410, title: Gone + """ + code = 410 + title = 'Gone' + explanation = ('This resource is no longer available. No forwarding ' + 'address is given.') + +class HTTPLengthRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the the server refuses to accept the request + without a defined Content-Length. + + code: 411, title: Length Required + """ + code = 411 + title = 'Length Required' + explanation = ('Content-Length header required.') + +class HTTPPreconditionFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the precondition given in one or more of the + request-header fields evaluated to false when it was tested on the + server. + + code: 412, title: Precondition Failed + """ + code = 412 + title = 'Precondition Failed' + explanation = ('Request precondition failed.') + +class HTTPRequestEntityTooLarge(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to process a request + because the request entity is larger than the server is willing or + able to process. + + code: 413, title: Request Entity Too Large + """ + code = 413 + title = 'Request Entity Too Large' + explanation = ('The body of your request was too large for this server.') + +class HTTPRequestURITooLong(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the Request-URI is longer than the server is willing to + interpret. + + code: 414, title: Request-URI Too Long + """ + code = 414 + title = 'Request-URI Too Long' + explanation = ('The request URI was too long for this server.') + +class HTTPUnsupportedMediaType(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the entity of the request is in a format not supported by + the requested resource for the requested method. + + code: 415, title: Unsupported Media Type + """ + # differences from webob.exc.HTTPUnsupportedMediaType: + # + # - body_template_obj not overridden (it tried to use request environ's + # CONTENT_TYPE) + code = 415 + title = 'Unsupported Media Type' + +class HTTPRequestRangeNotSatisfiable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + The server SHOULD return a response with this status code if a + request included a Range request-header field, and none of the + range-specifier values in this field overlap the current extent + of the selected resource, and the request did not include an + If-Range request-header field. + + code: 416, title: Request Range Not Satisfiable + """ + code = 416 + title = 'Request Range Not Satisfiable' + explanation = ('The Range requested is not available.') + +class HTTPExpectationFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indidcates that the expectation given in an Expect + request-header field could not be met by this server. + + code: 417, title: Expectation Failed + """ + code = 417 + title = 'Expectation Failed' + explanation = ('Expectation failed.') + +class HTTPUnprocessableEntity(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is unable to process the contained + instructions. Only for WebDAV. + + code: 422, title: Unprocessable Entity + """ + ## Note: from WebDAV + code = 422 + title = 'Unprocessable Entity' + explanation = 'Unable to process the contained instructions' + +class HTTPLocked(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the resource is locked. Only for WebDAV + + code: 423, title: Locked + """ + ## Note: from WebDAV + code = 423 + title = 'Locked' + explanation = ('The resource is locked') + +class HTTPFailedDependency(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method could not be performed because the + requested action depended on another action and that action failed. + Only for WebDAV. + + code: 424, title: Failed Dependency + """ + ## Note: from WebDAV + code = 424 + title = 'Failed Dependency' + explanation = ( + 'The method could not be performed because the requested ' + 'action dependended on another action and that action failed') + +############################################################ +## 5xx Server Error +############################################################ +# Response status codes beginning with the digit "5" indicate cases in +# which the server is aware that it has erred or is incapable of +# performing the request. Except when responding to a HEAD request, the +# server SHOULD include an entity containing an explanation of the error +# situation, and whether it is a temporary or permanent condition. User +# agents SHOULD display any included entity to the user. These response +# codes are applicable to any request method. + +class HTTPServerError(HTTPError): + """ + base class for the 500's, where the server is in-error + + This is an error condition in which the server is presumed to be + in-error. This is usually unexpected, and thus requires a traceback; + ideally, opening a support ticket for the customer. Unless specialized, + this is a '500 Internal Server Error' + """ + code = 500 + title = 'Internal Server Error' + explanation = ( + 'The server has either erred or is incapable of performing\r\n' + 'the requested operation.\r\n') + +class HTTPInternalServerError(HTTPServerError): + pass + +class HTTPNotImplemented(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support the functionality + required to fulfill the request. + + code: 501, title: Not Implemented + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 501 + title = 'Not Implemented' + +class HTTPBadGateway(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + received an invalid response from the upstream server it accessed + in attempting to fulfill the request. + + code: 502, title: Bad Gateway + """ + code = 502 + title = 'Bad Gateway' + explanation = ('Bad gateway.') + +class HTTPServiceUnavailable(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server is currently unable to handle the + request due to a temporary overloading or maintenance of the server. + + code: 503, title: Service Unavailable + """ + code = 503 + title = 'Service Unavailable' + explanation = ('The server is currently unavailable. ' + 'Please try again at a later time.') + +class HTTPGatewayTimeout(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + did not receive a timely response from the upstream server specified + by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server + (e.g. DNS) it needed to access in attempting to complete the request. + + code: 504, title: Gateway Timeout + """ + code = 504 + title = 'Gateway Timeout' + explanation = ('The gateway has timed out.') + +class HTTPVersionNotSupported(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support, or refuses to + support, the HTTP protocol version that was used in the request + message. + + code: 505, title: HTTP Version Not Supported + """ + code = 505 + title = 'HTTP Version Not Supported' + explanation = ('The HTTP version is not supported.') + +class HTTPInsufficientStorage(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not have enough space to save + the resource. + + code: 507, title: Insufficient Storage + """ + code = 507 + title = 'Insufficient Storage' + explanation = ('There was not enough space to save the resource') + +__all__ = ['status_map'] +status_map={} +for name, value in globals().items(): + if (isinstance(value, (type, types.ClassType)) and + issubclass(value, HTTPException) + and not name.startswith('_')): + __all__.append(name) + if getattr(value, 'code', None): + status_map[value.code]=value + if hasattr(value, 'explanation'): + value.explanation = ' '.join(value.explanation.strip().split()) +del name, value NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat -# patch our status map with subclasses -status_map[403] = HTTPForbidden -status_map[404] = HTTPNotFound - class PredicateMismatch(NotFound): """ Internal exception (not an API) raised by multiviews when no @@ -197,27 +1091,15 @@ def is_response(ob): return True return False -newstyle_exceptions = issubclass(Exception, object) +def default_exceptionresponse_view(context, request): + if not isinstance(context, Exception): + # backwards compat for an exception response view registered via + # config.set_notfound_view or config.set_forbidden_view + # instead of as a proper exception view + context = request.exception or context + # WSGIHTTPException, a Response (2.5+) + return context -if newstyle_exceptions: - # webob exceptions will be Response objects (Py 2.5+) - def default_exceptionresponse_view(context, request): - if not isinstance(context, Exception): - # backwards compat for an exception response view registered via - # config.set_notfound_view or config.set_forbidden_view - # instead of as a proper exception view - context = request.exception or context - # WSGIHTTPException, a Response (2.5+) - return context - -else: - # webob exceptions will not be Response objects (Py 2.4) - def default_exceptionresponse_view(context, request): - if not isinstance(context, Exception): - # backwards compat for an exception response view registered via - # config.set_notfound_view or config.set_forbidden_view - # instead of as a proper exception view - context = request.exception or context - # HTTPException, not a Response (2.4) - get_response = getattr(request, 'get_response', lambda c: c) # testing - return get_response(context) +__all__.extend(['NotFound', 'Forbidden', 'PredicateMismatch', 'URLDecodeError', + 'ConfigurationError', 'abort', 'redirect', 'is_response', + 'default_exceptionresponse_view']) diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 2e9279f66..3f303e3df 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -96,10 +96,301 @@ class Test_default_exceptionresponse_view(unittest.TestCase): result = self._callFUT(None, request) self.assertEqual(result, context) +class Test_no_escape(unittest.TestCase): + def _callFUT(self, val): + from pyramid.exceptions import no_escape + return no_escape(val) + + def test_null(self): + self.assertEqual(self._callFUT(None), '') + + def test_not_basestring(self): + self.assertEqual(self._callFUT(42), '42') + + def test_unicode(self): + class DummyUnicodeObject(object): + def __unicode__(self): + return u'42' + duo = DummyUnicodeObject() + self.assertEqual(self._callFUT(duo), u'42') + class DummyRequest(object): exception = None - def get_response(self, context): - return 'response' + +# from webob.request import Request +# from webob.exc import HTTPException +# from webob.exc import WSGIHTTPException +# from webob.exc import HTTPMethodNotAllowed +# from webob.exc import _HTTPMove +# from webob.exc import HTTPExceptionMiddleware + +# from nose.tools import eq_, ok_, assert_equal, assert_raises + +# def test_HTTPException(self): +# _called = [] +# _result = object() +# def _response(environ, start_response): +# _called.append((environ, start_response)) +# return _result +# environ = {} +# start_response = object() +# exc = HTTPException('testing', _response) +# ok_(exc.wsgi_response is _response) +# ok_(exc.exception is exc) +# result = exc(environ, start_response) +# ok_(result is result) +# assert_equal(_called, [(environ, start_response)]) + +# from webob.dec import wsgify +# @wsgify +# def method_not_allowed_app(req): +# if req.method != 'GET': +# raise HTTPMethodNotAllowed().exception +# return 'hello!' + + +# def test_exception_with_unicode_data(): +# req = Request.blank('/', method=u'POST') +# res = req.get_response(method_not_allowed_app) +# assert res.status_int == 405 + +# def test_WSGIHTTPException_headers(): +# exc = WSGIHTTPException(headers=[('Set-Cookie', 'a=1'), +# ('Set-Cookie', 'a=2')]) +# mixed = exc.headers.mixed() +# assert mixed['set-cookie'] == ['a=1', 'a=2'] + +# def test_WSGIHTTPException_w_body_template(): +# from string import Template +# TEMPLATE = '$foo: $bar' +# exc = WSGIHTTPException(body_template = TEMPLATE) +# assert_equal(exc.body_template, TEMPLATE) +# ok_(isinstance(exc.body_template_obj, Template)) +# eq_(exc.body_template_obj.substitute({'foo': 'FOO', 'bar': 'BAR'}), +# 'FOO: BAR') + +# def test_WSGIHTTPException_w_empty_body(): +# class EmptyOnly(WSGIHTTPException): +# empty_body = True +# exc = EmptyOnly(content_type='text/plain', content_length=234) +# ok_('content_type' not in exc.__dict__) +# ok_('content_length' not in exc.__dict__) + +# def test_WSGIHTTPException___str__(): +# exc1 = WSGIHTTPException(detail='Detail') +# eq_(str(exc1), 'Detail') +# class Explain(WSGIHTTPException): +# explanation = 'Explanation' +# eq_(str(Explain()), 'Explanation') + +# def test_WSGIHTTPException_plain_body_no_comment(): +# class Explain(WSGIHTTPException): +# code = '999' +# title = 'Testing' +# explanation = 'Explanation' +# exc = Explain(detail='Detail') +# eq_(exc.plain_body({}), +# '999 Testing\n\nExplanation\n\n Detail ') + +# def test_WSGIHTTPException_html_body_w_comment(): +# class Explain(WSGIHTTPException): +# code = '999' +# title = 'Testing' +# explanation = 'Explanation' +# exc = Explain(detail='Detail', comment='Comment') +# eq_(exc.html_body({}), +# '\n' +# ' \n' +# ' 999 Testing\n' +# ' \n' +# ' \n' +# '

999 Testing

\n' +# ' Explanation

\n' +# 'Detail\n' +# '\n\n' +# ' \n' +# '' +# ) + +# def test_WSGIHTTPException_generate_response(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'PUT', +# 'HTTP_ACCEPT': 'text/html' +# } +# excep = WSGIHTTPException() +# assert_equal( excep(environ,start_response), [ +# '\n' +# ' \n' +# ' None None\n' +# ' \n' +# ' \n' +# '

None None

\n' +# '

\n' +# '\n' +# '\n\n' +# ' \n' +# '' ] +# ) + +# def test_WSGIHTTPException_call_w_body(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'PUT' +# } +# excep = WSGIHTTPException() +# excep.body = 'test' +# assert_equal( excep(environ,start_response), ['test'] ) + + +# def test_WSGIHTTPException_wsgi_response(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# excep = WSGIHTTPException() +# assert_equal( excep.wsgi_response(environ,start_response), [] ) + +# def test_WSGIHTTPException_exception_newstyle(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# excep = WSGIHTTPException() +# exc.newstyle_exceptions = True +# assert_equal( excep.exception(environ,start_response), [] ) + +# def test_WSGIHTTPException_exception_no_newstyle(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# excep = WSGIHTTPException() +# exc.newstyle_exceptions = False +# assert_equal( excep.exception(environ,start_response), [] ) + +# def test_HTTPMove(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove() +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPMove_location_not_none(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove(location='http://example.com') +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPMove_add_slash_and_location(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# assert_raises( TypeError, _HTTPMove, location='http://example.com', add_slash=True ) + +# def test_HTTPMove_call_add_slash(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove() +# m.add_slash = True +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPMove_call_query_string(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove() +# m.add_slash = True +# environ[ 'QUERY_STRING' ] = 'querystring' +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPExceptionMiddleware_ok(): +# def app( environ, start_response ): +# return '123' +# application = app +# m = HTTPExceptionMiddleware(application) +# environ = {} +# start_response = None +# res = m( environ, start_response ) +# assert_equal( res, '123' ) - - +# def test_HTTPExceptionMiddleware_exception(): +# def wsgi_response( environ, start_response): +# return '123' +# def app( environ, start_response ): +# raise HTTPException( None, wsgi_response ) +# application = app +# m = HTTPExceptionMiddleware(application) +# environ = {} +# start_response = None +# res = m( environ, start_response ) +# assert_equal( res, '123' ) + +# def test_HTTPExceptionMiddleware_exception_exc_info_none(): +# class DummySys: +# def exc_info(self): +# return None +# def wsgi_response( environ, start_response): +# return start_response('200 OK', [], exc_info=None) +# def app( environ, start_response ): +# raise HTTPException( None, wsgi_response ) +# application = app +# m = HTTPExceptionMiddleware(application) +# environ = {} +# def start_response(status, headers, exc_info): +# pass +# try: +# from webob import exc +# old_sys = exc.sys +# sys = DummySys() +# res = m( environ, start_response ) +# assert_equal( res, None ) +# finally: +# exc.sys = old_sys -- cgit v1.2.3