diff options
| -rw-r--r-- | CHANGES.txt | 12 | ||||
| -rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
| -rw-r--r-- | HACKING.txt | 7 | ||||
| -rw-r--r-- | README.rst | 6 | ||||
| -rw-r--r-- | RELEASING.txt | 5 | ||||
| -rw-r--r-- | docs/narr/urldispatch.rst | 4 | ||||
| -rw-r--r-- | docs/narr/viewconfig.rst | 6 | ||||
| -rw-r--r-- | docs/tutorials/wiki/authorization.rst | 4 | ||||
| -rw-r--r-- | pyramid/config/__init__.py | 10 | ||||
| -rw-r--r-- | pyramid/config/views.py | 27 | ||||
| -rw-r--r-- | pyramid/registry.py | 12 | ||||
| -rw-r--r-- | pyramid/request.py | 1 | ||||
| -rw-r--r-- | pyramid/router.py | 41 | ||||
| -rw-r--r-- | pyramid/scripts/proutes.py | 21 | ||||
| -rw-r--r-- | pyramid/scripts/pviews.py | 41 | ||||
| -rw-r--r-- | pyramid/security.py | 2 | ||||
| -rw-r--r-- | pyramid/testing.py | 1 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_init.py | 19 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 105 | ||||
| -rw-r--r-- | pyramid/tests/test_registry.py | 10 | ||||
| -rw-r--r-- | pyramid/tests/test_request.py | 60 | ||||
| -rw-r--r-- | pyramid/tests/test_view.py | 9 | ||||
| -rw-r--r-- | pyramid/tweens.py | 17 | ||||
| -rw-r--r-- | pyramid/util.py | 14 | ||||
| -rw-r--r-- | pyramid/view.py | 165 | ||||
| -rw-r--r-- | tox.ini | 26 |
26 files changed, 518 insertions, 109 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 19d77eb68..471683d25 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 @@ -119,6 +123,11 @@ Features explicitly different from ``request.response``. This does not change the API of a renderer. See https://github.com/Pylons/pyramid/pull/1563 +- 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 --------- @@ -221,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/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 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/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 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/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/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``. 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, diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 7e8eb0326..5a1b7b122 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 eff1e2e95..a522880c4 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1359,6 +1359,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 ( @@ -1718,6 +1720,24 @@ class ViewsConfiguratorMixin(object): 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 + 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. + + .. versionchanged:: 1.6 .. versionadded:: 1.3 """ for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): @@ -1752,7 +1772,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/registry.py b/pyramid/registry.py index 0610a274e..1073134ff 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/request.py b/pyramid/request.py index d9fcd6d8b..83c9b53e2 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/router.py b/pyramid/router.py index 0b1ecade7..4054ef52e 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -13,8 +13,6 @@ from pyramid.interfaces import ( IRequestFactory, IRoutesMapper, ITraverser, - IView, - IViewClassifier, ITweens, ) @@ -24,9 +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 _call_view from pyramid.request import apply_request_extensions from pyramid.threadlocal import manager @@ -139,12 +137,15 @@ 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) + response = _call_view( + registry, + request, + context, + context_iface, + view_name + ) - # invoke the view callable - if view_callable is None: + if response is None: if self.debug_notfound: msg = ( 'debug_notfound of url %s; path_info: %r, ' @@ -159,28 +160,7 @@ class Router(object): else: msg = request.path_info raise HTTPNotFound(msg) - else: - try: - response = view_callable(context, request) - 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 - if view_callable is not None: - try: - response = view_callable(context, request) - break - except PredicateMismatch: - pass - else: - raise + return response def invoke_subrequest(self, request, use_tweens=False): @@ -242,4 +222,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/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 diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 15eebdfb4..9018eddb4 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) @@ -65,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 @@ -90,11 +89,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 @@ -147,17 +150,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 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) diff --git a/pyramid/testing.py b/pyramid/testing.py index fd24934ac..58dcb0b59 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -328,6 +328,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 0ed04eb06..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 @@ -343,6 +344,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) @@ -1830,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 180050941..1c2d300a1 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 @@ -1941,7 +2013,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 +2026,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): @@ -4059,7 +4155,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 @@ -4069,6 +4169,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_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/tests/test_request.py b/pyramid/tests/test_request.py index 79cf1abb8..2c2298f26 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -478,6 +478,66 @@ class Test_apply_request_extensions(unittest.TestCase): class Dummy(object): pass +class Test_subclassing_Request(unittest.TestCase): + def test_subclass(self): + from pyramid.interfaces import IRequest + from pyramid.request import Request + + class RequestSub(Request): + 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 + from pyramid.request import Request + from pyramid.util import InstancePropertyHelper + from zope.interface import implementer + + @implementer(IRequest) + class RequestSub(Request): + 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({}) + helper = InstancePropertyHelper() + helper.apply_properties(req, {'b': 'b'}) + + 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 pyramid.util import InstancePropertyHelper + + class RequestSub(Request): + pass + + req = RequestSub({}) + helper = InstancePropertyHelper() + helper.apply_properties(req, {'b': 'b'}) + + self.assertTrue(IRequest.providedBy(req)) + self.assertTrue(IRequest.implementedBy(RequestSub)) + + class DummyRequest(object): 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/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/util.py b/pyramid/util.py index de8ca34cf..1ae7e6afc 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -86,19 +86,19 @@ class InstancePropertyHelper(object): if attrs: parent = target.__class__ newcls = 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 # just-generated class to be created and set into the newly-created # class' __dict__. We don't want these instances to be created; we # 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__'): + # instead. See GitHub issues #1212, #1529 and #1568 for more + # information. + 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 diff --git a/pyramid/view.py b/pyramid/view.py index 1dd10e2ed..ed151d196 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -1,17 +1,20 @@ +import itertools import venusian from zope.interface import providedBy from pyramid.interfaces import ( IRoutesMapper, + IMultiView, + ISecuredView, IView, IViewClassifier, + IRequest, ) -from pyramid.compat import ( - map_, - decode_path_info, - ) +from pyramid.compat import decode_path_info + +from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import ( HTTPFound, @@ -40,24 +43,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 @@ -252,10 +255,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 +272,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() @@ -331,6 +335,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. """ @@ -380,7 +409,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 @@ -414,3 +443,93 @@ class forbidden_view_config(object): settings['_info'] = info.codeinfo # fbo "action_method" return wrapped +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 = [] + for req_type, ctx_type in itertools.product( + request_iface.__sro__, context_iface.__sro__ + ): + source_ifaces = (view_classifier, 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 views + +def _call_view( + registry, + request, + 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_iface, + context_iface, + view_name, + view_types=view_types, + view_classifier=view_classifier, + ) + + pme = None + response = None + + 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: + pme = _pme + + if pme is not None: + raise pme + + return response @@ -2,7 +2,7 @@ envlist = py26,py27,py32,py33,py34,pypy,pypy3,pep8, {py2,py3}-docs, - {py2,py3}-cover,coverage + {py2,py3}-cover,coverage, [testenv] # Most of these are defaults but if you specify any you can't fall back @@ -71,3 +71,27 @@ deps = coverage setenv = COVERAGE_FILE=.coverage + +[testenv:py2-scaffolds] +basepython = python2.7 +commands = + python pyramid/scaffolds/tests.py +deps = virtualenv + +[testenv:py3-scaffolds] +basepython = python3.4 +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 |
