summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt12
-rw-r--r--docs/api/paster.rst2
-rw-r--r--docs/api/request.rst5
-rw-r--r--docs/designdefense.rst8
-rw-r--r--docs/glossary.rst11
-rw-r--r--docs/narr/i18n.rst15
-rw-r--r--docs/narr/subrequest.rst50
-rw-r--r--docs/quick_tutorial/routing.rst16
-rw-r--r--docs/quick_tutorial/view_classes.rst9
-rw-r--r--docs/whatsnew-1.6.rst2
-rw-r--r--pyramid/paster.py18
-rw-r--r--pyramid/renderers.py23
-rw-r--r--pyramid/request.py2
-rw-r--r--pyramid/scripts/pserve.py2
-rw-r--r--pyramid/static.py4
-rw-r--r--pyramid/testing.py2
-rw-r--r--pyramid/tests/test_paster.py26
-rw-r--r--pyramid/tests/test_renderers.py42
-rw-r--r--pyramid/tests/test_util.py57
-rw-r--r--pyramid/tests/test_view.py132
-rw-r--r--pyramid/util.py20
-rw-r--r--pyramid/view.py76
-rw-r--r--setup.py2
23 files changed, 434 insertions, 102 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 3a1305c95..4a61dbffa 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -21,6 +21,18 @@ unreleased
``request.params['=abc'] == '1'``.
See https://github.com/Pylons/pyramid/pull/1370
+- A new ``request.invoke_exception_view(...)`` method which can be used to
+ invoke an exception view and get back a response. This is useful for
+ rendering an exception view outside of the context of the excview tween
+ where you may need more control over the request.
+ See https://github.com/Pylons/pyramid/pull/2393
+
+- Allow using variable substitutions like ``%(LOGGING_LOGGER_ROOT_LEVEL)s``
+ for logging sections of the .ini file and populate these variables from
+ the ``pserve`` command line -- e.g.:
+ ``pserve development.ini LOGGING_LOGGER_ROOT_LEVEL=DEBUG``
+ See https://github.com/Pylons/pyramid/pull/2399
+
1.6 (2015-04-14)
================
diff --git a/docs/api/paster.rst b/docs/api/paster.rst
index edc3738fc..27bc81a1f 100644
--- a/docs/api/paster.rst
+++ b/docs/api/paster.rst
@@ -11,4 +11,4 @@
.. autofunction:: get_appsettings(config_uri, name=None, options=None)
- .. autofunction:: setup_logging(config_uri)
+ .. autofunction:: setup_logging(config_uri, global_conf=None)
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
diff --git a/docs/designdefense.rst b/docs/designdefense.rst
index 28da84368..5f3295305 100644
--- a/docs/designdefense.rst
+++ b/docs/designdefense.rst
@@ -1011,8 +1011,8 @@ Self-described "microframeworks" exist. `Bottle <http://bottle.paws.de>`_ and
<http://bobo.digicool.com/>`_ doesn't describe itself as a microframework, but
its intended user base is much the same. Many others exist. We've even (only as
a teaching tool, not as any sort of official project) `created one using
-Pyramid <http://bfg.repoze.org/videos#groundhog1>`_. The videos use BFG, a
-precursor to Pyramid, but the resulting code is `available for Pyramid too
+Pyramid <http://static.repoze.org/casts/videotags.html>`_. The videos use BFG,
+a precursor to Pyramid, but the resulting code is `available for Pyramid too
<https://github.com/Pylons/groundhog>`_). Microframeworks are small frameworks
with one common feature: each allows its users to create a fully functional
application that lives in a single Python file.
@@ -1542,7 +1542,7 @@ inlined comments take into account what we've discussed in the
server.serve_forever() # explicitly WSGI
-Pyramid Doesn't Offer Pluggable Apps
+Pyramid doesn't offer pluggable apps
------------------------------------
It is "Pyramidic" to compose multiple external sources into the same
@@ -1550,7 +1550,7 @@ configuration using :meth:`~pyramid.config.Configurator.include`. Any
number of includes can be done to compose an application; includes can even
be done from within other includes. Any directive can be used within an
include that can be used outside of one (such as
-:meth:`~pyramid.config.Configurator.add_view`, etc).
+:meth:`~pyramid.config.Configurator.add_view`).
Pyramid has a conflict detection system that will throw an error if two
included externals try to add the same configuration in a conflicting way
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 5e6aa145c..039665926 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -558,12 +558,11 @@ Glossary
A popular `Javascript library <http://jquery.org>`_.
renderer
- A serializer that can be referred to via :term:`view
- configuration` which converts a non-:term:`Response` return
- values from a :term:`view` into a string (and ultimately a
- response). Using a renderer can make writing views that require
- templating or other serialization less tedious. See
- :ref:`views_which_use_a_renderer` for more information.
+ A serializer which converts non-:term:`Response` return values from a
+ :term:`view` into a string, and ultimately into a response, usually
+ through :term:`view configuration`. Using a renderer can make writing
+ views that require templating or other serialization, like JSON, less
+ tedious. See :ref:`views_which_use_a_renderer` for more information.
renderer factory
A factory which creates a :term:`renderer`. See
diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst
index 839a48df4..b385eaf96 100644
--- a/docs/narr/i18n.rst
+++ b/docs/narr/i18n.rst
@@ -670,6 +670,21 @@ There exists a recipe within the :term:`Pyramid Community Cookbook` named
:ref:`Mako Internationalization <cookbook:mako_i18n>` which explains how to add
idiomatic i18n support to :term:`Mako` templates.
+
+.. index::
+ single: Jinja2 i18n
+
+Jinja2 Pyramid i18n Support
+---------------------------
+
+The add-on `pyramid_jinja2 <https://github.com/Pylons/pyramid_jinja2>`_
+provides a scaffold with an example of how to use internationalization with
+Jinja2 in Pyramid. See the documentation sections `Internalization (i18n)
+<http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/#internalization-i18n>`_
+and `Paster Template I18N
+<http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/#paster-template-i18n>`_.
+
+
.. index::
single: localization deployment settings
single: default_locale_name
diff --git a/docs/narr/subrequest.rst b/docs/narr/subrequest.rst
index daa3cc43f..7c847de50 100644
--- a/docs/narr/subrequest.rst
+++ b/docs/narr/subrequest.rst
@@ -279,3 +279,53 @@ within a tween, because tweens already, by definition, have access to a
function that will cause a subrequest (they are passed a ``handle`` function).
It's fine to invoke :meth:`~pyramid.request.Request.invoke_subrequest` from
within an event handler, however.
+
+
+.. index::
+ pair: subrequest; exception view
+
+Invoking an Exception View
+--------------------------
+
+.. versionadded:: 1.7
+
+:app:`Pyramid` apps may define :term:`exception views <exception view>` which
+can handle any raised exceptions that escape from your code while processing
+a request. By default an unhandled exception will be caught by the ``EXCVIEW``
+:term:`tween`, which will then lookup an exception view that can handle the
+exception type, generating an appropriate error response.
+
+In :app:`Pyramid` 1.7 the :meth:`pyramid.request.Request.invoke_exception_view`
+was introduced, allowing a user to invoke an exception view while manually
+handling an exception. This can be useful in a few different circumstances:
+
+- Manually handling an exception losing the current call stack or flow.
+
+- Handling exceptions outside of the context of the ``EXCVIEW`` tween. The
+ tween only covers certain parts of the request processing pipeline (See
+ :ref:`router_chapter`). There are also some corner cases where an exception
+ can be raised that will still bubble up to middleware, and possibly to the
+ web server in which case a generic ``500 Internal Server Error`` will be
+ returned to the client.
+
+Below is an example usage of
+:meth:`pyramid.request.Request.invoke_exception_view`:
+
+.. code-block:: python
+ :linenos:
+
+ def foo(request):
+ try:
+ some_func_that_errors()
+ return response
+ except Exception:
+ response = request.invoke_exception_view()
+ if response is not None:
+ return response
+ else:
+ # there is no exception view for this exception, simply
+ # re-raise and let someone else handle it
+ raise
+
+Please note that in most cases you do not need to write code like this, and you
+may rely on the ``EXCVIEW`` tween to handle this for you.
diff --git a/docs/quick_tutorial/routing.rst b/docs/quick_tutorial/routing.rst
index 1b79a5889..416a346fa 100644
--- a/docs/quick_tutorial/routing.rst
+++ b/docs/quick_tutorial/routing.rst
@@ -23,14 +23,14 @@ Previously we saw the basics of routing URLs to views in Pyramid.
.. note::
- Why do this twice? Other Python web frameworks let you create a
- route and associate it with a view in one step. As
- illustrated in :ref:`routes_need_ordering`, multiple routes might match the
- same URL pattern. Rather than provide ways to help guess, Pyramid lets you
- be explicit in ordering. Pyramid also gives facilities to avoid the
- problem. It's relatively easy to build a system that uses implicit route
- ordering with Pyramid too. See `The Groundhog series of screencasts
- <http://bfg.repoze.org/videos#groundhog1>`_ if you're interested in
+ Why do this twice? Other Python web frameworks let you create a route and
+ associate it with a view in one step. As illustrated in
+ :ref:`routes_need_ordering`, multiple routes might match the same URL
+ pattern. Rather than provide ways to help guess, Pyramid lets you be
+ explicit in ordering. Pyramid also gives facilities to avoid the problem.
+ It's relatively easy to build a system that uses implicit route ordering
+ with Pyramid too. See `The Groundhog series of screencasts
+ <http://static.repoze.org/casts/videotags.html>`_ if you're interested in
doing so.
Objectives
diff --git a/docs/quick_tutorial/view_classes.rst b/docs/quick_tutorial/view_classes.rst
index 50a7ee0af..6198eed63 100644
--- a/docs/quick_tutorial/view_classes.rst
+++ b/docs/quick_tutorial/view_classes.rst
@@ -10,11 +10,10 @@ then move some declarations to the class level.
Background
==========
-So far our views have been simple, free-standing functions. Many times
-your views are related: different ways to look at or work on the same
-data or a REST API that handles multiple operations. Grouping these
-together as a
-:ref:`view class <class_as_view>` makes sense:
+So far our views have been simple, free-standing functions. Many times your
+views are related to one another. They may be different ways to look at or work
+on the same data, or be a REST API that handles multiple operations. Grouping
+these views together as a :ref:`view class <class_as_view>` makes sense:
- Group views
diff --git a/docs/whatsnew-1.6.rst b/docs/whatsnew-1.6.rst
index f5c307b5d..77d89b017 100644
--- a/docs/whatsnew-1.6.rst
+++ b/docs/whatsnew-1.6.rst
@@ -98,7 +98,7 @@ Feature Additions
relative to the top-level package. See
https://github.com/Pylons/pyramid/pull/1337
-- Overall improvments for the ``proutes`` command. Added ``--format`` and
+- Overall improvements for the ``proutes`` command. Added ``--format`` and
``--glob`` arguments to the command, introduced the ``method``
column for displaying available request methods, and improved the ``view``
output by showing the module instead of just ``__repr__``. See
diff --git a/pyramid/paster.py b/pyramid/paster.py
index 967543849..3916be8f0 100644
--- a/pyramid/paster.py
+++ b/pyramid/paster.py
@@ -52,25 +52,29 @@ def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig):
relative_to=here_dir,
global_conf=options)
-def setup_logging(config_uri, fileConfig=fileConfig,
+def setup_logging(config_uri, global_conf=None,
+ fileConfig=fileConfig,
configparser=configparser):
"""
- Set up logging via the logging module's fileConfig function with the
- filename specified via ``config_uri`` (a string in the form
+ Set up logging via :func:`logging.config.fileConfig` with the filename
+ specified via ``config_uri`` (a string in the form
``filename#sectionname``).
ConfigParser defaults are specified for the special ``__file__``
and ``here`` variables, similar to PasteDeploy config loading.
+ Extra defaults can optionally be specified as a dict in ``global_conf``.
"""
path, _ = _getpathsec(config_uri, None)
parser = configparser.ConfigParser()
parser.read([path])
if parser.has_section('loggers'):
config_file = os.path.abspath(path)
- return fileConfig(
- config_file,
- dict(__file__=config_file, here=os.path.dirname(config_file))
- )
+ full_global_conf = dict(
+ __file__=config_file,
+ here=os.path.dirname(config_file))
+ if global_conf:
+ full_global_conf.update(global_conf)
+ return fileConfig(config_file, full_global_conf)
def _getpathsec(config_uri, name):
if '#' in config_uri:
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/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/scripts/pserve.py b/pyramid/scripts/pserve.py
index 431afe6f4..74bda1dce 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -362,7 +362,7 @@ a real process manager for your processes like Systemd, Circus, or Supervisor.
log_fn = None
if log_fn:
log_fn = os.path.join(base, log_fn)
- setup_logging(log_fn)
+ setup_logging(log_fn, global_conf=vars)
server = self.loadserver(server_spec, name=server_name,
relative_to=base, global_conf=vars)
diff --git a/pyramid/static.py b/pyramid/static.py
index 4054d5be0..0965be95c 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -229,9 +229,9 @@ class ManifestCacheBuster(object):
By default, it is a JSON-serialized dictionary where the keys are the
source asset paths used in calls to
- :meth:`~pyramid.request.Request.static_url`. For example::
+ :meth:`~pyramid.request.Request.static_url`. For example:
- .. code-block:: python
+ .. code-block:: pycon
>>> request.static_url('myapp:static/css/main.css')
"http://www.example.com/static/css/main-678b7c80.css"
diff --git a/pyramid/testing.py b/pyramid/testing.py
index c25105c6c..ec06fe379 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/tests/test_paster.py b/pyramid/tests/test_paster.py
index 5e341172c..22a5cde3d 100644
--- a/pyramid/tests/test_paster.py
+++ b/pyramid/tests/test_paster.py
@@ -105,18 +105,38 @@ class Test_get_appsettings(unittest.TestCase):
self.assertEqual(result['foo'], 'baz')
class Test_setup_logging(unittest.TestCase):
- def _callFUT(self, config_file):
+ def _callFUT(self, config_file, global_conf=None):
from pyramid.paster import setup_logging
dummy_cp = DummyConfigParserModule
- return setup_logging(config_file, self.fileConfig, dummy_cp)
+ return setup_logging(
+ config_uri=config_file,
+ global_conf=global_conf,
+ fileConfig=self.fileConfig,
+ configparser=dummy_cp,
+ )
- def test_it(self):
+ def test_it_no_global_conf(self):
config_file, dict = self._callFUT('/abc')
# os.path.abspath is a sop to Windows
self.assertEqual(config_file, os.path.abspath('/abc'))
self.assertEqual(dict['__file__'], os.path.abspath('/abc'))
self.assertEqual(dict['here'], os.path.abspath('/'))
+ def test_it_global_conf_empty(self):
+ config_file, dict = self._callFUT('/abc', global_conf={})
+ # os.path.abspath is a sop to Windows
+ self.assertEqual(config_file, os.path.abspath('/abc'))
+ self.assertEqual(dict['__file__'], os.path.abspath('/abc'))
+ self.assertEqual(dict['here'], os.path.abspath('/'))
+
+ def test_it_global_conf_not_empty(self):
+ config_file, dict = self._callFUT('/abc', global_conf={'key': 'val'})
+ # os.path.abspath is a sop to Windows
+ self.assertEqual(config_file, os.path.abspath('/abc'))
+ self.assertEqual(dict['__file__'], os.path.abspath('/abc'))
+ self.assertEqual(dict['here'], os.path.abspath('/'))
+ self.assertEqual(dict['key'], 'val')
+
def fileConfig(self, config_file, dict):
return config_file, dict
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/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/util.py b/pyramid/util.py
index 27140936c..fc1d52af5 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
@@ -594,3 +595,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 7e8996ca4..0129526ce 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -1,4 +1,6 @@
import itertools
+import sys
+
import venusian
from zope.interface import providedBy
@@ -10,6 +12,7 @@ from pyramid.interfaces import (
IView,
IViewClassifier,
IRequest,
+ IExceptionViewClassifier,
)
from pyramid.compat import decode_path_info
@@ -22,6 +25,7 @@ from pyramid.httpexceptions import (
)
from pyramid.threadlocal import get_current_registry
+from pyramid.util import hide_attrs
_marker = object()
@@ -547,3 +551,75 @@ 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_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
+ ``sys.exc_info()``. If not provided,
+ ``sys.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 ``sys.exc_info()`` as ``exc_info``, the request
+ object that this method is attached to as the ``request``, and
+ ``True`` for ``secure``.
+
+ This method returns a :term:`response` object or ``None`` if no
+ matching exception view can be found."""
+
+ if request is None:
+ request = self
+ registry = getattr(request, 'registry', None)
+ if registry is None:
+ 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)
+
+ # 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
+ 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,
+ context_iface,
+ '',
+ view_types=None,
+ view_classifier=IExceptionViewClassifier,
+ secure=secure,
+ request_iface=request_iface.combined,
+ )
+ return response
diff --git a/setup.py b/setup.py
index e878b9932..e26ae80d8 100644
--- a/setup.py
+++ b/setup.py
@@ -95,7 +95,7 @@ setup(name='pyramid',
keywords='web wsgi pylons pyramid',
author="Chris McDonough, Agendaless Consulting",
author_email="pylons-discuss@googlegroups.com",
- url="http://docs.pylonsproject.org/en/latest/docs/pyramid.html",
+ url="https://trypyramid.com",
license="BSD-derived (http://www.repoze.org/LICENSE.txt)",
packages=find_packages(),
include_package_data=True,