Chris McDonough
2018-07-04 40d68c032df749edfd3817edae442a0768aeec76
Allow append_slash to use a subrequest rather than a redirect, closes #2607
10 files modified
146 ■■■■ changed files
CHANGES.rst 5 ●●●●● patch | view | raw | blame | history
docs/api/view.rst 5 ●●●●● patch | view | raw | blame | history
docs/glossary.rst 7 ●●●●● patch | view | raw | blame | history
docs/narr/subrequest.rst 4 ●●●● patch | view | raw | blame | history
docs/narr/urldispatch.rst 26 ●●●● patch | view | raw | blame | history
pyramid/config/views.py 8 ●●●● patch | view | raw | blame | history
pyramid/tests/pkgs/notfoundview/__init__.py 12 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_views.py 28 ●●●● patch | view | raw | blame | history
pyramid/tests/test_integration.py 2 ●●●●● patch | view | raw | blame | history
pyramid/view.py 49 ●●●● patch | view | raw | blame | history
CHANGES.rst
@@ -4,6 +4,11 @@
Features
--------
- Allow the ``append_slash`` argument of ``config.add_notfound_view`` to be
  the special value ``pyramid.view.UseSubrequest``, which will cause
  Pyramid to do a subrequest rather than a redirect when a slash-appended
  route is found associated with a view, rather than a redirect.
- Add a ``_depth`` and ``_category`` arguments to all of the venusian
  decorators. The ``_category`` argument can be used to affect which actions
  are registered when performing a ``config.scan(..., category=...)`` with a
docs/api/view.rst
@@ -26,3 +26,8 @@
  .. autoclass:: exception_view_config
     :members:
  .. attribute:: UseSubrequest
     Object passed to :meth:`pyramid.config.Configurator.add_notfound_view` as
     the value to ``append_slash`` if you wish to cause a :term:`subrequest`
     rather than a redirect.
docs/glossary.rst
@@ -1206,3 +1206,10 @@
   context manager
      A context manager is an object that defines the runtime context to be established when executing a :ref:`with <python:with>` statement in Python. The context manager handles the entry into, and the exit from, the desired runtime context for the execution of the block of code. Context managers are normally invoked using the ``with`` statement, but can also be used by directly invoking their methods. Pyramid adds context managers for :class:`pyramid.config.Configurator`, :meth:`pyramid.interfaces.IRouter.request_context`, :func:`pyramid.paster.bootstrap`, :func:`pyramid.scripting.prepare`, and :func:`pyramid.testing.testConfig`. See also the Python documentation for :ref:`With Statement Context Managers <python:context-managers>` and :pep:`343`.
   subrequest
      A Pyramid concept that implies that as the result of an HTTP request
      another "internal" request can be issued to find a view without
      requiring cooperation from the client in the form of e.g. a redirect.  See
      :ref:`subrequest_chapter`.
docs/narr/subrequest.rst
@@ -8,8 +8,8 @@
.. versionadded:: 1.4
:app:`Pyramid` allows you to invoke a subrequest at any point during the
processing of a request.  Invoking a subrequest allows you to obtain a
:app:`Pyramid` allows you to invoke a :term:`subrequest` at any point during
the processing of a request.  Invoking a subrequest allows you to obtain a
:term:`response` object from a view callable within your :app:`Pyramid`
application while you're executing a different view callable within the same
application.
docs/narr/urldispatch.rst
@@ -908,12 +908,28 @@
       config.add_route('hasslash', 'has_slash/')
       config.scan()
