Repository: Pylons/pyramid

Issue:
update pyramid's default execution policy to attempt to render any uncaught exception
If an exception propagates all the way to the execution policy there should be a last-ditch effort to render a response... this is part of the larger consistency change to pyramid_tm and pyramid_retry. With the addition of `request.invoke_exception_view` it's hopefully clear that exception views should be callable from various parts of the lifecycle and pyramid_retry will attempt to invoke exception views at the end if an exception is uncaught. Also pyramid_tm will attempt to invoke exceptions on commit/abort to handle those specifically.

I've documented the process here in a little ascii diagram[1] reproduced here:
```
   pyramid_retry   +-------->    if exc not retryable, render response
                                 if exc retryable, ignore exc and try again
        +
        |
        |
        v

pyramid_debugtoolbar

        +
        |
        |
        v

    pyramid_tm     +-------->    if exc caught then abort and reraise
                                 if request.exception then abort and return response
        +                        if abort or commit raises, render response
        |
        |
        v

     excview       +-------->    if exc caught, render response and set request.exception
                                                if no exception then reraise
        +
        |
        |
        v

       app
```

[1] https://gist.github.com/mmerickel/2e83f4af6c7d5eae34947b8f1f075f61


Discussion:

Resolving patch:
diff --git a/pyramid/router.py b/pyramid/router.py
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -1,3 +1,4 @@
+import sys
 from zope.interface import (
     implementer,
     providedBy,
@@ -24,6 +25,7 @@
     BeforeTraversal,
     )
 
+from pyramid.compat import reraise
 from pyramid.httpexceptions import HTTPNotFound
 from pyramid.request import Request
 from pyramid.view import _call_view
@@ -252,7 +254,15 @@ def __call__(self, environ, start_response):
         response = self.execution_policy(environ, self)
         return response(environ, start_response)
 
-
 def default_execution_policy(environ, router):
     request = router.make_request(environ)
-    return router.invoke_request(request)
+    try:
+        return router.invoke_request(request)
+    except Exception:
+        exc_info = sys.exc_info()
+        try:
+            return request.invoke_exception_view(exc_info)
+        except HTTPNotFound:
+            reraise(*exc_info)
+        finally:
+            del exc_info  # avoid local ref cycle


Test patch:
diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py
--- a/pyramid/tests/test_router.py
+++ b/pyramid/tests/test_router.py
@@ -1284,6 +1284,33 @@ def dummy_policy(environ, router):
         self.assertEqual(resp.status_code, 200)
         self.assertEqual(resp.body, b'foo')
 
+    def test_execution_policy_handles_exception(self):
+        from pyramid.interfaces import IViewClassifier
+        from pyramid.interfaces import IExceptionViewClassifier
+        from pyramid.interfaces import IRequest
+        class Exception1(Exception):
+            pass
+        class Exception2(Exception):
+            pass
+        req_iface = self._registerRouteRequest('foo')
+        self._connectRoute('foo', 'archives/:action/:article', None)
+        view = DummyView(DummyResponse(), raise_exception=Exception1)
+        self._registerView(view, '', IViewClassifier, req_iface, None)
+        exception_view1 = DummyView(DummyResponse(),
+                                    raise_exception=Exception2)
+        self._registerView(exception_view1, '', IExceptionViewClassifier,
+                           IRequest, Exception1)
+        response = DummyResponse()
+        response.app_iter = ["Hello, world"]
+        exception_view2 = DummyView(response)
+        self._registerView(exception_view2, '', IExceptionViewClassifier,
+                           IRequest, Exception2)
+        environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
+        start_response = DummyStartResponse()
+        router = self._makeOne()
+        result = router(environ, start_response)
+        self.assertEqual(result, ["Hello, world"])
+
 class DummyPredicate(object):
     def __call__(self, info, request):
         return True