diff options
| -rw-r--r-- | CHANGES.txt | 12 | ||||
| -rw-r--r-- | docs/api/paster.rst | 2 | ||||
| -rw-r--r-- | docs/api/request.rst | 5 | ||||
| -rw-r--r-- | docs/designdefense.rst | 8 | ||||
| -rw-r--r-- | docs/glossary.rst | 11 | ||||
| -rw-r--r-- | docs/narr/i18n.rst | 15 | ||||
| -rw-r--r-- | docs/narr/subrequest.rst | 50 | ||||
| -rw-r--r-- | docs/quick_tutorial/routing.rst | 16 | ||||
| -rw-r--r-- | docs/quick_tutorial/view_classes.rst | 9 | ||||
| -rw-r--r-- | docs/whatsnew-1.6.rst | 2 | ||||
| -rw-r--r-- | pyramid/paster.py | 18 | ||||
| -rw-r--r-- | pyramid/renderers.py | 23 | ||||
| -rw-r--r-- | pyramid/request.py | 2 | ||||
| -rw-r--r-- | pyramid/scripts/pserve.py | 2 | ||||
| -rw-r--r-- | pyramid/static.py | 4 | ||||
| -rw-r--r-- | pyramid/testing.py | 2 | ||||
| -rw-r--r-- | pyramid/tests/test_paster.py | 26 | ||||
| -rw-r--r-- | pyramid/tests/test_renderers.py | 42 | ||||
| -rw-r--r-- | pyramid/tests/test_util.py | 57 | ||||
| -rw-r--r-- | pyramid/tests/test_view.py | 132 | ||||
| -rw-r--r-- | pyramid/util.py | 20 | ||||
| -rw-r--r-- | pyramid/view.py | 76 | ||||
| -rw-r--r-- | setup.py | 2 |
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 @@ -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, |
