From 85d801817e2137848a3ccb4303b88ace063a81d5 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Feb 2015 16:39:16 -0600 Subject: provide a fix for view lookup fallbacks --- pyramid/router.py | 22 ++++++++++------------ pyramid/view.py | 12 ++++++++++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/pyramid/router.py b/pyramid/router.py index ba4f85b18..5190311d2 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -27,6 +27,7 @@ from pyramid.events import ( from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request +from pyramid.view import find_views from pyramid.threadlocal import manager from pyramid.traversal import ( @@ -138,9 +139,14 @@ class Router(object): # find a view callable context_iface = providedBy(context) - view_callable = adapters.lookup( - (IViewClassifier, request.request_iface, context_iface), - IView, name=view_name, default=None) + views_iter = find_views( + registry, + request.request_iface, + context_iface, + view_name, + ) + + view_callable = next(views_iter, None) # invoke the view callable if view_callable is None: @@ -164,14 +170,7 @@ class Router(object): except PredicateMismatch: # look for other views that meet the predicate # criteria - for iface in context_iface.__sro__[1:]: - previous_view_callable = view_callable - view_callable = adapters.lookup( - (IViewClassifier, request.request_iface, iface), - IView, name=view_name, default=None) - # intermediate bases may lookup same view_callable - if view_callable is previous_view_callable: - continue + for view_callable in views_iter: if view_callable is not None: try: response = view_callable(context, request) @@ -241,4 +240,3 @@ class Router(object): request = self.request_factory(environ) response = self.invoke_subrequest(request, use_tweens=True) return response(request.environ, start_response) - diff --git a/pyramid/view.py b/pyramid/view.py index 02ac8849f..9ee9503f7 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -4,6 +4,8 @@ from zope.interface import providedBy from pyramid.interfaces import ( IRoutesMapper, + IMultiView, + ISecuredView, IView, IViewClassifier, ) @@ -414,3 +416,13 @@ class forbidden_view_config(object): settings['_info'] = info.codeinfo # fbo "action_method" return wrapped +def find_views(registry, request_iface, context_iface, view_name): + adapters = registry.adapters + for req_type in request_iface.__sro__: + for iface in context_iface.__sro__: + for view_type in (IView, ISecuredView, IMultiView): + view_callable = adapters.registered( + (IViewClassifier, req_type, iface), + view_type, name=view_name) + if view_callable is not None: + yield view_callable -- cgit v1.2.3 From 0b03c334dfb219738929d39d7dda4a84a30c251d Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 5 Feb 2015 15:40:52 -0800 Subject: Add tests to verify view lookup with multiple inheritance --- pyramid/tests/test_config/test_views.py | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index b0d03fb72..a83a25e58 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1007,6 +1007,78 @@ class TestViewsConfigurationMixin(unittest.TestCase): request.params = {'param':'1'} self.assertEqual(wrapper(ctx, request), 'view8') + def test_view_with_most_specific_predicate(self): + from pyramid.renderers import null_renderer as nr + from pyramid.router import Router + + class OtherBase(object): pass + class Int1(object): pass + class Int2(object): pass + + class Resource(OtherBase, Int1, Int2): + def __init__(self, request): pass + + def unknown(context, request): return 'unknown' + def view(context, request): return 'hello' + + config = self._makeOne(autocommit=True) + config.add_route('root', '/', factory=Resource) + config.add_view(unknown, route_name='root', renderer=nr) + config.add_view( + view, renderer=nr, route_name='root', + context=Int1, request_method='GET' + ) + config.add_view( + view=view, renderer=nr, route_name='root', + context=Int2, request_method='POST' + ) + request = self._makeRequest(config) + request.method = 'POST' + request.params = {} + router = Router(config.registry) + response = router.handle_request(request) + self.assertEqual(response, 'hello') + + def test_view_with_most_specific_predicate_with_mismatch(self): + from pyramid.renderers import null_renderer as nr + from pyramid.router import Router + + class OtherBase(object): pass + class Int1(object): pass + class Int2(object): pass + + class Resource(OtherBase, Int1, Int2): + def __init__(self, request): pass + + def unknown(context, request): return 'unknown' + def view(context, request): return 'hello' + + config = self._makeOne(autocommit=True) + config.add_route('root', '/', factory=Resource) + + config.add_view( + unknown, + route_name='root', + renderer=nr, + request_method=('POST',), + xhr=True, + ) + + config.add_view( + view, renderer=nr, route_name='root', + context=Int1, request_method='GET' + ) + config.add_view( + view=view, renderer=nr, route_name='root', + context=Int2, request_method='POST' + ) + request = self._makeRequest(config) + request.method = 'POST' + request.params = {} + router = Router(config.registry) + response = router.handle_request(request) + self.assertEqual(response, 'hello') + def test_add_view_multiview___discriminator__(self): from pyramid.renderers import null_renderer from zope.interface import Interface -- cgit v1.2.3 From fb2824846c6bcdf5a42a2d0adcfe404d4c9a194e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 01:32:28 -0600 Subject: less dot operators in loops --- pyramid/view.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index 9ee9503f7..9f37b4436 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -1,3 +1,4 @@ +import itertools import venusian from zope.interface import providedBy @@ -417,12 +418,16 @@ class forbidden_view_config(object): return wrapped def find_views(registry, request_iface, context_iface, view_name): - adapters = registry.adapters - for req_type in request_iface.__sro__: - for iface in context_iface.__sro__: - for view_type in (IView, ISecuredView, IMultiView): - view_callable = adapters.registered( - (IViewClassifier, req_type, iface), - view_type, name=view_name) - if view_callable is not None: - yield view_callable + registered = registry.adapters.registered + for req_type, iface in itertools.product( + request_iface.__sro__, context_iface.__sro__ + ): + source_ifaces = (IViewClassifier, req_type, iface) + for view_type in (IView, ISecuredView, IMultiView): + view_callable = registered( + source_ifaces, + view_type, + name=view_name, + ) + if view_callable is not None: + yield view_callable -- cgit v1.2.3 From 44bfdb5e87c1e45a2c901dbd4895b7e6dcbaf8ae Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 01:41:27 -0600 Subject: req_type and ctx_type --- pyramid/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index 9f37b4436..5a02c6312 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -419,10 +419,10 @@ class forbidden_view_config(object): def find_views(registry, request_iface, context_iface, view_name): registered = registry.adapters.registered - for req_type, iface in itertools.product( + for req_type, ctx_type in itertools.product( request_iface.__sro__, context_iface.__sro__ ): - source_ifaces = (IViewClassifier, req_type, iface) + source_ifaces = (IViewClassifier, req_type, ctx_type) for view_type in (IView, ISecuredView, IMultiView): view_callable = registered( source_ifaces, -- cgit v1.2.3 From e218e1fb1b5996afdc64ee071de515557f516f5a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 7 Feb 2015 02:26:23 -0600 Subject: privitize _find_views --- pyramid/router.py | 4 ++-- pyramid/view.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/router.py b/pyramid/router.py index 5190311d2..908fdc46c 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -27,7 +27,7 @@ from pyramid.events import ( from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request -from pyramid.view import find_views +from pyramid.view import _find_views from pyramid.threadlocal import manager from pyramid.traversal import ( @@ -139,7 +139,7 @@ class Router(object): # find a view callable context_iface = providedBy(context) - views_iter = find_views( + views_iter = _find_views( registry, request.request_iface, context_iface, diff --git a/pyramid/view.py b/pyramid/view.py index 5a02c6312..2df79455c 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -417,7 +417,7 @@ class forbidden_view_config(object): settings['_info'] = info.codeinfo # fbo "action_method" return wrapped -def find_views(registry, request_iface, context_iface, view_name): +def _find_views(registry, request_iface, context_iface, view_name): registered = registry.adapters.registered for req_type, ctx_type in itertools.product( request_iface.__sro__, context_iface.__sro__ -- cgit v1.2.3 From 697213201fbd258e38eec92b80e0a1c631733ada Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 03:31:33 -0700 Subject: Add extra tests for sub-classing Request --- pyramid/tests/test_request.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 48af98f59..7134e3869 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -435,6 +435,34 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase): self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded) self.assertEqual(request.environ['PATH_INFO'], '/' + encoded) +class Test_subclassing_Request(unittest.TestCase): + + def test_subclass(self): + from pyramid.request import Request + from zope.interface import providedBy, implementedBy + + class RequestSub(Request): + pass + + self.assertTrue(hasattr(Request, '__provides__')) + self.assertFalse(hasattr(RequestSub, '__provides__')) + + def test_subclass_with_implementer(self): + from pyramid.interfaces import IRequest + from pyramid.request import Request + from zope.interface import providedBy, implementedBy, implementer + + @implementer(IRequest) + class RequestSub(Request): + pass + + self.assertTrue(hasattr(Request, '__provides__')) + self.assertTrue(hasattr(RequestSub, '__provides__')) + + req = RequestSub({}) + req._set_properties({'b': 'b'}) + self.assertEqual(providedBy(req), implementedBy(RequestSub)) + class DummyRequest: def __init__(self, environ=None): if environ is None: -- cgit v1.2.3 From 942e7dcaac03f8910ecef19bb4c610cf6e79ac76 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 12:48:05 -0700 Subject: Add more asserts --- pyramid/tests/test_request.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 7134e3869..cf591eb36 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -438,6 +438,7 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase): class Test_subclassing_Request(unittest.TestCase): def test_subclass(self): + from pyramid.interfaces import IRequest from pyramid.request import Request from zope.interface import providedBy, implementedBy @@ -445,7 +446,16 @@ class Test_subclassing_Request(unittest.TestCase): pass self.assertTrue(hasattr(Request, '__provides__')) + self.assertTrue(hasattr(Request, '__implemented__')) + self.assertTrue(hasattr(Request, '__providedBy__')) self.assertFalse(hasattr(RequestSub, '__provides__')) + self.assertTrue(hasattr(RequestSub, '__providedBy__')) + self.assertTrue(hasattr(RequestSub, '__implemented__')) + + self.assertTrue(IRequest.implementedBy(RequestSub)) + # The call to implementedBy will add __provides__ to the class + self.assertTrue(hasattr(RequestSub, '__provides__')) + def test_subclass_with_implementer(self): from pyramid.interfaces import IRequest @@ -457,11 +467,18 @@ class Test_subclassing_Request(unittest.TestCase): pass self.assertTrue(hasattr(Request, '__provides__')) + self.assertTrue(hasattr(Request, '__implemented__')) + self.assertTrue(hasattr(Request, '__providedBy__')) self.assertTrue(hasattr(RequestSub, '__provides__')) + self.assertTrue(hasattr(RequestSub, '__providedBy__')) + self.assertTrue(hasattr(RequestSub, '__implemented__')) req = RequestSub({}) req._set_properties({'b': 'b'}) - self.assertEqual(providedBy(req), implementedBy(RequestSub)) + + self.assertTrue(IRequest.providedBy(req)) + self.assertTrue(IRequest.implementedBy(RequestSub)) + class DummyRequest: def __init__(self, environ=None): -- cgit v1.2.3 From c7c02f44492da19d848d66556c2c13b9c79ee7ee Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 12:48:30 -0700 Subject: Add test that showcases failure When _set_properties() is called before implementedBy or providedBy, the new class that is generated does NOT conform to IRequest. --- pyramid/tests/test_request.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index cf591eb36..1a7b1a106 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -479,6 +479,20 @@ class Test_subclassing_Request(unittest.TestCase): self.assertTrue(IRequest.providedBy(req)) self.assertTrue(IRequest.implementedBy(RequestSub)) + def test_subclass_mutate_before_providedBy(self): + from pyramid.interfaces import IRequest + from pyramid.request import Request + from zope.interface import providedBy, implementedBy, implementer + + class RequestSub(Request): + pass + + req = RequestSub({}) + req._set_properties({'b': 'b'}) + + self.assertTrue(IRequest.providedBy(req)) + self.assertTrue(IRequest.implementedBy(RequestSub)) + class DummyRequest: def __init__(self, environ=None): -- cgit v1.2.3 From c600ab6fe31e2ea95f1aea9f8c40ae585f6afe39 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 12:57:11 -0700 Subject: Remove __providesBy__ attribute from being copied This change reverts part of what https://github.com/Pylons/pyramid/issues/1212 changed. We attempt to copy information used by zope.interfaces to the new class to avoid creating new objects that could possibly leak. Unfortunately copying over __providedBy__ causes issues with the sub-class after modification no longer implementing interfaces, for more information see: https://github.com/Pylons/pyramid/issues/1529#issuecomment-73373581 Once __providedBy__ is removed from the list, this is no longer the case and the new class that is created in `_set_properties` properly report that it implements the interfaces it used to implement before. --- pyramid/tests/test_request.py | 1 - pyramid/util.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 1a7b1a106..091454893 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -436,7 +436,6 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase): self.assertEqual(request.environ['PATH_INFO'], '/' + encoded) class Test_subclassing_Request(unittest.TestCase): - def test_subclass(self): from pyramid.interfaces import IRequest from pyramid.request import Request diff --git a/pyramid/util.py b/pyramid/util.py index 18cef4602..7903326a5 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -87,10 +87,10 @@ class InstancePropertyMixin(object): if attrs: parent = self.__class__ cls = type(parent.__name__, (parent, object), attrs) - # We assign __provides__, __implemented__ and __providedBy__ below - # to prevent a memory leak that results from from the usage of this - # instance's eventual use in an adapter lookup. Adapter lookup - # results in ``zope.interface.implementedBy`` being called with the + # We assign __provides__ and __implemented__ below to prevent a + # memory leak that results from from the usage of this instance's + # eventual use in an adapter lookup. Adapter lookup results in + # ``zope.interface.implementedBy`` being called with the # newly-created class as an argument. Because the newly-created # class has no interface specification data of its own, lookup # causes new ClassProvides and Implements instances related to our @@ -99,7 +99,7 @@ class InstancePropertyMixin(object): # want this new class to behave exactly like it is the parent class # instead. See https://github.com/Pylons/pyramid/issues/1212 for # more information. - for name in ('__implemented__', '__providedBy__', '__provides__'): + for name in ('__implemented__', '__provides__'): # we assign these attributes conditionally to make it possible # to test this class in isolation without having any interfaces # attached to it -- cgit v1.2.3 From c0070c95eeb39186c7c2ea0dea6b972c5782f185 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 22 Feb 2015 13:17:27 -0600 Subject: remove tuple creation from inner loop --- pyramid/view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 2df79455c..4bd036428 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -419,11 +419,12 @@ class forbidden_view_config(object): def _find_views(registry, request_iface, context_iface, view_name): registered = registry.adapters.registered + view_types = (IView, ISecuredView, IMultiView) for req_type, ctx_type in itertools.product( request_iface.__sro__, context_iface.__sro__ ): source_ifaces = (IViewClassifier, req_type, ctx_type) - for view_type in (IView, ISecuredView, IMultiView): + for view_type in view_types: view_callable = registered( source_ifaces, view_type, -- cgit v1.2.3 From b0218c806d684771b00eb93af58c8482376af349 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 13 Mar 2015 02:26:29 -0700 Subject: Update code example Modify `lines` to include closing parens in source and update corresponding `emphasize-lines`. Closes #1606. --- docs/tutorials/wiki/authorization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 93cd0c18e..6c98b6f3a 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -197,9 +197,9 @@ Add the following import statements to the head of ``tutorial/tutorial/views.py``: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 6-13,15-17 + :lines: 6-17 :linenos: - :emphasize-lines: 3,6-9,11 + :emphasize-lines: 3,6-11 :language: python (Only the highlighted lines, with other necessary modifications, -- cgit v1.2.3 From 59e7ccee7600af7f8c97102a98f675145fbeae8a Mon Sep 17 00:00:00 2001 From: Junya Hayashi Date: Fri, 13 Mar 2015 19:39:31 +0900 Subject: fix comment in forbidden_view_config --- pyramid/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 02ac8849f..f48592e16 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -380,7 +380,7 @@ class forbidden_view_config(object): @forbidden_view_config() def forbidden(request): - return Response('You are not allowed', status='401 Unauthorized') + return Response('You are not allowed', status='403 Forbidden') 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 12b6f58956a50a0ad8e6d9971a0248d8f7997122 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Sun, 15 Mar 2015 15:18:51 -0400 Subject: Allow passing a custom redirect class for appending slashes --- CHANGES.txt | 4 ++++ docs/narr/urldispatch.rst | 4 +++- pyramid/config/views.py | 11 ++++++++++- pyramid/tests/test_config/test_views.py | 26 +++++++++++++++++++++++++- pyramid/view.py | 5 +++-- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 19d77eb68..a9fe1be3c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -119,6 +119,10 @@ Features explicitly different from ``request.response``. This does not change the API of a renderer. See https://github.com/Pylons/pyramid/pull/1563 +- ``Configurator().add_notfound_view()`` will now accept anything that + implements the ``IResponse`` interface and will use that as the response + class instead of the default ``HTTPFound``. + Bug Fixes --------- diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index ca6a55164..fa3e734fe 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -842,7 +842,9 @@ route. When configured, along with at least one other route in your application, this view will be invoked if the value of ``PATH_INFO`` does not already end in a slash, and if the value of ``PATH_INFO`` *plus* a slash matches any route's pattern. In this case it does an HTTP redirect to the -slash-appended ``PATH_INFO``. +slash-appended ``PATH_INFO``. In addition you may pass anything that implements +:class:`pyramid.interfaces.IResponse` which will then be used in place of the +default class (:class:`pyramid.httpexceptions.HTTPFound`). Let's use an example. If the following routes are configured in your application: diff --git a/pyramid/config/views.py b/pyramid/config/views.py index aba28467d..af2045da9 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1703,7 +1703,11 @@ class ViewsConfiguratorMixin(object): Pyramid will return the result of the view callable provided as ``view``, as normal. + If ``append_slash`` implements IResponse then that will be used as the + response class instead of the default of ``HTTPFound``. + .. versionadded:: 1.3 + .. versionchanged:: 1.6 """ for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): if arg in predicates: @@ -1737,7 +1741,12 @@ class ViewsConfiguratorMixin(object): settings.update(predicates) if append_slash: view = self._derive_view(view, attr=attr, renderer=renderer) - view = AppendSlashNotFoundViewFactory(view) + if IResponse.implementedBy(append_slash): + view = AppendSlashNotFoundViewFactory( + view, redirect_class=append_slash, + ) + else: + view = AppendSlashNotFoundViewFactory(view) settings['view'] = view else: settings['attr'] = attr diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 180050941..c5db121a0 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1941,7 +1941,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.renderers import null_renderer from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.httpexceptions import HTTPNotFound + from pyramid.httpexceptions import HTTPFound, HTTPNotFound config = self._makeOne(autocommit=True) config.add_route('foo', '/foo/') def view(request): return Response('OK') @@ -1954,6 +1954,30 @@ class TestViewsConfigurationMixin(unittest.TestCase): ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) + self.assertTrue(isinstance(result, HTTPFound)) + self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2') + + def test_add_notfound_view_append_slash_custom_response(self): + from pyramid.response import Response + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.httpexceptions import HTTPMovedPermanently, HTTPNotFound + config = self._makeOne(autocommit=True) + config.add_route('foo', '/foo/') + def view(request): return Response('OK') + config.add_notfound_view( + view, renderer=null_renderer,append_slash=HTTPMovedPermanently + ) + request = self._makeRequest(config) + request.environ['PATH_INFO'] = '/foo' + request.query_string = 'a=1&b=2' + request.path = '/scriptname/foo' + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), + request_iface=IRequest) + result = view(None, request) + self.assertTrue(isinstance(result, HTTPMovedPermanently)) self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2') def test_add_notfound_view_with_view_defaults(self): diff --git a/pyramid/view.py b/pyramid/view.py index f48592e16..a84dde83f 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -252,10 +252,11 @@ class AppendSlashNotFoundViewFactory(object): .. deprecated:: 1.3 """ - def __init__(self, notfound_view=None): + def __init__(self, notfound_view=None, redirect_class=HTTPFound): if notfound_view is None: notfound_view = default_exceptionresponse_view self.notfound_view = notfound_view + self.redirect_class = redirect_class def __call__(self, context, request): path = decode_path_info(request.environ['PATH_INFO'] or '/') @@ -268,7 +269,7 @@ class AppendSlashNotFoundViewFactory(object): qs = request.query_string if qs: qs = '?' + qs - return HTTPFound(location=request.path+'/'+qs) + return self.redirect_class(location=request.path+'/'+qs) return self.notfound_view(context, request) append_slash_notfound_view = AppendSlashNotFoundViewFactory() -- cgit v1.2.3 From 0b527bab1b7745695475b894fdafed9480748b16 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Sun, 15 Mar 2015 15:37:45 -0400 Subject: Add myself to CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 4f9bd6e41..3d574f99d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -244,3 +244,5 @@ Contributors - Geoffrey T. Dairiki, 2015/02/06 - David Glick, 2015/02/12 + +- Donald Stufft, 2015/03/15 -- cgit v1.2.3 From 24358c9d1d474bb81ce423d270041464bc731ce9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 15 Mar 2015 16:04:43 -0400 Subject: add docs to notfound_view_config decorator code, expand docs to inlcude an example --- pyramid/config/views.py | 20 +++++++++++++++++--- pyramid/view.py | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index af2045da9..6be81163f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1703,11 +1703,25 @@ class ViewsConfiguratorMixin(object): Pyramid will return the result of the view callable provided as ``view``, as normal. - If ``append_slash`` implements IResponse then that will be used as the - response class instead of the default of ``HTTPFound``. + If the argument provided as ``append_slash`` is not a boolean but + instead implements :class:`~pyramid.interfaces.IResponse`, the + append_slash logic will behave as if ``append_slash=True`` was passed, + but the provided class will be used as the response class instead of + the default :class:`~pyramid.httpexceptions.HTTPFound` response class + when a redirect is performed. For example: + + .. code-block:: python + + from pyramid.httpexceptions import HTTPMovedPermanently + config.add_notfound_view(append_slash=HTTPMovedPermanently) + + The above means that a redirect to a slash-appended route will be + attempted, but instead of :class:`~pyramid.httpexceptions.HTTPFound` + being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will + be used` for the redirect response if a slash-appended route is found. - .. versionadded:: 1.3 .. versionchanged:: 1.6 + .. versionadded:: 1.3 """ for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): if arg in predicates: diff --git a/pyramid/view.py b/pyramid/view.py index a84dde83f..b30383003 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -332,6 +332,31 @@ class notfound_view_config(object): redirect to the URL implied by the route; if it does not, Pyramid will return the result of the view callable provided as ``view``, as normal. + If the argument provided as ``append_slash`` is not a boolean but + instead implements :class:`~pyramid.interfaces.IResponse`, the + append_slash logic will behave as if ``append_slash=True`` was passed, + but the provided class will be used as the response class instead of + the default :class:`~pyramid.httpexceptions.HTTPFound` response class + when a redirect is performed. For example: + + .. code-block:: python + + from pyramid.httpexceptions import ( + HTTPMovedPermanently, + HTTPNotFound + ) + + @notfound_view_config(append_slash=HTTPMovedPermanently) + def aview(request): + return HTTPNotFound('not found') + + The above means that a redirect to a slash-appended route will be + attempted, but instead of :class:`~pyramid.httpexceptions.HTTPFound` + being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will + be used` for the redirect response if a slash-appended route is found. + + .. versionchanged:: 1.6 + See :ref:`changing_the_notfound_view` for detailed usage information. """ -- cgit v1.2.3 From a7d77fdecaa120f4e095d271b687e3bb628e8696 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 15 Mar 2015 16:41:29 -0400 Subject: be more specific about the append_slash feature addition --- CHANGES.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index a9fe1be3c..639b9b802 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -119,9 +119,10 @@ Features explicitly different from ``request.response``. This does not change the API of a renderer. See https://github.com/Pylons/pyramid/pull/1563 -- ``Configurator().add_notfound_view()`` will now accept anything that - implements the ``IResponse`` interface and will use that as the response - class instead of the default ``HTTPFound``. +- The ``append_slash`` argument of ```Configurator().add_notfound_view()`` will + now accept anything that implements the ``IResponse`` interface and will use + that as the response class instead of the default ``HTTPFound``. See + https://github.com/Pylons/pyramid/pull/1610 Bug Fixes --------- -- cgit v1.2.3 From 4d4ee6a553dcde3a292ee2439c1fe5524fc66aa7 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 21 Mar 2015 02:51:16 -0700 Subject: update pylons sphinx theme on master branch, too --- docs/_themes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_themes b/docs/_themes index b14bf8c2a..02096489e 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit b14bf8c2a0d95ae8e3d38d07ad3721370ae6f3f8 +Subproject commit 02096489e62ec6c4b3c64e2e6ee874fb79c6ba10 -- cgit v1.2.3 From c4fe07f2bd053d41859f2caf5ef93dd4819a60a4 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 25 Mar 2015 21:27:16 -0700 Subject: Add a badge and link for latest documentation to complement master --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index adf7eea5e..6de42ea40 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,11 @@ Pyramid .. image:: https://readthedocs.org/projects/pyramid/badge/?version=master :target: http://docs.pylonsproject.org/projects/pyramid/en/master/ - :alt: Documentation Status + :alt: Master Documentation Status + +.. image:: https://readthedocs.org/projects/pyramid/badge/?version=latest + :target: http://docs.pylonsproject.org/projects/pyramid/en/latest/ + :alt: Latest Documentation Status Pyramid is a small, fast, down-to-earth, open source Python web framework. It makes real-world web application development and -- cgit v1.2.3 From 610b6edef76168e6a499871be10ba9ea5ea6aa6d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 26 Mar 2015 11:54:30 -0500 Subject: fix out of date match_param docs --- docs/narr/viewconfig.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index a0feef8d7..d5203c6ba 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -325,7 +325,7 @@ configured view. ``match_param`` This param may be either a single string of the format "key=value" or a - dict of key/value pairs. + tuple containing one or more of these strings. This argument ensures that the view will only be called when the :term:`request` has key/value pairs in its :term:`matchdict` that equal @@ -334,8 +334,8 @@ configured view. hand side of the expression (``edit``) for the view to "match" the current request. - If the ``match_param`` is a dict, every key/value pair must match for the - predicate to pass. + If the ``match_param`` is a tuple, every key/value pair must match + for the predicate to pass. If ``match_param`` is not supplied, the view will be invoked without consideration of the keys and values in ``request.matchdict``. -- cgit v1.2.3 From 594c252a58428433905dfc66e43c8d087c8bb51d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 30 Mar 2015 02:35:15 -0700 Subject: remove italics from internal references --- docs/_themes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_themes b/docs/_themes index 02096489e..382cba80f 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit 02096489e62ec6c4b3c64e2e6ee874fb79c6ba10 +Subproject commit 382cba80fbd6a7424818d17ec63ca520e485f108 -- cgit v1.2.3 From c15cbce92826cdee1dcc78b2060100b59a6d4caa Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 3 Apr 2015 16:09:22 -0400 Subject: cache view lookups; see #1557 --- pyramid/config/__init__.py | 10 ++++++ pyramid/config/views.py | 2 ++ pyramid/registry.py | 12 +++++++ pyramid/router.py | 59 +++++++++++++++------------------- pyramid/tests/test_config/test_init.py | 15 +++++++++ pyramid/tests/test_registry.py | 10 ++++++ pyramid/view.py | 40 +++++++++++++++-------- 7 files changed, 102 insertions(+), 46 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 401def208..549144fab 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -4,6 +4,7 @@ import logging import operator import os import sys +import threading import venusian from webob.exc import WSGIHTTPException as WebobWSGIHTTPException @@ -485,6 +486,15 @@ class Configurator( info=info, event=event) _registry.registerSelfAdapter = registerSelfAdapter + if not hasattr(_registry, '_lock'): + _registry._lock = threading.Lock() + + if not hasattr(_registry, '_clear_view_lookup_cache'): + def _clear_view_lookup_cache(): + _registry._view_lookup_cache = {} + _registry._clear_view_lookup_cache = _clear_view_lookup_cache + + # API def _get_introspector(self): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index aba28467d..4c927f5b5 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1344,6 +1344,8 @@ class ViewsConfiguratorMixin(object): 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 ( diff --git a/pyramid/registry.py b/pyramid/registry.py index 8c05940b9..fb4ec52e2 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -1,4 +1,5 @@ import operator +import threading from zope.interface import implementer @@ -39,6 +40,17 @@ class Registry(Components, dict): _settings = None + def __init__(self, *arg, **kw): + # add a registry-instance-specific lock, which is used when the lookup + # cache is mutated + self._lock = threading.Lock() + # add a view lookup cache + self._clear_view_lookup_cache() + Components.__init__(self, *arg, **kw) + + def _clear_view_lookup_cache(self): + self._view_lookup_cache = {} + def __nonzero__(self): # defeat bool determination via dict.__len__ return True diff --git a/pyramid/router.py b/pyramid/router.py index eac7f7976..9ce5d2487 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -138,47 +138,40 @@ class Router(object): # find a view callable context_iface = providedBy(context) - views_iter = _find_views( + view_callables = _find_views( registry, request.request_iface, context_iface, view_name, ) - view_callable = next(views_iter, None) - - # invoke the view callable - if view_callable is None: - if self.debug_notfound: - msg = ( - 'debug_notfound of url %s; path_info: %r, ' - 'context: %r, view_name: %r, subpath: %r, ' - 'traversed: %r, root: %r, vroot: %r, ' - 'vroot_path: %r' % ( - request.url, request.path_info, context, - view_name, subpath, traversed, root, vroot, - vroot_path) - ) - logger and logger.debug(msg) - else: - msg = request.path_info - raise HTTPNotFound(msg) - else: + pme = None + + for view_callable in view_callables: + # look for views that meet the predicate criteria try: response = view_callable(context, request) - except PredicateMismatch: - # look for other views that meet the predicate - # criteria - for view_callable in views_iter: - if view_callable is not None: - try: - response = view_callable(context, request) - break - except PredicateMismatch: - pass - else: - raise - return response + return response + except PredicateMismatch as _pme: + pme = _pme + + if pme is not None: + raise pme + + if self.debug_notfound: + msg = ( + 'debug_notfound of url %s; path_info: %r, ' + 'context: %r, view_name: %r, subpath: %r, ' + 'traversed: %r, root: %r, vroot: %r, ' + 'vroot_path: %r' % ( + request.url, request.path_info, context, + view_name, subpath, traversed, root, vroot, + vroot_path) + ) + logger and logger.debug(msg) + else: + msg = request.path_info + raise HTTPNotFound(msg) def invoke_subrequest(self, request, use_tweens=False): """Obtain a response object from the Pyramid application based on diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 0ed04eb06..ec58e2466 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -343,6 +343,21 @@ class ConfiguratorTests(unittest.TestCase): {'info': '', 'provided': 'provided', 'required': 'required', 'name': 'abc', 'event': True}) + def test__fix_registry_adds__lock(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() + self.assertTrue(hasattr(reg, '_lock')) + + def test__fix_registry_adds_clear_view_lookup_cache(self): + reg = DummyRegistry() + config = self._makeOne(reg) + self.assertFalse(hasattr(reg, '_clear_view_lookup_cache')) + config._fix_registry() + self.assertFalse(hasattr(reg, '_view_lookup_cache')) + reg._clear_view_lookup_cache() + self.assertEqual(reg._view_lookup_cache, {}) + def test_setup_registry_calls_fix_registry(self): reg = DummyRegistry() config = self._makeOne(reg) diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py index 50f49f24d..c9dff5b22 100644 --- a/pyramid/tests/test_registry.py +++ b/pyramid/tests/test_registry.py @@ -12,6 +12,16 @@ class TestRegistry(unittest.TestCase): registry = self._makeOne() self.assertEqual(registry.__nonzero__(), True) + def test__lock(self): + registry = self._makeOne() + self.assertTrue(registry._lock) + + def test_clear_view_cache_lookup(self): + registry = self._makeOne() + registry._view_lookup_cache[1] = 2 + registry._clear_view_lookup_cache() + self.assertEqual(registry._view_lookup_cache, {}) + def test_package_name(self): package_name = 'testing' registry = self._getTargetClass()(package_name) diff --git a/pyramid/view.py b/pyramid/view.py index 4bd036428..8cde1d004 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -419,16 +419,30 @@ class forbidden_view_config(object): def _find_views(registry, request_iface, context_iface, view_name): registered = registry.adapters.registered - view_types = (IView, ISecuredView, IMultiView) - for req_type, ctx_type in itertools.product( - request_iface.__sro__, context_iface.__sro__ - ): - source_ifaces = (IViewClassifier, req_type, ctx_type) - for view_type in view_types: - view_callable = registered( - source_ifaces, - view_type, - name=view_name, - ) - if view_callable is not None: - yield view_callable + cache = registry._view_lookup_cache + views = cache.get((request_iface, context_iface, view_name)) + if views is None: + views = [] + view_types = (IView, ISecuredView, IMultiView) + for req_type, ctx_type in itertools.product( + request_iface.__sro__, context_iface.__sro__ + ): + source_ifaces = (IViewClassifier, req_type, ctx_type) + for view_type in view_types: + view_callable = registered( + source_ifaces, + view_type, + name=view_name, + ) + if view_callable is not None: + views.append(view_callable) + if views: + # do not cache view lookup misses. rationale: dont allow cache to + # grow without bound if somebody tries to hit the site with many + # missing URLs. we could use an LRU cache instead, but then + # purposeful misses by an attacker would just blow out the cache + # anyway. downside: misses will almost always consume more CPU than + # hits in steady state. + with registry._lock: + cache[(request_iface, context_iface, view_name)] = views + return iter(views) -- cgit v1.2.3 From 99bc0c2f8ef7615fb3b1e4d8e3c6253157cf4ac8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 3 Apr 2015 16:30:44 -0400 Subject: no need to iter() the result --- pyramid/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 8cde1d004..127122a36 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -445,4 +445,4 @@ def _find_views(registry, request_iface, context_iface, view_name): # hits in steady state. with registry._lock: cache[(request_iface, context_iface, view_name)] = views - return iter(views) + return views -- cgit v1.2.3 From 13f5943a213bbf82c09d02a04cc610b77cfe1c79 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Apr 2015 00:06:17 -0400 Subject: use _find_views in pviews --- pyramid/scripts/pviews.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 5e92ec4e6..15431e9b2 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -6,6 +6,7 @@ from pyramid.interfaces import IMultiView from pyramid.paster import bootstrap from pyramid.request import Request from pyramid.scripts.common import parse_vars +from pyramid.view import _find_views def main(argv=sys.argv, quiet=False): command = PViewsCommand(argv, quiet) @@ -90,11 +91,15 @@ class PViewsCommand(object): IRouteRequest, name=route.name, default=IRequest) - view = adapters.lookup( - (IViewClassifier, request_iface, context_iface), - IView, name='', default=None) - if view is None: + views = _find_views( + request.registry, + request_iface, + context_iface, + '' + ) + if not views: continue + view = views[0] view.__request_attrs__ = {} view.__request_attrs__['matchdict'] = match view.__request_attrs__['matched_route'] = route @@ -149,17 +154,31 @@ class PViewsCommand(object): # find a view callable context_iface = providedBy(context) if routes_multiview is None: - view = adapters.lookup( - (IViewClassifier, request_iface, context_iface), - IView, name=view_name, default=None) + views = _find_views( + request.registry, + request_iface, + context_iface, + view_name, + ) + if views: + view = views[0] + else: + view = None else: view = RoutesMultiView(infos, context_iface, root_factory, request) # routes are not registered with a view name if view is None: - view = adapters.lookup( - (IViewClassifier, request_iface, context_iface), - IView, name='', default=None) + views = _find_views( + request.registry, + request_iface, + context_iface, + '', + ) + if views: + view = views[0] + else: + view = None # we don't want a multiview here if IMultiView.providedBy(view): view = None -- cgit v1.2.3 From c8f1ea7e7185e7ea22abcd5abb9a23738165d140 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Apr 2015 05:02:23 -0400 Subject: unused imports --- pyramid/scripts/pviews.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 15431e9b2..4922c3a70 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -66,8 +66,6 @@ class PViewsCommand(object): from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest from pyramid.interfaces import IRoutesMapper - from pyramid.interfaces import IView - from pyramid.interfaces import IViewClassifier from pyramid.interfaces import ITraverser from pyramid.traversal import DefaultRootFactory from pyramid.traversal import ResourceTreeTraverser -- cgit v1.2.3 From e028e0a6fbb0050a007236721277b2fa62a00e5c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Apr 2015 05:15:57 -0400 Subject: use _find_views instead of lookup in proutes --- pyramid/scripts/proutes.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 544947724..a389c303c 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -4,18 +4,16 @@ import sys import textwrap import re +from zope.interface import Interface + from pyramid.paster import bootstrap from pyramid.compat import (string_types, configparser) -from pyramid.interfaces import ( - IRouteRequest, - IViewClassifier, - IView, -) +from pyramid.interfaces import IRouteRequest from pyramid.config import not_ from pyramid.scripts.common import parse_vars from pyramid.static import static_view -from zope.interface import Interface +from pyramid.view import _find_views PAD = 3 @@ -159,12 +157,11 @@ def get_route_data(route, registry): (route.name, _get_pattern(route), UNKNOWN_KEY, ANY_KEY) ] - view_callable = registry.adapters.lookup( - (IViewClassifier, request_iface, Interface), - IView, - name='', - default=None - ) + view_callables = _find_views(registry, request_iface, Interface, '') + if view_callables: + view_callable = view_callables[0] + else: + view_callable = None view_module = _get_view_module(view_callable) # Introspectables can be turned off, so there could be a chance -- cgit v1.2.3 From eb3ac8e33b0fe516fb6cf8d27061eff6dec1afc2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Apr 2015 05:32:47 -0400 Subject: break out _call_view into separate importable callable for use in scripts --- pyramid/router.py | 52 +++++++++++++++++++++------------------------------- pyramid/view.py | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/pyramid/router.py b/pyramid/router.py index 9ce5d2487..4054ef52e 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -22,10 +22,9 @@ from pyramid.events import ( NewResponse, ) -from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request -from pyramid.view import _find_views +from pyramid.view import _call_view from pyramid.request import apply_request_extensions from pyramid.threadlocal import manager @@ -138,40 +137,31 @@ class Router(object): # find a view callable context_iface = providedBy(context) - view_callables = _find_views( + response = _call_view( registry, - request.request_iface, + request, + context, context_iface, - view_name, + view_name ) - pme = None + if response is None: + if self.debug_notfound: + msg = ( + 'debug_notfound of url %s; path_info: %r, ' + 'context: %r, view_name: %r, subpath: %r, ' + 'traversed: %r, root: %r, vroot: %r, ' + 'vroot_path: %r' % ( + request.url, request.path_info, context, + view_name, subpath, traversed, root, vroot, + vroot_path) + ) + logger and logger.debug(msg) + else: + msg = request.path_info + raise HTTPNotFound(msg) - for view_callable in view_callables: - # look for views that meet the predicate criteria - try: - response = view_callable(context, request) - return response - except PredicateMismatch as _pme: - pme = _pme - - if pme is not None: - raise pme - - if self.debug_notfound: - msg = ( - 'debug_notfound of url %s; path_info: %r, ' - 'context: %r, view_name: %r, subpath: %r, ' - 'traversed: %r, root: %r, vroot: %r, ' - 'vroot_path: %r' % ( - request.url, request.path_info, context, - view_name, subpath, traversed, root, vroot, - vroot_path) - ) - logger and logger.debug(msg) - else: - msg = request.path_info - raise HTTPNotFound(msg) + return response def invoke_subrequest(self, request, use_tweens=False): """Obtain a response object from the Pyramid application based on diff --git a/pyramid/view.py b/pyramid/view.py index 127122a36..8565cd875 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -16,6 +16,8 @@ from pyramid.compat import ( decode_path_info, ) +from pyramid.exceptions import PredicateMismatch + from pyramid.httpexceptions import ( HTTPFound, default_exceptionresponse_view, @@ -446,3 +448,27 @@ def _find_views(registry, request_iface, context_iface, view_name): with registry._lock: cache[(request_iface, context_iface, view_name)] = views return views + +def _call_view(registry, request, context, context_iface, view_name): + view_callables = _find_views( + registry, + request.request_iface, + context_iface, + view_name, + ) + + pme = None + response = None + + for view_callable in view_callables: + # look for views that meet the predicate criteria + try: + response = view_callable(context, request) + return response + except PredicateMismatch as _pme: + pme = _pme + + if pme is not None: + raise pme + + return response -- cgit v1.2.3 From 8491964f35f5a3327751eb9dc42c9d6f128c9c22 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Apr 2015 06:08:43 -0400 Subject: convert render_view_to_response to use _call_view --- pyramid/request.py | 1 + pyramid/testing.py | 3 +- pyramid/tests/test_config/test_init.py | 4 ++- pyramid/tests/test_config/test_views.py | 7 +++- pyramid/tests/test_view.py | 9 +++-- pyramid/view.py | 59 +++++++++++++++++++++------------ 6 files changed, 54 insertions(+), 29 deletions(-) diff --git a/pyramid/request.py b/pyramid/request.py index 3cbe5d9e3..13b8cd339 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -182,6 +182,7 @@ class Request( exc_info = None matchdict = None matched_route = None + request_iface = IRequest ResponseClass = Response diff --git a/pyramid/testing.py b/pyramid/testing.py index 667e6af4e..772914f3b 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -21,7 +21,7 @@ from pyramid.compat import ( from pyramid.config import Configurator from pyramid.decorator import reify from pyramid.path import caller_package -from pyramid.response import Response, _get_response_factory +from pyramid.response import _get_response_factory from pyramid.registry import Registry from pyramid.security import ( @@ -327,6 +327,7 @@ class DummyRequest( charset = 'UTF-8' script_name = '' _registry = None + request_iface = IRequest def __init__(self, params=None, environ=None, headers=None, path='/', cookies=None, post=None, **kw): diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index ec58e2466..de199d079 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1,5 +1,4 @@ import unittest -import warnings import os @@ -16,6 +15,8 @@ from pyramid.tests.test_config import DummyContext from pyramid.exceptions import ConfigurationExecutionError from pyramid.exceptions import ConfigurationConflictError +from pyramid.interfaces import IRequest + class ConfiguratorTests(unittest.TestCase): def _makeOne(self, *arg, **kw): from pyramid.config import Configurator @@ -1845,6 +1846,7 @@ class TestGlobalRegistriesIntegration(unittest.TestCase): class DummyRequest: subpath = () matchdict = None + request_iface = IRequest def __init__(self, environ=None): if environ is None: environ = {} diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 706d7ff77..b4ad357ff 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -4131,7 +4131,11 @@ class DummyRegistry: self.settings = {} from zope.interface import implementer -from pyramid.interfaces import IResponse +from pyramid.interfaces import ( + IResponse, + IRequest, + ) + @implementer(IResponse) class DummyResponse(object): content_type = None @@ -4141,6 +4145,7 @@ class DummyResponse(object): class DummyRequest: subpath = () matchdict = None + request_iface = IRequest def __init__(self, environ=None): if environ is None: diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 309fd47e2..ff73a93ab 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -5,6 +5,8 @@ from zope.interface import implementer from pyramid import testing +from pyramid.interfaces import IRequest + class BaseTest(object): def setUp(self): self.config = testing.setUp() @@ -13,7 +15,6 @@ class BaseTest(object): testing.tearDown() def _registerView(self, reg, app, name): - from pyramid.interfaces import IRequest from pyramid.interfaces import IViewClassifier for_ = (IViewClassifier, IRequest, IContext) from pyramid.interfaces import IView @@ -32,14 +33,11 @@ class BaseTest(object): return environ def _makeRequest(self, **environ): - from pyramid.interfaces import IRequest - from zope.interface import directlyProvides - from webob import Request + from pyramid.request import Request from pyramid.registry import Registry environ = self._makeEnviron(**environ) request = Request(environ) request.registry = Registry() - directlyProvides(request, IRequest) return request def _makeContext(self): @@ -676,6 +674,7 @@ def make_view(response): class DummyRequest: exception = None + request_iface = IRequest def __init__(self, environ=None): if environ is None: diff --git a/pyramid/view.py b/pyramid/view.py index 8565cd875..1ea4e3b47 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -11,10 +11,7 @@ from pyramid.interfaces import ( IViewClassifier, ) -from pyramid.compat import ( - map_, - decode_path_info, - ) +from pyramid.compat import decode_path_info from pyramid.exceptions import PredicateMismatch @@ -45,24 +42,24 @@ def render_view_to_response(context, request, name='', secure=True): disallowed. If ``secure`` is ``False``, no permission checking is done.""" - provides = [IViewClassifier] + map_(providedBy, (request, context)) - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() - view = reg.adapters.lookup(provides, IView, name=name) - if view is None: - return None - if not secure: - # the view will have a __call_permissive__ attribute if it's - # secured; otherwise it won't. - view = getattr(view, '__call_permissive__', view) + registry = getattr(request, 'registry', None) + if registry is None: + registry = get_current_registry() + + context_iface = providedBy(context) + + response = _call_view( + registry, + request, + context, + context_iface, + name, + secure = secure, + ) + + return response # NB: might be None - # if this view is secured, it will raise a Forbidden - # appropriately if the executing user does not have the proper - # permission - return view(context, request) def render_view_to_iterable(context, request, name='', secure=True): """ Call the :term:`view callable` configured with a :term:`view @@ -447,9 +444,17 @@ def _find_views(registry, request_iface, context_iface, view_name): # hits in steady state. with registry._lock: cache[(request_iface, context_iface, view_name)] = views + return views -def _call_view(registry, request, context, context_iface, view_name): +def _call_view( + registry, + request, + context, + context_iface, + view_name, + secure=True, + ): view_callables = _find_views( registry, request.request_iface, @@ -463,6 +468,18 @@ def _call_view(registry, request, context, context_iface, view_name): for view_callable in view_callables: # look for views that meet the predicate criteria try: + if not secure: + # the view will have a __call_permissive__ attribute if it's + # secured; otherwise it won't. + view_callable = getattr( + view_callable, + '__call_permissive__', + view_callable + ) + + # if this view is secured, it will raise a Forbidden + # appropriately if the executing user does not have the proper + # permission response = view_callable(context, request) return response except PredicateMismatch as _pme: -- cgit v1.2.3 From 2a842e615d5f36b4dfb26e843de7d73147e546f8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Apr 2015 06:18:29 -0400 Subject: add comment about not changing view_execution_permitted use of .lookup for view finding --- pyramid/security.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyramid/security.py b/pyramid/security.py index f993ef353..82e6b73a9 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -225,6 +225,8 @@ def view_execution_permitted(context, request, name=''): """ reg = _get_registry(request) provides = [IViewClassifier] + map_(providedBy, (request, context)) + # XXX not sure what to do here about using _find_views or analogue; + # for now let's just keep it as-is view = reg.adapters.lookup(provides, ISecuredView, name=name) if view is None: view = reg.adapters.lookup(provides, IView, name=name) -- cgit v1.2.3 From 17c7f4091a3e747fb215328803462ff14de556d2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Apr 2015 06:44:14 -0400 Subject: change excview_tween to use _call_view instead of adapters.lookup; parameterize _call_view and _find_views to cope --- pyramid/tweens.py | 17 +++++++++++------ pyramid/view.py | 26 ++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/pyramid/tweens.py b/pyramid/tweens.py index 831de8481..d6044dcdc 100644 --- a/pyramid/tweens.py +++ b/pyramid/tweens.py @@ -3,17 +3,16 @@ import sys from pyramid.interfaces import ( IExceptionViewClassifier, IRequest, - IView, ) from zope.interface import providedBy +from pyramid.view import _call_view def excview_tween_factory(handler, registry): """ A :term:`tween` factory which produces a tween that catches an exception raised by downstream tweens (or the main Pyramid request handler) and, if possible, converts it into a Response using an :term:`exception view`.""" - adapters = registry.adapters def excview_tween(request): attrs = request.__dict__ @@ -39,11 +38,17 @@ def excview_tween_factory(handler, registry): # https://github.com/Pylons/pyramid/issues/700 request_iface = attrs.get('request_iface', IRequest) provides = providedBy(exc) - for_ = (IExceptionViewClassifier, request_iface.combined, provides) - view_callable = adapters.lookup(for_, IView, default=None) - if view_callable is None: + response = _call_view( + registry, + request, + exc, + provides, + '', + view_classifier=IExceptionViewClassifier, + request_iface=request_iface.combined + ) + if response is None: raise - response = view_callable(exc, request) return response diff --git a/pyramid/view.py b/pyramid/view.py index 1ea4e3b47..ffa3fa791 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -9,6 +9,7 @@ from pyramid.interfaces import ( ISecuredView, IView, IViewClassifier, + IRequest, ) from pyramid.compat import decode_path_info @@ -416,17 +417,27 @@ class forbidden_view_config(object): settings['_info'] = info.codeinfo # fbo "action_method" return wrapped -def _find_views(registry, request_iface, context_iface, view_name): +def _find_views( + registry, + request_iface, + context_iface, + view_name, + view_types=None, + view_classifier=None, + ): + if view_types is None: + view_types = (IView, ISecuredView, IMultiView) + if view_classifier is None: + view_classifier = IViewClassifier registered = registry.adapters.registered cache = registry._view_lookup_cache views = cache.get((request_iface, context_iface, view_name)) if views is None: views = [] - view_types = (IView, ISecuredView, IMultiView) for req_type, ctx_type in itertools.product( request_iface.__sro__, context_iface.__sro__ ): - source_ifaces = (IViewClassifier, req_type, ctx_type) + source_ifaces = (view_classifier, req_type, ctx_type) for view_type in view_types: view_callable = registered( source_ifaces, @@ -453,13 +464,20 @@ def _call_view( context, context_iface, view_name, + view_types=None, + view_classifier=None, secure=True, + request_iface=None, ): + if request_iface is None: + request_iface = getattr(request, 'request_iface', IRequest) view_callables = _find_views( registry, - request.request_iface, + request_iface, context_iface, view_name, + view_types=view_types, + view_classifier=view_classifier, ) pme = None -- cgit v1.2.3 From 200c9eb4cd69f7bdccb19cfa41d2c0ede79665e2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 12 Apr 2015 16:04:39 -0400 Subject: add change note related to #1529 --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 639b9b802..850098de1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,10 @@ Next release Features -------- +- Make it possible to subclass ``pyramid.request.Request`` and also use + ``pyramid.request.Request.add_request.method``. See + https://github.com/Pylons/pyramid/issues/1529 + - The ``pyramid.config.Configurator`` has grown the ability to allow actions to call other actions during a commit-cycle. This enables much more logic to be placed into actions, such as the ability to invoke other actions -- cgit v1.2.3 From d7734b0ca17e08fa12f8f8b9d6e44c9ef3aba99e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 12 Apr 2015 16:48:05 -0400 Subject: omg how embarrassing, forgot to run tests before merging the request subclass fix branch --- pyramid/tests/test_request.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 257e70435..eedef3af2 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -435,7 +435,6 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase): self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded) self.assertEqual(request.environ['PATH_INFO'], '/' + encoded) -<<<<<<< HEAD class Test_apply_request_extensions(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -503,7 +502,8 @@ class Test_subclassing_Request(unittest.TestCase): def test_subclass_with_implementer(self): from pyramid.interfaces import IRequest from pyramid.request import Request - from zope.interface import providedBy, implementedBy, implementer + from pyramid.util import InstancePropertyHelper + from zope.interface import implementer @implementer(IRequest) class RequestSub(Request): @@ -517,7 +517,8 @@ class Test_subclassing_Request(unittest.TestCase): self.assertTrue(hasattr(RequestSub, '__implemented__')) req = RequestSub({}) - req._set_properties({'b': 'b'}) + helper = InstancePropertyHelper() + helper.apply_properties(req, {'b': 'b'}) self.assertTrue(IRequest.providedBy(req)) self.assertTrue(IRequest.implementedBy(RequestSub)) @@ -525,13 +526,14 @@ class Test_subclassing_Request(unittest.TestCase): def test_subclass_mutate_before_providedBy(self): from pyramid.interfaces import IRequest from pyramid.request import Request - from zope.interface import providedBy, implementedBy, implementer + from pyramid.util import InstancePropertyHelper class RequestSub(Request): pass req = RequestSub({}) - req._set_properties({'b': 'b'}) + helper = InstancePropertyHelper() + helper.apply_properties(req, {'b': 'b'}) self.assertTrue(IRequest.providedBy(req)) self.assertTrue(IRequest.implementedBy(RequestSub)) -- cgit v1.2.3 From 109b2a0b29e0d63a521a91021c736851248ac7d3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Apr 2015 10:43:21 -0400 Subject: make it possible to run scaffold functests using tox --- CHANGES.txt | 3 +++ HACKING.txt | 7 +++++++ RELEASING.txt | 5 ++++- pyramid/tests/test_request.py | 1 - tox.ini | 27 ++++++++++++++++++++++++++- 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 850098de1..471683d25 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -230,6 +230,9 @@ Scaffolds - Removed non-ascii copyright symbol from templates, as this was causing the scaffolds to fail for project generation. +- You can now run the scaffolding func tests via ``tox py2-scaffolds`` and + ``tox py3-scaffolds``. + 1.5 (2014-04-08) ================ diff --git a/HACKING.txt b/HACKING.txt index e104869ec..91366cc28 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -191,6 +191,13 @@ Running Tests $ $VENV/bin/easy_install pytest $ py.test --strict pyramid/ +- 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: + + $ tox py2-scaffolds + $ tox py3-scaffolds + Test Coverage ------------- diff --git a/RELEASING.txt b/RELEASING.txt index 0adef552c..c22c40000 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -18,7 +18,10 @@ Releasing Pyramid - Make sure all scaffold tests pass (Py 2.6, 2.7, 3.2, 3.3 and pypy on UNIX; this doesn't work on Windows): - $ python pyramid/scaffolds/tests.py + $ tox py3-scaffolds + $ tox py2-scaffolds + $ tox pypy-scaffolds + $ tox pypy3-scaffolds - Ensure all features of the release are documented (audit CHANGES.txt or communicate with contributors). diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index eedef3af2..2c2298f26 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -482,7 +482,6 @@ class Test_subclassing_Request(unittest.TestCase): def test_subclass(self): from pyramid.interfaces import IRequest from pyramid.request import Request - from zope.interface import providedBy, implementedBy class RequestSub(Request): pass diff --git a/tox.ini b/tox.ini index e0f99e7f6..e0ef2277b 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,8 @@ envlist = py26,py27,py32,py33,py34,pypy,pypy3, {py2,py3}-docs, - {py2,py3}-cover,coverage + {py2,py3}-cover,coverage, + scaffolds [testenv] # Most of these are defaults but if you specify any you can't fall back @@ -61,3 +62,27 @@ deps = coverage setenv = COVERAGE_FILE=.coverage + +[testenv:py3-scaffolds] +basepython = python3.4 +commands = + python pyramid/scaffolds/tests.py +deps = virtualenv + +[testenv:py2-scaffolds] +basepython = python2.7 +commands = + python pyramid/scaffolds/tests.py +deps = virtualenv + +[testenv:pypy-scaffolds] +basepython = pypy +commands = + python pyramid/scaffolds/tests.py +deps = virtualenv + +[testenv:pypy3-scaffolds] +basepython = pypy3 +commands = + python pyramid/scaffolds/tests.py +deps = virtualenv -- cgit v1.2.3 From 46805450633291682f97e87055313fc3be477326 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Apr 2015 10:59:18 -0400 Subject: unbreak tox --- tox.ini | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index e0ef2277b..bfaf8c008 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,6 @@ envlist = py26,py27,py32,py33,py34,pypy,pypy3, {py2,py3}-docs, {py2,py3}-cover,coverage, - scaffolds [testenv] # Most of these are defaults but if you specify any you can't fall back @@ -63,14 +62,14 @@ deps = setenv = COVERAGE_FILE=.coverage -[testenv:py3-scaffolds] -basepython = python3.4 +[testenv:py2-scaffolds] +basepython = python2.7 commands = python pyramid/scaffolds/tests.py deps = virtualenv -[testenv:py2-scaffolds] -basepython = python2.7 +[testenv:py3-scaffolds] +basepython = python3.4 commands = python pyramid/scaffolds/tests.py deps = virtualenv -- cgit v1.2.3