From 499b78aea5bc94626a48022afcf8cf92afb55cf8 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Sun, 23 Aug 2015 11:59:51 -0400 Subject: Add a RouteFound event which will fire after a route is found --- pyramid/events.py | 19 +++++++++++++++++++ pyramid/interfaces.py | 8 ++++++++ pyramid/router.py | 3 +++ 3 files changed, 30 insertions(+) diff --git a/pyramid/events.py b/pyramid/events.py index 97375638e..78ebf4d70 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -11,6 +11,7 @@ from pyramid.interfaces import ( INewResponse, IApplicationCreated, IBeforeRender, + IRouteFound, ) class subscriber(object): @@ -129,6 +130,24 @@ class NewResponse(object): self.request = request self.response = response +@implementer(IRouteFound) +class RouteFound(object): + """ + An instance of this class is emitted as an :term:`event` after the + :app:`Pyramid` :term:`router` finds a :term:`route` object but before any + traversal or view code is executed. The instance has an attribute, + ``request``, which is the request object generated by :app:`Pyramid`. + + Notably, the request object will have an attributed named + ``matched_route``, which is the matched route that was found. + + This class implements the :class:`pyramid.interfaces.IRouteFound` + interface. + """ + + def __init__(self, request): + self.request = request + @implementer(IContextFound) class ContextFound(object): """ An instance of this class is emitted as an :term:`event` after diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 90534593c..baf36610a 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -25,6 +25,14 @@ class IContextFound(Interface): IAfterTraversal = IContextFound +class IRouteFound(Interface): + """ + An event type that is emitted whenever :app:`Pyramid` has found a route + but before it calls any traversal or view code. See the documentation + attached to :class:`pyramid.events.Routefound` for more information. + """ + request = Attribute('The request object') + class INewRequest(Interface): """ An event type that is emitted whenever :app:`Pyramid` begins to process a new request. See the documentation attached diff --git a/pyramid/router.py b/pyramid/router.py index 4054ef52e..c4b86f89d 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -20,6 +20,7 @@ from pyramid.events import ( ContextFound, NewRequest, NewResponse, + RouteFound, ) from pyramid.httpexceptions import HTTPNotFound @@ -112,6 +113,8 @@ class Router(object): name=route.name, default=IRequest) + has_listeners and notify(RouteFound(request)) + root_factory = route.factory or self.root_factory root = root_factory(request) -- cgit v1.2.3 From 9e9fa9ac40bdd79fbce69f94a13d705e40f3d458 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 22 Mar 2016 01:02:05 -0500 Subject: add a csrf_view to the view pipeline supporting a require_csrf option --- docs/narr/hooks.rst | 28 ++++++++++++++-------- pyramid/config/views.py | 34 ++++++++++++++++++++++++++- pyramid/tests/test_config/test_views.py | 41 +++++++++++++++++++++++++++++++++ pyramid/tests/test_viewderivers.py | 31 +++++++++++++++++++++++++ pyramid/viewderivers.py | 15 ++++++++++++ 5 files changed, 139 insertions(+), 10 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 2c3782387..e7db97565 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1580,6 +1580,11 @@ There are several built-in view derivers that :app:`Pyramid` will automatically apply to any view. Below they are defined in order from furthest to closest to the user-defined :term:`view callable`: +``csrf_view`` + + Used to check the CSRF token provided in the request. This element is a + no-op if ``require_csrf`` is not defined. + ``secured_view`` Enforce the ``permission`` defined on the view. This element is a no-op if no @@ -1656,27 +1661,32 @@ 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. -Let's look at one more example which will protect views by requiring a CSRF -token unless ``disable_csrf=True`` is passed to the view: +Let's override the default CSRF checker to default to on instead of off and +only check ``POST`` requests: .. code-block:: python :linenos: from pyramid.response import Response from pyramid.session import check_csrf_token + from pyramid.viewderivers import INGRESS - def require_csrf_view(view, info): + def csrf_view(view, info): + val = info.options.get('require_csrf', True) wrapper_view = view - if not info.options.get('disable_csrf', False): - def wrapper_view(context, request): + if val: + if val is True: + val = 'csrf_token' + def csrf_view(context, request): if request.method == 'POST': - check_csrf_token(request) + check_csrf_token(request, val, raises=True) return view(context, request) + wrapper_view = csrf_view return wrapper_view - require_csrf_view.options = ('disable_csrf',) + csrf_view.options = ('require_csrf',) - config.add_view_deriver(require_csrf_view) + config.add_view_deriver(csrf_view, 'csrf_view', over='secured_view', under=INGRESS) def protected_view(request): return Response('protected') @@ -1685,7 +1695,7 @@ token unless ``disable_csrf=True`` is passed to the view: return Response('unprotected') config.add_view(protected_view, name='safe') - config.add_view(unprotected_view, name='unsafe', disable_csrf=True) + config.add_view(unprotected_view, name='unsafe', require_csrf=False) Navigating to ``/safe`` with a POST request will then fail when the call to :func:`pyramid.session.check_csrf_token` raises a diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 3f6a9080d..58fdbfd06 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -213,6 +213,7 @@ class ViewsConfiguratorMixin(object): http_cache=None, match_param=None, check_csrf=None, + require_csrf=None, **view_options): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken @@ -366,6 +367,32 @@ class ViewsConfiguratorMixin(object): before returning the response from the view. This effectively disables any HTTP caching done by ``http_cache`` for that response. + require_csrf + + .. versionadded:: 1.7 + + If specified, this value should be one of ``None``, ``True``, + ``False``, or a string representing the 'check name'. If the value + is ``True`` or a string, CSRF checking will be performed. If the + value is ``False`` or ``None``, CSRF checking will not be performed. + + If the value provided is a string, that string will be used as the + 'check name'. If the value provided is ``True``, ``csrf_token`` will + be used as the check name. + + If CSRF checking is performed, the checked value will be the value + of ``request.params[check_name]``. This value will be compared + against the value of ``request.session.get_csrf_token()``, and the + check will pass if these two values are the same. If the check + passes, the associated view will be permitted to execute. If the + check fails, the associated view will not be permitted to execute + and a :class:`pyramid.exceptions.BadCSRFToken` exception will + be raised. This exception may be caught and handled by an + :term:`exception view`. + + Note that using this feature requires a :term:`session factory` to + have been configured. + wrapper The :term:`view name` of a different :term:`view @@ -805,6 +832,8 @@ class ViewsConfiguratorMixin(object): path_info=path_info, match_param=match_param, check_csrf=check_csrf, + http_cache=http_cache, + require_csrf=require_csrf, callable=view, mapper=mapper, decorator=decorator, @@ -860,6 +889,7 @@ class ViewsConfiguratorMixin(object): decorator=decorator, mapper=mapper, http_cache=http_cache, + require_csrf=require_csrf, extra_options=ovals, ) derived_view.__discriminator__ = lambda *arg: discriminator @@ -1183,6 +1213,7 @@ class ViewsConfiguratorMixin(object): def add_default_view_derivers(self): d = pyramid.viewderivers derivers = [ + ('csrf_view', d.csrf_view), ('secured_view', d.secured_view), ('owrapped_view', d.owrapped_view), ('http_cached_view', d.http_cached_view), @@ -1284,7 +1315,7 @@ class ViewsConfiguratorMixin(object): viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH, decorator=None, mapper=None, http_cache=None, context=None, - extra_options=None): + require_csrf=None, extra_options=None): view = self.maybe_dotted(view) mapper = self.maybe_dotted(mapper) if isinstance(renderer, string_types): @@ -1311,6 +1342,7 @@ class ViewsConfiguratorMixin(object): mapper=mapper, decorator=decorator, http_cache=http_cache, + require_csrf=require_csrf, ) if extra_options: options.update(extra_options) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index b2513c42c..55ead55c2 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1570,6 +1570,43 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=view2) self.assertRaises(ConfigurationConflictError, config.commit) + def test_add_view_with_csrf_header(self): + from pyramid.renderers import null_renderer + def view(request): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view, require_csrf=True, renderer=null_renderer) + view = self._getViewCallable(config) + request = self._makeRequest(config) + request.headers = {'X-CSRF-Token': 'foo'} + request.session = DummySession({'csrf_token': 'foo'}) + self.assertEqual(view(None, request), 'OK') + + def test_add_view_with_csrf_param(self): + from pyramid.renderers import null_renderer + def view(request): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view, require_csrf='st', renderer=null_renderer) + view = self._getViewCallable(config) + request = self._makeRequest(config) + request.params = {'st': 'foo'} + request.headers = {} + request.session = DummySession({'csrf_token': 'foo'}) + self.assertEqual(view(None, request), 'OK') + + def test_add_view_with_missing_csrf_header(self): + from pyramid.exceptions import BadCSRFToken + from pyramid.renderers import null_renderer + def view(request): return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view, require_csrf=True, renderer=null_renderer) + view = self._getViewCallable(config) + request = self._makeRequest(config) + request.headers = {} + request.session = DummySession({'csrf_token': 'foo'}) + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + def test_add_view_with_permission(self): from pyramid.renderers import null_renderer view1 = lambda *arg: 'OK' @@ -3233,3 +3270,7 @@ class DummyIntrospector(object): return self.getval def relate(self, a, b): self.related.append((a, b)) + +class DummySession(dict): + def get_csrf_token(self): + return self['csrf_token'] diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index 1823beb4d..0dd70b74a 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -1090,6 +1090,28 @@ class TestDeriveView(unittest.TestCase): self.assertRaises(ConfigurationError, self.config._derive_view, view, http_cache=(None,)) + def test_csrf_view_requires_header(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.session = DummySession({'csrf_token': 'foo'}) + request.headers = {'X-CSRF-Token': 'foo'} + view = self.config._derive_view(inner_view, require_csrf=True) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_requires_param(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.session = DummySession({'csrf_token': 'foo'}) + request.params['DUMMY'] = 'foo' + view = self.config._derive_view(inner_view, require_csrf='DUMMY') + result = view(None, request) + self.assertTrue(result is response) + class TestDerivationOrder(unittest.TestCase): def setUp(self): @@ -1110,6 +1132,7 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ + 'csrf_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1132,6 +1155,7 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ + 'csrf_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1152,6 +1176,7 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ + 'csrf_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1173,6 +1198,7 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ + 'csrf_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1408,6 +1434,7 @@ class DummyRequest: self.environ = environ self.params = {} self.cookies = {} + self.headers = {} self.response = DummyResponse() class DummyLogger: @@ -1428,6 +1455,10 @@ class DummySecurityPolicy: def permits(self, context, principals, permission): return self.permitted +class DummySession(dict): + def get_csrf_token(self): + return self['csrf_token'] + def parse_httpdate(s): import datetime # cannot use %Z, must use literal GMT; Jython honors timezone diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 8061e5d4a..7560fa67f 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -6,6 +6,7 @@ from zope.interface import ( ) from pyramid.security import NO_PERMISSION_REQUIRED +from pyramid.session import check_csrf_token from pyramid.response import Response from pyramid.interfaces import ( @@ -455,5 +456,19 @@ def decorated_view(view, info): decorated_view.options = ('decorator',) +def csrf_view(view, info): + val = info.options.get('require_csrf') + wrapped_view = view + if val: + if val is True: + val = 'csrf_token' + def csrf_view(context, request): + check_csrf_token(request, val, raises=True) + return view(context, request) + wrapped_view = csrf_view + return wrapped_view + +csrf_view.options = ('require_csrf',) + VIEW = 'VIEW' INGRESS = 'INGRESS' -- cgit v1.2.3 From 6b35eb6ca3b271e2943d37307c925c5733e082d9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 10 Apr 2016 20:50:10 -0500 Subject: rewrite csrf checks to support a global setting to turn it on - only check csrf on POST - support "pyramid.require_default_csrf" setting - support "require_csrf=True" to fallback to the global setting to determine the token name --- docs/glossary.rst | 8 ++ docs/narr/hooks.rst | 52 ++----------- docs/narr/sessions.rst | 42 ++++++++++- pyramid/config/settings.py | 7 +- pyramid/config/views.py | 37 +++++---- pyramid/settings.py | 7 +- pyramid/tests/test_config/test_views.py | 17 +++-- pyramid/tests/test_viewderivers.py | 129 +++++++++++++++++++++++++++++++- pyramid/viewderivers.py | 34 ++++++++- 9 files changed, 246 insertions(+), 87 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 039665926..ef9c66b99 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1098,3 +1098,11 @@ Glossary implementing the :class:`pyramid.interfaces.IViewDeriver` interface. Examples of built-in derivers including view mapper, the permission checker, and applying a renderer to a dictionary returned from the view. + + truthy string + A string represeting a value of ``True``. Acceptable values are + ``t``, ``true``, ``y``, ``yes``, ``on`` and ``1``. + + falsey string + A string represeting a value of ``False``. Acceptable values are + ``f``, ``false``, ``n``, ``no``, ``off`` and ``0``. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index e7db97565..28d1e09d5 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1580,11 +1580,6 @@ There are several built-in view derivers that :app:`Pyramid` will automatically apply to any view. Below they are defined in order from furthest to closest to the user-defined :term:`view callable`: -``csrf_view`` - - Used to check the CSRF token provided in the request. This element is a - no-op if ``require_csrf`` is not defined. - ``secured_view`` Enforce the ``permission`` defined on the view. This element is a no-op if no @@ -1595,6 +1590,12 @@ the user-defined :term:`view callable`: This element will also output useful debugging information when ``pyramid.debug_authorization`` is enabled. +``csrf_view`` + + Used to check the CSRF token provided in the request. This element is a + no-op if both the ``require_csrf`` view option and the + ``pyramid.require_default_csrf`` setting are disabled. + ``owrapped_view`` Invokes the wrapped view defined by the ``wrapper`` option. @@ -1661,47 +1662,6 @@ 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. -Let's override the default CSRF checker to default to on instead of off and -only check ``POST`` requests: - -.. code-block:: python - :linenos: - - from pyramid.response import Response - from pyramid.session import check_csrf_token - from pyramid.viewderivers import INGRESS - - def csrf_view(view, info): - val = info.options.get('require_csrf', True) - wrapper_view = view - if val: - if val is True: - val = 'csrf_token' - def csrf_view(context, request): - if request.method == 'POST': - check_csrf_token(request, val, raises=True) - return view(context, request) - wrapper_view = csrf_view - return wrapper_view - - csrf_view.options = ('require_csrf',) - - config.add_view_deriver(csrf_view, 'csrf_view', over='secured_view', under=INGRESS) - - def protected_view(request): - return Response('protected') - - def unprotected_view(request): - return Response('unprotected') - - config.add_view(protected_view, name='safe') - config.add_view(unprotected_view, name='unsafe', require_csrf=False) - -Navigating to ``/safe`` with a POST request will then fail when the call to -:func:`pyramid.session.check_csrf_token` raises a -:class:`pyramid.exceptions.BadCSRFToken` exception. However, ``/unsafe`` will -not error. - Ordering View Derivers ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index db554a93b..3baed1cb8 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -389,8 +389,43 @@ header named ``X-CSRF-Token``. # ... -.. index:: - single: session.new_csrf_token +.. _auto_csrf_checking: + +Checking CSRF Tokens Automatically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.7 + +:app:`Pyramid` supports automatically checking CSRF tokens on POST requests. +Any other request may be checked manually. This feature can be turned on +globally for an application using the ``pyramid.require_default_csrf`` setting. + +If the ``pyramid.required_default_csrf`` setting is a :term:`truthy string` or +``True`` then the default CSRF token parameter will be ``csrf_token``. If a +different token is desired, it may be passed as the value. Finally, a +:term:`falsey string` or ``False`` will turn off automatic CSRF checking +globally on every POST request. + +No matter what, CSRF checking may be explicitly enabled or disabled on a +per-view basis using the ``require_csrf`` view option. This option is of the +same format as the ``pyramid.require_default_csrf`` setting, accepting strings +or boolean values. + +If ``require_csrf`` is ``True`` but does not explicitly define a token to +check, then the token name is pulled from whatever was set in the +``pyramid.require_default_csrf`` setting. Finally, if that setting does not +explicitly define a token, then ``csrf_token`` is the token required. This token +name will be required in ``request.params`` which is a combination of the +query string and a submitted form body. + +It is always possible to pass the token in the ``X-CSRF-Token`` header as well. +There is currently no way to define an alternate name for this header without +performing CSRF checking manually. + +If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` exception +will be raised. This exception may be caught and handled by an +:term:`exception view` but, by default, will result in a ``400 Bad Request`` +resposne being sent to the client. Checking CSRF Tokens with a View Predicate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -411,6 +446,9 @@ include ``check_csrf=True`` as a view predicate. See instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different from calling :func:`pyramid.session.check_csrf_token`. +.. index:: + single: session.new_csrf_token + Using the ``session.new_csrf_token`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/pyramid/config/settings.py b/pyramid/config/settings.py index 492b7d524..78b61e4ef 100644 --- a/pyramid/config/settings.py +++ b/pyramid/config/settings.py @@ -6,6 +6,7 @@ from zope.interface import implementer from pyramid.interfaces import ISettings from pyramid.settings import asbool +from pyramid.settings import truthy class SettingsConfiguratorMixin(object): def _set_settings(self, mapping): @@ -122,6 +123,8 @@ class Settings(dict): config_prevent_cachebust) eff_prevent_cachebust = asbool(eget('PYRAMID_PREVENT_CACHEBUST', config_prevent_cachebust)) + require_default_csrf = self.get('pyramid.require_default_csrf') + eff_require_default_csrf = require_default_csrf update = { 'debug_authorization': eff_debug_all or eff_debug_auth, @@ -134,6 +137,7 @@ class Settings(dict): 'default_locale_name':eff_locale_name, 'prevent_http_cache':eff_prevent_http_cache, 'prevent_cachebust':eff_prevent_cachebust, + 'require_default_csrf':eff_require_default_csrf, 'pyramid.debug_authorization': eff_debug_all or eff_debug_auth, 'pyramid.debug_notfound': eff_debug_all or eff_debug_notfound, @@ -145,7 +149,8 @@ class Settings(dict): 'pyramid.default_locale_name':eff_locale_name, 'pyramid.prevent_http_cache':eff_prevent_http_cache, 'pyramid.prevent_cachebust':eff_prevent_cachebust, - } + 'pyramid.require_default_csrf':eff_require_default_csrf, + } self.update(update) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 58fdbfd06..8b066bc1e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -371,27 +371,26 @@ class ViewsConfiguratorMixin(object): .. versionadded:: 1.7 - If specified, this value should be one of ``None``, ``True``, - ``False``, or a string representing the 'check name'. If the value - is ``True`` or a string, CSRF checking will be performed. If the - value is ``False`` or ``None``, CSRF checking will not be performed. + CSRF checks only affect POST requests. Any other request methods + will pass untouched. This option is used in combination with the + ``pyramid.require_default_csrf`` setting to control which + request parameters are checked for CSRF tokens. - If the value provided is a string, that string will be used as the - 'check name'. If the value provided is ``True``, ``csrf_token`` will - be used as the check name. + This feature requires a configured :term:`session factory`. - If CSRF checking is performed, the checked value will be the value - of ``request.params[check_name]``. This value will be compared - against the value of ``request.session.get_csrf_token()``, and the - check will pass if these two values are the same. If the check - passes, the associated view will be permitted to execute. If the - check fails, the associated view will not be permitted to execute - and a :class:`pyramid.exceptions.BadCSRFToken` exception will - be raised. This exception may be caught and handled by an - :term:`exception view`. + If this option is set to ``True`` then CSRF checks will be enabled + for POST requests to this view. The required token will be whatever + was specified by the ``pyramid.require_default_csrf`` setting, or + will fallback to ``csrf_token``. - Note that using this feature requires a :term:`session factory` to - have been configured. + If this option is set to a string then CSRF checks will be enabled + and it will be used as the required token regardless of the + ``pyramid.require_default_csrf`` setting. + + If this option is set to ``False`` then CSRF checks will be disabled + regardless of the ``pyramid.require_default_csrf`` setting. + + See :ref:`auto_csrf_checking` for more information. wrapper @@ -1213,8 +1212,8 @@ class ViewsConfiguratorMixin(object): def add_default_view_derivers(self): d = pyramid.viewderivers derivers = [ - ('csrf_view', d.csrf_view), ('secured_view', d.secured_view), + ('csrf_view', d.csrf_view), ('owrapped_view', d.owrapped_view), ('http_cached_view', d.http_cached_view), ('decorated_view', d.decorated_view), diff --git a/pyramid/settings.py b/pyramid/settings.py index e2cb3cb3c..8a498d572 100644 --- a/pyramid/settings.py +++ b/pyramid/settings.py @@ -1,13 +1,12 @@ from pyramid.compat import string_types truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1')) +falsey = frozenset(('f', 'false', 'n', 'no', 'off', '0')) def asbool(s): """ Return the boolean value ``True`` if the case-lowered value of string - input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise - return the boolean value ``False``. If ``s`` is the value ``None``, - return ``False``. If ``s`` is already one of the boolean values ``True`` - or ``False``, return it.""" + input ``s`` is a :term:`truthy string`. If ``s`` is already one of the + boolean values ``True`` or ``False``, return it.""" if s is None: return False if isinstance(s, bool): diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 55ead55c2..f3c51f985 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1570,28 +1570,30 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=view2) self.assertRaises(ConfigurationConflictError, config.commit) - def test_add_view_with_csrf_header(self): + def test_add_view_with_csrf_param(self): from pyramid.renderers import null_renderer def view(request): return 'OK' config = self._makeOne(autocommit=True) - config.add_view(view, require_csrf=True, renderer=null_renderer) + config.add_view(view, require_csrf='st', renderer=null_renderer) view = self._getViewCallable(config) request = self._makeRequest(config) - request.headers = {'X-CSRF-Token': 'foo'} + request.method = 'POST' + request.params = {'st': 'foo'} + request.headers = {} request.session = DummySession({'csrf_token': 'foo'}) self.assertEqual(view(None, request), 'OK') - def test_add_view_with_csrf_param(self): + def test_add_view_with_csrf_header(self): from pyramid.renderers import null_renderer def view(request): return 'OK' config = self._makeOne(autocommit=True) - config.add_view(view, require_csrf='st', renderer=null_renderer) + config.add_view(view, require_csrf=True, renderer=null_renderer) view = self._getViewCallable(config) request = self._makeRequest(config) - request.params = {'st': 'foo'} - request.headers = {} + request.method = 'POST' + request.headers = {'X-CSRF-Token': 'foo'} request.session = DummySession({'csrf_token': 'foo'}) self.assertEqual(view(None, request), 'OK') @@ -1603,6 +1605,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view, require_csrf=True, renderer=null_renderer) view = self._getViewCallable(config) request = self._makeRequest(config) + request.method = 'POST' request.headers = {} request.session = DummySession({'csrf_token': 'foo'}) self.assertRaises(BadCSRFToken, lambda: view(None, request)) diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index 0dd70b74a..c8fbe6f36 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -1090,11 +1090,36 @@ class TestDeriveView(unittest.TestCase): self.assertRaises(ConfigurationError, self.config._derive_view, view, http_cache=(None,)) + def test_csrf_view_requires_bool_or_str_in_require_csrf(self): + def view(request): pass + try: + self.config._derive_view(view, require_csrf=object()) + except ConfigurationError as ex: + self.assertEqual( + 'View option "require_csrf" must be a string or boolean value', + ex.args[0]) + else: # pragma: no cover + raise AssertionError + + def test_csrf_view_requires_bool_or_str_in_config_setting(self): + def view(request): pass + self.config.add_settings({'pyramid.require_default_csrf': object()}) + try: + self.config._derive_view(view) + except ConfigurationError as ex: + self.assertEqual( + 'Config setting "pyramid.require_csrf_default" must be a ' + 'string or boolean value', + ex.args[0]) + else: # pragma: no cover + raise AssertionError + def test_csrf_view_requires_header(self): response = DummyResponse() def inner_view(request): return response request = self._makeRequest() + request.method = 'POST' request.session = DummySession({'csrf_token': 'foo'}) request.headers = {'X-CSRF-Token': 'foo'} view = self.config._derive_view(inner_view, require_csrf=True) @@ -1106,12 +1131,108 @@ class TestDeriveView(unittest.TestCase): def inner_view(request): return response request = self._makeRequest() + request.method = 'POST' request.session = DummySession({'csrf_token': 'foo'}) request.params['DUMMY'] = 'foo' view = self.config._derive_view(inner_view, require_csrf='DUMMY') result = view(None, request) self.assertTrue(result is response) + def test_csrf_view_ignores_GET(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.method = 'GET' + view = self.config._derive_view(inner_view, require_csrf=True) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_fails_on_bad_POST_param(self): + from pyramid.exceptions import BadCSRFToken + def inner_view(request): pass + request = self._makeRequest() + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.params['DUMMY'] = 'bar' + view = self.config._derive_view(inner_view, require_csrf='DUMMY') + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_csrf_view_fails_on_bad_POST_header(self): + from pyramid.exceptions import BadCSRFToken + def inner_view(request): pass + request = self._makeRequest() + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.headers = {'X-CSRF-Token': 'bar'} + view = self.config._derive_view(inner_view, require_csrf='DUMMY') + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_csrf_view_uses_config_setting_truthy(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.params['csrf_token'] = 'foo' + self.config.add_settings({'pyramid.require_default_csrf': 'yes'}) + view = self.config._derive_view(inner_view) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_uses_config_setting_with_custom_token(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.params['DUMMY'] = 'foo' + self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'}) + view = self.config._derive_view(inner_view) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_uses_config_setting_falsey(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.params['csrf_token'] = 'foo' + self.config.add_settings({'pyramid.require_default_csrf': 'no'}) + view = self.config._derive_view(inner_view) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_uses_view_option_override(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.params['DUMMY'] = 'foo' + self.config.add_settings({'pyramid.require_default_csrf': 'yes'}) + view = self.config._derive_view(inner_view, require_csrf='DUMMY') + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_uses_config_setting_when_view_option_is_true(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.params['DUMMY'] = 'foo' + self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'}) + view = self.config._derive_view(inner_view, require_csrf=True) + result = view(None, request) + self.assertTrue(result is response) + class TestDerivationOrder(unittest.TestCase): def setUp(self): @@ -1132,8 +1253,8 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'csrf_view', 'secured_view', + 'csrf_view', 'owrapped_view', 'http_cached_view', 'decorated_view', @@ -1155,8 +1276,8 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'csrf_view', 'secured_view', + 'csrf_view', 'owrapped_view', 'http_cached_view', 'decorated_view', @@ -1176,8 +1297,8 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'csrf_view', 'secured_view', + 'csrf_view', 'owrapped_view', 'http_cached_view', 'decorated_view', @@ -1198,8 +1319,8 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'csrf_view', 'secured_view', + 'csrf_view', 'owrapped_view', 'http_cached_view', 'decorated_view', diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 7560fa67f..41102319d 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -19,6 +19,7 @@ from pyramid.interfaces import ( ) from pyramid.compat import ( + string_types, is_bound_method, is_unbound_method, ) @@ -34,6 +35,10 @@ from pyramid.exceptions import ( PredicateMismatch, ) from pyramid.httpexceptions import HTTPForbidden +from pyramid.settings import ( + falsey, + truthy, +) from pyramid.util import object_description from pyramid.view import render_view_to_response from pyramid import renderers @@ -456,14 +461,35 @@ def decorated_view(view, info): decorated_view.options = ('decorator',) +def _parse_csrf_setting(val, error_source): + if val: + if isinstance(val, string_types): + if val.lower() in truthy: + val = True + elif val.lower() in falsey: + val = False + elif not isinstance(val, bool): + raise ConfigurationError( + '{0} must be a string or boolean value' + .format(error_source)) + return val + def csrf_view(view, info): - val = info.options.get('require_csrf') + default_val = _parse_csrf_setting( + info.settings.get('pyramid.require_default_csrf'), + 'Config setting "pyramid.require_csrf_default"') + val = _parse_csrf_setting( + info.options.get('require_csrf'), + 'View option "require_csrf"') + if (val is True and default_val) or val is None: + val = default_val + if val is True: + val = 'csrf_token' wrapped_view = view if val: - if val is True: - val = 'csrf_token' def csrf_view(context, request): - check_csrf_token(request, val, raises=True) + if request.method == 'POST': + check_csrf_token(request, val, raises=True) return view(context, request) wrapped_view = csrf_view return wrapped_view -- cgit v1.2.3 From 15b97dc81c8bcdc039f8f2293f85812f68a076da Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 10 Apr 2016 20:51:23 -0500 Subject: deprecate the check_csrf predicate --- docs/narr/sessions.rst | 4 ++++ pyramid/config/views.py | 18 +++++++++++++++++- pyramid/tests/test_config/test_views.py | 16 ++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 3baed1cb8..4e8f6db88 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -430,6 +430,10 @@ resposne being sent to the client. Checking CSRF Tokens with a View Predicate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. deprecated:: 1.7 + Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead + to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised. + A convenient way to require a valid CSRF token for a particular view is to include ``check_csrf=True`` as a view predicate. See :meth:`pyramid.config.Configurator.add_view`. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 8b066bc1e..6fe31fd4a 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -613,6 +613,11 @@ class ViewsConfiguratorMixin(object): check_csrf + .. deprecated:: 1.7 + Use the ``require_csrf`` option or see :ref:`auto_csrf_checking` + instead to have :class:`pyramid.exceptions.BadCSRFToken` + exceptions raised. + If specified, this value should be one of ``None``, ``True``, ``False``, or a string representing the 'check name'. If the value is ``True`` or a string, CSRF checking will be performed. If the @@ -708,7 +713,18 @@ class ViewsConfiguratorMixin(object): 'Predicate" in the "Hooks" chapter of the documentation ' 'for more information.'), DeprecationWarning, - stacklevel=4 + stacklevel=4, + ) + + if check_csrf is not None: + warnings.warn( + ('The "check_csrf" argument to Configurator.add_view is ' + 'deprecated as of Pyramid 1.7. Use the "require_csrf" option ' + 'instead or see "Checking CSRF Tokens Automatically" in the ' + '"Sessions" chapter of the documentation for more ' + 'information.'), + DeprecationWarning, + stacklevel=4, ) view = self.maybe_dotted(view) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index f3c51f985..0bf0bd0b3 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1491,6 +1491,22 @@ class TestViewsConfigurationMixin(unittest.TestCase): request.upath_info = text_('/') self._assertNotFound(wrapper, None, request) + def test_add_view_with_check_csrf_predicates_match(self): + import warnings + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + with warnings.catch_warnings(record=True) as w: + warnings.filterwarnings('always') + config.add_view(view=view, check_csrf=True, renderer=null_renderer) + self.assertEqual(len(w), 1) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.session = DummySession({'csrf_token': 'foo'}) + request.params = {'csrf_token': 'foo'} + request.headers = {} + self.assertEqual(wrapper(None, request), 'OK') + def test_add_view_with_custom_predicates_match(self): import warnings from pyramid.renderers import null_renderer -- cgit v1.2.3 From 769da1215a0287f4161e58f36d8d4b7650154202 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 10 Apr 2016 21:14:22 -0500 Subject: cleanup some references in the docs --- docs/narr/sessions.rst | 32 ++++++++++++++++---------------- docs/narr/viewconfig.rst | 26 ++++++++++++++++++++++++++ pyramid/config/settings.py | 1 - pyramid/session.py | 3 +++ pyramid/view.py | 3 ++- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 4e8f6db88..d66e86258 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -367,6 +367,21 @@ Or include it as a header in a jQuery AJAX request: The handler for the URL that receives the request should then require that the correct CSRF token is supplied. +.. index:: + single: session.new_csrf_token + +Using the ``session.new_csrf_token`` Method +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To explicitly create a new CSRF token, use the ``session.new_csrf_token()`` +method. This differs only from ``session.get_csrf_token()`` inasmuch as it +clears any existing CSRF token, creates a new CSRF token, sets the token into +the session, and returns the token. + +.. code-block:: python + + token = request.session.new_csrf_token() + Checking CSRF Tokens Manually ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -425,7 +440,7 @@ performing CSRF checking manually. If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` exception will be raised. This exception may be caught and handled by an :term:`exception view` but, by default, will result in a ``400 Bad Request`` -resposne being sent to the client. +response being sent to the client. Checking CSRF Tokens with a View Predicate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -449,18 +464,3 @@ include ``check_csrf=True`` as a view predicate. See predicate system, when it doesn't find a view, raises ``HTTPNotFound`` instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different from calling :func:`pyramid.session.check_csrf_token`. - -.. index:: - single: session.new_csrf_token - -Using the ``session.new_csrf_token`` Method -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To explicitly create a new CSRF token, use the ``session.new_csrf_token()`` -method. This differs only from ``session.get_csrf_token()`` inasmuch as it -clears any existing CSRF token, creates a new CSRF token, sets the token into -the session, and returns the token. - -.. code-block:: python - - token = request.session.new_csrf_token() diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 0bd52b6e2..e645185f5 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -192,6 +192,32 @@ Non-Predicate Arguments only influence ``Cache-Control`` headers, pass a tuple as ``http_cache`` with the first element of ``None``, i.e., ``(None, {'public':True})``. + +``require_csrf`` + + CSRF checks only affect POST requests. Any other request methods will pass + untouched. This option is used in combination with the + ``pyramid.require_default_csrf`` setting to control which request parameters + are checked for CSRF tokens. + + This feature requires a configured :term:`session factory`. + + If this option is set to ``True`` then CSRF checks will be enabled for POST + requests to this view. The required token will be whatever was specified by + the ``pyramid.require_default_csrf`` setting, or will fallback to + ``csrf_token``. + + If this option is set to a string then CSRF checks will be enabled and it + will be used as the required token regardless of the + ``pyramid.require_default_csrf`` setting. + + If this option is set to ``False`` then CSRF checks will be disabled + regardless of the ``pyramid.require_default_csrf`` setting. + + See :ref:`auto_csrf_checking` for more information. + + .. versionadded:: 1.7 + ``wrapper`` The :term:`view name` of a different :term:`view configuration` which will receive the response body of this view as the ``request.wrapped_body`` diff --git a/pyramid/config/settings.py b/pyramid/config/settings.py index 78b61e4ef..b66986327 100644 --- a/pyramid/config/settings.py +++ b/pyramid/config/settings.py @@ -6,7 +6,6 @@ from zope.interface import implementer from pyramid.interfaces import ISettings from pyramid.settings import asbool -from pyramid.settings import truthy class SettingsConfiguratorMixin(object): def _set_settings(self, mapping): diff --git a/pyramid/session.py b/pyramid/session.py index a4cdf910d..fd7b5f8d5 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -123,6 +123,9 @@ def check_csrf_token(request, Note that using this function requires that a :term:`session factory` is configured. + See :ref:`auto_csrf_checking` for information about how to secure your + application automatically against CSRF attacks. + .. versionadded:: 1.4a2 """ supplied_token = request.params.get(token, request.headers.get(header, "")) diff --git a/pyramid/view.py b/pyramid/view.py index 0129526ce..62ac5310e 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -169,7 +169,8 @@ class view_config(object): ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, - ``match_param``, ``check_csrf``, ``physical_path``, and ``predicates``. + ``require_csrf``, ``match_param``, ``check_csrf``, ``physical_path``, and + ``view_options``. The meanings of these arguments are the same as the arguments passed to :meth:`pyramid.config.Configurator.add_view`. If any argument is left -- cgit v1.2.3 From d4f5a87e4859cfc3959afa4d85a516f8c307ef70 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 10 Apr 2016 21:51:38 -0600 Subject: Rename RouteFound to BeforeTraversal --- pyramid/events.py | 8 ++++---- pyramid/interfaces.py | 2 +- pyramid/router.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyramid/events.py b/pyramid/events.py index 78ebf4d70..e467e4012 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -11,7 +11,7 @@ from pyramid.interfaces import ( INewResponse, IApplicationCreated, IBeforeRender, - IRouteFound, + IBeforeTraversal, ) class subscriber(object): @@ -130,8 +130,8 @@ class NewResponse(object): self.request = request self.response = response -@implementer(IRouteFound) -class RouteFound(object): +@implementer(IBeforeTraversal) +class BeforeTraversal(object): """ An instance of this class is emitted as an :term:`event` after the :app:`Pyramid` :term:`router` finds a :term:`route` object but before any @@ -141,7 +141,7 @@ class RouteFound(object): Notably, the request object will have an attributed named ``matched_route``, which is the matched route that was found. - This class implements the :class:`pyramid.interfaces.IRouteFound` + This class implements the :class:`pyramid.interfaces.IBeforeTraversal` interface. """ diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index cf559ef06..298eaf303 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -25,7 +25,7 @@ class IContextFound(Interface): IAfterTraversal = IContextFound -class IRouteFound(Interface): +class IBeforeTraversal(Interface): """ An event type that is emitted whenever :app:`Pyramid` has found a route but before it calls any traversal or view code. See the documentation diff --git a/pyramid/router.py b/pyramid/router.py index c4b86f89d..99ea6ffa5 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -20,7 +20,7 @@ from pyramid.events import ( ContextFound, NewRequest, NewResponse, - RouteFound, + BeforeTraversal, ) from pyramid.httpexceptions import HTTPNotFound @@ -113,7 +113,7 @@ class Router(object): name=route.name, default=IRequest) - has_listeners and notify(RouteFound(request)) + has_listeners and notify(BeforeTraversal(request)) root_factory = route.factory or self.root_factory -- cgit v1.2.3 From 732d80b476ccc883fc7b6209a4256ef97946e1eb Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 10 Apr 2016 21:55:52 -0600 Subject: Add API docs for BeforeTraversal --- docs/api/events.rst | 2 ++ docs/api/interfaces.rst | 3 +++ 2 files changed, 5 insertions(+) diff --git a/docs/api/events.rst b/docs/api/events.rst index 31a0e22c1..0a8463740 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -21,6 +21,8 @@ Event Types .. autoclass:: ContextFound +.. autoclass:: BeforeTraversal + .. autoclass:: NewResponse .. autoclass:: BeforeRender diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index 635d3c5b6..272820a91 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -17,6 +17,9 @@ Event-Related Interfaces .. autointerface:: IContextFound :members: + .. autointerface:: IBeforeTraversal + :members: + .. autointerface:: INewResponse :members: -- cgit v1.2.3 From 20bc06ed03142bd184cb2f7322bc229e8e4dc9fa Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 10 Apr 2016 22:15:38 -0600 Subject: Move event to the appropriate location This way the notification is not sent only after finding a route, but anytime generically before attempting traversal. --- pyramid/router.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pyramid/router.py b/pyramid/router.py index 99ea6ffa5..19773cf62 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -113,14 +113,21 @@ class Router(object): name=route.name, default=IRequest) - has_listeners and notify(BeforeTraversal(request)) - root_factory = route.factory or self.root_factory + # Notify anyone listening that we are about to start traversal + # + # Notify before creating root_factory in case we want to do something + # special on a route we may have matched. See + # https://github.com/Pylons/pyramid/pull/1876 for ideas of what is + # possible. + has_listeners and notify(BeforeTraversal(request)) + + # Create the root factory root = root_factory(request) attrs['root'] = root - # find a context + # We are about to traverse and find a context traverser = adapters.queryAdapter(root, ITraverser) if traverser is None: traverser = ResourceTreeTraverser(root) @@ -136,6 +143,9 @@ class Router(object): ) attrs.update(tdict) + + # Notify anyone listening that we have a context and traversal is + # complete has_listeners and notify(ContextFound(request)) # find a view callable -- cgit v1.2.3 From 4112d67d7dd987b9b34fed9b01165c01308790b6 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 10 Apr 2016 22:16:32 -0600 Subject: Update doc in interfaces.py for BeforeTraversal --- pyramid/interfaces.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 298eaf303..2b00752cf 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -27,8 +27,8 @@ IAfterTraversal = IContextFound class IBeforeTraversal(Interface): """ - An event type that is emitted whenever :app:`Pyramid` has found a route - but before it calls any traversal or view code. See the documentation + An event type that is emitted after :app:`Pyramid` attempted to find a + route but before it calls any traversal or view code. See the documentation attached to :class:`pyramid.events.Routefound` for more information. """ request = Attribute('The request object') -- cgit v1.2.3 From 3f34aa05a48e5d9ae7b43f717b5ad9061effb08c Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 10 Apr 2016 22:17:19 -0600 Subject: Update documentation in event.py for BeforeTraversal --- pyramid/events.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyramid/events.py b/pyramid/events.py index e467e4012..35da2fa6f 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -134,12 +134,14 @@ class NewResponse(object): class BeforeTraversal(object): """ An instance of this class is emitted as an :term:`event` after the - :app:`Pyramid` :term:`router` finds a :term:`route` object but before any - traversal or view code is executed. The instance has an attribute, - ``request``, which is the request object generated by :app:`Pyramid`. + :app:`Pyramid` :term:`router` has attempted to find a :term:`route` object + but before any traversal or view code is executed. The instance has an + attribute, ``request``, which is the request object generated by + :app:`Pyramid`. - Notably, the request object will have an attributed named - ``matched_route``, which is the matched route that was found. + Notably, the request object **may** have an attribute named + ``matched_route``, which is the matched route if found. If no route + matched, this attribute is not available. This class implements the :class:`pyramid.interfaces.IBeforeTraversal` interface. @@ -175,7 +177,7 @@ class ContextFound(object): AfterTraversal = ContextFound # b/c as of 1.0 @implementer(IApplicationCreated) -class ApplicationCreated(object): +class ApplicationCreated(object): """ An instance of this class is emitted as an :term:`event` when the :meth:`pyramid.config.Configurator.make_wsgi_app` is called. The instance has an attribute, ``app``, which is an @@ -262,4 +264,3 @@ class BeforeRender(dict): dict.__init__(self, system) self.rendering_val = rendering_val - -- cgit v1.2.3 From 5b918737fb361584d820893fc82c6b898ae1fc69 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 10 Apr 2016 22:18:21 -0600 Subject: Update router documentation --- docs/narr/router.rst | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/narr/router.rst b/docs/narr/router.rst index e02142e6e..d4c0cc885 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -46,16 +46,22 @@ request enters a :app:`Pyramid` application through to the point that object. The former contains a dictionary representing the matched dynamic elements of the request's ``PATH_INFO`` value, and the latter contains the :class:`~pyramid.interfaces.IRoute` object representing the route which - matched. The root object associated with the route found is also generated: + matched. + + A :class:`~pyramid.events.BeforeTraversal` :term:`event` is sent to any + subscribers. + + The root object associated with the route found is also generated: if the :term:`route configuration` which matched has an associated ``factory`` argument, this factory is used to generate the root object, otherwise a default :term:`root factory` is used. #. If a route match was *not* found, and a ``root_factory`` argument was passed to the :term:`Configurator` constructor, that callable is used to generate - the root object. If the ``root_factory`` argument passed to the - Configurator constructor was ``None``, a default root factory is used to - generate a root object. + the root object after a :class:`~pyramid.events.BeforeTraversal` + :term:`event` is sent to any subscribers. If the ``root_factory`` argument + passed to the Configurator constructor was ``None``, a default root factory + is used to generate a root object. #. The :app:`Pyramid` router calls a "traverser" function with the root object and the request. The traverser function attempts to traverse the root -- cgit v1.2.3 From 59428d50df0e9c4221fff3b46f205b9d573d1056 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 10 Apr 2016 22:32:29 -0600 Subject: Add explicit tests for IBeforeTraversal/BeforeTraversal Although the new code was already being covered by other tests, this adds some explicit testing to make sure it all works. --- pyramid/tests/test_events.py | 21 +++++++++++++++++++++ pyramid/tests/test_router.py | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py index 2c72c07e8..eec395012 100644 --- a/pyramid/tests/test_events.py +++ b/pyramid/tests/test_events.py @@ -124,6 +124,27 @@ class AfterTraversalEventTests(ContextFoundEventTests): from pyramid.interfaces import IAfterTraversal verifyObject(IAfterTraversal, self._makeOne()) +class BeforeTraversalEventTests(unittest.TestCase): + def _getTargetClass(self): + from pyramid.events import BeforeTraversal + return BeforeTraversal + + def _makeOne(self, request=None): + if request is None: + request = DummyRequest() + return self._getTargetClass()(request) + + def test_class_conforms_to_IBeforeTraversal(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IBeforeTraversal + verifyClass(IBeforeTraversal, self._getTargetClass()) + + def test_instance_conforms_to_IBeforeTraversal(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IBeforeTraversal + verifyObject(IBeforeTraversal, self._makeOne()) + + class TestSubscriber(unittest.TestCase): def setUp(self): self.config = testing.setUp() diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 1cdc4abaa..7aa42804c 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -591,6 +591,7 @@ class TestRouter(unittest.TestCase): def test_call_eventsends(self): from pyramid.interfaces import INewRequest from pyramid.interfaces import INewResponse + from pyramid.interfaces import IBeforeTraversal from pyramid.interfaces import IContextFound from pyramid.interfaces import IViewClassifier context = DummyContext() @@ -601,6 +602,7 @@ class TestRouter(unittest.TestCase): environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, None, None) request_events = self._registerEventListener(INewRequest) + beforetraversal_events = self._registerEventListener(IBeforeTraversal) context_found_events = self._registerEventListener(IContextFound) response_events = self._registerEventListener(INewResponse) router = self._makeOne() @@ -608,6 +610,8 @@ class TestRouter(unittest.TestCase): result = router(environ, start_response) self.assertEqual(len(request_events), 1) self.assertEqual(request_events[0].request.environ, environ) + self.assertEqual(len(beforetraversal_events), 1) + self.assertEqual(beforetraversal_events[0].request.environ, environ) self.assertEqual(len(context_found_events), 1) self.assertEqual(context_found_events[0].request.environ, environ) self.assertEqual(context_found_events[0].request.context, context) -- cgit v1.2.3 From e224ffabfc70beadd583629d325e598ffd286361 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 10 Apr 2016 22:33:17 -0600 Subject: Fix whitespace --- pyramid/tests/test_events.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py index eec395012..52e53c34e 100644 --- a/pyramid/tests/test_events.py +++ b/pyramid/tests/test_events.py @@ -14,7 +14,7 @@ class NewRequestEventTests(unittest.TestCase): from zope.interface.verify import verifyClass klass = self._getTargetClass() verifyClass(INewRequest, klass) - + def test_instance_conforms_to_INewRequest(self): from pyramid.interfaces import INewRequest from zope.interface.verify import verifyObject @@ -40,7 +40,7 @@ class NewResponseEventTests(unittest.TestCase): from zope.interface.verify import verifyClass klass = self._getTargetClass() verifyClass(INewResponse, klass) - + def test_instance_conforms_to_INewResponse(self): from pyramid.interfaces import INewResponse from zope.interface.verify import verifyObject @@ -103,7 +103,7 @@ class ContextFoundEventTests(unittest.TestCase): from zope.interface.verify import verifyClass from pyramid.interfaces import IContextFound verifyClass(IContextFound, self._getTargetClass()) - + def test_instance_conforms_to_IContextFound(self): from zope.interface.verify import verifyObject from pyramid.interfaces import IContextFound @@ -118,7 +118,7 @@ class AfterTraversalEventTests(ContextFoundEventTests): from zope.interface.verify import verifyClass from pyramid.interfaces import IAfterTraversal verifyClass(IAfterTraversal, self._getTargetClass()) - + def test_instance_conforms_to_IAfterTraversal(self): from zope.interface.verify import verifyObject from pyramid.interfaces import IAfterTraversal @@ -242,7 +242,7 @@ class TestBeforeRender(unittest.TestCase): result = event.setdefault('a', 1) self.assertEqual(result, 1) self.assertEqual(event, {'a':1}) - + def test_setdefault_success(self): event = self._makeOne({}) event['a'] = 1 @@ -303,7 +303,7 @@ class DummyConfigurator(object): class DummyRegistry(object): pass - + class DummyVenusian(object): def __init__(self): self.attached = [] @@ -313,7 +313,7 @@ class DummyVenusian(object): class Dummy: pass - + class DummyRequest: pass -- cgit v1.2.3 From 0da7b0de590b835d6d7df361394b8bf70797f566 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 11 Apr 2016 13:29:50 -0700 Subject: - upgrade `BeforeTraversal` event in router.rst --- docs/narr/router.rst | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/narr/router.rst b/docs/narr/router.rst index d4c0cc885..e45e6f4a8 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -41,27 +41,26 @@ request enters a :app:`Pyramid` application through to the point that user-defined :term:`route` matches the current WSGI environment. The :term:`router` passes the request as an argument to the mapper. -#. If any route matches, the route mapper adds attributes to the request: - ``matchdict`` and ``matched_route`` attributes are added to the request - object. The former contains a dictionary representing the matched dynamic - elements of the request's ``PATH_INFO`` value, and the latter contains the +#. If any route matches, the route mapper adds the attributes ``matchdict`` + and ``matched_route`` to the request object. The former contains a + dictionary representing the matched dynamic elements of the request's + ``PATH_INFO`` value, and the latter contains the :class:`~pyramid.interfaces.IRoute` object representing the route which matched. - - A :class:`~pyramid.events.BeforeTraversal` :term:`event` is sent to any + +#. A :class:`~pyramid.events.BeforeTraversal` :term:`event` is sent to any subscribers. - The root object associated with the route found is also generated: - if the :term:`route configuration` which matched has an associated - ``factory`` argument, this factory is used to generate the root object, - otherwise a default :term:`root factory` is used. +#. Continuing, if any route matches, the root object associated with the found + route is generated. If the :term:`route configuration` which matched has an + associated ``factory`` argument, then this factory is used to generate the + root object; otherwise a default :term:`root factory` is used. -#. If a route match was *not* found, and a ``root_factory`` argument was passed + However, if no route matches, and if a ``root_factory`` argument was passed to the :term:`Configurator` constructor, that callable is used to generate - the root object after a :class:`~pyramid.events.BeforeTraversal` - :term:`event` is sent to any subscribers. If the ``root_factory`` argument - passed to the Configurator constructor was ``None``, a default root factory - is used to generate a root object. + the root object. If the ``root_factory`` argument passed to the + Configurator constructor was ``None``, a default root factory is used to + generate a root object. #. The :app:`Pyramid` router calls a "traverser" function with the root object and the request. The traverser function attempts to traverse the root -- cgit v1.2.3 From 17905a39040b8a2f4b57341909eef9d0fac218f5 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Tue, 12 Apr 2016 18:05:30 -0600 Subject: Add CHANGES for BeforeTraversal --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index fd8c636a0..488c38c7b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,11 @@ unreleased ========== +- A new event and interface (BeforeTraversal) has been introduced that will + notify listeners before traversal starts in the router. See + https://github.com/Pylons/pyramid/pull/2469 and + https://github.com/Pylons/pyramid/pull/1876 + - Python 2.6 is no longer supported by Pyramid. See https://github.com/Pylons/pyramid/issues/2368 -- cgit v1.2.3 From e8caaef7919eb3bb52ba5c53dd44013a125f59d4 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 13 Apr 2016 00:28:19 -0700 Subject: - update Pyramid Request Processing Diagram. - Closes #2473. - See also #2413 and #2469. --- docs/_static/pyramid_request_processing.graffle | 883 +++++++++++++++--------- docs/_static/pyramid_request_processing.png | Bin 123953 -> 130377 bytes docs/_static/pyramid_request_processing.svg | 2 +- 3 files changed, 555 insertions(+), 330 deletions(-) diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle index 16b360543..8da968574 100644 --- a/docs/_static/pyramid_request_processing.graffle +++ b/docs/_static/pyramid_request_processing.graffle @@ -22,7 +22,7 @@ Font Helvetica Size - 12 + 13 ID 2 @@ -58,6 +58,179 @@ 8 GraphicsList + + Bounds + {{238.74999618530273, 294.65604172230951}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 11 + + ID + 169515 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.637876 + g + 1 + r + 1 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 CSRF checks} + VerticalPad + 0 + + + + Class + LineGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 12 + + Head + + ID + 169513 + + ID + 169514 + Layer + 0 + Points + + {208.33329937855831, 164.83237970227628} + {239.8333613077798, 164.77232074737549} + + Style + + stroke + + Color + + b + 0.0980392 + g + 0.0980392 + r + 0.0980392 + + HeadArrow + 0 + Legacy + + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 169418 + + + + Bounds + {{239.83336130777977, 153.5}, {105.66666412353516, 22.544641494750977}} + Class + ShapedGraphic + ID + 169513 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.999449 + g + 0.743511 + r + 0.872276 + + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 BeforeTraversal} + VerticalPad + 0 + + Class LineGraphic @@ -84,8 +257,8 @@ 0 Points - {344.41668319702148, 402.88506673894034} - {375.5, 402.77232108797347} + {344.41668319702148, 411.88506673894034} + {375.5, 411.77232108797347} Style @@ -237,378 +410,430 @@ + Bounds + {{238.74999618530273, 275.99999999999994}, {105.75002924601222, 18.656048080136394}} Class - Group - Graphics + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 11 + + ID + 169506 + Layer + 0 + Magnets + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill - Bounds - {{238.74999618530273, 284.99999999999994}, {105.75002924601222, 18.656048080136394}} - Class - ShapedGraphic - ID - 169506 - Magnets - - {0, 1} - {0, -1} - {1, 0} - {-1, 0} - {0.50000000000000089, -0.49999999999999645} - {-0.49526813868737474, -0.4689979626999552} - - Shape - Rectangle - Style + Color - fill - - Color - - b - 0.637876 - g - 1 - r - 1 - - - shadow - - Draws - NO - ShadowVector - {2, 2} - + b + 0.637876 + g + 1 + r + 1 - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc \f0\fs20 \cf0 authorization} - VerticalPad - 0 - - - - Bounds - {{238.74999618530273, 412.15071036499205}, {105.66668701171875, 18.656048080136394}} - Class - ShapedGraphic - ID - 169507 - Magnets - - {0, 1} - {0, -1} - {1, 0} - {-1, 0} - {0.50000000000000089, 0.5} - {-0.49999999999999911, 0.49999999999999289} - - Shape - Rectangle - Style + VerticalPad + 0 + + + + Bounds + {{238.74999618530273, 421.15071036499205}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 11 + + ID + 169507 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, 0.5} + {-0.49999999999999911, 0.49999999999999289} + + Shape + Rectangle + Style + + fill + + Color - fill - - Color - - b - 0.637876 - g - 1 - r - 1 - - - shadow - - Draws - NO - ShadowVector - {2, 2} - + b + 0.637876 + g + 1 + r + 1 - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc \f0\fs20 \cf0 decorators egress} - VerticalPad - 0 - - + VerticalPad + 0 + + + + Bounds + {{238.74999618530273, 312.65604172230951}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 11 + + ID + 169508 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + {0.50000000000000089, -0.49999999999999645} + {-0.49526813868737474, -0.4689979626999552} + + Shape + Rectangle + Style + + fill - Bounds - {{238.74999618530273, 303.65604172230951}, {105.66668701171875, 18.656048080136394}} - Class - ShapedGraphic - ID - 169508 - Magnets - - {0, 1} - {0, -1} - {1, 0} - {-1, 0} - {0.50000000000000089, -0.49999999999999645} - {-0.49526813868737474, -0.4689979626999552} - - Shape - Rectangle - Style + Color - fill - - Color - - b - 0.637876 - g - 1 - r - 1 - - - shadow - - Draws - NO - ShadowVector - {2, 2} - + b + 0.637876 + g + 1 + r + 1 - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc \f0\fs20 \cf0 decorators ingress} - VerticalPad - 0 - - + VerticalPad + 0 + + + + Bounds + {{238.74999618530273, 402.55704269887212}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 11 + + ID + 169509 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill - Bounds - {{238.74999618530273, 393.55704269887212}, {105.66668701171875, 18.656048080136394}} - Class - ShapedGraphic - ID - 169509 - Magnets - - {0, 1} - {0, -1} - {1, 0} - {-1, 0} - - Shape - Rectangle - Style + Color - fill - - Color - - b - 0.637876 - g - 1 - r - 1 - - - shadow - - Draws - NO - ShadowVector - {2, 2} - + b + 0.637876 + g + 1 + r + 1 - Text + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response adapter} + VerticalPad + 0 + + + + Bounds + {{238.74999618530273, 383.90099016834085}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 11 + + ID + 169510 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 -\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc - -\f0\fs20 \cf0 response adapter} - VerticalPad - 0 + b + 0.637876 + g + 1 + r + 1 + shadow - Bounds - {{238.74999618530273, 374.90099016834085}, {105.66668701171875, 18.656048080136394}} - Class - ShapedGraphic - ID - 169510 - Magnets - - {0, 1} - {0, -1} - {1, 0} - {-1, 0} - - Shape - Rectangle - Style - - fill - - Color - - b - 0.637876 - g - 1 - r - 1 - - - shadow - - Draws - NO - ShadowVector - {2, 2} - - - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc \f0\fs20 \cf0 view mapper egress} - VerticalPad - 0 - - + VerticalPad + 0 + + + + Bounds + {{238.74999618530273, 350.36561209044055}, {105.66668701171875, 33.089282989501953}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 11 + + ID + 169511 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill - Bounds - {{238.74999618530273, 341.36561209044055}, {105.66668701171875, 33.089282989501953}} - Class - ShapedGraphic - ID - 169511 - Magnets - - {0, 1} - {0, -1} - {1, 0} - {-1, 0} - - Shape - Rectangle - Style + Color - fill - - Color - - b - 0.422927 - g - 1 - r - 1 - - - shadow - - Draws - NO - ShadowVector - {2, 2} - + b + 0.422927 + g + 1 + r + 1 - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc \f0\fs20 \cf0 view} - VerticalPad - 0 - - + VerticalPad + 0 + + + + Bounds + {{238.74999618530273, 331.26348241170439}, {105.66668701171875, 18.656048080136394}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 11 + + ID + 169512 + Layer + 0 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill - Bounds - {{238.74999618530273, 322.26348241170439}, {105.66668701171875, 18.656048080136394}} - Class - ShapedGraphic - ID - 169512 - Magnets - - {0, 1} - {0, -1} - {1, 0} - {-1, 0} - - Shape - Rectangle - Style + Color - fill - - Color - - b - 0.637876 - g - 1 - r - 1 - - - shadow - - Draws - NO - ShadowVector - {2, 2} - + b + 0.637876 + g + 1 + r + 1 - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 + + shadow + + Draws + NO + ShadowVector + {2, 2} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc \f0\fs20 \cf0 view mapper ingress} - VerticalPad - 0 - - - - ID - 169505 - Layer - 0 + VerticalPad + 0 + Class @@ -1094,7 +1319,7 @@ 0 Points - {238.74999618530282, 430.80675844512831} + {238.74999618530282, 439.80675844512831} {207.66666666666765, 385.656005859375} Style @@ -1144,7 +1369,7 @@ 0 Points - {239.25039065750093, 285.57837549845181} + {239.25039065750093, 276.57837549845181} {207.66666666666777, 353.07514659563753} Style @@ -1742,7 +1967,7 @@ Bounds - {{375.5, 391.5}, {105.66666412353516, 22.544642175946908}} + {{375.5, 400.5}, {105.66666412353516, 22.544642175946908}} Class ShapedGraphic ID @@ -9637,7 +9862,7 @@ MasterSheets ModificationDate - 2016-03-13 08:04:48 +0000 + 2016-04-13 07:24:56 +0000 Modifier Steve Piercy NotesVisible @@ -9732,15 +9957,15 @@ SidebarWidth 163 VisibleRegion - {{152.25, 226.5}, {255.75, 292.75}} + {{-231, -226}, {1037, 1186}} Zoom - 4 + 1 ZoomValues Request Processing + 1 4 - 8 diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png index c684255fa..6f1e4592b 100644 Binary files a/docs/_static/pyramid_request_processing.png and b/docs/_static/pyramid_request_processing.png differ diff --git a/docs/_static/pyramid_request_processing.svg b/docs/_static/pyramid_request_processing.svg index d32d5c5bc..f6b49327f 100644 --- a/docs/_static/pyramid_request_processing.svg +++ b/docs/_static/pyramid_request_processing.svg @@ -1,3 +1,3 @@ -2016-03-13 08:04ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackview deriverexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization +2016-04-13 07:24ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackview deriverexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorizationBeforeTraversalCSRF checks -- cgit v1.2.3 From ce91e9303b61523789dea2d075c951ad30f8d82a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 13 Apr 2016 00:41:13 -0700 Subject: - Deprecated support for Python 3.3. See #2477 --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 488c38c7b..00232abc3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,9 @@ unreleased ========== +- Deprecated support for Python 3.3. + https://github.com/Pylons/pyramid/issues/2477 + - A new event and interface (BeforeTraversal) has been introduced that will notify listeners before traversal starts in the router. See https://github.com/Pylons/pyramid/pull/2469 and -- cgit v1.2.3 From a2c28ff6fcdbc1ae221bbc2ed379392503ab20ec Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 13 Apr 2016 01:35:48 -0700 Subject: - nudge `BeforeTraversal` --- docs/_static/pyramid_request_processing.graffle | 24 +++++++++++++++--------- docs/_static/pyramid_request_processing.png | Bin 130377 -> 130688 bytes docs/_static/pyramid_request_processing.svg | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle index 8da968574..56e4e13f2 100644 --- a/docs/_static/pyramid_request_processing.graffle +++ b/docs/_static/pyramid_request_processing.graffle @@ -146,8 +146,8 @@ 0 Points - {208.33329937855831, 164.83237970227628} - {239.8333613077798, 164.77232074737549} + {154.9999760464211, 209.11365574251681} + {239.8333613077798, 209.14732074737549} Style @@ -175,12 +175,14 @@ Tail ID - 169418 + 169373 + Position + 0.47711458802223206 Bounds - {{239.83336130777977, 153.5}, {105.66666412353516, 22.544641494750977}} + {{239.83336130777977, 197.875}, {105.66666412353516, 22.544641494750977}} Class ShapedGraphic ID @@ -9803,6 +9805,10 @@ YES HPages 1 + HorizontalGuides + + 209.875 + ImageCounter 3 KeepToScale @@ -9862,7 +9868,7 @@ MasterSheets ModificationDate - 2016-04-13 07:24:56 +0000 + 2016-04-13 08:32:47 +0000 Modifier Steve Piercy NotesVisible @@ -9943,7 +9949,7 @@ Frame - {{35, 93}, {1394, 1325}} + {{35, 93}, {2284, 1325}} ListView OutlineWidth @@ -9957,14 +9963,14 @@ SidebarWidth 163 VisibleRegion - {{-231, -226}, {1037, 1186}} + {{110.125, 77.875}, {239.125, 146.375}} Zoom - 1 + 8 ZoomValues Request Processing - 1 + 8 4 diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png index 6f1e4592b..2f44f4824 100644 Binary files a/docs/_static/pyramid_request_processing.png and b/docs/_static/pyramid_request_processing.png differ diff --git a/docs/_static/pyramid_request_processing.svg b/docs/_static/pyramid_request_processing.svg index f6b49327f..03f6d56fa 100644 --- a/docs/_static/pyramid_request_processing.svg +++ b/docs/_static/pyramid_request_processing.svg @@ -1,3 +1,3 @@ -2016-04-13 07:24ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackview deriverexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorizationBeforeTraversalCSRF checks +2016-04-13 08:32ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackview deriverexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorizationBeforeTraversalCSRF checks -- cgit v1.2.3 From a51ca284503910e4090973a4d8991fee92f3381b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 13 Apr 2016 16:01:45 -0700 Subject: update deprecation log entry --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 00232abc3..da59c3e6f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,7 @@ unreleased ========== -- Deprecated support for Python 3.3. +- (Deprecation) Support for Python 3.3 will be removed in Pyramid 1.8. https://github.com/Pylons/pyramid/issues/2477 - A new event and interface (BeforeTraversal) has been introduced that will -- cgit v1.2.3 From 88637857ca84eb74fd318ad1bf8c4464e50ae662 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 14 Apr 2016 11:23:00 -0500 Subject: add a note in the todo about python 3.3 --- TODO.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.txt b/TODO.txt index 837c9d681..ff567bca8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -124,6 +124,7 @@ Future ``hashalg`` to ``sha512``. - 1.8: Remove set_request_property. +- 1.8: Drop Python 3.3 support. - 1.9: Remove extra code enabling ``pyramid.security.remember(principal=...)`` and force use of ``userid``. -- cgit v1.2.3