Skip to content

Commit

Permalink
work towards unifying NotFound/HTTPNotFound and Forbidden/HTTPForbidd…
Browse files Browse the repository at this point in the history
…en; 2 tests fail
  • Loading branch information
mcdonc committed May 26, 2011
1 parent e1e0df9 commit 8c2a9e6
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 326 deletions.
52 changes: 26 additions & 26 deletions pyramid/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,10 @@
from pyramid.compat import any
from pyramid.events import ApplicationCreated
from pyramid.exceptions import ConfigurationError
from pyramid.exceptions import default_exceptionresponse_view
from pyramid.exceptions import Forbidden
from pyramid.exceptions import NotFound
from pyramid.exceptions import PredicateMismatch
from pyramid.httpexceptions import HTTPException
from pyramid.httpexceptions import default_httpexception_view
from pyramid.i18n import get_localizer
from pyramid.log import make_stream_logger
from pyramid.mako_templating import renderer_factory as mako_renderer_factory
Expand All @@ -82,7 +81,6 @@
from pyramid.traversal import traversal_path
from pyramid.urldispatch import RoutesMapper
from pyramid.util import DottedNameResolver
from pyramid.view import default_exceptionresponse_view
from pyramid.view import render_view_to_response
from pyramid.view import is_response

Expand Down Expand Up @@ -142,7 +140,7 @@ class Configurator(object):
``authorization_policy``, ``renderers`` ``debug_logger``,
``locale_negotiator``, ``request_factory``, ``renderer_globals_factory``,
``default_permission``, ``session_factory``, ``default_view_mapper``,
``autocommit``, and ``httpexception_view``.
``autocommit``, and ``exceptionresponse_view``.
If the ``registry`` argument is passed as a non-``None`` value, it
must be an instance of the :class:`pyramid.registry.Registry`
Expand Down Expand Up @@ -259,15 +257,18 @@ class or a :term:`dotted Python name` to same. The debug logger
default_view_mapper is not passed, a superdefault view mapper will be
used.
If ``httpexception_view`` is passed, it must be a :term:`view callable`
or ``None``. If it is a view callable, it will be used as an exception
view callable when an :term:`HTTP exception` is raised (any named
exception from the ``pyramid.httpexceptions`` module) by
:func:`pyramid.httpexceptions.abort`,
:func:`pyramid.httpexceptions.redirect` or 'by hand'. If it is ``None``,
no httpexception view will be registered. By default, the
``pyramid.httpexceptions.default_httpexception_view`` function is
used. This behavior is new in Pyramid 1.1. """
If ``exceptionresponse_view`` is passed, it must be a :term:`view
callable` or ``None``. If it is a view callable, it will be used as an
exception view callable when an :term:`exception response` is raised (any
named exception from the ``pyramid.exceptions`` module that begins with
``HTTP`` as well as the ``NotFound`` and ``Forbidden`` exceptions) as
well as exceptions raised via :func:`pyramid.exceptions.abort`,
:func:`pyramid.exceptions.redirect`. If ``exceptionresponse_view`` is
``None``, no exception response view will be registered, and all
raised exception responses will be bubbled up to Pyramid's caller. By
default, the ``pyramid.exceptions.default_exceptionresponse_view``
function is used as the ``exceptionresponse_view``. This argument is new
in Pyramid 1.1. """

manager = manager # for testing injection
venusian = venusian # for testing injection
Expand All @@ -290,7 +291,7 @@ def __init__(self,
session_factory=None,
default_view_mapper=None,
autocommit=False,
httpexception_view=default_httpexception_view,
exceptionresponse_view=default_exceptionresponse_view,
):
if package is None:
package = caller_package()
Expand All @@ -316,7 +317,7 @@ def __init__(self,
default_permission=default_permission,
session_factory=session_factory,
default_view_mapper=default_view_mapper,
httpexception_view=httpexception_view,
exceptionresponse_view=exceptionresponse_view,
)

