From b0d20b5c3fe7df472633899024cdab685483807a Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Thu, 2 Jun 2016 14:50:53 -0700 Subject: add exception_only argument to add_view to only register exception views. --- pyramid/config/views.py | 25 +++++++++++++++++++++---- pyramid/tests/test_config/test_views.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9e46ba155..198fde5e8 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -213,6 +213,7 @@ class ViewsConfiguratorMixin(object): match_param=None, check_csrf=None, require_csrf=None, + exception_only=False, **view_options): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken @@ -701,6 +702,14 @@ class ViewsConfiguratorMixin(object): Support setting view deriver options. Previously, only custom view predicate values could be supplied. + exception_only + + .. versionadded:: 1.8 + + A boolean indicating whether the view is registered only as an + exception view. When this argument is true, the view context must + be an exception. + """ if custom_predicates: warnings.warn( @@ -759,6 +768,11 @@ class ViewsConfiguratorMixin(object): raise ConfigurationError( 'request_type must be an interface, not %s' % request_type) + if exception_only and not isexception(context): + raise ConfigurationError( + 'context must be an exception when exception_only is true' + ) + if context is None: context = for_ @@ -942,10 +956,13 @@ class ViewsConfiguratorMixin(object): view_iface = ISecuredView else: view_iface = IView - self.registry.registerAdapter( - derived_view, - (IViewClassifier, request_iface, context), view_iface, name - ) + if not exception_only: + self.registry.registerAdapter( + derived_view, + (IViewClassifier, request_iface, context), + view_iface, + name + ) if isexc: self.registry.registerAdapter( derived_view, diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 878574e88..c93175ff9 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1815,6 +1815,38 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertRaises(ConfigurationError, configure_view) + def test_add_view_exception_only_no_regular_view(self): + from zope.interface import implementedBy + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view1, context=Exception, renderer=null_renderer, + exception_only=True) + view = self._getViewCallable(config, ctx_iface=implementedBy(Exception)) + self.assertTrue(view is None) + + def test_add_view_exception_only(self): + from zope.interface import implementedBy + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view1, context=Exception, renderer=null_renderer, + exception_only=True) + view = self._getViewCallable( + config, ctx_iface=implementedBy(Exception), exception_view=True + ) + self.assertEqual(view1, view) + + def test_add_view_exception_only_misconfiguration(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + class NotAnException(object): + pass + self.assertRaises( + ConfigurationError, + config.add_view, view, context=NotAnException, exception_only=True + ) + def test_derive_view_function(self): from pyramid.renderers import null_renderer def view(request): -- cgit v1.2.3 From d6c90d154b74107da325a1c45c7a2c4f35ea03c5 Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Thu, 2 Jun 2016 16:39:45 -0700 Subject: Add add_exception_view --- pyramid/config/views.py | 65 +++++++++++++++++++++++++ pyramid/tests/test_config/test_views.py | 84 +++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 198fde5e8..b13885833 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1613,6 +1613,71 @@ class ViewsConfiguratorMixin(object): set_notfound_view = add_notfound_view # deprecated sorta-bw-compat alias + @viewdefaults + @action_method + def add_exception_view( + self, + view=None, + context=None, + attr=None, + renderer=None, + wrapper=None, + route_name=None, + request_type=None, + request_method=None, + request_param=None, + containment=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + decorator=None, + mapper=None, + match_param=None, + **view_options + ): + """ Add a view for an exception to the current configuration state. + The view will be called when Pyramid or application code raises an + the given exception. + + .. versionadded:: 1.8 + """ + for arg in ( + 'name', 'permission', 'for_', 'http_cache', + 'require_csrf', 'exception_only', + ): + if arg in view_options: + raise ConfigurationError( + '%s may not be used as an argument to add_exception_view' + % arg + ) + if context is None: + raise ConfigurationError('context exception must be specified') + settings = dict( + view=view, + context=context, + wrapper=wrapper, + renderer=renderer, + request_type=request_type, + request_method=request_method, + request_param=request_param, + containment=containment, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + custom_predicates=custom_predicates, + decorator=decorator, + mapper=mapper, + match_param=match_param, + route_name=route_name, + permission=NO_PERMISSION_REQUIRED, + require_csrf=False, + exception_only=True, + ) + return self.add_view(**settings) + @action_method def set_view_mapper(self, mapper): """ diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index c93175ff9..1adde9225 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1847,6 +1847,90 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view, view, context=NotAnException, exception_only=True ) + def test_add_exception_view(self): + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_exception_view(view=view1, context=Exception, renderer=null_renderer) + wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(Exception), exception_view=True, + ) + context = Exception() + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + + def test_add_exception_view_disallows_name(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + name='foo') + + def test_add_exception_view_disallows_permission(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + permission='foo') + + def test_add_exception_view_disallows_for_(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + for_='foo') + + def test_add_exception_view_disallows_http_cache(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + http_cache='foo') + + def test_add_exception_view_disallows_exception_only(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + exception_only=True) + + def test_add_exception_view_requires_context(self): + config = self._makeOne(autocommit=True) + view = lambda *a: 'OK' + self.assertRaises(ConfigurationError, + config.add_exception_view, view=view) + + def test_add_exception_view_with_view_defaults(self): + from pyramid.renderers import null_renderer + from pyramid.exceptions import PredicateMismatch + from pyramid.httpexceptions import HTTPNotFound + from zope.interface import directlyProvides + from zope.interface import implementedBy + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + def __init__(self, request): + pass + def __call__(self): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_exception_view( + view=view, + context=Exception, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(Exception), exception_view=True) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + context = DummyContext() + request = self._makeRequest(config) + self.assertRaises(PredicateMismatch, wrapper, context, request) + def test_derive_view_function(self): from pyramid.renderers import null_renderer def view(request): -- cgit v1.2.3 From 93c94b9ea69d25da63604e494f89d8c619e7babb Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Thu, 2 Jun 2016 17:03:18 -0700 Subject: Add exception_view_config decorator. --- pyramid/tests/test_view.py | 46 +++++++++++++++++++++++++++++++++++++++-- pyramid/view.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 2de44d579..d18c6eca4 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -132,7 +132,49 @@ class Test_forbidden_view_config(BaseTest, unittest.TestCase): self.assertEqual(settings[0]['view'], None) # comes from call_venusian self.assertEqual(settings[0]['attr'], 'view') self.assertEqual(settings[0]['_info'], 'codeinfo') - + +class Test_exception_view_config(BaseTest, unittest.TestCase): + def _makeOne(self, **kw): + from pyramid.view import exception_view_config + return exception_view_config(**kw) + + def test_ctor(self): + inst = self._makeOne(context=Exception, path_info='path_info') + self.assertEqual(inst.__dict__, + {'context':Exception, 'path_info':'path_info'}) + + def test_it_function(self): + def view(request): pass + decorator = self._makeOne(context=Exception, renderer='renderer') + venusian = DummyVenusian() + decorator.venusian = venusian + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual( + settings, + [{'venusian': venusian, 'context': Exception, + 'renderer': 'renderer', '_info': 'codeinfo', 'view': None}] + ) + + def test_it_class(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + class view(object): pass + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['attr'], 'view') + self.assertEqual(settings[0]['_info'], 'codeinfo') + class RenderViewToResponseTests(BaseTest, unittest.TestCase): def _callFUT(self, *arg, **kw): from pyramid.view import render_view_to_response @@ -898,7 +940,7 @@ class DummyConfig(object): def add_view(self, **kw): self.settings.append(kw) - add_notfound_view = add_forbidden_view = add_view + add_notfound_view = add_forbidden_view = add_exception_view = add_view def with_package(self, pkg): self.pkg = pkg diff --git a/pyramid/view.py b/pyramid/view.py index 88c6397af..5a9f2a068 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -463,6 +463,57 @@ class forbidden_view_config(object): settings['_info'] = info.codeinfo # fbo "action_method" return wrapped +class exception_view_config(object): + """ + .. versionadded:: 1.8 + + An analogue of :class:`pyramid.view.view_config` which registers an + exception view. + + The exception_view_config constructor requires an exception context, and + additionally accepts most of the same argumenta as the constructor of + :class:`pyramid.view.view_config`. It can be used in the same places, + and behaves in largely the same way, except it always registers an exception + view instead of a 'normal' view. + + Example: + + .. code-block:: python + + from pyramid.view import exception_view_config + from pyramid.response import Response + + @exception_view_config(context=ValueError) + def error_view(request): + return Response('A value error ocurred') + + All arguments passed to this function have the same meaning as + :meth:`pyramid.view.view_config` and each predicate argument restricts + the set of circumstances under which this exception view will be invoked. + """ + + def __init__(self, **settings): + self.__dict__.update(settings) + + def __call__(self, wrapped): + settings = self.__dict__.copy() + + def callback(context, name, ob): + config = context.config.with_package(info.module) + config.add_exception_view(view=ob, **settings) + + info = self.venusian.attach(wrapped, callback, category='pyramid') + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' into the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped + def _find_views( registry, request_iface, -- cgit v1.2.3 From 74842ab6a6025d5058da4321658e7f51eb697ea0 Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Thu, 2 Jun 2016 17:18:59 -0700 Subject: fix broken decorator. --- pyramid/view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyramid/view.py b/pyramid/view.py index 5a9f2a068..74c57d272 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -491,6 +491,7 @@ class exception_view_config(object): :meth:`pyramid.view.view_config` and each predicate argument restricts the set of circumstances under which this exception view will be invoked. """ + venusian = venusian def __init__(self, **settings): self.__dict__.update(settings) -- cgit v1.2.3 From 3fd41dcb5a94a73f43862f6d5c063af7b54e6ff3 Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Thu, 30 Jun 2016 10:08:16 -0700 Subject: fix docs typo, expand example. --- pyramid/view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index 74c57d272..1895de96d 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -471,7 +471,7 @@ class exception_view_config(object): exception view. The exception_view_config constructor requires an exception context, and - additionally accepts most of the same argumenta as the constructor of + additionally accepts most of the same arguments as the constructor of :class:`pyramid.view.view_config`. It can be used in the same places, and behaves in largely the same way, except it always registers an exception view instead of a 'normal' view. @@ -483,9 +483,9 @@ class exception_view_config(object): from pyramid.view import exception_view_config from pyramid.response import Response - @exception_view_config(context=ValueError) - def error_view(request): - return Response('A value error ocurred') + @exception_view_config(context=ValueError, renderer='json') + def error_view(context, request): + return {'error': str(context)} All arguments passed to this function have the same meaning as :meth:`pyramid.view.view_config` and each predicate argument restricts -- cgit v1.2.3 From ddc0ee35b2bf954c8d4b0a69cea3cf0f4f477e10 Mon Sep 17 00:00:00 2001 From: Mark Jones Date: Wed, 17 Aug 2016 11:58:12 -0500 Subject: Clarify that headers are ADDED to the response Make it clear that headers are added to the response instead of overwriting existing headers. Explain how to set the content_type instead of just assuming they will know. --- pyramid/httpexceptions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index e76f43c8a..b44227fc5 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -98,7 +98,9 @@ be forwarded to its :class:`~pyramid.response.Response` superclass: a plain-text override of the default ``detail`` ``headers`` - a list of (k,v) header pairs + a list of (k,v) header pairs or a dict to be added to the + response, use content_type='application/json' kwarg to change + the content type of the response. ``comment`` a plain-text additional information which is -- cgit v1.2.3 From 24e1b3b0bd5e15ab13d7c89ef9a291f1b464724c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 19 Sep 2016 21:01:07 -0500 Subject: clarify #2750 a bit further --- pyramid/httpexceptions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index b44227fc5..054917dfa 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -98,9 +98,10 @@ be forwarded to its :class:`~pyramid.response.Response` superclass: a plain-text override of the default ``detail`` ``headers`` - a list of (k,v) header pairs or a dict to be added to the - response, use content_type='application/json' kwarg to change - the content type of the response. + a list of (k,v) header pairs, or a dict, to be added to the + response; use the content_type='application/json' kwarg and other + similar kwargs to to change properties of the response supported by the + :class:`pyramid.response.Response` superclass ``comment`` a plain-text additional information which is -- cgit v1.2.3 From 5b33ff66090e4f2b0249cc85341cce09d0b57fca Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 19 Sep 2016 21:07:34 -0500 Subject: changelog for #2750 --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index f17a04f92..85e71aef3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -76,3 +76,8 @@ Documentation Changes - Fix an inconsistency in the documentation between view predicates and route predicates and highlight the differences in their APIs. See https://github.com/Pylons/pyramid/pull/2764 + +- Clarify a possible misuse of the ``headers`` kwarg to subclasses of + :class:`pyramid.httpexceptions.HTTPException` in which more appropriate + kwargs from the parent class :class:`pyramid.response.Response` should be + used instead. See https://github.com/Pylons/pyramid/pull/2750 -- cgit v1.2.3 From 21f2b61272f83de110087344e68f32953cd7b42f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 24 Sep 2016 20:39:45 -0700 Subject: Why use venv/bin/pip instead of source bin/activate then pip? - Closes #2610 --- docs/narr/install.rst | 21 +++++++++++++++++++++ docs/quick_tour.rst | 4 +++- docs/quick_tutorial/requirements.rst | 3 +++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 677c27e4a..7b5d8361b 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -191,6 +191,24 @@ After installing Python as described previously in :ref:`for-mac-os-x-users` or $ $VENV/bin/pip install "pyramid==\ |release|\ " +.. index:: + single: $VENV/bin/pip vs. source bin/activate + +.. _venv-bin-pip-vs-source-bin-activate: + +.. note:: Why use ``$VENV/bin/pip`` instead of ``source bin/activate``, then + ``pip``? + + Although the latter method requires fewer key strokes to issue commands once + invoked, there are numerous reasons why one should avoid using ``activate`` + within a virtual environment. Michael F. Lamb (datagrok) presents a summary + in `Virtualenv's bin/activate is Doing It Wrong + `_, and proposes alternatives, + followed by comments from other developers. + + However, we prefer to keep things simple. ``$VENV/bin/pip`` is already + UNIX-y. The few extra key strokes are worth avoiding the mess altogether. + .. index:: single: installing on Windows @@ -227,6 +245,9 @@ After installing Python as described previously in c:\\> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ " +.. note:: See the note above for :ref:`Why use $VENV/bin/pip instead of source + bin/activate, then pip `. + What Gets Installed ------------------- diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index b2dec77e9..39b4cafb3 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -59,7 +59,9 @@ show only UNIX commands. .. seealso:: See also: :ref:`Quick Tutorial section on Requirements `, - :ref:`installing_unix`, :ref:`Before You Install `, and + :ref:`installing_unix`, :ref:`Before You Install `, + :ref:`Why use $VENV/bin/pip instead of source bin/activate, then pip + `, and :ref:`Installing Pyramid on a Windows System `. diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst index 1de9a8acf..afa8ed104 100644 --- a/docs/quick_tutorial/requirements.rst +++ b/docs/quick_tutorial/requirements.rst @@ -179,6 +179,9 @@ time of its release. # Windows c:\> %VENV%\Scripts\pip install --upgrade pip setuptools +.. seealso:: See also :ref:`Why use $VENV/bin/pip instead of source + bin/activate, then pip `. + .. _install-pyramid: -- cgit v1.2.3 From b0576535de6d0e0cec4be22528d5df6947e2446d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 25 Sep 2016 11:30:54 -0400 Subject: clarify what changed in 1.6 with ``config.add_notfound_view`` --- pyramid/config/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 5cb3f5099..542f541f0 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1546,6 +1546,10 @@ class ViewsConfiguratorMixin(object): be used` for the redirect response if a slash-appended route is found. .. versionchanged:: 1.6 + The ``append_slash`` argument was modified to allow any object that + implements the ``IResponse`` interface to specify the response class + used when a redirect is performed. + .. versionadded:: 1.3 """ for arg in ( -- cgit v1.2.3 From 781cf1a5d685daf21ab7b885fda4ab21b05ce940 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 26 Sep 2016 00:18:34 -0700 Subject: Revise Why use venv/bin/pip instead of source bin/activate then pip? - Closes #2610 --- docs/narr/install.rst | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 7b5d8361b..570cb2285 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -199,15 +199,20 @@ After installing Python as described previously in :ref:`for-mac-os-x-users` or .. note:: Why use ``$VENV/bin/pip`` instead of ``source bin/activate``, then ``pip``? - Although the latter method requires fewer key strokes to issue commands once - invoked, there are numerous reasons why one should avoid using ``activate`` - within a virtual environment. Michael F. Lamb (datagrok) presents a summary - in `Virtualenv's bin/activate is Doing It Wrong - `_, and proposes alternatives, - followed by comments from other developers. - - However, we prefer to keep things simple. ``$VENV/bin/pip`` is already - UNIX-y. The few extra key strokes are worth avoiding the mess altogether. + ``$VENV/bin/pip`` clearly specifies that ``pip`` is run from within the + virtual environment and not at the system level. + + ``activate`` drops turds into the user's shell environment, leaving them + vulnerable to executing commands in the wrong context. ``deactivate`` might + not correctly restore previous shell environment variables. + + Although using ``source bin/activate``, then ``pip``, requires fewer key + strokes to issue commands once invoked, there are other things to consider. + Michael F. Lamb (datagrok) presents a summary in `Virtualenv's bin/activate + is Doing It Wrong `_. + + Ultimately we prefer to keep things clear and simple, so we use + ``$VENV/bin/pip``. .. index:: -- cgit v1.2.3 From e8c66a339e9f7d83bd2408952de53ef30dba0794 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 19 Sep 2016 23:52:05 -0500 Subject: derive exception views separately from normal views - previously the multiview was shared for both exception and hot-route, but now that we allow some exception-only views this needed to be separated - add ViewDeriverInfo.exception_only to detect exception views - do not prevent http_cache on exception views - optimize secured_view and csrf_view derivers to remove themselves from the view pipeline for exception views --- docs/narr/hooks.rst | 23 ++- docs/narr/viewconfig.rst | 19 +- docs/narr/views.rst | 40 ++-- pyramid/config/views.py | 319 ++++++++++++++++++-------------- pyramid/exceptions.py | 1 + pyramid/interfaces.py | 1 + pyramid/tests/test_config/test_views.py | 258 +++++++++++++++++++------- pyramid/tests/test_exceptions.py | 2 - pyramid/tests/test_view.py | 13 +- pyramid/tests/test_viewderivers.py | 22 +++ pyramid/view.py | 39 ++-- pyramid/viewderivers.py | 57 +++--- 12 files changed, 522 insertions(+), 272 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 49ef29d3f..7fbac2080 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1639,7 +1639,8 @@ the user-defined :term:`view callable`: Enforce the ``permission`` defined on the view. This element is a no-op if no permission is defined. Note there will always be a permission defined if a default permission was assigned via - :meth:`pyramid.config.Configurator.set_default_permission`. + :meth:`pyramid.config.Configurator.set_default_permission` unless the + view is an :term:`exception view`. This element will also output useful debugging information when ``pyramid.debug_authorization`` is enabled. @@ -1649,7 +1650,8 @@ the user-defined :term:`view callable`: Used to check the CSRF token provided in the request. This element is a no-op if ``require_csrf`` view option is not ``True``. Note there will always be a ``require_csrf`` option if a default value was assigned via - :meth:`pyramid.config.Configurator.set_default_csrf_options`. + :meth:`pyramid.config.Configurator.set_default_csrf_options` unless + the view is an :term:`exception view`. ``owrapped_view`` @@ -1695,6 +1697,8 @@ around monitoring and security. In order to register a custom :term:`view deriver`, you should create a callable that conforms to the :class:`pyramid.interfaces.IViewDeriver` interface, and then register it with your application using :meth:`pyramid.config.Configurator.add_view_deriver`. +The callable should accept the ``view`` to be wrapped and the ``info`` object +which is an instance of :class:`pyramid.interfaces.IViewDeriverInfo`. For example, below is a callable that can provide timing information for the view pipeline: @@ -1745,6 +1749,21 @@ View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what to do, and they have a chance to affect every view in the application. +.. _exception_view_derivers: + +Exception Views and View Derivers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A :term:`view deriver` has the opportunity to wrap any view, including +an :term:`exception view`. In general this is fine, but certain view derivers +may wish to avoid doing certain things when handling exceptions. For example, +the ``csrf_view`` and ``secured_view`` built-in view derivers will not perform +security checks on exception views unless explicitly told to do so. + +You can check for ``info.exception_only`` on the +:class:`pyramid.interfaces.IViewDeriverInfo` object when wrapping the view +to determine whether you are wrapping an exception view or a normal view. + Ordering View Derivers ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index cd5b8feb0..76eaf3cc5 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -34,7 +34,7 @@ determine the set of circumstances which must be true for the view callable to be invoked. A view configuration statement is made about information present in the -:term:`context` resource and the :term:`request`. +:term:`context` resource (or exception) and the :term:`request`. View configuration is performed in one of two ways: @@ -306,9 +306,26 @@ configured view. represented class or if the :term:`context` resource provides the represented interface; it is otherwise false. + It is possible to pass an exception class as the context if your context may + subclass an exception. In this case **two** views will be registered. One + will match normal incoming requests and the other will match as an + :term:`exception view` which only occurs when an exception is raised during + the normal request processing pipeline. + If ``context`` is not supplied, the value ``None``, which matches any resource, is used. +``exception_only`` + + When this value is ``True`` the ``context`` argument must be a subclass of + ``Exception``. This flag indicates that only an :term:`exception view` should + be created and that this view should not match if the traversal + :term:`context` matches the ``context`` argument. If the ``context`` is a + subclass of ``Exception`` and this value is ``False`` (the default) then a + view will be registered to match the traversal :term:`context` as well. + + .. versionadded:: 1.8 + ``route_name`` If ``route_name`` is supplied, the view callable will be invoked only when the named route has matched. diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 770d27919..465062651 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -262,10 +262,16 @@ specialized views as described in :ref:`special_exceptions_in_callables` can also be used by application developers to convert arbitrary exceptions to responses. -To register a view that should be called whenever a particular exception is -raised from within :app:`Pyramid` view code, use the exception class (or one of -its superclasses) as the :term:`context` of a view configuration which points -at a view callable for which you'd like to generate a response. +To register a :term:`exception view` that should be called whenever a +particular exception is raised from within :app:`Pyramid` view code, use +:meth:`pyramid.config.Configurator.add_exception_view` to register a view +configuration which matches the exception (or a subclass of the exception) and +points at a view callable for which you'd like to generate a response. The +exception will be passed as the ``context`` argument to any +:term:`view predicate` registered with the view as well as to the view itself. +For convenience a new decorator exists, +:class:`pyramid.views.exception_view_config`, which may be used to easily +register exception views. For example, given the following exception class in a module named ``helloworld.exceptions``: @@ -277,17 +283,16 @@ For example, given the following exception class in a module named def __init__(self, msg): self.msg = msg - You can wire a view callable to be called whenever any of your *other* code raises a ``helloworld.exceptions.ValidationFailure`` exception: .. code-block:: python :linenos: - from pyramid.view import view_config + from pyramid.view import exception_view_config from helloworld.exceptions import ValidationFailure - @view_config(context=ValidationFailure) + @exception_view_config(ValidationFailure) def failed_validation(exc, request): response = Response('Failed validation: %s' % exc.msg) response.status_int = 500 @@ -308,7 +313,7 @@ view registration: from pyramid.view import view_config from helloworld.exceptions import ValidationFailure - @view_config(context=ValidationFailure, route_name='home') + @exception_view_config(ValidationFailure, route_name='home') def failed_validation(exc, request): response = Response('Failed validation: %s' % exc.msg) response.status_int = 500 @@ -327,14 +332,21 @@ which have a name will be ignored. .. note:: - Normal (i.e., non-exception) views registered against a context resource type - which inherits from :exc:`Exception` will work normally. When an exception - view configuration is processed, *two* views are registered. One as a - "normal" view, the other as an "exception" view. This means that you can use - an exception as ``context`` for a normal view. + In most cases, you should register an :term:`exception view` by using + :meth:`pyramid.config.Configurator.add_exception_view`. However, it is + possible to register 'normal' (i.e., non-exception) views against a context + resource type which inherits from :exc:`Exception` (i.e., + ``config.add_view(context=Exception)``). When the view configuration is + processed, *two* views are registered. One as a "normal" view, the other + as an :term:`exception view`. This means that you can use an exception as + ``context`` for a normal view. + + The view derivers that wrap these two views may behave differently. + See :ref:`exception_view_derivers` for more information about this. Exception views can be configured with any view registration mechanism: -``@view_config`` decorator or imperative ``add_view`` styles. +``@exception_view_config`` decorator or imperative ``add_exception_view`` +styles. .. note:: diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e341922d3..ae180fb10 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -9,12 +9,11 @@ from zope.interface import ( implementedBy, implementer, ) - from zope.interface.interfaces import IInterface from pyramid.interfaces import ( - IException, IExceptionViewClassifier, + IException, IMultiView, IPackageOverrides, IRendererFactory, @@ -503,7 +502,20 @@ class ViewsConfiguratorMixin(object): if the :term:`context` provides the represented interface; it is otherwise false. This argument may also be provided to ``add_view`` as ``for_`` (an older, still-supported - spelling). + spelling). If the view should **only** match when handling + exceptions then set the ``exception_only`` to ``True``. + + exception_only + + .. versionadded:: 1.8 + + When this value is ``True`` the ``context`` argument must be + a subclass of ``Exception``. This flag indicates that only an + :term:`exception view` should be created and that this view should + not match if the traversal :term:`context` matches the ``context`` + argument. If the ``context`` is a subclass of ``Exception`` and + this value is ``False`` (the default) then a view will be + registered to match the traversal :term:`context` as well. route_name @@ -685,7 +697,7 @@ class ViewsConfiguratorMixin(object): obsoletes this argument, but it is kept around for backwards compatibility. - view_options: + view_options Pass a key/value pair here to use a third-party predicate or set a value for a view deriver. See @@ -702,14 +714,6 @@ class ViewsConfiguratorMixin(object): Support setting view deriver options. Previously, only custom view predicate values could be supplied. - exception_only - - .. versionadded:: 1.8 - - A boolean indicating whether the view is registered only as an - exception view. When this argument is true, the view context must - be an exception. - """ if custom_predicates: warnings.warn( @@ -768,14 +772,15 @@ class ViewsConfiguratorMixin(object): raise ConfigurationError( 'request_type must be an interface, not %s' % request_type) - if exception_only and not isexception(context): - raise ConfigurationError( - 'context must be an exception when exception_only is true' - ) - if context is None: context = for_ + isexc = isexception(context) + if exception_only and not isexc: + raise ConfigurationError( + 'view "context" must be an exception type when ' + '"exception_only" is True') + r_context = context if r_context is None: r_context = Interface @@ -811,6 +816,7 @@ class ViewsConfiguratorMixin(object): # is. It can't be computed any sooner because thirdparty # predicates/view derivers may not yet exist when add_view is # called. + predlist = self.get_predlist('view') valid_predicates = predlist.names() pvals = {} dvals = {} @@ -849,6 +855,7 @@ class ViewsConfiguratorMixin(object): view_intr.update(dict( name=name, context=context, + exception_only=exception_only, containment=containment, request_param=request_param, request_methods=request_method, @@ -868,7 +875,6 @@ class ViewsConfiguratorMixin(object): )) view_intr.update(view_options) introspectables.append(view_intr) - predlist = self.get_predlist('view') def register(permission=permission, renderer=renderer): request_iface = IRequest @@ -891,12 +897,54 @@ class ViewsConfiguratorMixin(object): registry=self.registry ) + renderer_type = getattr(renderer, 'type', None) + intrspc = self.introspector + if ( + renderer_type is not None and + tmpl_intr is not None and + intrspc is not None and + intrspc.get('renderer factories', renderer_type) is not None + ): + # allow failure of registered template factories to be deferred + # until view execution, like other bad renderer factories; if + # we tried to relate this to an existing renderer factory + # without checking if it the factory actually existed, we'd end + # up with a KeyError at startup time, which is inconsistent + # with how other bad renderer registrations behave (they throw + # a ValueError at view execution time) + tmpl_intr.relate('renderer factories', renderer.type) + + # make a new view separately for normal and exception paths + if not exception_only: + derived_view = derive_view(False, renderer) + register_view(IViewClassifier, request_iface, derived_view) + if isexc: + derived_exc_view = derive_view(True, renderer) + register_view(IExceptionViewClassifier, request_iface, + derived_exc_view) + + if exception_only: + derived_view = derived_exc_view + + # if there are two derived views, combine them into one for + # introspection purposes + if not exception_only and isexc: + derived_view = runtime_exc_view(derived_view, derived_exc_view) + + derived_view.__discriminator__ = lambda *arg: discriminator + # __discriminator__ is used by superdynamic systems + # that require it for introspection after manual view lookup; + # see also MultiView.__discriminator__ + view_intr['derived_callable'] = derived_view + + self.registry._clear_view_lookup_cache() + + def derive_view(isexc_only, renderer): # added by discrim_func above during conflict resolving preds = view_intr['predicates'] order = view_intr['order'] phash = view_intr['phash'] - # __no_permission_required__ handled by _secure_view derived_view = self._derive_view( view, route_name=route_name, @@ -904,6 +952,7 @@ class ViewsConfiguratorMixin(object): predicates=preds, attr=attr, context=context, + exception_only=isexc_only, renderer=renderer, wrapper_viewname=wrapper, viewname=name, @@ -916,14 +965,9 @@ class ViewsConfiguratorMixin(object): require_csrf=require_csrf, extra_options=ovals, ) - derived_view.__discriminator__ = lambda *arg: discriminator - # __discriminator__ is used by superdynamic systems - # that require it for introspection after manual view lookup; - # see also MultiView.__discriminator__ - view_intr['derived_callable'] = derived_view - - registered = self.registry.adapters.registered + return derived_view + def register_view(classifier, request_iface, derived_view): # A multiviews is a set of views which are registered for # exactly the same context type/request type/name triad. Each # consituent view in a multiview differs only by the @@ -943,32 +987,27 @@ class ViewsConfiguratorMixin(object): # matches on all the arguments it receives. old_view = None + order, phash = view_intr['order'], view_intr['phash'] + registered = self.registry.adapters.registered for view_type in (IView, ISecuredView, IMultiView): - old_view = registered((IViewClassifier, request_iface, - r_context), view_type, name) + old_view = registered( + (classifier, request_iface, r_context), + view_type, name) if old_view is not None: break - isexc = isexception(context) - def regclosure(): if hasattr(derived_view, '__call_permissive__'): view_iface = ISecuredView else: view_iface = IView - if not exception_only: - self.registry.registerAdapter( - derived_view, - (IViewClassifier, request_iface, context), - view_iface, - name - ) - if isexc: - self.registry.registerAdapter( - derived_view, - (IExceptionViewClassifier, request_iface, context), - view_iface, name) + self.registry.registerAdapter( + derived_view, + (classifier, request_iface, context), + view_iface, + name + ) is_multiview = IMultiView.providedBy(old_view) old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH) @@ -1005,39 +1044,12 @@ class ViewsConfiguratorMixin(object): for view_type in (IView, ISecuredView): # unregister any existing views self.registry.adapters.unregister( - (IViewClassifier, request_iface, r_context), + (classifier, request_iface, r_context), view_type, name=name) - if isexc: - self.registry.adapters.unregister( - (IExceptionViewClassifier, request_iface, - r_context), view_type, name=name) self.registry.registerAdapter( multiview, - (IViewClassifier, request_iface, context), + (classifier, request_iface, context), IMultiView, name=name) - if isexc: - self.registry.registerAdapter( - multiview, - (IExceptionViewClassifier, request_iface, context), - IMultiView, name=name) - - self.registry._clear_view_lookup_cache() - renderer_type = getattr(renderer, 'type', None) # gard against None - intrspc = self.introspector - if ( - renderer_type is not None and - tmpl_intr is not None and - intrspc is not None and - intrspc.get('renderer factories', renderer_type) is not None - ): - # allow failure of registered template factories to be deferred - # until view execution, like other bad renderer factories; if - # we tried to relate this to an existing renderer factory - # without checking if it the factory actually existed, we'd end - # up with a KeyError at startup time, which is inconsistent - # with how other bad renderer registrations behave (they throw - # a ValueError at view execution time) - tmpl_intr.relate('renderer factories', renderer.type) if mapper: mapper_intr = self.introspectable( @@ -1351,7 +1363,8 @@ class ViewsConfiguratorMixin(object): viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH, decorator=None, route_name=None, mapper=None, http_cache=None, context=None, - require_csrf=None, extra_options=None): + require_csrf=None, exception_only=False, + extra_options=None): view = self.maybe_dotted(view) mapper = self.maybe_dotted(mapper) if isinstance(renderer, string_types): @@ -1389,6 +1402,7 @@ class ViewsConfiguratorMixin(object): registry=self.registry, package=self.package, predicates=predicates, + exception_only=exception_only, options=options, ) @@ -1443,21 +1457,25 @@ class ViewsConfiguratorMixin(object): argument restricts the set of circumstances under which this forbidden view will be invoked. Unlike :meth:`pyramid.config.Configurator.add_view`, this method will raise - an exception if passed ``name``, ``permission``, ``context``, - ``for_``, or ``http_cache`` keyword arguments. These argument values - make no sense in the context of a forbidden view. + an exception if passed ``name``, ``permission``, ``require_csrf``, + ``context``, ``for_`` or ``exception_only`` keyword arguments. These + argument values make no sense in the context of a forbidden + :term:`exception view`. .. versionadded:: 1.3 + + .. versionchanged:: 1.8 + + The view is created using ``exception_only=True``. """ for arg in ( - 'name', 'permission', 'context', 'for_', 'http_cache', - 'require_csrf', + 'name', 'permission', 'context', 'for_', 'require_csrf', + 'exception_only', ): if arg in view_options: raise ConfigurationError( '%s may not be used as an argument to add_forbidden_view' - % arg - ) + % (arg,)) if view is None: view = default_exceptionresponse_view @@ -1465,6 +1483,7 @@ class ViewsConfiguratorMixin(object): settings = dict( view=view, context=HTTPForbidden, + exception_only=True, wrapper=wrapper, request_type=request_type, request_method=request_method, @@ -1513,9 +1532,9 @@ class ViewsConfiguratorMixin(object): append_slash=False, **view_options ): - """ Add a default Not Found View to the current configuration state. - The view will be called when Pyramid or application code raises an - :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g. when a + """ Add a default :term:`Not Found View` to the current configuration + state. The view will be called when Pyramid or application code raises + an :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g. when a view cannot be found for the request). The simplest example is: .. code-block:: python @@ -1533,9 +1552,9 @@ class ViewsConfiguratorMixin(object): argument restricts the set of circumstances under which this notfound view will be invoked. Unlike :meth:`pyramid.config.Configurator.add_view`, this method will raise - an exception if passed ``name``, ``permission``, ``context``, - ``for_``, or ``http_cache`` keyword arguments. These argument values - make no sense in the context of a Not Found View. + an exception if passed ``name``, ``permission``, ``require_csrf``, + ``context``, ``for_``, or ``exception_only`` keyword arguments. These + argument values make no sense in the context of a Not Found View. If ``append_slash`` is ``True``, when this Not Found View is invoked, and the current path info does not end in a slash, the notfound logic @@ -1562,18 +1581,22 @@ class ViewsConfiguratorMixin(object): being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will be used` for the redirect response if a slash-appended route is found. - .. versionchanged:: 1.6 .. versionadded:: 1.3 + + .. versionchanged:: 1.6 + + .. versionchanged:: 1.8 + + The view is created using ``exception_only=True``. """ for arg in ( - 'name', 'permission', 'context', 'for_', 'http_cache', - 'require_csrf', + 'name', 'permission', 'context', 'for_', 'require_csrf', + 'exception_only', ): if arg in view_options: raise ConfigurationError( '%s may not be used as an argument to add_notfound_view' - % arg - ) + % (arg,)) if view is None: view = default_exceptionresponse_view @@ -1581,6 +1604,7 @@ class ViewsConfiguratorMixin(object): settings = dict( view=view, context=HTTPNotFound, + exception_only=True, wrapper=wrapper, request_type=request_type, request_method=request_method, @@ -1621,64 +1645,40 @@ class ViewsConfiguratorMixin(object): self, view=None, context=None, - attr=None, - renderer=None, - wrapper=None, - route_name=None, - request_type=None, - request_method=None, - request_param=None, - containment=None, - xhr=None, - accept=None, - header=None, - path_info=None, - custom_predicates=(), - decorator=None, - mapper=None, - match_param=None, + # force all other arguments to be specified as key=value **view_options - ): - """ Add a view for an exception to the current configuration state. - The view will be called when Pyramid or application code raises an - the given exception. + ): + """ Add an :term:`exception view` for the specified ``exception`` to + the current configuration state. The view will be called when Pyramid + or application code raises the given exception. + + This method accepts accepts almost all of the same arguments as + :meth:`pyramid.config.Configurator.add_view` except for ``name``, + ``permission``, ``for_``, ``require_csrf`` and ``exception_only``. + + By default, this method will set ``context=Exception`` thus + registering for most default Python exceptions. Any subclass of + ``Exception`` may be specified. .. versionadded:: 1.8 """ for arg in ( - 'name', 'permission', 'for_', 'http_cache', - 'require_csrf', 'exception_only', + 'name', 'for_', 'exception_only', 'require_csrf', 'permission', ): if arg in view_options: raise ConfigurationError( '%s may not be used as an argument to add_exception_view' - % arg - ) + % (arg,)) if context is None: - raise ConfigurationError('context exception must be specified') - settings = dict( + context = Exception + view_options.update(dict( view=view, context=context, - wrapper=wrapper, - renderer=renderer, - request_type=request_type, - request_method=request_method, - request_param=request_param, - containment=containment, - xhr=xhr, - accept=accept, - header=header, - path_info=path_info, - custom_predicates=custom_predicates, - decorator=decorator, - mapper=mapper, - match_param=match_param, - route_name=route_name, + exception_only=True, permission=NO_PERMISSION_REQUIRED, require_csrf=False, - exception_only=True, - ) - return self.add_view(**settings) + )) + return self.add_view(**view_options) @action_method def set_view_mapper(self, mapper): @@ -1859,14 +1859,63 @@ def isexception(o): (inspect.isclass(o) and (issubclass(o, Exception))) ) +def runtime_exc_view(view, excview): + # create a view callable which can pretend to be both a normal view + # and an exception view, dispatching to the appropriate one based + # on the state of request.exception + def wrapper_view(context, request): + if getattr(request, 'exception', None): + return excview(context, request) + return view(context, request) + + # these constants are the same between the two views + wrapper_view.__wraps__ = wrapper_view + wrapper_view.__original_view__ = getattr(view, '__original_view__', view) + wrapper_view.__module__ = view.__module__ + wrapper_view.__doc__ = view.__doc__ + wrapper_view.__name__ = view.__name__ + + wrapper_view.__accept__ = getattr(view, '__accept__', None) + wrapper_view.__order__ = getattr(view, '__order__', MAX_ORDER) + wrapper_view.__phash__ = getattr(view, '__phash__', DEFAULT_PHASH) + wrapper_view.__view_attr__ = getattr(view, '__view_attr__', None) + wrapper_view.__permission__ = getattr(view, '__permission__', None) + + def wrap_fn(attr): + def wrapper(context, request): + if getattr(request, 'exception', None): + selected_view = excview + else: + selected_view = view + fn = getattr(selected_view, attr, None) + if fn is not None: + return fn(context, request) + return wrapper + + # these methods are dynamic per-request and should dispatch to their + # respective views based on whether it's an exception or not + wrapper_view.__call_permissive__ = wrap_fn('__call_permissive__') + wrapper_view.__permitted__ = wrap_fn('__permitted__') + wrapper_view.__predicated__ = wrap_fn('__predicated__') + wrapper_view.__predicates__ = wrap_fn('__predicates__') + return wrapper_view + @implementer(IViewDeriverInfo) class ViewDeriverInfo(object): - def __init__(self, view, registry, package, predicates, options): + def __init__(self, + view, + registry, + package, + predicates, + exception_only, + options, + ): self.original_view = view self.registry = registry self.package = package self.predicates = predicates or [] self.options = options or {} + self.exception_only = exception_only @reify def settings(self): diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index a8a10f927..c95922eb0 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -109,6 +109,7 @@ class ConfigurationExecutionError(ConfigurationError): def __str__(self): return "%s: %s\n in:\n %s" % (self.etype, self.evalue, self.info) + class CyclicDependencyError(Exception): """ The exception raised when the Pyramid topological sorter detects a cyclic dependency.""" diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index b252d0f4a..114f802aa 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1234,6 +1234,7 @@ class IViewDeriverInfo(Interface): 'default values that were not overriden') predicates = Attribute('The list of predicates active on the view') original_view = Attribute('The original view object being wrapped') + exception_only = Attribute('The view will only be invoked for exceptions') class IViewDerivers(Interface): """ Interface for view derivers list """ diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 1adde9225..f020485de 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -20,15 +20,16 @@ class TestViewsConfigurationMixin(unittest.TestCase): config = Configurator(*arg, **kw) return config - def _getViewCallable(self, config, ctx_iface=None, request_iface=None, - name='', exception_view=False): + def _getViewCallable(self, config, ctx_iface=None, exc_iface=None, + request_iface=None, name=''): from zope.interface import Interface from pyramid.interfaces import IRequest from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IExceptionViewClassifier - if exception_view: + if exc_iface: classifier = IExceptionViewClassifier + ctx_iface = exc_iface else: classifier = IViewClassifier if ctx_iface is None: @@ -489,7 +490,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=newview, xhr=True, context=RuntimeError, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(RuntimeError), exception_view=True) + config, exc_iface=implementedBy(RuntimeError)) self.assertFalse(IMultiView.providedBy(wrapper)) request = DummyRequest() request.is_xhr = True @@ -533,7 +534,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=newview, context=RuntimeError, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(RuntimeError), exception_view=True) + config, exc_iface=implementedBy(RuntimeError)) self.assertFalse(IMultiView.providedBy(wrapper)) request = DummyRequest() request.is_xhr = True @@ -581,7 +582,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=newview, context=RuntimeError, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(RuntimeError), exception_view=True) + config, exc_iface=implementedBy(RuntimeError)) self.assertFalse(IMultiView.providedBy(wrapper)) request = DummyRequest() request.is_xhr = True @@ -626,7 +627,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=view, context=RuntimeError, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(RuntimeError), exception_view=True) + config, exc_iface=implementedBy(RuntimeError)) self.assertTrue(IMultiView.providedBy(wrapper)) self.assertEqual(wrapper(None, None), 'OK') @@ -669,7 +670,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): ISecuredView, name='') config.add_view(view=view, context=RuntimeError, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(RuntimeError), exception_view=True) + config, exc_iface=implementedBy(RuntimeError)) self.assertTrue(IMultiView.providedBy(wrapper)) self.assertEqual(wrapper(None, None), 'OK') @@ -755,7 +756,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=view2, accept='text/html', context=RuntimeError, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(RuntimeError), exception_view=True) + config, exc_iface=implementedBy(RuntimeError)) self.assertTrue(IMultiView.providedBy(wrapper)) self.assertEqual(len(wrapper.views), 1) self.assertEqual(len(wrapper.media_views), 1) @@ -816,7 +817,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=view2, context=RuntimeError, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(RuntimeError), exception_view=True) + config, exc_iface=implementedBy(RuntimeError)) self.assertTrue(IMultiView.providedBy(wrapper)) self.assertEqual(len(wrapper.views), 1) self.assertEqual(len(wrapper.media_views), 1) @@ -843,31 +844,71 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertEqual([x[:2] for x in wrapper.views], [(view2, None)]) self.assertEqual(wrapper(None, None), 'OK1') - def test_add_view_exc_multiview_replaces_multiview(self): + def test_add_view_exc_multiview_replaces_multiviews(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy from pyramid.interfaces import IRequest from pyramid.interfaces import IMultiView from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IExceptionViewClassifier - view = DummyMultiView() + hot_view = DummyMultiView() + exc_view = DummyMultiView() config = self._makeOne(autocommit=True) config.registry.registerAdapter( - view, + hot_view, (IViewClassifier, IRequest, implementedBy(RuntimeError)), IMultiView, name='') config.registry.registerAdapter( - view, + exc_view, (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), IMultiView, name='') view2 = lambda *arg: 'OK2' config.add_view(view=view2, context=RuntimeError, renderer=null_renderer) - wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(RuntimeError), exception_view=True) - self.assertTrue(IMultiView.providedBy(wrapper)) - self.assertEqual([x[:2] for x in wrapper.views], [(view2, None)]) - self.assertEqual(wrapper(None, None), 'OK1') + hot_wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(hot_wrapper)) + self.assertEqual([x[:2] for x in hot_wrapper.views], [(view2, None)]) + self.assertEqual(hot_wrapper(None, None), 'OK1') + + exc_wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(exc_wrapper)) + self.assertEqual([x[:2] for x in exc_wrapper.views], [(view2, None)]) + self.assertEqual(exc_wrapper(None, None), 'OK1') + + def test_add_view_exc_multiview_replaces_only_exc_multiview(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + hot_view = DummyMultiView() + exc_view = DummyMultiView() + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + hot_view, + (IViewClassifier, IRequest, implementedBy(RuntimeError)), + IMultiView, name='') + config.registry.registerAdapter( + exc_view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IMultiView, name='') + view2 = lambda *arg: 'OK2' + config.add_view(view=view2, context=RuntimeError, exception_only=True, + renderer=null_renderer) + hot_wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(hot_wrapper)) + self.assertEqual(len(hot_wrapper.views), 0) + self.assertEqual(hot_wrapper(None, None), 'OK1') + + exc_wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(exc_wrapper)) + self.assertEqual([x[:2] for x in exc_wrapper.views], [(view2, None)]) + self.assertEqual(exc_wrapper(None, None), 'OK1') def test_add_view_multiview_context_superclass_then_subclass(self): from pyramid.renderers import null_renderer @@ -886,10 +927,12 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.registry.registerAdapter( view, (IViewClassifier, IRequest, ISuper), IView, name='') config.add_view(view=view2, for_=ISub, renderer=null_renderer) - wrapper = self._getViewCallable(config, ISuper, IRequest) + wrapper = self._getViewCallable(config, ctx_iface=ISuper, + request_iface=IRequest) self.assertFalse(IMultiView.providedBy(wrapper)) self.assertEqual(wrapper(None, None), 'OK') - wrapper = self._getViewCallable(config, ISub, IRequest) + wrapper = self._getViewCallable(config, ctx_iface=ISub, + request_iface=IRequest) self.assertFalse(IMultiView.providedBy(wrapper)) self.assertEqual(wrapper(None, None), 'OK2') @@ -914,16 +957,16 @@ class TestViewsConfigurationMixin(unittest.TestCase): view, (IExceptionViewClassifier, IRequest, Super), IView, name='') config.add_view(view=view2, for_=Sub, renderer=null_renderer) wrapper = self._getViewCallable( - config, implementedBy(Super), IRequest) + config, ctx_iface=implementedBy(Super), request_iface=IRequest) wrapper_exc_view = self._getViewCallable( - config, implementedBy(Super), IRequest, exception_view=True) + config, exc_iface=implementedBy(Super), request_iface=IRequest) self.assertEqual(wrapper_exc_view, wrapper) self.assertFalse(IMultiView.providedBy(wrapper_exc_view)) self.assertEqual(wrapper_exc_view(None, None), 'OK') wrapper = self._getViewCallable( - config, implementedBy(Sub), IRequest) + config, ctx_iface=implementedBy(Sub), request_iface=IRequest) wrapper_exc_view = self._getViewCallable( - config, implementedBy(Sub), IRequest, exception_view=True) + config, exc_iface=implementedBy(Sub), request_iface=IRequest) self.assertEqual(wrapper_exc_view, wrapper) self.assertFalse(IMultiView.providedBy(wrapper_exc_view)) self.assertEqual(wrapper_exc_view(None, None), 'OK2') @@ -1233,8 +1276,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): renderer=null_renderer) request_iface = self._getRouteRequestIface(config, 'foo') wrapper_exc_view = self._getViewCallable( - config, ctx_iface=implementedBy(RuntimeError), - request_iface=request_iface, exception_view=True) + config, exc_iface=implementedBy(RuntimeError), + request_iface=request_iface) self.assertNotEqual(wrapper_exc_view, None) wrapper = self._getViewCallable( config, ctx_iface=implementedBy(RuntimeError), @@ -1820,8 +1863,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.renderers import null_renderer view1 = lambda *arg: 'OK' config = self._makeOne(autocommit=True) - config.add_view(view=view1, context=Exception, renderer=null_renderer, - exception_only=True) + config.add_view(view=view1, context=Exception, exception_only=True, + renderer=null_renderer) view = self._getViewCallable(config, ctx_iface=implementedBy(Exception)) self.assertTrue(view is None) @@ -1830,11 +1873,10 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.renderers import null_renderer view1 = lambda *arg: 'OK' config = self._makeOne(autocommit=True) - config.add_view(view=view1, context=Exception, renderer=null_renderer, - exception_only=True) + config.add_view(view=view1, context=Exception, exception_only=True, + renderer=null_renderer) view = self._getViewCallable( - config, ctx_iface=implementedBy(Exception), exception_view=True - ) + config, exc_iface=implementedBy(Exception)) self.assertEqual(view1, view) def test_add_view_exception_only_misconfiguration(self): @@ -1844,23 +1886,33 @@ class TestViewsConfigurationMixin(unittest.TestCase): pass self.assertRaises( ConfigurationError, - config.add_view, view, context=NotAnException, exception_only=True - ) + config.add_view, view, context=NotAnException, exception_only=True) def test_add_exception_view(self): from zope.interface import implementedBy - from pyramid.interfaces import IRequest from pyramid.renderers import null_renderer view1 = lambda *arg: 'OK' config = self._makeOne(autocommit=True) - config.add_exception_view(view=view1, context=Exception, renderer=null_renderer) + config.add_exception_view(view=view1, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(Exception), exception_view=True, - ) + config, exc_iface=implementedBy(Exception)) context = Exception() request = self._makeRequest(config) self.assertEqual(wrapper(context, request), 'OK') + def test_add_exception_view_with_subclass(self): + from zope.interface import implementedBy + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_exception_view(view=view1, context=ValueError, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(ValueError)) + context = ValueError() + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + def test_add_exception_view_disallows_name(self): config = self._makeOne(autocommit=True) self.assertRaises(ConfigurationError, @@ -1875,19 +1927,19 @@ class TestViewsConfigurationMixin(unittest.TestCase): context=Exception(), permission='foo') - def test_add_exception_view_disallows_for_(self): + def test_add_exception_view_disallows_require_csrf(self): config = self._makeOne(autocommit=True) self.assertRaises(ConfigurationError, config.add_exception_view, context=Exception(), - for_='foo') + require_csrf=True) - def test_add_exception_view_disallows_http_cache(self): + def test_add_exception_view_disallows_for_(self): config = self._makeOne(autocommit=True) self.assertRaises(ConfigurationError, config.add_exception_view, context=Exception(), - http_cache='foo') + for_='foo') def test_add_exception_view_disallows_exception_only(self): config = self._makeOne(autocommit=True) @@ -1896,21 +1948,14 @@ class TestViewsConfigurationMixin(unittest.TestCase): context=Exception(), exception_only=True) - def test_add_exception_view_requires_context(self): - config = self._makeOne(autocommit=True) - view = lambda *a: 'OK' - self.assertRaises(ConfigurationError, - config.add_exception_view, view=view) - def test_add_exception_view_with_view_defaults(self): from pyramid.renderers import null_renderer from pyramid.exceptions import PredicateMismatch - from pyramid.httpexceptions import HTTPNotFound from zope.interface import directlyProvides from zope.interface import implementedBy class view(object): __view_defaults__ = { - 'containment':'pyramid.tests.test_config.IDummy' + 'containment': 'pyramid.tests.test_config.IDummy' } def __init__(self, request): pass @@ -1922,7 +1967,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): context=Exception, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(Exception), exception_view=True) + config, exc_iface=implementedBy(Exception)) context = DummyContext() directlyProvides(context, IDummy) request = self._makeRequest(config) @@ -2043,7 +2088,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_forbidden_view(view, renderer=null_renderer) request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPForbidden), + exc_iface=implementedBy(HTTPForbidden), request_iface=IRequest) result = view(None, request) self.assertEqual(result, 'OK') @@ -2057,7 +2102,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_forbidden_view() request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPForbidden), + exc_iface=implementedBy(HTTPForbidden), request_iface=IRequest) context = HTTPForbidden() result = view(context, request) @@ -2080,6 +2125,11 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertRaises(ConfigurationError, config.add_forbidden_view, permission='foo') + def test_add_forbidden_view_disallows_require_csrf(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, require_csrf=True) + def test_add_forbidden_view_disallows_context(self): config = self._makeOne(autocommit=True) self.assertRaises(ConfigurationError, @@ -2090,11 +2140,6 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertRaises(ConfigurationError, config.add_forbidden_view, for_='foo') - def test_add_forbidden_view_disallows_http_cache(self): - config = self._makeOne(autocommit=True) - self.assertRaises(ConfigurationError, - config.add_forbidden_view, http_cache='foo') - def test_add_forbidden_view_with_view_defaults(self): from pyramid.interfaces import IRequest from pyramid.renderers import null_renderer @@ -2115,7 +2160,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): view=view, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(HTTPForbidden), + config, exc_iface=implementedBy(HTTPForbidden), request_iface=IRequest) context = DummyContext() directlyProvides(context, IDummy) @@ -2135,7 +2180,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_notfound_view(view, renderer=null_renderer) request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPNotFound), + exc_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, (None, request)) @@ -2149,7 +2194,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_notfound_view() request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPNotFound), + exc_iface=implementedBy(HTTPNotFound), request_iface=IRequest) context = HTTPNotFound() result = view(context, request) @@ -2172,6 +2217,11 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertRaises(ConfigurationError, config.add_notfound_view, permission='foo') + def test_add_notfound_view_disallows_require_csrf(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, require_csrf=True) + def test_add_notfound_view_disallows_context(self): config = self._makeOne(autocommit=True) self.assertRaises(ConfigurationError, @@ -2182,11 +2232,6 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertRaises(ConfigurationError, config.add_notfound_view, for_='foo') - def test_add_notfound_view_disallows_http_cache(self): - config = self._makeOne(autocommit=True) - self.assertRaises(ConfigurationError, - config.add_notfound_view, http_cache='foo') - def test_add_notfound_view_append_slash(self): from pyramid.response import Response from pyramid.renderers import null_renderer @@ -2202,7 +2247,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): request.query_string = 'a=1&b=2' request.path = '/scriptname/foo' view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPNotFound), + exc_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertTrue(isinstance(result, HTTPFound)) @@ -2225,7 +2270,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): request.query_string = 'a=1&b=2' request.path = '/scriptname/foo' view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPNotFound), + exc_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertTrue(isinstance(result, HTTPMovedPermanently)) @@ -2251,7 +2296,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): view=view, renderer=null_renderer) wrapper = self._getViewCallable( - config, ctx_iface=implementedBy(HTTPNotFound), + config, exc_iface=implementedBy(HTTPNotFound), request_iface=IRequest) context = DummyContext() directlyProvides(context, IDummy) @@ -2281,7 +2326,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): renderer='json') request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPNotFound), + exc_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self._assertBody(result, '{}') @@ -2298,7 +2343,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): renderer='json') request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPForbidden), + exc_iface=implementedBy(HTTPForbidden), request_iface=IRequest) result = view(None, request) self._assertBody(result, '{}') @@ -2319,6 +2364,75 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.tests import test_config self.assertEqual(result, test_config) + def test_add_normal_and_exception_view_intr_derived_callable(self): + from pyramid.renderers import null_renderer + from pyramid.exceptions import BadCSRFToken + config = self._makeOne(autocommit=True) + introspector = DummyIntrospector() + config.introspector = introspector + view = lambda r: 'OK' + config.set_default_csrf_options(require_csrf=True) + config.add_view(view, context=Exception, renderer=null_renderer) + view_intr = introspector.introspectables[1] + self.assertTrue(view_intr.type_name, 'view') + self.assertEqual(view_intr['callable'], view) + derived_view = view_intr['derived_callable'] + + request = self._makeRequest(config) + request.method = 'POST' + request.scheme = 'http' + request.POST = {} + request.headers = {} + request.session = DummySession({'csrf_token': 'foo'}) + self.assertRaises(BadCSRFToken, lambda: derived_view(None, request)) + request.exception = Exception() + self.assertEqual(derived_view(None, request), 'OK') + +class Test_runtime_exc_view(unittest.TestCase): + def _makeOne(self, view1, view2): + from pyramid.config.views import runtime_exc_view + return runtime_exc_view(view1, view2) + + def test_call(self): + def view1(context, request): return 'OK' + def view2(context, request): raise AssertionError + result_view = self._makeOne(view1, view2) + request = DummyRequest() + result = result_view(None, request) + self.assertEqual(result, 'OK') + + def test_call_dispatches_on_exception(self): + def view1(context, request): raise AssertionError + def view2(context, request): return 'OK' + result_view = self._makeOne(view1, view2) + request = DummyRequest() + request.exception = Exception() + result = result_view(None, request) + self.assertEqual(result, 'OK') + + def test_permitted(self): + def errfn(context, request): raise AssertionError + def view1(context, request): raise AssertionError + view1.__permitted__ = lambda c, r: 'OK' + def view2(context, request): raise AssertionError + view2.__permitted__ = errfn + result_view = self._makeOne(view1, view2) + request = DummyRequest() + result = result_view.__permitted__(None, request) + self.assertEqual(result, 'OK') + + def test_permitted_dispatches_on_exception(self): + def errfn(context, request): raise AssertionError + def view1(context, request): raise AssertionError + view1.__permitted__ = errfn + def view2(context, request): raise AssertionError + view2.__permitted__ = lambda c, r: 'OK' + result_view = self._makeOne(view1, view2) + request = DummyRequest() + request.exception = Exception() + result = result_view.__permitted__(None, request) + self.assertEqual(result, 'OK') + class Test_requestonly(unittest.TestCase): def _callFUT(self, view, attr=None): from pyramid.config.views import requestonly diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 993209046..9cb0f58d1 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -90,5 +90,3 @@ class TestCyclicDependencyError(unittest.TestCase): result = str(exc) self.assertTrue("'a' sorts before ['c', 'd']" in result) self.assertTrue("'c' sorts before ['a']" in result) - - diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index d18c6eca4..cab42cf48 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -134,15 +134,24 @@ class Test_forbidden_view_config(BaseTest, unittest.TestCase): self.assertEqual(settings[0]['_info'], 'codeinfo') class Test_exception_view_config(BaseTest, unittest.TestCase): - def _makeOne(self, **kw): + def _makeOne(self, *args, **kw): from pyramid.view import exception_view_config - return exception_view_config(**kw) + return exception_view_config(*args, **kw) def test_ctor(self): inst = self._makeOne(context=Exception, path_info='path_info') self.assertEqual(inst.__dict__, {'context':Exception, 'path_info':'path_info'}) + def test_ctor_positional_exception(self): + inst = self._makeOne(Exception, path_info='path_info') + self.assertEqual(inst.__dict__, + {'context':Exception, 'path_info':'path_info'}) + + def test_ctor_positional_extras(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, lambda: self._makeOne(Exception, True)) + def test_it_function(self): def view(request): pass decorator = self._makeOne(context=Exception, renderer='renderer') diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index 79fcd6e71..676c6f66a 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -551,6 +551,28 @@ class TestDeriveView(unittest.TestCase): "'view_name' against context None): " "Allowed (NO_PERMISSION_REQUIRED)") + def test_debug_auth_permission_authpol_permitted_excview(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view( + view, context=Exception, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(Exception(), request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context Exception()): True") + def test_secured_view_authn_policy_no_authz_policy(self): response = DummyResponse() view = lambda *arg: response diff --git a/pyramid/view.py b/pyramid/view.py index 1895de96d..2af42b1e7 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -17,7 +17,10 @@ from pyramid.interfaces import ( from pyramid.compat import decode_path_info -from pyramid.exceptions import PredicateMismatch +from pyramid.exceptions import ( + ConfigurationError, + PredicateMismatch, +) from pyramid.httpexceptions import ( HTTPFound, @@ -166,7 +169,7 @@ class view_config(object): :class:`pyramid.view.bfg_view`. :class:`pyramid.view.view_config` supports the following keyword - arguments: ``context``, ``permission``, ``name``, + arguments: ``context``, ``exception``, ``permission``, ``name``, ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, @@ -325,7 +328,8 @@ class notfound_view_config(object): .. versionadded:: 1.3 An analogue of :class:`pyramid.view.view_config` which registers a - :term:`Not Found View`. + :term:`Not Found View` using + :meth:`pyramid.config.Configurator.add_notfound_view`. The ``notfound_view_config`` constructor accepts most of the same arguments as the constructor of :class:`pyramid.view.view_config`. It can be used @@ -413,7 +417,8 @@ class forbidden_view_config(object): .. versionadded:: 1.3 An analogue of :class:`pyramid.view.view_config` which registers a - :term:`forbidden view`. + :term:`forbidden view` using + :meth:`pyramid.config.Configurator.add_forbidden_view`. The forbidden_view_config constructor accepts most of the same arguments as the constructor of :class:`pyramid.view.view_config`. It can be used @@ -468,13 +473,15 @@ class exception_view_config(object): .. versionadded:: 1.8 An analogue of :class:`pyramid.view.view_config` which registers an - exception view. + :term:`exception view` using + :meth:`pyramid.config.Configurator.add_exception_view`. - The exception_view_config constructor requires an exception context, and - additionally accepts most of the same arguments as the constructor of + The ``exception_view_config`` constructor requires an exception context, + and additionally accepts most of the same arguments as the constructor of :class:`pyramid.view.view_config`. It can be used in the same places, - and behaves in largely the same way, except it always registers an exception - view instead of a 'normal' view. + and behaves in largely the same way, except it always registers an + exception view instead of a 'normal' view that dispatches on the request + :term:`context`. Example: @@ -483,17 +490,23 @@ class exception_view_config(object): from pyramid.view import exception_view_config from pyramid.response import Response - @exception_view_config(context=ValueError, renderer='json') - def error_view(context, request): - return {'error': str(context)} + @exception_view_config(ValueError, renderer='json') + def error_view(request): + return {'error': str(request.exception)} All arguments passed to this function have the same meaning as :meth:`pyramid.view.view_config` and each predicate argument restricts the set of circumstances under which this exception view will be invoked. + """ venusian = venusian - def __init__(self, **settings): + def __init__(self, *args, **settings): + if 'context' not in settings and len(args) > 0: + exception, args = args[0], args[1:] + settings['context'] = exception + if len(args) > 0: + raise ConfigurationError('unknown positional arguments') self.__dict__.update(settings) def __call__(self, wrapped): diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 5d138a02a..513ddf022 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -286,18 +286,16 @@ def _secured_view(view, info): authn_policy = info.registry.queryUtility(IAuthenticationPolicy) authz_policy = info.registry.queryUtility(IAuthorizationPolicy) + # no-op on exception-only views without an explicit permission + if explicit_val is None and info.exception_only: + return view + if authn_policy and authz_policy and (permission is not None): - def _permitted(context, request): + def permitted(context, request): principals = authn_policy.effective_principals(request) return authz_policy.permits(context, principals, permission) - def _secured_view(context, request): - if ( - getattr(request, 'exception', None) is not None and - explicit_val is None - ): - return view(context, request) - - result = _permitted(context, request) + def secured_view(context, request): + result = permitted(context, request) if result: return view(context, request) view_name = getattr(view, '__name__', view) @@ -305,10 +303,10 @@ def _secured_view(view, info): request, 'authdebug_message', 'Unauthorized: %s failed permission check' % view_name) raise HTTPForbidden(msg, result=result) - _secured_view.__call_permissive__ = view - _secured_view.__permitted__ = _permitted - _secured_view.__permission__ = permission - wrapped_view = _secured_view + wrapped_view = secured_view + wrapped_view.__call_permissive__ = view + wrapped_view.__permitted__ = permitted + wrapped_view.__permission__ = permission return wrapped_view @@ -321,14 +319,13 @@ def _authdebug_view(view, info): authn_policy = info.registry.queryUtility(IAuthenticationPolicy) authz_policy = info.registry.queryUtility(IAuthorizationPolicy) logger = info.registry.queryUtility(IDebugLogger) - if settings and settings.get('debug_authorization', False): - def _authdebug_view(context, request): - if ( - getattr(request, 'exception', None) is not None and - explicit_val is None - ): - return view(context, request) + # no-op on exception-only views without an explicit permission + if explicit_val is None and info.exception_only: + return view + + if settings and settings.get('debug_authorization', False): + def authdebug_view(context, request): view_name = getattr(request, 'view_name', None) if authn_policy and authz_policy: @@ -352,8 +349,7 @@ def _authdebug_view(view, info): if request is not None: request.authdebug_message = msg return view(context, request) - - wrapped_view = _authdebug_view + wrapped_view = authdebug_view return wrapped_view @@ -490,23 +486,22 @@ def csrf_view(view, info): token = defaults.token header = defaults.header safe_methods = defaults.safe_methods + enabled = ( explicit_val is True or - (explicit_val is not False and default_val) + # fallback to the default val if not explicitly enabled + # but only if the view is not an exception view + ( + explicit_val is not False and default_val and + not info.exception_only + ) ) # disable if both header and token are disabled enabled = enabled and (token or header) wrapped_view = view if enabled: def csrf_view(context, request): - if ( - request.method not in safe_methods and - ( - # skip exception views unless value is explicitly defined - getattr(request, 'exception', None) is None or - explicit_val is not None - ) - ): + if request.method not in safe_methods: check_csrf_origin(request, raises=True) check_csrf_token(request, token, header, raises=True) return view(context, request) -- cgit v1.2.3 From 4ce8d4f2894819f17385e835b0b329fbc793fac6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 28 Sep 2016 21:00:02 -0500 Subject: add changelog for #2660 --- CHANGES.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 85e71aef3..8f971b2ee 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -35,6 +35,23 @@ Features and pop threadlocals off of the stack to prevent memory leaks. See https://github.com/Pylons/pyramid/pull/2760 +- Added ``pyramid.config.Configurator.add_exception_view`` and the + ``pyramid.view.exception_view_config`` decorator. It is now possible using + these methods or via the new ``exception_only=True`` option to ``add_view`` + to add a view which will only be matched when handling an exception. + Previously any exception views were also registered for a traversal + context that inherited from the exception class which prevented any + exception-only optimizations. + See https://github.com/Pylons/pyramid/pull/2660 + +- Added the ``exception_only`` boolean to + ``pyramid.interfaces.IViewDeriverInfo`` which can be used by view derivers + to determine if they are wrapping a view which only handles exceptions. + This means that it is no longer necessary to perform request-time checks + for ``request.exception`` to determine if the view is handling an exception + - the pipeline can be optimized at config-time. + See https://github.com/Pylons/pyramid/pull/2660 + Bug Fixes --------- -- cgit v1.2.3 From 140edbb5eb8e3b5a5b6ca0c1bfc2240dd6ca234b Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 28 Sep 2016 22:12:36 -0500 Subject: make --interactive the default for pcreate --- pyramid/scripts/pcreate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index 1e8074fc5..f3121a915 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -56,7 +56,9 @@ class PCreateCommand(object): parser.add_option('--interactive', dest='interactive', action='store_true', - help='When a file would be overwritten, interrogate') + help='When a file would be overwritten, interrogate ' + '(this is the default, but you may specify it to ' + 'override --overwrite)') parser.add_option('--ignore-conflicting-name', dest='force_bad_name', action='store_true', @@ -70,6 +72,8 @@ class PCreateCommand(object): def __init__(self, argv, quiet=False): self.quiet = quiet self.options, self.args = self.parser.parse_args(argv[1:]) + if not self.options.interactive and not self.options.overwrite: + self.options.interactive = True self.scaffolds = self.all_scaffolds() def run(self): -- cgit v1.2.3 From 20c92a00a704c87242005d9ebcc7e6f5ae5093b2 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 28 Sep 2016 22:20:56 -0500 Subject: add changelog for #2775 --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 8f971b2ee..d4afe5f7a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -16,6 +16,12 @@ Backward Incompatibilities See https://github.com/Pylons/pyramid/pull/2615 +- ``pcreate`` is now interactive by default. You will be prompted if it + a file already exists with different content. Previously if there were + similar files it would silently skip them unless you specified + ``--interactive`` or ``--overwrite``. + See https://github.com/Pylons/pyramid/pull/2775 + Features -------- -- cgit v1.2.3 From 160aabb3798b4f613c89054ce4a60cba7374ddae Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 29 Sep 2016 00:04:01 -0700 Subject: Clean up docstrings/narr docs from PR #2660 - Closes #2768 --- docs/narr/viewconfig.rst | 10 +++++----- docs/narr/views.rst | 6 +++--- pyramid/config/views.py | 22 +++++++++++----------- pyramid/view.py | 6 +++--- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 76eaf3cc5..7cb8e0306 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -307,8 +307,8 @@ configured view. interface; it is otherwise false. It is possible to pass an exception class as the context if your context may - subclass an exception. In this case **two** views will be registered. One - will match normal incoming requests and the other will match as an + subclass an exception. In this case *two* views will be registered. One + will match normal incoming requests, and the other will match as an :term:`exception view` which only occurs when an exception is raised during the normal request processing pipeline. @@ -317,11 +317,11 @@ configured view. ``exception_only`` - When this value is ``True`` the ``context`` argument must be a subclass of + When this value is ``True``, the ``context`` argument must be a subclass of ``Exception``. This flag indicates that only an :term:`exception view` should - be created and that this view should not match if the traversal + be created, and that this view should not match if the traversal :term:`context` matches the ``context`` argument. If the ``context`` is a - subclass of ``Exception`` and this value is ``False`` (the default) then a + subclass of ``Exception`` and this value is ``False`` (the default), then a view will be registered to match the traversal :term:`context` as well. .. versionadded:: 1.8 diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 465062651..ab139ea19 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -262,13 +262,13 @@ specialized views as described in :ref:`special_exceptions_in_callables` can also be used by application developers to convert arbitrary exceptions to responses. -To register a :term:`exception view` that should be called whenever a +To register an :term:`exception view` that should be called whenever a particular exception is raised from within :app:`Pyramid` view code, use :meth:`pyramid.config.Configurator.add_exception_view` to register a view configuration which matches the exception (or a subclass of the exception) and points at a view callable for which you'd like to generate a response. The exception will be passed as the ``context`` argument to any -:term:`view predicate` registered with the view as well as to the view itself. +:term:`view predicate` registered with the view, as well as to the view itself. For convenience a new decorator exists, :class:`pyramid.views.exception_view_config`, which may be used to easily register exception views. @@ -334,7 +334,7 @@ which have a name will be ignored. In most cases, you should register an :term:`exception view` by using :meth:`pyramid.config.Configurator.add_exception_view`. However, it is - possible to register 'normal' (i.e., non-exception) views against a context + possible to register "normal" (i.e., non-exception) views against a context resource type which inherits from :exc:`Exception` (i.e., ``config.add_view(context=Exception)``). When the view configuration is processed, *two* views are registered. One as a "normal" view, the other diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 0b025911e..04e41d56e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -502,19 +502,19 @@ class ViewsConfiguratorMixin(object): if the :term:`context` provides the represented interface; it is otherwise false. This argument may also be provided to ``add_view`` as ``for_`` (an older, still-supported - spelling). If the view should **only** match when handling - exceptions then set the ``exception_only`` to ``True``. + spelling). If the view should *only* match when handling + exceptions, then set the ``exception_only`` to ``True``. exception_only .. versionadded:: 1.8 - When this value is ``True`` the ``context`` argument must be + When this value is ``True``, the ``context`` argument must be a subclass of ``Exception``. This flag indicates that only an - :term:`exception view` should be created and that this view should + :term:`exception view` should be created, and that this view should not match if the traversal :term:`context` matches the ``context`` argument. If the ``context`` is a subclass of ``Exception`` and - this value is ``False`` (the default) then a view will be + this value is ``False`` (the default), then a view will be registered to match the traversal :term:`context` as well. route_name @@ -908,7 +908,7 @@ class ViewsConfiguratorMixin(object): # allow failure of registered template factories to be deferred # until view execution, like other bad renderer factories; if # we tried to relate this to an existing renderer factory - # without checking if it the factory actually existed, we'd end + # without checking if the factory actually existed, we'd end # up with a KeyError at startup time, which is inconsistent # with how other bad renderer registrations behave (they throw # a ValueError at view execution time) @@ -1458,7 +1458,7 @@ class ViewsConfiguratorMixin(object): view will be invoked. Unlike :meth:`pyramid.config.Configurator.add_view`, this method will raise an exception if passed ``name``, ``permission``, ``require_csrf``, - ``context``, ``for_`` or ``exception_only`` keyword arguments. These + ``context``, ``for_``, or ``exception_only`` keyword arguments. These argument values make no sense in the context of a forbidden :term:`exception view`. @@ -1534,7 +1534,7 @@ class ViewsConfiguratorMixin(object): ): """ Add a default :term:`Not Found View` to the current configuration state. The view will be called when Pyramid or application code raises - an :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g. when a + an :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g., when a view cannot be found for the request). The simplest example is: .. code-block:: python @@ -1656,11 +1656,11 @@ class ViewsConfiguratorMixin(object): the current configuration state. The view will be called when Pyramid or application code raises the given exception. - This method accepts accepts almost all of the same arguments as + This method accepts almost all of the same arguments as :meth:`pyramid.config.Configurator.add_view` except for ``name``, - ``permission``, ``for_``, ``require_csrf`` and ``exception_only``. + ``permission``, ``for_``, ``require_csrf``, and ``exception_only``. - By default, this method will set ``context=Exception`` thus + By default, this method will set ``context=Exception``, thus registering for most default Python exceptions. Any subclass of ``Exception`` may be specified. diff --git a/pyramid/view.py b/pyramid/view.py index 28318b750..498bdde45 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -480,7 +480,7 @@ class exception_view_config(object): and additionally accepts most of the same arguments as the constructor of :class:`pyramid.view.view_config`. It can be used in the same places, and behaves in largely the same way, except it always registers an - exception view instead of a 'normal' view that dispatches on the request + exception view instead of a "normal" view that dispatches on the request :term:`context`. Example: @@ -495,7 +495,7 @@ class exception_view_config(object): return {'error': str(request.exception)} All arguments passed to this function have the same meaning as - :meth:`pyramid.view.view_config` and each predicate argument restricts + :meth:`pyramid.view.view_config`, and each predicate argument restricts the set of circumstances under which this exception view will be invoked. """ @@ -521,7 +521,7 @@ class exception_view_config(object): if info.scope == 'class': # if the decorator was attached to a method in a class, or # otherwise executed at class scope, we need to set an - # 'attr' into the settings if one isn't already in there + # 'attr' in the settings if one isn't already in there if settings.get('attr') is None: settings['attr'] = wrapped.__name__ -- cgit v1.2.3 From 0fdafb4336b83f567861260ee1b1cac51a23bb1f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 29 Sep 2016 00:06:11 -0700 Subject: two spaces should be one --- pyramid/config/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 04e41d56e..acdc00704 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1458,7 +1458,7 @@ class ViewsConfiguratorMixin(object): view will be invoked. Unlike :meth:`pyramid.config.Configurator.add_view`, this method will raise an exception if passed ``name``, ``permission``, ``require_csrf``, - ``context``, ``for_``, or ``exception_only`` keyword arguments. These + ``context``, ``for_``, or ``exception_only`` keyword arguments. These argument values make no sense in the context of a forbidden :term:`exception view`. -- cgit v1.2.3 From 17fa5e3ce891064231707bf30413b38b89bd6d7f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 30 Sep 2016 18:55:25 -0500 Subject: add a callback hook to set_default_csrf_options for disabling checks per-request fixes #2596 --- pyramid/config/security.py | 19 +++++++++++++++++-- pyramid/interfaces.py | 1 + pyramid/tests/test_config/test_security.py | 6 +++++- pyramid/tests/test_viewderivers.py | 28 ++++++++++++++++++++++++++++ pyramid/viewderivers.py | 7 ++++++- 5 files changed, 57 insertions(+), 4 deletions(-) diff --git a/pyramid/config/security.py b/pyramid/config/security.py index e387eade9..02732c042 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -169,6 +169,7 @@ class SecurityConfiguratorMixin(object): token='csrf_token', header='X-CSRF-Token', safe_methods=('GET', 'HEAD', 'OPTIONS', 'TRACE'), + callback=None, ): """ Set the default CSRF options used by subsequent view registrations. @@ -192,8 +193,20 @@ class SecurityConfiguratorMixin(object): never be automatically checked for CSRF tokens. Default: ``('GET', 'HEAD', 'OPTIONS', TRACE')``. + If ``callback`` is set, it must be a callable accepting ``(request)`` + and returning ``True`` if the request should be checked for a valid + CSRF token. This callback allows an application to support + alternate authentication methods that do not rely on cookies which + are not subject to CSRF attacks. For example, if a request is + authenticated using the ``Authorization`` header instead of a cookie, + this may return ``False`` for that request so that clients do not + need to send the ``X-CSRF-Token` header. The callback is only tested + for non-safe methods as defined by ``safe_methods``. + """ - options = DefaultCSRFOptions(require_csrf, token, header, safe_methods) + options = DefaultCSRFOptions( + require_csrf, token, header, safe_methods, callback, + ) def register(): self.registry.registerUtility(options, IDefaultCSRFOptions) intr = self.introspectable('default csrf view options', @@ -204,13 +217,15 @@ class SecurityConfiguratorMixin(object): intr['token'] = token intr['header'] = header intr['safe_methods'] = as_sorted_tuple(safe_methods) + intr['callback'] = callback self.action(IDefaultCSRFOptions, register, order=PHASE1_CONFIG, introspectables=(intr,)) @implementer(IDefaultCSRFOptions) class DefaultCSRFOptions(object): - def __init__(self, require_csrf, token, header, safe_methods): + def __init__(self, require_csrf, token, header, safe_methods, callback): self.require_csrf = require_csrf self.token = token self.header = header self.safe_methods = frozenset(safe_methods) + self.callback = callback diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 114f802aa..c1ddea63f 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -925,6 +925,7 @@ class IDefaultCSRFOptions(Interface): token = Attribute('The key to be matched in the body of the request.') header = Attribute('The header to be matched with the CSRF token.') safe_methods = Attribute('A set of safe methods that skip CSRF checks.') + callback = Attribute('A callback to disable CSRF checks per-request.') class ISessionFactory(Interface): """ An interface representing a factory which accepts a request object and diff --git a/pyramid/tests/test_config/test_security.py b/pyramid/tests/test_config/test_security.py index e461bfd4a..5db8e21fc 100644 --- a/pyramid/tests/test_config/test_security.py +++ b/pyramid/tests/test_config/test_security.py @@ -108,14 +108,18 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase): self.assertEqual(result.header, 'X-CSRF-Token') self.assertEqual(list(sorted(result.safe_methods)), ['GET', 'HEAD', 'OPTIONS', 'TRACE']) + self.assertTrue(result.callback is None) def test_changing_set_default_csrf_options(self): from pyramid.interfaces import IDefaultCSRFOptions config = self._makeOne(autocommit=True) + def callback(request): return True config.set_default_csrf_options( - require_csrf=False, token='DUMMY', header=None, safe_methods=('PUT',)) + require_csrf=False, token='DUMMY', header=None, + safe_methods=('PUT',), callback=callback) result = config.registry.getUtility(IDefaultCSRFOptions) self.assertEqual(result.require_csrf, False) self.assertEqual(result.token, 'DUMMY') self.assertEqual(result.header, None) self.assertEqual(list(sorted(result.safe_methods)), ['PUT']) + self.assertTrue(result.callback is callback) diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index 676c6f66a..51d0bd367 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -1291,6 +1291,34 @@ class TestDeriveView(unittest.TestCase): view = self.config._derive_view(inner_view) self.assertRaises(BadCSRFToken, lambda: view(None, request)) + def test_csrf_view_enabled_via_callback(self): + def callback(request): + return True + from pyramid.exceptions import BadCSRFToken + def inner_view(request): pass + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + self.config.set_default_csrf_options(require_csrf=True, callback=callback) + view = self.config._derive_view(inner_view) + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_csrf_view_disabled_via_callback(self): + def callback(request): + return False + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + self.config.set_default_csrf_options(require_csrf=True, callback=callback) + view = self.config._derive_view(inner_view) + result = view(None, request) + self.assertTrue(result is response) + def test_csrf_view_uses_custom_csrf_token(self): response = DummyResponse() def inner_view(request): diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 513ddf022..4eb0ce704 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -481,11 +481,13 @@ def csrf_view(view, info): token = 'csrf_token' header = 'X-CSRF-Token' safe_methods = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"]) + callback = None else: default_val = defaults.require_csrf token = defaults.token header = defaults.header safe_methods = defaults.safe_methods + callback = defaults.callback enabled = ( explicit_val is True or @@ -501,7 +503,10 @@ def csrf_view(view, info): wrapped_view = view if enabled: def csrf_view(context, request): - if request.method not in safe_methods: + if ( + request.method not in safe_methods and + (callback is None or callback(request)) + ): check_csrf_origin(request, raises=True) check_csrf_token(request, token, header, raises=True) return view(context, request) -- cgit v1.2.3 From 84b4962b2bb6ed065cb5bcc35d6302811f5637cd Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 2 Oct 2016 12:04:22 -0700 Subject: update HACKING.txt (somehow this got overlooked back on Apr 14, 2016) --- HACKING.txt | 221 +++++++++++++++++++++++++++++++----------------------------- 1 file changed, 115 insertions(+), 106 deletions(-) diff --git a/HACKING.txt b/HACKING.txt index 4b237b56c..73de79014 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -3,14 +3,16 @@ Hacking on Pyramid Here are some guidelines for hacking on Pyramid. + Using a Development Checkout ---------------------------- You'll have to create a development environment to hack on Pyramid, using a Pyramid checkout. You can either do this by hand, or if you have ``tox`` -installed (it's on PyPI), you can use tox to set up a working development +installed (it's on PyPI), you can use ``tox`` to set up a working development environment. Each installation method is described below. + By Hand +++++++ @@ -26,180 +28,186 @@ By Hand substituting your account username and specifying the destination as "hack-on-pyramid". - $ cd ~ - $ git clone git@github.com:USERNAME/pyramid.git hack-on-pyramid - $ cd hack-on-pyramid - # Configure remotes such that you can pull changes from the Pyramid - # repository into your local repository. - $ git remote add upstream https://github.com/Pylons/pyramid.git - # fetch and merge changes from upstream into master - $ git fetch upstream - $ git merge upstream/master + $ cd ~ + $ git clone git@github.com:USERNAME/pyramid.git hack-on-pyramid + $ cd hack-on-pyramid + # Configure remotes such that you can pull changes from the Pyramid + # repository into your local repository. + $ git remote add upstream https://github.com/Pylons/pyramid.git + # fetch and merge changes from upstream into master + $ git fetch upstream + $ git merge upstream/master Now your local repo is set up such that you will push changes to your GitHub repo, from which you can submit a pull request. -- Create a virtualenv in which to install Pyramid: - - $ cd ~/hack-on-pyramid - $ virtualenv -ppython2.7 env +- Create a virtual environment in which to install Pyramid: - Note that very old versions of virtualenv (virtualenv versions below, say, - 1.10 or thereabouts) require you to pass a ``--no-site-packages`` flag to - get a completely isolated environment. - - You can choose which Python version you want to use by passing a ``-p`` - flag to ``virtualenv``. For example, ``virtualenv -ppython2.7`` - chooses the Python 2.7 interpreter to be installed. + $ cd ~/hack-on-pyramid + $ python3 -m venv env From here on in within these instructions, the ``~/hack-on-pyramid/env`` virtual environment you created above will be referred to as ``$VENV``. To use the instructions in the steps that follow literally, use the ``export VENV=~/hack-on-pyramid/env`` command. -- Install ``setuptools-git`` into the virtualenv (for good measure, as we're - using git to do version control): +- Install ``setuptools-git`` into the virtual environment (for good measure, as + we're using git to do version control): - $ $VENV/bin/easy_install setuptools-git + $ $VENV/bin/pip install setuptools-git -- Install Pyramid from the checkout into the virtualenv using ``setup.py - dev``. ``setup.py dev`` is an alias for "setup.py develop" which also - installs testing requirements such as nose and coverage. Running - ``setup.py dev`` *must* be done while the current working directory is the - ``pyramid`` checkout directory: +- Install Pyramid from the checkout into the virtual environment, where the + current working directory is the ``pyramid`` checkout directory. We will + install Pyramid in editable (development) mode as well as its testing + requirements. - $ cd ~/hack-on-pyramid - $ $VENV/bin/python setup.py dev + $ cd ~/hack-on-pyramid + $ $VENV/bin/pip install -e ".[testing,docs]" - Optionally create a new Pyramid project using ``pcreate``: - $ cd $VENV - $ bin/pcreate -s starter starter + $ cd $VENV + $ bin/pcreate -s starter starter + +- ...and install the new project into the virtual environment: -- ...and install the new project (also using ``setup.py develop``) into the - virtualenv: + $ cd $VENV/starter + $ $VENV/bin/pip install -e . - $ cd $VENV/starter - $ $VENV/bin/python setup.py develop -Using Tox -+++++++++ +Using ``Tox`` ++++++++++++++ Alternatively, if you already have ``tox`` installed, there is an easier way to get going. - Create a new directory somewhere and ``cd`` to it: - $ mkdir ~/hack-on-pyramid - $ cd ~/hack-on-pyramid + $ mkdir ~/hack-on-pyramid + $ cd ~/hack-on-pyramid - Check out a read-only copy of the Pyramid source: - $ git clone git://github.com/Pylons/pyramid.git . + $ git clone git://github.com/Pylons/pyramid.git . - (alternately, create a writeable fork on GitHub and check that out). + Alternatively, create a writeable fork on GitHub and check that out. -Since Pyramid is a framework and not an application, it can be -convenient to work against a sample application, preferably in its own -virtualenv. A quick way to achieve this is to (ab-)use ``tox`` -(http://tox.readthedocs.org/en/latest/) with a custom configuration -file that's part of the checkout: +Since Pyramid is a framework and not an application, it can be convenient to +work against a sample application, preferably in its own virtual environment. A +quick way to achieve this is to use `tox +`_ with a custom configuration file +that is part of the checkout: - tox -c hacking-tox.ini + $ tox -c hacking-tox.ini -This will create a python-2.7 based virtualenv named ``env27`` (Pyramid's -``.gitconfig` ignores all top-level folders that start with ``env`` specifically -for this use case) and inside that a simple pyramid application named -``hacking`` that you can then fire up like so: +This will create a python-2.7 based virtual environment named ``env27`` +(Pyramid's ``.gitconfig` ignores all top-level folders that start with ``env`` +specifically in our use case), and inside that a simple pyramid application +named ``hacking`` that you can then fire up like so: + + $ cd env27/hacking + $ ../bin/pip install -e ".[testing,docs]" + $ ../bin/pserve development.ini - cd env27/hacking - ../bin/python setup.py develop - ../bin/pserve development.ini Adding Features --------------- In order to add a feature to Pyramid: -- The feature must be documented in both the API and narrative - documentation (in ``docs/``). +- The feature must be documented in both the API and narrative documentation + (in ``docs/``). - The feature must work fully on the following CPython versions: 2.7, 3.4, and 3.5 on both UNIX and Windows. - The feature must work on the latest version of PyPy. -- The feature must not cause installation or runtime failure on App Engine. - If it doesn't cause installation or runtime failure, but doesn't actually - *work* on these platforms, that caveat should be spelled out in the - documentation. +- The feature must not depend on any particular persistence layer (filesystem, + SQL, etc). -- The feature must not depend on any particular persistence layer - (filesystem, SQL, etc). +- The feature must not add unnecessary dependencies (where "unnecessary" is of + course subjective, but new dependencies should be discussed). -- The feature must not add unnecessary dependencies (where - "unnecessary" is of course subjective, but new dependencies should - be discussed). +The above requirements are relaxed for scaffolding dependencies. If a scaffold +has an install-time dependency on something that doesn't work on a particular +platform, that caveat should be spelled out clearly in *its* documentation +(within its ``docs/`` directory). -The above requirements are relaxed for scaffolding dependencies. If a -scaffold has an install-time dependency on something that doesn't work on a -particular platform, that caveat should be spelled out clearly in *its* -documentation (within its ``docs/`` directory). Coding Style ------------ -- PEP8 compliance. Whitespace rules are relaxed: not necessary to put - 2 newlines between classes. But 79-column lines, in particular, are - mandatory. See - http://docs.pylonsproject.org/en/latest/community/codestyle.html for more +- PEP8 compliance. Whitespace rules are relaxed: not necessary to put two + newlines between classes. But 79-column lines, in particular, are mandatory. + See http://docs.pylonsproject.org/en/latest/community/codestyle.html for more information. - Please do not remove trailing whitespace. Configure your editor to reduce diff noise. See https://github.com/Pylons/pyramid/issues/788 for more. + Running Tests -------------- -- To run all tests for Pyramid on a single Python version, run ``nosetests`` - from your development virtualenv (See *Using a Development Checkout* above). +- To run all tests for Pyramid on a single Python version from your development + virtual environment (See *Using a Development Checkout* above), run + ``nosetests``: -- To run individual tests (i.e. during development) you can use a regular + $ $VENV/bin/nosetests + +- To run individual tests (i.e., during development) you can use a regular expression with the ``-t`` parameter courtesy of the `nose-selecttests - `_ plugin that's been - installed (along with nose itself) via ``python setup.py dev``. The - easiest usage is to simply provide the verbatim name of the test you're - working on. - -- To run the full set of Pyramid tests on all platforms, install ``tox`` - (http://codespeak.net/~hpk/tox/) into a system Python. The ``tox`` console - script will be installed into the scripts location for that Python. While + `_ plugin that was installed + (along with nose itself) via ``pip install -e .``. The easiest usage is to + simply provide the verbatim name of the test you're working on. + +- The ``tox.ini`` uses ``nose`` and ``coverage``. As such ``tox`` may be used + to run groups of tests or only a specific version of Python. For example, the + following command will run tests on Python 2.7 only without coverage: + + $ tox -e py27 + + This command will run tests on the latest versions of Python 2 and 3 with + coverage totaled for both versions. + + $ tox -e py2-cover,py3-cover,coverage + +- To run the full set of Pyramid tests on all platforms, install `tox + `_ into a system Python. The ``tox`` console + script will be installed into the scripts location for that Python. While ``cd``'ed to the Pyramid checkout root directory (it contains ``tox.ini``), - invoke the ``tox`` console script. This will read the ``tox.ini`` file and - execute the tests on multiple Python versions and platforms; while it runs, - it creates a virtualenv for each version/platform combination. For - example:: + invoke the ``tox`` console script. This will read the ``tox.ini`` file and + execute the tests on multiple Python versions and platforms. While it runs, + it creates a virtual environment for each version/platform combination. For + example: + + $ sudo /usr/bin/pip install tox + $ cd ~/hack-on-pyramid/ + $ /usr/bin/tox - $ sudo /usr/bin/easy_install tox - $ cd ~/hack-on-pyramid/ - $ /usr/bin/tox +- The tests can also be run using `pytest `_. This is + intended as a convenience for people who are more used to or fond of + ``pytest``. Run the tests like so: -- The tests can also be run using ``pytest`` (http://pytest.org/). This is - intended as a convenience for people who are more used or fond of ``pytest``. - Run the tests like so:: + $ $VENV/bin/pip install pytest + $ $VENV/bin/py.test --strict pyramid/ - $ $VENV/bin/easy_install pytest - $ $VENV/bin/py.test --strict pyramid/ + To run individual tests (i.e., during development), see "py.test usage - + Specifying tests / selecting tests": + http://pytest.org/latest/usage.html#specifying-tests-selecting-tests - Functional tests related to the "scaffolds" (starter, zodb, alchemy) which - create a virtualenv, install the scaffold package and its dependencies, start - a server, and hit a URL on the server can be run like so:: + create a virtual environment, install the scaffold package and its + dependencies, start a server, and hit a URL on the server, can be run like + so: - $ ./scaffoldtests.sh + $ ./scaffoldtests.sh - Alternately:: + Alternatively: + + $ tox -e{py27,py34,py35,pypy}-scaffolds, - $ tox -e{py27,py34,py35,pypy}-scaffolds, Test Coverage ------------- @@ -208,6 +216,7 @@ Test Coverage can test coverage via ``./coverage.sh`` (which itself just executes ``tox -epy2-cover,py3-cover,coverage``). + Documentation Coverage and Building HTML Documentation ------------------------------------------------------ @@ -217,13 +226,14 @@ changed to reflect the bug fix, ideally in the same commit that fixes the bug or adds the feature. To build and review docs, use the following steps. 1. In the main Pyramid checkout directory, run ``./builddocs.sh`` (which just - turns around and runs ``tox -e docs``):: + turns around and runs ``tox -e docs``): - $ ./builddocs.sh + $ ./builddocs.sh 2. Open the ``docs/_build/html/index.html`` file to see the resulting HTML rendering. + Change Log ---------- @@ -231,4 +241,3 @@ Change Log file in the prevailing style. Changelog entries should be long and descriptive, not cryptic. Other developers should be able to know what your changelog entry means. - -- cgit v1.2.3 From 24c63558d04dc9ad2a1ee2243a63ace0d355ffe5 Mon Sep 17 00:00:00 2001 From: goodwillcoding Date: Sun, 2 Oct 2016 18:12:32 -0700 Subject: Add --package-name option to pcreate. This solves the problem of scaffold creating an existing directory where the package created should not be named after the base folder. For example if I am in I run pcreate in ~/code/trypyramid.com and would like to create package called tpc this is currently impossible. This solves the issues by allowing me to specify the package name on the command line --- pyramid/scripts/pcreate.py | 17 ++++++++++++++--- pyramid/tests/test_scripts/test_pcreate.py | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index f3121a915..7da501c49 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -45,6 +45,13 @@ class PCreateCommand(object): action='store_true', help=("A backwards compatibility alias for -l/--list. " "List all available scaffold names.")) + parser.add_option('--package-name', + dest='package_name', + action='store', + type='string', + help='Package name to use. Has to be a valid python ' + 'package name. (By default package name is derived ' + 'from output_directory base folder name)') parser.add_option('--simulate', dest='simulate', action='store_true', @@ -99,9 +106,13 @@ class PCreateCommand(object): def project_vars(self): output_dir = self.output_path project_name = os.path.basename(os.path.split(output_dir)[1]) - pkg_name = _bad_chars_re.sub( - '', project_name.lower().replace('-', '_')) - safe_name = pkg_resources.safe_name(project_name) + if self.options.package_name is None: + pkg_name = _bad_chars_re.sub( + '', project_name.lower().replace('-', '_')) + safe_name = pkg_resources.safe_name(project_name) + else: + pkg_name = self.options.package_name + safe_name = pkg_name egg_name = pkg_resources.to_filename(safe_name) # get pyramid package version diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index eaa7c1464..b7013bc73 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -80,6 +80,27 @@ class TestPCreateCommand(unittest.TestCase): {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + def test_scaffold_with_package_name(self): + import os + cmd = self._makeOne('-s', 'dummy', '--package-name', 'dummy_package', + 'Distro') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.1") + result = cmd.run() + + self.assertEqual(result, 0) + self.assertEqual( + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'dummy_package', + 'package': 'dummy_package', 'pyramid_version': '0.1', + 'pyramid_docs_branch':'0.1-branch'}) + + def test_scaffold_with_hyphen_in_project_name(self): import os cmd = self._makeOne('-s', 'dummy', 'Distro-') -- cgit v1.2.3 From 5aa7573db77281bd09981ae29d4700d839a864b8 Mon Sep 17 00:00:00 2001 From: goodwillcoding Date: Sun, 2 Oct 2016 18:33:20 -0700 Subject: update pcreate --package-name help with better verbiage which more explicitly states that package name is assumed to be safe --- pyramid/scripts/pcreate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index 7da501c49..5cae5cc5d 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -49,8 +49,9 @@ class PCreateCommand(object): dest='package_name', action='store', type='string', - help='Package name to use. Has to be a valid python ' - 'package name. (By default package name is derived ' + help='Package name to use. Named provided is assumed to ' + 'be a valid python package name and will not be ' + 'validated. (By default package name is derived ' 'from output_directory base folder name)') parser.add_option('--simulate', dest='simulate', -- cgit v1.2.3 From 44d0f6ae8d706b542f0bcc9f96b9fb9c502187b7 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 2 Oct 2016 20:46:19 -0600 Subject: Add CHANGES.txt for #2783 --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index d4afe5f7a..63ad37c81 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -25,6 +25,10 @@ Backward Incompatibilities Features -------- +- pcreate learned about --package-name to allow you to create a new project in + an existing folder with a different package name than the project name. See + https://github.com/Pylons/pyramid/pull/2783 + - The `_get_credentials` private method of `BasicAuthAuthenticationPolicy` has been extracted into standalone function ``extract_http_basic_credentials` in `pyramid.authentication` module, this function extracts HTTP Basic -- cgit v1.2.3 From a897b56fef11df0c1691cd21e990dcb6027cba1a Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 2 Oct 2016 22:27:37 -0600 Subject: All of the tests assume that there is a Content-Type set But the application in this case doesn't actually send a Content-Type, and thus there is no charset, and thus the tests should be assuming the output is binary not text. Add a Content-Type to be sent back from the app. This is required to pass all tests on WebOb >1.7 where Request.get_response(app) will no longer add the default_content_type if a headerlist is passed in to Response.__init__, this allows the Response to match what was provided by the app. --- pyramid/tests/test_scripts/test_prequest.py | 43 +++++++++++++++++++---------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/pyramid/tests/test_scripts/test_prequest.py b/pyramid/tests/test_scripts/test_prequest.py index 95cec0518..45db0dbaf 100644 --- a/pyramid/tests/test_scripts/test_prequest.py +++ b/pyramid/tests/test_scripts/test_prequest.py @@ -34,7 +34,8 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._out, ['You must provide at least two arguments']) def test_command_two_args(self): - command = self._makeOne(['', 'development.ini', '/']) + command = self._makeOne(['', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._path_info, '/') self.assertEqual(self._spec, 'development.ini') @@ -42,7 +43,8 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._out, ['abc']) def test_command_path_doesnt_start_with_slash(self): - command = self._makeOne(['', 'development.ini', 'abc']) + command = self._makeOne(['', 'development.ini', 'abc'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._path_info, '/abc') self.assertEqual(self._spec, 'development.ini') @@ -60,7 +62,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_has_good_header_var(self): command = self._makeOne( - ['', '--header=name:value','development.ini', '/']) + ['', '--header=name:value','development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['HTTP_NAME'], 'value') self.assertEqual(self._path_info, '/') @@ -71,7 +74,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_w_basic_auth(self): command = self._makeOne( ['', '--login=user:password', - '--header=name:value','development.ini', '/']) + '--header=name:value','development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['HTTP_NAME'], 'value') self.assertEqual(self._environ['HTTP_AUTHORIZATION'], @@ -83,7 +87,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_has_content_type_header_var(self): command = self._makeOne( - ['', '--header=content-type:app/foo','development.ini', '/']) + ['', '--header=content-type:app/foo','development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['CONTENT_TYPE'], 'app/foo') self.assertEqual(self._path_info, '/') @@ -97,7 +102,9 @@ class TestPRequestCommand(unittest.TestCase): '--header=name:value', '--header=name2:value2', 'development.ini', - '/']) + '/'], + [('Content-Type', 'text/html; charset=UTF-8')] + ) command.run() self.assertEqual(self._environ['HTTP_NAME'], 'value') self.assertEqual(self._environ['HTTP_NAME2'], 'value2') @@ -107,7 +114,8 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._out, ['abc']) def test_command_method_get(self): - command = self._makeOne(['', '--method=GET', 'development.ini', '/']) + command = self._makeOne(['', '--method=GET', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['REQUEST_METHOD'], 'GET') self.assertEqual(self._path_info, '/') @@ -117,7 +125,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_post(self): from pyramid.compat import NativeIO - command = self._makeOne(['', '--method=POST', 'development.ini', '/']) + command = self._makeOne(['', '--method=POST', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -131,7 +140,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_put(self): from pyramid.compat import NativeIO - command = self._makeOne(['', '--method=PUT', 'development.ini', '/']) + command = self._makeOne(['', '--method=PUT', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -145,7 +155,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_patch(self): from pyramid.compat import NativeIO - command = self._makeOne(['', '--method=PATCH', 'development.ini', '/']) + command = self._makeOne(['', '--method=PATCH', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -160,7 +171,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_propfind(self): from pyramid.compat import NativeIO command = self._makeOne(['', '--method=PROPFIND', 'development.ini', - '/']) + '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -173,7 +185,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_options(self): from pyramid.compat import NativeIO command = self._makeOne(['', '--method=OPTIONS', 'development.ini', - '/']) + '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -184,7 +197,8 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._out, ['abc']) def test_command_with_query_string(self): - command = self._makeOne(['', 'development.ini', '/abc?a=1&b=2&c']) + command = self._makeOne(['', 'development.ini', '/abc?a=1&b=2&c'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['QUERY_STRING'], 'a=1&b=2&c') self.assertEqual(self._path_info, '/abc') @@ -194,7 +208,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_display_headers(self): command = self._makeOne( - ['', '--display-headers', 'development.ini', '/']) + ['', '--display-headers', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._path_info, '/') self.assertEqual(self._spec, 'development.ini') -- cgit v1.2.3 From e39cc85a8573148889a82318fb7c727361835790 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 3 Oct 2016 01:50:06 -0700 Subject: per @mmerickel comment https://github.com/Pylons/pyramid/pull/2782#pullrequestreview-2456763 --- HACKING.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING.txt b/HACKING.txt index 73de79014..cdfe5cf9c 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -90,7 +90,7 @@ way to get going. $ git clone git://github.com/Pylons/pyramid.git . - Alternatively, create a writeable fork on GitHub and check that out. + Alternatively, create a writeable fork on GitHub and clone it. Since Pyramid is a framework and not an application, it can be convenient to work against a sample application, preferably in its own virtual environment. A -- cgit v1.2.3 From 8edd76825a37708c74e6e7d6055b6cca73fd8f47 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 3 Oct 2016 02:03:39 -0700 Subject: Update HACKING.txt for running individual tests - nose-selecttests is optional, not pre-installed - Closes #2781 --- HACKING.txt | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/HACKING.txt b/HACKING.txt index cdfe5cf9c..953c386f9 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -148,7 +148,7 @@ Coding Style Running Tests --------------- +------------- - To run all tests for Pyramid on a single Python version from your development virtual environment (See *Using a Development Checkout* above), run @@ -156,11 +156,21 @@ Running Tests $ $VENV/bin/nosetests -- To run individual tests (i.e., during development) you can use a regular - expression with the ``-t`` parameter courtesy of the `nose-selecttests - `_ plugin that was installed - (along with nose itself) via ``pip install -e .``. The easiest usage is to - simply provide the verbatim name of the test you're working on. +- To run individual tests (i.e., during development), you can use ``nosetests`` + syntax as follows: + + # run a single test + $ $VENV/bin/nosetests pyramid.tests.test_module:ClassName.test_mytestname + + # run all tests in a class + $ $VENV/bin/nosetests pyramid.tests.test_module:ClassName + + Optionally you can install a nose plugin, `nose-selecttests + `_, and use a regular + expression with the ``-t`` parameter to run tests. + + # run a single test + $ $VENV/bin/nosetests -t test_mytestname - The ``tox.ini`` uses ``nose`` and ``coverage``. As such ``tox`` may be used to run groups of tests or only a specific version of Python. For example, the -- cgit v1.2.3 From f27435e8feecc78b29c58b1bd4bfd9f0b52e7e2c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 3 Oct 2016 02:38:25 -0700 Subject: Update changes for #2782 --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 63ad37c81..77129bdb1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -97,6 +97,9 @@ Deprecations Documentation Changes --------------------- +- Update HACKING.txt from stale branch that was never merged to master. + See https://github.com/Pylons/pyramid/pull/2782 + - Updated Windows installation instructions and related bits. See https://github.com/Pylons/pyramid/issues/2661 -- cgit v1.2.3 From 70876d093da872bc0246fcec3a2cc02cbf59807d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 3 Oct 2016 02:47:08 -0700 Subject: Minor grammar fixes to pcreate --package-name help --- pyramid/scripts/pcreate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index 5cae5cc5d..a954d3be6 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -49,10 +49,10 @@ class PCreateCommand(object): dest='package_name', action='store', type='string', - help='Package name to use. Named provided is assumed to ' - 'be a valid python package name and will not be ' - 'validated. (By default package name is derived ' - 'from output_directory base folder name)') + help='Package name to use. The name provided is assumed ' + 'to be a valid Python package name, and will not ' + 'be validated. By default the package name is ' + 'derived from the value of output_directory.') parser.add_option('--simulate', dest='simulate', action='store_true', -- cgit v1.2.3 From bf22edc86d0f8dc9b072b974cfd5b4604688efa6 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 6 Oct 2016 11:43:22 -0700 Subject: Add pyramid_nacl_session to session factories - closes #2791 --- docs/narr/sessions.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index a1319e45f..5b24201a9 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -157,6 +157,12 @@ The following session factories exist at the time of this writing. ======================= ======= ============================= Session Factory Backend Description ======================= ======= ============================= +pyramid_nacl_session_ PyNaCl_ Defines an encrypting, + pickle-based cookie + serializer, using PyNaCl to + generate the symmetric + encryption for the cookie + state. pyramid_redis_sessions_ Redis_ Server-side session library for Pyramid, using Redis for storage. @@ -165,6 +171,9 @@ pyramid_beaker_ Beaker_ Session factory for Pyramid sessioning system. ======================= ======= ============================= +.. _pyramid_nacl_session: https://pypi.python.org/pypi/pyramid_nacl_session +.. _PyNaCl: https://pynacl.readthedocs.io/en/latest/secret/ + .. _pyramid_redis_sessions: https://pypi.python.org/pypi/pyramid_redis_sessions .. _Redis: http://redis.io/ -- cgit v1.2.3 From 4c5e14f3a843f588e380c865ac362bcc017b69a6 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 6 Oct 2016 11:44:57 -0700 Subject: Update changes with pyramid_nacl_session --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 77129bdb1..19708fde3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -97,6 +97,9 @@ Deprecations Documentation Changes --------------------- +- Add pyramid_nacl_session to session factories. + See https://github.com/Pylons/pyramid/issues/2791 + - Update HACKING.txt from stale branch that was never merged to master. See https://github.com/Pylons/pyramid/pull/2782 -- cgit v1.2.3 From 814f19943abfb4643add4d2454325616acb080b3 Mon Sep 17 00:00:00 2001 From: Jonathan Ballet Date: Mon, 10 Oct 2016 10:42:29 +0200 Subject: doc: fix typo --- pyramid/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 9b3f19510..47705d5d9 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -194,7 +194,7 @@ class JSON(object): Once this renderer is registered as above, you can use ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or - :meth:`~pyramid.config.Configurator.add_view``: + :meth:`~pyramid.config.Configurator.add_view`: .. code-block:: python -- cgit v1.2.3 From b716bb4beed508ae8ccd859c82f5b8672dd165a6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 14 Oct 2016 01:41:40 -0500 Subject: fix pserve to work with gevent workers - refactored code to remove ``pyramid.threadlocal`` from pserve's import tree. It was coming from the ``pyramid.paster`` module which imports ``pyramid.scripting`` which imports ``pyramid.threadlocal``. - dropped ``pyramid.scripts.common.logging_file_config`` as it appears to be unused and almost identical to ``setup_logging`` except without ``global_conf`` support. --- pyramid/paster.py | 27 +-------------------------- pyramid/scripts/common.py | 26 ++++++++++++++++---------- pyramid/scripts/prequest.py | 3 ++- pyramid/scripts/pserve.py | 3 +-- pyramid/scripts/pshell.py | 3 +-- pyramid/tests/test_scripts/test_common.py | 30 ------------------------------ 6 files changed, 21 insertions(+), 71 deletions(-) diff --git a/pyramid/paster.py b/pyramid/paster.py index 1b7afb5dc..5429a7860 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -5,9 +5,8 @@ from paste.deploy import ( appconfig, ) -from pyramid.compat import configparser -from logging.config import fileConfig from pyramid.scripting import prepare +from pyramid.scripts.common import setup_logging # noqa, api def get_app(config_uri, name=None, options=None, loadapp=loadapp): """ Return the WSGI application named ``name`` in the PasteDeploy @@ -52,30 +51,6 @@ def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig): relative_to=here_dir, global_conf=options) -def setup_logging(config_uri, global_conf=None, - fileConfig=fileConfig, - configparser=configparser): - """ - 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) - 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: path, section = config_uri.split('#', 1) diff --git a/pyramid/scripts/common.py b/pyramid/scripts/common.py index cbc172e9b..fc141f6e2 100644 --- a/pyramid/scripts/common.py +++ b/pyramid/scripts/common.py @@ -17,20 +17,26 @@ def parse_vars(args): result[name] = value return result -def logging_file_config(config_file, fileConfig=fileConfig, - configparser=configparser): +def setup_logging(config_uri, global_conf=None, + fileConfig=fileConfig, + configparser=configparser): """ - Setup logging via the logging module's fileConfig function with the - specified ``config_file``, if applicable. + 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 = config_uri.split('#', 1)[0] parser = configparser.ConfigParser() - parser.read([config_file]) + parser.read([path]) if parser.has_section('loggers'): - config_file = os.path.abspath(config_file) - return fileConfig( - config_file, - dict(__file__=config_file, here=os.path.dirname(config_file)) - ) + config_file = os.path.abspath(path) + 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) diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py index e07f9d10e..14a132bdb 100644 --- a/pyramid/scripts/prequest.py +++ b/pyramid/scripts/prequest.py @@ -5,8 +5,9 @@ import textwrap from pyramid.compat import url_unquote from pyramid.request import Request -from pyramid.paster import get_app, setup_logging +from pyramid.paster import get_app from pyramid.scripts.common import parse_vars +from pyramid.scripts.common import setup_logging def main(argv=sys.argv, quiet=False): command = PRequestCommand(argv, quiet) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index ec7f31704..0d22c9f3f 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -30,9 +30,8 @@ from paste.deploy.loadwsgi import loadcontext, SERVER from pyramid.compat import PY2 from pyramid.compat import WIN -from pyramid.paster import setup_logging - from pyramid.scripts.common import parse_vars +from pyramid.scripts.common import setup_logging MAXFD = 1024 diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index 0a7cfbbe5..56b1a15fa 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -10,11 +10,10 @@ from pyramid.compat import exec_ from pyramid.util import DottedNameResolver from pyramid.paster import bootstrap -from pyramid.paster import setup_logging - from pyramid.settings import aslist from pyramid.scripts.common import parse_vars +from pyramid.scripts.common import setup_logging def main(argv=sys.argv, quiet=False): command = PShellCommand(argv, quiet) diff --git a/pyramid/tests/test_scripts/test_common.py b/pyramid/tests/test_scripts/test_common.py index 13ab0ae6a..60741db92 100644 --- a/pyramid/tests/test_scripts/test_common.py +++ b/pyramid/tests/test_scripts/test_common.py @@ -1,22 +1,5 @@ -import os import unittest -class Test_logging_file_config(unittest.TestCase): - def _callFUT(self, config_file): - from pyramid.scripts.common import logging_file_config - dummy_cp = DummyConfigParserModule - return logging_file_config(config_file, self.fileConfig, dummy_cp) - - def test_it(self): - config_file, dict = self._callFUT('/abc') - # use of os.path.abspath here 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 fileConfig(self, config_file, dict): - return config_file, dict - class TestParseVars(unittest.TestCase): def test_parse_vars_good(self): from pyramid.scripts.common import parse_vars @@ -28,16 +11,3 @@ class TestParseVars(unittest.TestCase): from pyramid.scripts.common import parse_vars vars = ['a'] self.assertRaises(ValueError, parse_vars, vars) - - -class DummyConfigParser(object): - def read(self, x): - pass - - def has_section(self, name): - return True - -class DummyConfigParserModule(object): - ConfigParser = DummyConfigParser - - -- cgit v1.2.3 From 325fc180ccf93716cdd1c959257a9864fcbee359 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 17 Oct 2016 22:06:45 -0500 Subject: changelog for #2797 --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 19708fde3..434557f89 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -62,6 +62,11 @@ Features - the pipeline can be optimized at config-time. See https://github.com/Pylons/pyramid/pull/2660 +- ``pserve`` should now work with ``gevent`` and other workers that need + to monkeypatch the process, assuming the server and / or the app do so + as soon as possible before importing the rest of pyramid. + See https://github.com/Pylons/pyramid/pull/2797 + Bug Fixes --------- -- cgit v1.2.3