summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt12
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--HACKING.txt7
-rw-r--r--README.rst6
-rw-r--r--RELEASING.txt5
-rw-r--r--docs/narr/urldispatch.rst4
-rw-r--r--docs/narr/viewconfig.rst6
-rw-r--r--docs/tutorials/wiki/authorization.rst4
-rw-r--r--pyramid/config/__init__.py10
-rw-r--r--pyramid/config/views.py27
-rw-r--r--pyramid/registry.py12
-rw-r--r--pyramid/request.py1
-rw-r--r--pyramid/router.py41
-rw-r--r--pyramid/scripts/proutes.py21
-rw-r--r--pyramid/scripts/pviews.py41
-rw-r--r--pyramid/security.py2
-rw-r--r--pyramid/testing.py1
-rw-r--r--pyramid/tests/test_config/test_init.py19
-rw-r--r--pyramid/tests/test_config/test_views.py105
-rw-r--r--pyramid/tests/test_registry.py10
-rw-r--r--pyramid/tests/test_request.py60
-rw-r--r--pyramid/tests/test_view.py9
-rw-r--r--pyramid/tweens.py17
-rw-r--r--pyramid/util.py14
-rw-r--r--pyramid/view.py165
-rw-r--r--tox.ini26
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
diff --git a/tox.ini b/tox.ini
index b52eac67e..e8d7a057b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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