.. warning::
You **should not** rely on the default mechanism to redirect ``POST`` requests.
The redirect of the slash-appending :term:`Not Found View` will turn a ``POST``
request into a ``GET``, losing any ``POST`` data in the original request.  But
if the argument supplied as ``append_slash`` is the special object
:attr:`~pyramid.views.UseSubrequest`, a :term:`subrequest` will be issued
instead of a redirect.  This makes it possible to successfully invoke a
slash-appended URL without losing the HTTP verb, POST data, or any other
information contained in the original request.  Instead of returning a redirect
response when a slash-appended route is detected during the not-found
processing, Pyramid will call the view associated with the slash-appended route
"under the hood" and will return whatever response is returned by that view.
This has the potential downside that both URLs (the slash-appended and the
non-slash-appended URLs) in an application will be "canonical" to clients; they
will behave exactly the same, and the client will never be notified that the
slash-appended URL is "better than" the non-slash-appended URL by virtue of a
redirect.  It, however, has the upside that a POST request with a body can be
handled successfully with an append-slash during notfound processing.
   You **should not** rely on this mechanism to redirect ``POST`` requests.
   The redirect  of the slash-appending :term:`Not Found View` will turn a
   ``POST`` request into a ``GET``, losing any ``POST`` data in the original
   request.
.. versionchanged:: 1.10
   Added the functionality to use a subrequest rather than a redirect by
   using :class:`~pyramid.views.UseSubrequest` as an argument to
   ``append_slash``.
See :ref:`view_module` and :ref:`changing_the_notfound_view` for a more
general description of how to configure a view and/or a :term:`Not Found View`.
pyramid/config/views.py
@@ -64,7 +64,10 @@
from pyramid.url import parse_url_overrides
from pyramid.view import AppendSlashNotFoundViewFactory
from pyramid.view import (
    AppendSlashNotFoundViewFactory,
    UseSubrequest,
    )
