Repository: Pylons/pyramid

Issue:
update request.invoke_exception_view to set request.exception
Currently `request.invoke_exception_view` restores `request.exception` to whatever it was before when it completes. I think instead it should follow the behavior of the excview tween in #3029 and overwrite `request.exception` and `request.exc_info` if it manages to return a response. This will simplify the lives of most people who would want to use the method in trying to conform to this idea of having the request.exception match the response object it was used to create.


Discussion:

Resolving patch:
diff --git a/pyramid/tweens.py b/pyramid/tweens.py
--- a/pyramid/tweens.py
+++ b/pyramid/tweens.py
@@ -1,55 +1,18 @@
 import sys
 
 from pyramid.compat import reraise
-from pyramid.exceptions import PredicateMismatch
-from pyramid.interfaces import (
-    IExceptionViewClassifier,
-    IRequest,
-    )
-
-from zope.interface import providedBy
-from pyramid.view import _call_view
+from pyramid.httpexceptions import HTTPNotFound
 
 def _error_handler(request, exc):
     # NOTE: we do not need to delete exc_info because this function
     # should never be in the call stack of the exception
     exc_info = sys.exc_info()
 
-    attrs = request.__dict__
-    attrs['exc_info'] = exc_info
-    attrs['exception'] = 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)
-    if 'response' in attrs:
-        del attrs['response']
-    # we use .get instead of .__getitem__ below due to
-    # https://github.com/Pylons/pyramid/issues/700
-    request_iface = attrs.get('request_iface', IRequest)
-    provides = providedBy(exc)
     try:
-        response = _call_view(
-            request.registry,
-            request,
-            exc,
-            provides,
-            '',
-            view_classifier=IExceptionViewClassifier,
-            request_iface=request_iface.combined
-            )
-
-    # if views matched but did not pass predicates then treat the
-    # same as not finding any matching views
-    except PredicateMismatch:
-        response = None
-
-    # re-raise the original exception as no exception views were
-    # able to handle the error
-    if response is None:
-        if 'exception' in attrs:
-            del attrs['exception']
-        if 'exc_info' in attrs:
-            del attrs['exc_info']
+        response = request.invoke_exception_view(exc_info)
+    except HTTPNotFound:
+        # re-raise the original exception as no exception views were
+        # able to handle the error
         reraise(*exc_info)
 
     return response
diff --git a/pyramid/view.py b/pyramid/view.py
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -657,8 +657,14 @@ def invoke_exception_view(
 
         This method returns a :term:`response` object or raises
         :class:`pyramid.httpexceptions.HTTPNotFound` if a matching view cannot
-        be found."""
+        be found.
 
+        If a response is generated then ``request.exception`` and
+        ``request.exc_info`` will be left at the values used to render the
+        response. Otherwise the previous values for ``request.exception`` and
+        ``request.exc_info`` will be restored.
+
+        """
         if request is None:
             request = self
         registry = getattr(request, 'registry', None)
@@ -673,7 +679,7 @@ def invoke_exception_view(
         # 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'):
+        with hide_attrs(request, 'response', 'exc_info', 'exception'):
             attrs['exception'] = exc
             attrs['exc_info'] = exc_info
             # we use .get instead of .__getitem__ below due to
@@ -690,6 +696,11 @@ def invoke_exception_view(
                 secure=secure,
                 request_iface=request_iface.combined,
                 )
-            if response is None:
-                raise HTTPNotFound
-            return response
+
+        if response is None:
+            raise HTTPNotFound
+
+        # successful response, overwrite exception/exc_info
+        attrs['exception'] = exc
+        attrs['exc_info'] = exc_info
+        return response


Test patch:
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
--- a/pyramid/tests/test_view.py
+++ b/pyramid/tests/test_view.py
@@ -778,11 +778,11 @@ def exc_view(exc, request):
         orig_response = request.response = DummyResponse(b'foo')
         try:
             raise RuntimeError
-        except RuntimeError:
+        except RuntimeError as ex:
             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.exception is ex)
+            self.assertTrue(request.exc_info[1] is ex)
             self.assertTrue(request.response is orig_response)
         else: # pragma: no cover
             self.fail()