diff options
| author | Chris McDonough <chrism@plope.com> | 2011-05-26 14:13:45 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-05-26 14:13:45 -0400 |
| commit | 8c2a9e6d8d4f089222db7b30324774e94279b0e4 (patch) | |
| tree | 4e92fbc6f359c2c1ca13bf5bdd52cb88e599ff62 | |
| parent | e1e0df9ff85d5d31b355527549e0b5654cce3af9 (diff) | |
| download | pyramid-8c2a9e6d8d4f089222db7b30324774e94279b0e4.tar.gz pyramid-8c2a9e6d8d4f089222db7b30324774e94279b0e4.tar.bz2 pyramid-8c2a9e6d8d4f089222db7b30324774e94279b0e4.zip | |
work towards unifying NotFound/HTTPNotFound and Forbidden/HTTPForbidden; 2 tests fail
| -rw-r--r-- | pyramid/config.py | 52 | ||||
| -rw-r--r-- | pyramid/exceptions.py | 215 | ||||
| -rw-r--r-- | pyramid/httpexceptions.py | 83 | ||||
| -rw-r--r-- | pyramid/router.py | 2 | ||||
| -rw-r--r-- | pyramid/testing.py | 2 | ||||
| -rw-r--r-- | pyramid/tests/forbiddenapp/__init__.py | 4 | ||||
| -rw-r--r-- | pyramid/tests/test_config.py | 45 | ||||
| -rw-r--r-- | pyramid/tests/test_exceptions.py | 114 | ||||
| -rw-r--r-- | pyramid/tests/test_httpexceptions.py | 80 | ||||
| -rw-r--r-- | pyramid/view.py | 31 |
10 files changed, 302 insertions, 326 deletions
diff --git a/pyramid/config.py b/pyramid/config.py index 85a66a837..a20f93a90 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -56,11 +56,10 @@ from pyramid.compat import md5 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 @@ -82,7 +81,6 @@ from pyramid.traversal import find_interface 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 @@ -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` @@ -259,15 +257,18 @@ class Configurator(object): 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 @@ -290,7 +291,7 @@ class Configurator(object): 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() @@ -316,7 +317,7 @@ class Configurator(object): 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): @@ -674,7 +675,7 @@ class Configurator(object): 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 @@ -704,11 +705,9 @@ class Configurator(object): 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) @@ -724,7 +723,7 @@ class Configurator(object): 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: @@ -2703,7 +2702,7 @@ class MultiView(object): 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) @@ -2722,7 +2721,7 @@ class MultiView(object): return view(context, request) except PredicateMismatch: continue - raise PredicateMismatch(self.name) + raise PredicateMismatch(self.name).exception def wraps_view(wrapped): def inner(self, view): @@ -2845,7 +2844,7 @@ class ViewDeriver(object): 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 @@ -2894,7 +2893,8 @@ class ViewDeriver(object): 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)) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 771d71b88..60e3c7b9b 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -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 @@ -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): """ @@ -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) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index cbd87520b..8b2a012cc 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -1,83 +1,2 @@ -from webob.exc import __doc__ -from webob.exc import status_map - -# 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 - -# 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 HTTPForbidden -from webob.exc import HTTPNotFound -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 - -from webob.response import Response - -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 default_httpexception_view(context, request): - if isinstance(context, Response): - # WSGIHTTPException, a Response (2.5+) - return context - # HTTPException, a WSGI app (2.4) - return request.get_response(context) +from pyramid.exceptions import * # bw compat diff --git a/pyramid/router.py b/pyramid/router.py index b8a8639aa..069db52bc 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -153,7 +153,7 @@ class Router(object): logger and logger.debug(msg) else: msg = request.path_info - raise NotFound(msg) + raise NotFound(msg).exception else: response = view_callable(context, request) diff --git a/pyramid/testing.py b/pyramid/testing.py index 36cc38830..a512ede4b 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -217,7 +217,7 @@ def registerView(name, result='', view=None, for_=(Interface, Interface), else: def _secure(context, request): if not has_permission(permission, context, request): - raise Forbidden('no permission') + raise Forbidden('no permission').exception else: return view(context, request) _secure.__call_permissive__ = view diff --git a/pyramid/tests/forbiddenapp/__init__.py b/pyramid/tests/forbiddenapp/__init__.py index ed9aa8357..614aff037 100644 --- a/pyramid/tests/forbiddenapp/__init__.py +++ b/pyramid/tests/forbiddenapp/__init__.py @@ -1,6 +1,4 @@ -from cgi import escape from webob import Response -from pyramid.httpexceptions import HTTPForbidden from pyramid.exceptions import Forbidden def x_view(request): # pragma: no cover @@ -10,7 +8,7 @@ def forbidden_view(context, request): msg = context.message result = context.result message = msg + '\n' + str(result) - resp = HTTPForbidden() + resp = Forbidden() resp.body = message return resp diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 9c8f4875b..7c6389253 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -204,36 +204,33 @@ class ConfiguratorTests(unittest.TestCase): mapper) def test_ctor_httpexception_view_default(self): - from zope.interface import implementedBy - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import default_httpexception_view + from pyramid.interfaces import IExceptionResponse + from pyramid.exceptions import default_exceptionresponse_view from pyramid.interfaces import IRequest config = self._makeOne() view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPException), + ctx_iface=IExceptionResponse, request_iface=IRequest) - self.failUnless(view is default_httpexception_view) + self.failUnless(view is default_exceptionresponse_view) - def test_ctor_httpexception_view_None(self): - from zope.interface import implementedBy - from pyramid.httpexceptions import HTTPException + def test_ctor_exceptionresponse_view_None(self): + from pyramid.interfaces import IExceptionResponse from pyramid.interfaces import IRequest - config = self._makeOne(httpexception_view=None) + config = self._makeOne(exceptionresponse_view=None) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPException), + ctx_iface=IExceptionResponse, request_iface=IRequest) self.failUnless(view is None) - def test_ctor_httpexception_view_custom(self): - from zope.interface import implementedBy - from pyramid.httpexceptions import HTTPException + def test_ctor_exceptionresponse_view_custom(self): + from pyramid.interfaces import IExceptionResponse from pyramid.interfaces import IRequest - def httpexception_view(context, request): pass - config = self._makeOne(httpexception_view=httpexception_view) + def exceptionresponse_view(context, request): pass + config = self._makeOne(exceptionresponse_view=exceptionresponse_view) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPException), + ctx_iface=IExceptionResponse, request_iface=IRequest) - self.failUnless(view is httpexception_view) + self.failUnless(view is exceptionresponse_view) def test_with_package_module(self): from pyramid.tests import test_configuration @@ -321,20 +318,6 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) - def test_setup_registry_registers_default_httpexception_view(self): - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import default_httpexception_view - class DummyRegistry(object): - def registerUtility(self, *arg, **kw): - pass - reg = DummyRegistry() - config = self._makeOne(reg) - views = [] - config.add_view = lambda *arg, **kw: views.append((arg, kw)) - config.setup_registry() - self.assertEqual(views[1], ((default_httpexception_view,), - {'context':HTTPException})) - def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 5d0fa1e1a..2e9279f66 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -1,37 +1,16 @@ import unittest -class TestExceptionResponse(unittest.TestCase): - def _makeOne(self, message): - from pyramid.exceptions import ExceptionResponse - return ExceptionResponse(message) - - def test_app_iter(self): - exc = self._makeOne('') - self.assertTrue('<code></code>' in exc.app_iter[0]) - - def test_headerlist(self): - exc = self._makeOne('') - headerlist = exc.headerlist - headerlist.sort() - app_iter = exc.app_iter - clen = str(len(app_iter[0])) - self.assertEqual(headerlist[0], ('Content-Length', clen)) - self.assertEqual(headerlist[1], ('Content-Type', 'text/html')) - - def test_withmessage(self): - exc = self._makeOne('abc&123') - self.assertTrue('<code>abc&123</code>' in exc.app_iter[0]) - class TestNotFound(unittest.TestCase): def _makeOne(self, message): from pyramid.exceptions import NotFound return NotFound(message) def test_it(self): - from pyramid.exceptions import ExceptionResponse + from pyramid.interfaces import IExceptionResponse e = self._makeOne('notfound') - self.assertTrue(isinstance(e, ExceptionResponse)) + self.assertTrue(IExceptionResponse.providedBy(e)) self.assertEqual(e.status, '404 Not Found') + self.assertEqual(e.message, 'notfound') class TestForbidden(unittest.TestCase): def _makeOne(self, message): @@ -39,7 +18,88 @@ class TestForbidden(unittest.TestCase): return Forbidden(message) def test_it(self): - from pyramid.exceptions import ExceptionResponse - e = self._makeOne('unauthorized') - self.assertTrue(isinstance(e, ExceptionResponse)) + from pyramid.interfaces import IExceptionResponse + e = self._makeOne('forbidden') + self.assertTrue(IExceptionResponse.providedBy(e)) self.assertEqual(e.status, '403 Forbidden') + self.assertEqual(e.message, 'forbidden') + +class Test_abort(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.exceptions import abort + return abort(*arg, **kw) + + def test_status_404(self): + from pyramid.exceptions import HTTPNotFound + self.assertRaises(HTTPNotFound().exception.__class__, + self._callFUT, 404) + + def test_status_201(self): + from pyramid.exceptions import HTTPCreated + self.assertRaises(HTTPCreated().exception.__class__, + self._callFUT, 201) + + def test_extra_kw(self): + from pyramid.exceptions import HTTPNotFound + try: + self._callFUT(404, headers=[('abc', 'def')]) + except HTTPNotFound().exception.__class__, exc: + self.assertEqual(exc.headers['abc'], 'def') + else: # pragma: no cover + raise AssertionError + +class Test_redirect(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.exceptions import redirect + return redirect(*arg, **kw) + + def test_default(self): + from pyramid.exceptions import HTTPFound + try: + self._callFUT('http://example.com') + except HTTPFound().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + + def test_custom_code(self): + from pyramid.exceptions import HTTPMovedPermanently + try: + self._callFUT('http://example.com', 301) + except HTTPMovedPermanently().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '301 Moved Permanently') + + def test_extra_kw(self): + from pyramid.exceptions import HTTPFound + try: + self._callFUT('http://example.com', headers=[('abc', 'def')]) + except HTTPFound().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + self.assertEqual(exc.headers['abc'], 'def') + + +class Test_default_exceptionresponse_view(unittest.TestCase): + def _callFUT(self, context, request): + from pyramid.exceptions import default_exceptionresponse_view + return default_exceptionresponse_view(context, request) + + def test_call_with_exception(self): + context = Exception() + result = self._callFUT(context, None) + self.assertEqual(result, context) + + def test_call_with_nonexception(self): + request = DummyRequest() + context = Exception() + request.exception = context + result = self._callFUT(None, request) + self.assertEqual(result, context) + +class DummyRequest(object): + exception = None + def get_response(self, context): + return 'response' + + + diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 843f9485a..afa5a94de 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -1,79 +1,9 @@ import unittest -class Test_abort(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.httpexceptions import abort - return abort(*arg, **kw) - - def test_status_404(self): - from pyramid.httpexceptions import HTTPNotFound - self.assertRaises(HTTPNotFound().exception.__class__, - self._callFUT, 404) - - def test_status_201(self): - from pyramid.httpexceptions import HTTPCreated - self.assertRaises(HTTPCreated().exception.__class__, - self._callFUT, 201) - - def test_extra_kw(self): - from pyramid.httpexceptions import HTTPNotFound - try: - self._callFUT(404, headers=[('abc', 'def')]) - except HTTPNotFound().exception.__class__, exc: - self.assertEqual(exc.headers['abc'], 'def') - else: # pragma: no cover - raise AssertionError - -class Test_redirect(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.httpexceptions import redirect - return redirect(*arg, **kw) - - def test_default(self): - from pyramid.httpexceptions import HTTPFound - try: - self._callFUT('http://example.com') - except HTTPFound().exception.__class__, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - - def test_custom_code(self): - from pyramid.httpexceptions import HTTPMovedPermanently - try: - self._callFUT('http://example.com', 301) - except HTTPMovedPermanently().exception.__class__, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '301 Moved Permanently') +class TestIt(unittest.TestCase): + def test_bwcompat_imports(self): + from pyramid.httpexceptions import HTTPNotFound as one + from pyramid.exceptions import HTTPNotFound as two + self.assertTrue(one is two) - def test_extra_kw(self): - from pyramid.httpexceptions import HTTPFound - try: - self._callFUT('http://example.com', headers=[('abc', 'def')]) - except HTTPFound().exception.__class__, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - self.assertEqual(exc.headers['abc'], 'def') - - -class Test_default_httpexception_view(unittest.TestCase): - def _callFUT(self, context, request): - from pyramid.httpexceptions import default_httpexception_view - return default_httpexception_view(context, request) - - def test_call_with_response(self): - from pyramid.response import Response - r = Response() - result = self._callFUT(r, None) - self.assertEqual(result, r) - def test_call_with_nonresponse(self): - request = DummyRequest() - result = self._callFUT(None, request) - self.assertEqual(result, 'response') - -class DummyRequest(object): - def get_response(self, context): - return 'response' - - - diff --git a/pyramid/view.py b/pyramid/view.py index 2563f1e43..d6b666cf2 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -8,7 +8,9 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.httpexceptions import HTTPFound +from pyramid.exceptions import HTTPFound +from pyramid.exceptions import default_exceptionresponse_view +from pyramid.exceptions import is_response # API from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -130,21 +132,6 @@ def render_view(context, request, name='', secure=True): return None return ''.join(iterable) -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 - class view_config(object): """ A function, class or method :term:`decorator` which allows a developer to create view registrations nearer to a :term:`view @@ -243,14 +230,6 @@ deprecated( 'pyramid.view.view_config instead (API-compat, simple ' 'rename).') -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 - return context - class AppendSlashNotFoundViewFactory(object): """ There can only be one :term:`Not Found view` in any :app:`Pyramid` application. Even if you use @@ -273,7 +252,7 @@ class AppendSlashNotFoundViewFactory(object): from pyramid.exceptions import NotFound from pyramid.view import AppendSlashNotFoundViewFactory - from pyramid.httpexceptions import HTTPNotFound + from pyramid.exceptions import HTTPNotFound def notfound_view(context, request): return HTTPNotFound('nope') @@ -294,7 +273,7 @@ class AppendSlashNotFoundViewFactory(object): if not isinstance(context, Exception): # backwards compat for an append_notslash_view registered via # config.set_notfound_view instead of as a proper exception view - context = request.exception + context = getattr(request, 'exception', None) or context path = request.path registry = request.registry mapper = registry.queryUtility(IRoutesMapper) |