def _set_settings(self, mapping):
Expand Down Expand Up @@ -674,7 +675,7 @@ def setup_registry(self, settings=None, root_factory=None,
locale_negotiator=None, request_factory=None,
renderer_globals_factory=None, default_permission=None,
session_factory=None, default_view_mapper=None,
httpexception_view=default_httpexception_view):
exceptionresponse_view=default_exceptionresponse_view):
""" When you pass a non-``None`` ``registry`` argument to the
:term:`Configurator` constructor, no initial 'setup' is performed
against the registry. This is because the registry you pass in may
Expand Down Expand Up @@ -704,11 +705,9 @@ def setup_registry(self, settings=None, root_factory=None,
authorization_policy)
for name, renderer in renderers:
self.add_renderer(name, renderer)
self.add_view(default_exceptionresponse_view,
context=IExceptionResponse)
if httpexception_view is not None:
httpexception_view = self.maybe_dotted(httpexception_view)
self.add_view(httpexception_view, context=HTTPException)
if exceptionresponse_view is not None:
exceptionresponse_view = self.maybe_dotted(exceptionresponse_view)
self.add_view(exceptionresponse_view, context=IExceptionResponse)
if locale_negotiator:
locale_negotiator = self.maybe_dotted(locale_negotiator)
registry.registerUtility(locale_negotiator, ILocaleNegotiator)
Expand All @@ -724,7 +723,7 @@ def setup_registry(self, settings=None, root_factory=None,
if session_factory is not None:
self.set_session_factory(session_factory)
# commit before adding default_view_mapper, as the
# default_exceptionresponse_view above requires the superdefault view
# exceptionresponse_view above requires the superdefault view
# mapper
self.commit()
if default_view_mapper is not None:
Expand Down Expand Up @@ -2703,7 +2702,7 @@ def match(self, context, request):
return view
if view.__predicated__(context, request):
return view
raise PredicateMismatch(self.name)
raise PredicateMismatch(self.name).exception

def __permitted__(self, context, request):
view = self.match(context, request)
Expand All @@ -2722,7 +2721,7 @@ def __call__(self, context, request):
return view(context, request)
except PredicateMismatch:
continue
raise PredicateMismatch(self.name)
raise PredicateMismatch(self.name).exception

def wraps_view(wrapped):
def inner(self, view):
Expand Down Expand Up @@ -2845,7 +2844,7 @@ def _secured_view(context, request):
return view(context, request)
msg = getattr(request, 'authdebug_message',
'Unauthorized: %s failed permission check' % view)
raise Forbidden(msg, result)
raise Forbidden(msg, result=result).exception
_secured_view.__call_permissive__ = view
_secured_view.__permitted__ = _permitted
_secured_view.__permission__ = permission
Expand Down Expand Up @@ -2894,7 +2893,8 @@ def predicated_view(self, view):
def predicate_wrapper(context, request):
if all((predicate(context, request) for predicate in predicates)):
return view(context, request)
raise PredicateMismatch('predicate mismatch for view %s' % view)
raise PredicateMismatch(
'predicate mismatch for view %s' % view).exception
def checker(context, request):
return all((predicate(context, request) for predicate in
predicates))
Expand Down
215 changes: 161 additions & 54 deletions pyramid/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,91 @@
from zope.configuration.exceptions import ConfigurationError as ZCE
from zope.interface import implements

from pyramid.decorator import reify
from zope.interface import classImplements
from pyramid.interfaces import IExceptionResponse
import cgi

class ExceptionResponse(Exception):
""" Abstract class to support behaving as a WSGI response object """
implements(IExceptionResponse)
status = None

def __init__(self, message=''):
Exception.__init__(self, message) # B / C
self.message = message

@reify # defer execution until asked explicitly
def app_iter(self):
return [
"""
<html>
<title>%s</title>
<body>
<h1>%s</h1>
<code>%s</code>
</body>
</html>
""" % (self.status, self.status, cgi.escape(self.message))
]

@reify # defer execution until asked explicitly
def headerlist(self):
return [
('Content-Length', str(len(self.app_iter[0]))),
('Content-Type', 'text/html')
]


class Forbidden(ExceptionResponse):
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`.
"""
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)

class HTTPForbidden(_HTTPForbidden):
"""
Raise this exception within :term:`view` code to immediately return the
:term:`forbidden view` to the invoking user. Usually this is a basic
Expand All @@ -58,25 +107,20 @@ class Forbidden(ExceptionResponse):
exception as necessary to provide extended information in an error
report shown to a user.
"""
status = '403 Forbidden'
def __init__(self, message='', result=None):
ExceptionResponse.__init__(self, message)
self.message = message
self.result = result
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)

class NotFound(ExceptionResponse):
"""
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`.
NotFound = HTTPNotFound # bw compat
Forbidden = HTTPForbidden # bw compat

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`.
"""
status = '404 Not Found'
# patch our status map with subclasses
status_map[403] = HTTPForbidden
status_map[404] = HTTPNotFound

class PredicateMismatch(NotFound):
"""
Expand All @@ -102,3 +146,66 @@ class ConfigurationError(ZCE):
""" Raised when inappropriate input values are supplied to an API
method of a :term:`Configurator`"""


def abort(status_code, **kw):
"""Aborts the request immediately by raising an HTTP exception. The
values in ``*kw`` will be passed to the HTTP exception constructor.
Example::
abort(404) # raises an HTTPNotFound exception.
"""
exc = status_map[status_code](**kw)
raise exc.exception


def redirect(url, code=302, **kw):
"""Raises a redirect exception to the specified URL.
Optionally, a code variable may be passed with the status code of
the redirect, ie::
redirect(route_url('foo', request), code=303)
"""
exc = status_map[code]
raise exc(location=url, **kw).exception

def is_response(ob):
""" Return ``True`` if ``ob`` implements the interface implied by
:ref:`the_response`. ``False`` if not.
.. note:: This isn't a true interface or subclass check. Instead, it's a
duck-typing check, as response objects are not obligated to be of a
particular class or provide any particular Zope interface."""

# response objects aren't obligated to implement a Zope interface,
# so we do it the hard way
if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and
hasattr(ob, 'status') ):
return True
return False

newstyle_exceptions = issubclass(Exception, object)

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)
Loading

0 comments on commit 8c2a9e6

Please sign in to comment.