import pyramid.util
from pyramid.util import (
@@ -1635,7 +1638,8 @@
        settings.update(view_options)
        if append_slash:
            view = self._derive_view(view, attr=attr, renderer=renderer)
            if IResponse.implementedBy(append_slash):
            if (append_slash is UseSubrequest or
                IResponse.implementedBy(append_slash)):
                view = AppendSlashNotFoundViewFactory(
                    view, redirect_class=append_slash,
                )
pyramid/tests/pkgs/notfoundview/__init__.py
@@ -1,4 +1,4 @@
from pyramid.view import notfound_view_config, view_config
from pyramid.view import notfound_view_config, view_config, UseSubrequest
from pyramid.response import Response
@notfound_view_config(route_name='foo', append_slash=True)
@@ -21,10 +21,20 @@
def foo2(request):
    return Response('OK foo2')
@notfound_view_config(route_name='wiz', append_slash=UseSubrequest)
def wiz_notfound(request): # pragma: no cover
    return Response('wiz_notfound')
@view_config(route_name='wiz2')
def wiz2(request):
    return Response('OK wiz2')
def includeme(config):
    config.add_route('foo', '/foo')
    config.add_route('foo2', '/foo/')
    config.add_route('bar', '/bar/')
    config.add_route('baz', '/baz')
    config.add_route('wiz', '/wiz')
    config.add_route('wiz2', '/wiz/')
    config.scan('pyramid.tests.pkgs.notfoundview')
    
pyramid/tests/test_config/test_views.py
@@ -2233,28 +2233,44 @@
        self.assertRaises(ConfigurationError,
                          config.add_notfound_view, for_='foo')
    def test_add_notfound_view_append_slash(self):
    def test_add_notfound_view_append_slash_use_subrequest(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 HTTPFound, HTTPNotFound
        from pyramid.httpexceptions import HTTPNotFound
        from pyramid.view import UseSubrequest
        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=True)
        config.add_notfound_view(
            view,
            renderer=null_renderer,
            append_slash=UseSubrequest,
        )
        request = self._makeRequest(config)
        request.environ['PATH_INFO'] = '/foo'
        request.query_string = 'a=1&b=2'
        request.path = '/scriptname/foo'
        def copy():
            request.copied = True
            return request
        request.copy = copy
        resp = Response()
        def invoke_subrequest(req, **kw):
            self.assertEqual(req.path_info, '/scriptname/foo/')
            self.assertEqual(req.query_string, 'a=1&b=2')
            self.assertEqual(kw, {'use_tweens':True})
            return resp
        request.invoke_subrequest = invoke_subrequest
        view = self._getViewCallable(config,
                                     exc_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')
        self.assertTrue(request.copied)
        self.assertEqual(result, resp)
    def test_add_notfound_view_append_slash_custom_response(self):
    def test_add_notfound_view_append_slash_using_redirect(self):
        from pyramid.response import Response
        from pyramid.renderers import null_renderer
        from zope.interface import implementedBy
pyramid/tests/test_integration.py
@@ -396,6 +396,8 @@
        self.assertTrue(b'OK foo2' in res.body)
        res = self.testapp.get('/baz', status=200)
        self.assertTrue(b'baz_notfound' in res.body)
        res = self.testapp.get('/wiz', status=200) # uses subrequest
        self.assertTrue(b'OK wiz2' in res.body)
class TestForbiddenView(IntegrationBase, unittest.TestCase):
    package = 'pyramid.tests.pkgs.forbiddenview'
pyramid/view.py
@@ -38,6 +38,13 @@
_marker = object()
class _UseSubrequest(object):
    """ Object passed to :meth:`pyramid.config.Configurator.add_notfound_view`
    as the value to ``append_slash`` if you wish to cause a subrequest
    rather than a redirect """
UseSubrequest = _UseSubrequest() # singleton
def render_view_to_response(context, request, name='', secure=True):
    """ Call the :term:`view callable` configured with a :term:`view
    configuration` that matches the :term:`view name` ``name``
@@ -289,8 +296,6 @@
    view callable calling convention of ``(context, request)``
    (``context`` will be the exception object).
    .. deprecated:: 1.3
    """
    def __init__(self, notfound_view=None, redirect_class=HTTPFound):
        if notfound_view is None:
@@ -306,10 +311,21 @@
            slashpath = path + '/'
            for route in mapper.get_routes():
                if route.match(slashpath) is not None:
                    qs = request.query_string
                    if qs:
                        qs = '?' + qs
                    return self.redirect_class(location=request.path + '/' + qs)
                    if self.redirect_class is UseSubrequest:
                        subreq = request.copy()
                        subreq.path_info = request.path + '/'
                        return request.invoke_subrequest(
                            subreq,
                            use_tweens=True
                        )
                    else:
                        qs = request.query_string
                        if qs:
                            qs = '?' + qs
                        return self.redirect_class(
                            location=request.path + '/' + qs
                        )
        return self.notfound_view(context, request)
append_slash_notfound_view = AppendSlashNotFoundViewFactory()
@@ -396,11 +412,32 @@
    being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will
    be used` for the redirect response if a slash-appended route is found.
    If the argument supplied as ``append_slash`` is the special object
    :attr:`~pyramid.views.UseSubrequest`, a :term:`subrequest` will be issued
    instead of a redirect.  This makes it possible to successfully invoke a
    slash-appended URL without losing the HTTP verb, POST data, or any other
    information contained in the original request.  Instead of returning a
    redirect response when a slash-appended route is detected during the
    not-found processing, Pyramid will call the view associated with the
    slash-appended route "under the hood" and will return whatever response is
    returned by that view.  This has the potential downside that both URLs (the
    slash-appended and the non-slash-appended URLs) in an application will be
    "canonical" to clients; they will behave exactly the same, and the client
    will never be notified that the slash-appended URL is "better than" the
    non-slash-appended URL by virtue of a redirect.  It, however, has the
    upside that a POST request with a body can be handled successfully
    with an append-slash during notfound processing.
    See :ref:`changing_the_notfound_view` for detailed usage information.
    .. versionchanged:: 1.9.1
       Added the ``_depth`` and ``_category`` arguments.
    .. versionchanged:: 1.10
       Added the functionality to use a subrequest rather than a redirect by
       using :class:`~pyramid.views.UseSubrequest` as an argument to
       ``append_slash``.
    """
    venusian = venusian