From 53142852745ddd5668d11801a179b03e343554c4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:25:53 -0500 Subject: check in sketch code so raydeo can look at it --- pyramid/view.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyramid/view.py b/pyramid/view.py index 7e8996ca4..cedcfaef1 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -10,6 +10,7 @@ from pyramid.interfaces import ( IView, IViewClassifier, IRequest, + IExceptionViewClassifier, ) from pyramid.compat import decode_path_info @@ -547,3 +548,32 @@ def _call_view( raise pme return response + +class ViewMethodsMixin(object): + """ Request methods mixin for BaseRequest having to do with executing + views """ + def invoke_exception_view( + self, + exc, + request=None, + secure=True + ): + if request is None: + request = self + registry = getattr(request, 'registry', None) + if registry is None: + registry = get_current_registry() + context_iface = providedBy(exc) + view_name = getattr(request, 'view_name', '') + response = _call_view( + registry, + request, + exc, + context_iface, + view_name, + view_types=None, + view_classifier=IExceptionViewClassifier, + secure=secure, + request_iface=None, + ) + return response -- cgit v1.2.3 From e40ef23c9c364249318e030e5f1393a9fff17cdd Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:40:58 -0500 Subject: use exc_info instead of exc, add better docstring, mix the mixin in --- pyramid/request.py | 2 ++ pyramid/view.py | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/pyramid/request.py b/pyramid/request.py index 45d936cef..c1c1da514 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -32,6 +32,7 @@ from pyramid.util import ( InstancePropertyHelper, InstancePropertyMixin, ) +from pyramid.view import ViewMethodsMixin class TemplateContext(object): pass @@ -154,6 +155,7 @@ class Request( LocalizerRequestMixin, AuthenticationAPIMixin, AuthorizationAPIMixin, + ViewMethodsMixin, ): """ A subclass of the :term:`WebOb` Request class. An instance of diff --git a/pyramid/view.py b/pyramid/view.py index cedcfaef1..b09c08ccc 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -1,4 +1,5 @@ import itertools +import traceback import venusian from zope.interface import providedBy @@ -554,21 +555,52 @@ class ViewMethodsMixin(object): views """ def invoke_exception_view( self, - exc, + exc_info=None, request=None, secure=True ): + """ Executes an exception view related to the request it's called upon. + The arguments it takes are these: + + ``exc_info`` + + If provided, should be a 3-tuple in the form provided by + ``traceback.exc_info()``. If not provided, + ``traceback.exc_info()`` will be called to obtain the current + interpreter exception information. Default: ``None``. + + ``request`` + + If the request to be used is not the same one as the instance that + this method is called upon, it may be passed here. Default: + ``None``. + + ``secure`` + + If the exception view should not be rendered if the current user + does not have the appropriate permission, this should be ``True``. + Default: ``True``. + + If called with no arguments, it uses the global exception information + returned by ``traceback.exc_info()`` as ``exc_info``, the request + object that the method is a member of as the ``request``, and + ``secure`` is ``True``. + + This method returns a :term:`response` object.""" + if request is None: request = self registry = getattr(request, 'registry', None) if registry is None: registry = get_current_registry() - context_iface = providedBy(exc) + if exc_info is None: + exc_info = traceback.exc_info() + context_iface = providedBy(exc_info[0]) view_name = getattr(request, 'view_name', '') response = _call_view( registry, request, - exc, + exc_info[0], context_iface, view_name, view_types=None, -- cgit v1.2.3 From ff3dd9e9431035b479136abcc761a8f20341e3e2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:42:43 -0500 Subject: add request.exception and request.exc_info --- pyramid/view.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index b09c08ccc..0f1a5e41c 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -583,8 +583,8 @@ class ViewMethodsMixin(object): If called with no arguments, it uses the global exception information returned by ``traceback.exc_info()`` as ``exc_info``, the request - object that the method is a member of as the ``request``, and - ``secure`` is ``True``. + object that this method is attached to as the ``request``, and + ``True`` for ``secure``. This method returns a :term:`response` object.""" @@ -597,6 +597,8 @@ class ViewMethodsMixin(object): exc_info = traceback.exc_info() context_iface = providedBy(exc_info[0]) view_name = getattr(request, 'view_name', '') + request.exception = exc_info[0] + request.exc_info = exc_info response = _call_view( registry, request, -- cgit v1.2.3 From 7d8175fe8f893668f6061e2a26f9c673c2f00ccc Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:47:39 -0500 Subject: docs about what happens when no excview can be found --- pyramid/view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 0f1a5e41c..edf3d8585 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -586,7 +586,8 @@ class ViewMethodsMixin(object): object that this method is attached to as the ``request``, and ``True`` for ``secure``. - This method returns a :term:`response` object.""" + This method returns a :term:`response` object or ``None`` if no + matching exception view can be found..""" if request is None: request = self -- cgit v1.2.3 From ca529f0d7d3870d851181375f6a15805cef208b5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:51:35 -0500 Subject: bring into line with excview --- pyramid/view.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index edf3d8585..2555cbe30 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -596,10 +596,17 @@ class ViewMethodsMixin(object): registry = get_current_registry() if exc_info is None: exc_info = traceback.exc_info() + attrs = request.__dict__ context_iface = providedBy(exc_info[0]) - view_name = getattr(request, 'view_name', '') - request.exception = exc_info[0] - request.exc_info = exc_info + view_name = attrs.get('view_name', '') + # probably need something like "with temporarily_munged_request(req)" + # here, which adds exception and exc_info as request attrs, and + # removes response object temporarily (as per the excview tween) + attrs['exception'] = exc_info[0] + attrs['exc_info'] = request.exc_info = exc_info + # we use .get instead of .__getitem__ below due to + # https://github.com/Pylons/pyramid/issues/700 + request_iface = attrs.get('request_iface', IRequest) response = _call_view( registry, request, @@ -609,6 +616,6 @@ class ViewMethodsMixin(object): view_types=None, view_classifier=IExceptionViewClassifier, secure=secure, - request_iface=None, + request_iface=request_iface.combined, ) return response -- cgit v1.2.3 From 22bf919e5eaa21bf7d826bb4143958d7d20e0469 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:52:43 -0500 Subject: dont set it twice --- pyramid/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 2555cbe30..242a1b7de 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -603,7 +603,7 @@ class ViewMethodsMixin(object): # here, which adds exception and exc_info as request attrs, and # removes response object temporarily (as per the excview tween) attrs['exception'] = exc_info[0] - attrs['exc_info'] = request.exc_info = exc_info + attrs['exc_info'] = exc_info # we use .get instead of .__getitem__ below due to # https://github.com/Pylons/pyramid/issues/700 request_iface = attrs.get('request_iface', IRequest) -- cgit v1.2.3 From 252fa52ee7628253dfec7636f4e55b8124efea2a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:53:41 -0500 Subject: its sys, not traceback --- pyramid/view.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index 242a1b7de..1f20622dc 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -1,5 +1,6 @@ import itertools -import traceback +import sys + import venusian from zope.interface import providedBy @@ -565,8 +566,8 @@ class ViewMethodsMixin(object): ``exc_info`` If provided, should be a 3-tuple in the form provided by - ``traceback.exc_info()``. If not provided, - ``traceback.exc_info()`` will be called to obtain the current + ``sys.exc_info()``. If not provided, + ``sys.exc_info()`` will be called to obtain the current interpreter exception information. Default: ``None``. ``request`` @@ -582,7 +583,7 @@ class ViewMethodsMixin(object): Default: ``True``. If called with no arguments, it uses the global exception information - returned by ``traceback.exc_info()`` as ``exc_info``, the request + returned by ``sys.exc_info()`` as ``exc_info``, the request object that this method is attached to as the ``request``, and ``True`` for ``secure``. @@ -595,7 +596,7 @@ class ViewMethodsMixin(object): if registry is None: registry = get_current_registry() if exc_info is None: - exc_info = traceback.exc_info() + exc_info = sys.exc_info() attrs = request.__dict__ context_iface = providedBy(exc_info[0]) view_name = attrs.get('view_name', '') -- cgit v1.2.3 From 19016b0e5ab6c66a4b5eea01ab7a53e34145fbe6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 22 Feb 2016 23:48:55 -0600 Subject: move p.response.temporary_response context manager to p.util.hide_attrs --- pyramid/renderers.py | 23 +++-------------- pyramid/tests/test_renderers.py | 42 ------------------------------ pyramid/tests/test_util.py | 57 +++++++++++++++++++++++++++++++++++++++++ pyramid/util.py | 20 +++++++++++++++ pyramid/view.py | 50 +++++++++++++++++++++--------------- 5 files changed, 110 insertions(+), 82 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 456b16c82..bcbcbb0aa 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -1,4 +1,3 @@ -import contextlib import json import os import re @@ -30,6 +29,7 @@ from pyramid.path import caller_package from pyramid.response import _get_response_factory from pyramid.threadlocal import get_current_registry +from pyramid.util import hide_attrs # API @@ -77,7 +77,7 @@ def render(renderer_name, value, request=None, package=None): helper = RendererHelper(name=renderer_name, package=package, registry=registry) - with temporary_response(request): + with hide_attrs(request, 'response'): result = helper.render(value, None, request=request) return result @@ -138,30 +138,13 @@ def render_to_response(renderer_name, helper = RendererHelper(name=renderer_name, package=package, registry=registry) - with temporary_response(request): + with hide_attrs(request, 'response'): if response is not None: request.response = response result = helper.render_to_response(value, None, request=request) return result -_marker = object() - -@contextlib.contextmanager -def temporary_response(request): - """ - Temporarily delete request.response and restore it afterward. - """ - attrs = request.__dict__ if request is not None else {} - saved_response = attrs.pop('response', _marker) - try: - yield - finally: - if saved_response is not _marker: - attrs['response'] = saved_response - elif 'response' in attrs: - del attrs['response'] - def get_renderer(renderer_name, package=None): """ Return the renderer object for the renderer ``renderer_name``. diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 2458ea830..65bfa5582 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -592,48 +592,6 @@ class Test_render_to_response(unittest.TestCase): self.assertEqual(result.body, b'{"a": 1}') self.assertFalse('response' in request.__dict__) -class Test_temporary_response(unittest.TestCase): - def _callFUT(self, request): - from pyramid.renderers import temporary_response - return temporary_response(request) - - def test_restores_response(self): - request = testing.DummyRequest() - orig_response = request.response - with self._callFUT(request): - request.response = object() - self.assertEqual(request.response, orig_response) - - def test_restores_response_on_exception(self): - request = testing.DummyRequest() - orig_response = request.response - try: - with self._callFUT(request): - request.response = object() - raise RuntimeError() - except RuntimeError: - self.assertEqual(request.response, orig_response) - else: # pragma: no cover - self.fail("RuntimeError not raised") - - def test_restores_response_to_none(self): - request = testing.DummyRequest(response=None) - with self._callFUT(request): - request.response = object() - self.assertEqual(request.response, None) - - def test_deletes_response(self): - request = testing.DummyRequest() - with self._callFUT(request): - request.response = object() - self.assertTrue('response' not in request.__dict__) - - def test_does_not_delete_response_if_no_response_to_delete(self): - request = testing.DummyRequest() - with self._callFUT(request): - pass - self.assertTrue('response' not in request.__dict__) - class Test_get_renderer(unittest.TestCase): def setUp(self): self.config = testing.setUp() diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 0be99e949..c606a4b6b 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -794,6 +794,63 @@ class TestCallableName(unittest.TestCase): self.assertRaises(ConfigurationError, get_bad_name) +class Test_hide_attrs(unittest.TestCase): + def _callFUT(self, obj, *attrs): + from pyramid.util import hide_attrs + return hide_attrs(obj, *attrs) + + def _makeDummy(self): + from pyramid.decorator import reify + class Dummy(object): + x = 1 + + @reify + def foo(self): + return self.x + return Dummy() + + def test_restores_attrs(self): + obj = self._makeDummy() + obj.bar = 'asdf' + orig_foo = obj.foo + with self._callFUT(obj, 'foo', 'bar'): + obj.foo = object() + obj.bar = 'nope' + self.assertEqual(obj.foo, orig_foo) + self.assertEqual(obj.bar, 'asdf') + + def test_restores_attrs_on_exception(self): + obj = self._makeDummy() + orig_foo = obj.foo + try: + with self._callFUT(obj, 'foo'): + obj.foo = object() + raise RuntimeError() + except RuntimeError: + self.assertEqual(obj.foo, orig_foo) + else: # pragma: no cover + self.fail("RuntimeError not raised") + + def test_restores_attrs_to_none(self): + obj = self._makeDummy() + obj.foo = None + with self._callFUT(obj, 'foo'): + obj.foo = object() + self.assertEqual(obj.foo, None) + + def test_deletes_attrs(self): + obj = self._makeDummy() + with self._callFUT(obj, 'foo'): + obj.foo = object() + self.assertTrue('foo' not in obj.__dict__) + + def test_does_not_delete_attr_if_no_attr_to_delete(self): + obj = self._makeDummy() + with self._callFUT(obj, 'foo'): + pass + self.assertTrue('foo' not in obj.__dict__) + + def dummyfunc(): pass diff --git a/pyramid/util.py b/pyramid/util.py index 0a73cedaf..e1113e0ec 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -1,3 +1,4 @@ +import contextlib import functools try: # py2.7.7+ and py3.3+ have native comparison support @@ -591,3 +592,22 @@ def get_callable_name(name): 'used on __name__ of the method' ) raise ConfigurationError(msg % name) + +@contextlib.contextmanager +def hide_attrs(obj, *attrs): + """ + Temporarily delete object attrs and restore afterward. + """ + obj_vals = obj.__dict__ if obj is not None else {} + saved_vals = {} + for name in attrs: + saved_vals[name] = obj_vals.pop(name, _marker) + try: + yield + finally: + for name in attrs: + saved_val = saved_vals[name] + if saved_val is not _marker: + obj_vals[name] = saved_val + elif name in obj_vals: + del obj_vals[name] diff --git a/pyramid/view.py b/pyramid/view.py index 1f20622dc..2966b0a50 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -25,6 +25,7 @@ from pyramid.httpexceptions import ( ) from pyramid.threadlocal import get_current_registry +from pyramid.util import hide_attrs _marker = object() @@ -600,23 +601,32 @@ class ViewMethodsMixin(object): attrs = request.__dict__ context_iface = providedBy(exc_info[0]) view_name = attrs.get('view_name', '') - # probably need something like "with temporarily_munged_request(req)" - # here, which adds exception and exc_info as request attrs, and - # removes response object temporarily (as per the excview tween) - attrs['exception'] = exc_info[0] - attrs['exc_info'] = exc_info - # we use .get instead of .__getitem__ below due to - # https://github.com/Pylons/pyramid/issues/700 - request_iface = attrs.get('request_iface', IRequest) - response = _call_view( - registry, - request, - exc_info[0], - context_iface, - view_name, - view_types=None, - view_classifier=IExceptionViewClassifier, - secure=secure, - request_iface=request_iface.combined, - ) - return response + + # WARNING: do not assign the result of sys.exc_info() to a local + # var here, doing so will cause a leak. We used to actually + # explicitly delete both "exception" and "exc_info" from ``attrs`` + # in a ``finally:`` clause below, but now we do not because these + # attributes are useful to upstream tweens. This actually still + # apparently causes a reference cycle, but it is broken + # successfully by the garbage collector (see + # https://github.com/Pylons/pyramid/issues/1223). + + # clear old generated request.response, if any; it may + # have been mutated by the view, and its state is not + # sane (e.g. caching headers) + with hide_attrs(request, 'exception', 'exc_info', 'response'): + # we use .get instead of .__getitem__ below due to + # https://github.com/Pylons/pyramid/issues/700 + request_iface = attrs.get('request_iface', IRequest) + response = _call_view( + registry, + request, + exc_info[0], + context_iface, + view_name, + view_types=None, + view_classifier=IExceptionViewClassifier, + secure=secure, + request_iface=request_iface.combined, + ) + return response -- cgit v1.2.3 From 7f1174bc5aafff4050ba0863629401c7641588b2 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 23 Feb 2016 00:07:04 -0600 Subject: add methods to DummyRequest --- pyramid/testing.py | 2 ++ pyramid/view.py | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pyramid/testing.py b/pyramid/testing.py index 14432b01f..3cb5d17b9 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -41,6 +41,7 @@ from pyramid.i18n import LocalizerRequestMixin from pyramid.request import CallbackMethodsMixin from pyramid.url import URLMethodsMixin from pyramid.util import InstancePropertyMixin +from pyramid.view import ViewMethodsMixin _marker = object() @@ -293,6 +294,7 @@ class DummyRequest( LocalizerRequestMixin, AuthenticationAPIMixin, AuthorizationAPIMixin, + ViewMethodsMixin, ): """ A DummyRequest object (incompletely) imitates a :term:`request` object. diff --git a/pyramid/view.py b/pyramid/view.py index 2966b0a50..822fb29d6 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -602,15 +602,6 @@ class ViewMethodsMixin(object): context_iface = providedBy(exc_info[0]) view_name = attrs.get('view_name', '') - # WARNING: do not assign the result of sys.exc_info() to a local - # var here, doing so will cause a leak. We used to actually - # explicitly delete both "exception" and "exc_info" from ``attrs`` - # in a ``finally:`` clause below, but now we do not because these - # attributes are useful to upstream tweens. This actually still - # apparently causes a reference cycle, but it is broken - # successfully by the garbage collector (see - # https://github.com/Pylons/pyramid/issues/1223). - # clear old generated request.response, if any; it may # have been mutated by the view, and its state is not # sane (e.g. caching headers) -- cgit v1.2.3 From 68b303afa462db0cdfb903e02e65813a08f2e0cf Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 29 Feb 2016 21:20:35 -0600 Subject: add exception and exc_info to request --- pyramid/view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyramid/view.py b/pyramid/view.py index 822fb29d6..589b13578 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -606,6 +606,8 @@ class ViewMethodsMixin(object): # have been mutated by the view, and its state is not # sane (e.g. caching headers) with hide_attrs(request, 'exception', 'exc_info', 'response'): + attrs['exception'] = exc_info[0] + attrs['exc_info'] = exc_info # we use .get instead of .__getitem__ below due to # https://github.com/Pylons/pyramid/issues/700 request_iface = attrs.get('request_iface', IRequest) -- cgit v1.2.3 From 4ff751448a205d14ad03eeab205e51a17a0c05f9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 29 Feb 2016 21:21:46 -0600 Subject: exception views should never have a name= --- pyramid/view.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index 589b13578..16b60b77a 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -600,7 +600,6 @@ class ViewMethodsMixin(object): exc_info = sys.exc_info() attrs = request.__dict__ context_iface = providedBy(exc_info[0]) - view_name = attrs.get('view_name', '') # clear old generated request.response, if any; it may # have been mutated by the view, and its state is not @@ -616,7 +615,7 @@ class ViewMethodsMixin(object): request, exc_info[0], context_iface, - view_name, + '', view_types=None, view_classifier=IExceptionViewClassifier, secure=secure, -- cgit v1.2.3 From d5225795b3b98b8ef746ba5f9a807eb701d099bb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 2 Mar 2016 23:46:37 -0600 Subject: fix and add tests for invoke_exception_view --- pyramid/tests/test_view.py | 132 +++++++++++++++++++++++++++++++++++++++++++++ pyramid/view.py | 7 +-- 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index e6b9f9e7e..2be47e318 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -673,6 +673,138 @@ class Test_view_defaults(unittest.TestCase): class Bar(Foo): pass self.assertEqual(Bar.__view_defaults__, {}) +class TestViewMethodsMixin(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self, environ=None): + from pyramid.decorator import reify + from pyramid.view import ViewMethodsMixin + if environ is None: + environ = {} + class Request(ViewMethodsMixin): + def __init__(self, environ): + self.environ = environ + + @reify + def response(self): + return DummyResponse() + request = Request(environ) + request.registry = self.config.registry + return request + + def test_it(self): + def exc_view(exc, request): + self.assertTrue(exc is dummy_exc) + self.assertTrue(request.exception is dummy_exc) + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + dummy_exc = RuntimeError() + try: + raise dummy_exc + except RuntimeError: + response = request.invoke_exception_view() + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_hides_attrs(self): + def exc_view(exc, request): + self.assertTrue(exc is not orig_exc) + self.assertTrue(request.exception is not orig_exc) + self.assertTrue(request.exc_info is not orig_exc_info) + self.assertTrue(request.response is not orig_response) + request.response.app_iter = [b'bar'] + return request.response + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + orig_exc = request.exception = DummyContext() + orig_exc_info = request.exc_info = DummyContext() + orig_response = request.response = DummyResponse(b'foo') + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view() + self.assertEqual(response.app_iter, [b'bar']) + self.assertTrue(request.exception is orig_exc) + self.assertTrue(request.exc_info is orig_exc_info) + self.assertTrue(request.response is orig_response) + else: # pragma: no cover + self.fail() + + def test_it_supports_alternate_requests(self): + def exc_view(exc, request): + self.assertTrue(request is other_req) + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + other_req = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view(request=other_req) + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_supports_threadlocal_registry(self): + def exc_view(exc, request): + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + del request.registry + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view() + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_supports_alternate_exc_info(self): + def exc_view(exc, request): + self.assertTrue(request.exc_info is exc_info) + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + exc_info = sys.exc_info() + response = request.invoke_exception_view(exc_info=exc_info) + self.assertEqual(response.app_iter, [b'foo']) + + def test_it_rejects_secured_view(self): + from pyramid.exceptions import Forbidden + def exc_view(exc, request): pass + self.config.testing_securitypolicy(permissive=False) + self.config.add_view(exc_view, context=RuntimeError, permission='view') + request = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + self.assertRaises(Forbidden, request.invoke_exception_view) + else: # pragma: no cover + self.fail() + + def test_it_allows_secured_view(self): + def exc_view(exc, request): + return DummyResponse(b'foo') + self.config.testing_securitypolicy(permissive=False) + self.config.add_view(exc_view, context=RuntimeError, permission='view') + request = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view(secure=False) + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + class ExceptionResponse(Exception): status = '404 Not Found' app_iter = ['Not Found'] diff --git a/pyramid/view.py b/pyramid/view.py index 16b60b77a..9108f120e 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -598,14 +598,15 @@ class ViewMethodsMixin(object): registry = get_current_registry() if exc_info is None: exc_info = sys.exc_info() + exc = exc_info[1] attrs = request.__dict__ - context_iface = providedBy(exc_info[0]) + context_iface = providedBy(exc) # clear old generated request.response, if any; it may # have been mutated by the view, and its state is not # sane (e.g. caching headers) with hide_attrs(request, 'exception', 'exc_info', 'response'): - attrs['exception'] = exc_info[0] + attrs['exception'] = exc attrs['exc_info'] = exc_info # we use .get instead of .__getitem__ below due to # https://github.com/Pylons/pyramid/issues/700 @@ -613,7 +614,7 @@ class ViewMethodsMixin(object): response = _call_view( registry, request, - exc_info[0], + exc, context_iface, '', view_types=None, -- cgit v1.2.3 From bc092500e047d14a8ca1a97f1abc00a5678748fd Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 3 Mar 2016 20:14:09 -0600 Subject: link invoke_exception_view to api docs --- docs/api/request.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index 105ffb5a7..52bf50078 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -13,7 +13,8 @@ current_route_path, static_url, static_path, model_url, resource_url, resource_path, set_property, effective_principals, authenticated_userid, - unauthenticated_userid, has_permission + unauthenticated_userid, has_permission, + invoke_exception_view .. attribute:: context @@ -259,6 +260,8 @@ See also :ref:`subrequest_chapter`. + .. automethod:: invoke_exception_view + .. automethod:: has_permission .. automethod:: add_response_callback -- cgit v1.2.3