Skip to content

Commit

Permalink
Allow append_slash to use a subrequest rather than a redirect, closes #…
Browse files Browse the repository at this point in the history
  • Loading branch information
mcdonc committed Jul 4, 2018
1 parent 0c8ca23 commit 40d68c0
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 23 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ unreleased
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
Expand Down
5 changes: 5 additions & 0 deletions docs/api/view.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 7 additions & 0 deletions docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1206,3 +1206,10 @@ Glossary

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`.

4 changes: 2 additions & 2 deletions docs/narr/subrequest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Invoking a Subrequest

.. 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.
Expand Down
28 changes: 22 additions & 6 deletions docs/narr/urldispatch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -908,12 +908,28 @@ exactly the same job:
config.add_route('hasslash', 'has_slash/')
config.scan()
.. warning::

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.
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.

.. 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`.
Expand Down
8 changes: 6 additions & 2 deletions pyramid/config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -1635,7 +1638,8 @@ def notfound(request):
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,
)
Expand Down
12 changes: 11 additions & 1 deletion pyramid/tests/pkgs/notfoundview/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -21,10 +21,20 @@ def bar(request):
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')

28 changes: 22 additions & 6 deletions pyramid/tests/test_config/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2233,28 +2233,44 @@ def test_add_notfound_view_disallows_for_(self):
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
Expand Down
2 changes: 2 additions & 0 deletions pyramid/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ def test_it(self):
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'
Expand Down
49 changes: 43 additions & 6 deletions pyramid/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand Down Expand Up @@ -289,8 +296,6 @@ def notfound_view(context, request): return HTTPNotFound('nope')
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:
Expand All @@ -306,10 +311,21 @@ def __call__(self, context, request):
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()
Expand Down Expand Up @@ -396,11 +412,32 @@ def aview(request):
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
Expand Down

0 comments on commit 40d68c0

Please sign in to comment.