summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt12
-rw-r--r--TODO.txt9
-rw-r--r--docs/api/request.rst49
-rw-r--r--docs/index.rst1
-rw-r--r--docs/latexindex.rst1
-rw-r--r--docs/narr/subrequest.rst186
-rw-r--r--docs/whatsnew-1.4.rst16
-rw-r--r--pyramid/router.py70
-rw-r--r--pyramid/tests/pkgs/subrequestapp/__init__.py20
-rw-r--r--pyramid/tests/test_integration.py55
10 files changed, 381 insertions, 38 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 6c210600c..66ac42136 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -163,6 +163,9 @@ Features
- Added the ``pyramid.testing.testConfig`` context manager, which can be used
to generate a configurator in a test, e.g. ``with testing.testConfig(...):``.
+- Users can now invoke a subrequest from within view code using a new
+ ``request.invoke_subrequest`` API.
+
Deprecations
------------
@@ -249,6 +252,12 @@ Backwards Incompatibilities
* ``registerSettings``, use
``pyramid.config.Configurator.add_settings`` instead.
+- In Pyramid 1.3 and previous, the ``__call__`` method of a Response object
+ was invoked before any finished callbacks were executed. As of this
+ release, the ``__call__`` method of a Response object is invoked *after*
+ finished callbacks are executed. This is in support of the
+ ``request.invoke_subrequest`` feature.
+
Documentation
-------------
@@ -257,6 +266,9 @@ Documentation
how to show Pyramid-generated deprecation warnings while running tests and
while running a server.
+- Added a "Invoking a Subrequest" chapter to the documentation. It describes
+ how to use the new ``request.invoke_subrequest`` API.
+
Dependencies
------------
diff --git a/TODO.txt b/TODO.txt
index 202d1afbb..1686c27a2 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -82,15 +82,6 @@ Nice-to-Have
- Deprecate pyramid.security.view_execution_permitted (it only works for
traversal).
-- Create a function which performs a recursive request.
-
-- Create a ``render_view`` that works by using config.derive_view against an
- existing view callable instead of querying the registry (some sort of API
- for rendering a view callable object to a response from within another view
- callable). Possible idea: have config.add_view mark up the
- function/method/class like @view_config does, then use the attached info to
- derive a view callable whenever called via some API.
-
- Provide a ``has_view`` function.
- Update App engine chapter with less creaky directions.
diff --git a/docs/api/request.rst b/docs/api/request.rst
index 1112ea069..1718d0743 100644
--- a/docs/api/request.rst
+++ b/docs/api/request.rst
@@ -161,6 +161,55 @@
request, the value of this attribute will be ``None``. See
:ref:`matched_route`.
+ .. method:: invoke_subrequest(request, use_tweens=False)
+
+ .. warning::
+
+ This API was added in Pyramid 1.4a1.
+
+ Obtain a response object from the Pyramid application based on
+ information in the ``request`` object provided. The ``request`` object
+ must be an object that implements the Pyramid request interface (such
+ as a :class:`pyramid.request.Request` instance). If ``use_tweens`` is
+ ``True``, the request will be sent to the :term:`tween` in the tween
+ stack closest to the request ingress. If ``use_tweens`` is ``False``,
+ the request will be sent to the main router handler, and no tweens will
+ be invoked. This isn't *actually* a method of the Request object; it's
+ a callable added when the Pyramid router is invoked, or when a
+ subrequest is invoked. This function also:
+
+ - manages the threadlocal stack (so that
+ :func:`~pyramid.threadlocal.get_current_request` and
+ :func:`~pyramid.threadlocal.get_current_registry` work during a
+ request)
+
+ - Adds a ``registry`` attribute (the current Pyramid registry) and a
+ ``invoke_subrequest`` attribute (a callable) to the request object it's
+ handed.
+
+ - sets request extensions (such as those added via
+ :meth:`~pyramid.config.Configurator.add_request_method` or
+ :meth:`~pyramid.config.Configurator.set_request_property`) on the
+ request it's passed.
+
+ - causes a :class:`~pyramid.event.NewRequest` event to be sent at the
+ beginning of request processing.
+
+ - causes a :class:`~pyramid.event.ContextFound` event to be sent
+ when a context resource is found.
+
+ - causes a :class:`~pyramid.event.NewResponse` event to be sent when
+ the Pyramid application returns a response.
+
+ - Calls any :term:`response callback` functions defined within the
+ request's lifetime if a response is obtained from the Pyramid
+ application.
+
+ - Calls any :term:`finished callback` functions defined within the
+ request's lifetime.
+
+ See also :ref:`subrequest_chapter`.
+
.. automethod:: add_response_callback
.. automethod:: add_finished_callback
diff --git a/docs/index.rst b/docs/index.rst
index 4a9b3951b..faf8258c1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -83,6 +83,7 @@ Narrative documentation in chapter form explaining how to use
narr/traversal
narr/security
narr/hybrid
+ narr/subrequest
narr/hooks
narr/introspector
narr/extending
diff --git a/docs/latexindex.rst b/docs/latexindex.rst
index 604e6e7c6..6bb875f73 100644
--- a/docs/latexindex.rst
+++ b/docs/latexindex.rst
@@ -54,6 +54,7 @@ Narrative Documentation
narr/traversal
narr/security
narr/hybrid
+ narr/subrequest
narr/hooks
narr/introspector
narr/extending
diff --git a/docs/narr/subrequest.rst b/docs/narr/subrequest.rst
new file mode 100644
index 000000000..bd50b6053
--- /dev/null
+++ b/docs/narr/subrequest.rst
@@ -0,0 +1,186 @@
+.. index::
+ single: subrequest
+
+.. _subrequest_chapter:
+
+Invoking a Subrequest
+=====================
+
+.. warning::
+
+ This feature was added in Pyramid 1.4a1.
+
+: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
+:term:`response` object from a view callable within your :app:`Pyramid`
+application while you're executing a different view callable within the same
+application.
+
+Here's an example application which uses a subrequest:
+
+.. code-block:: python
+
+ from wsgiref.simple_server import make_server
+ from pyramid.config import Configurator
+ from pyramid.request import Request
+
+ def view_one(request):
+ subreq = Request.blank('/view_two')
+ response = request.invoke_subrequest(subreq)
+ return response
+
+ def view_two(request):
+ request.response.body = 'This came from view_two'
+ return request.response
+
+ if __name__ == '__main__':
+ config = Configurator()
+ config.add_route('one', '/view_one')
+ config.add_route('two', '/view_two')
+ config.add_view(view_one, route_name='one')
+ config.add_view(view_two, route_name='two')
+ app = config.make_wsgi_app()
+ server = make_server('0.0.0.0', 8080, app)
+ server.serve_forever()
+
+When ``/view_one`` is visted in a browser, the text printed in the browser
+pane will be ``This came from view_two``. The ``view_one`` view used the
+:meth:`pyramid.request.Request.invoke_subrequest` API to obtain a response
+from another view (``view_two``) within the same application when it
+executed. It did so by constructing a new request that had a URL that it
+knew would match the ``view_two`` view registration, and passed that new
+request along to :meth:`pyramid.request.Request.invoke_subrequest`. The
+``view_two`` view callable was invoked, and it returned a response. The
+``view_one`` view callable then simply returned the response it obtained from
+the ``view_two`` view callable.
+
+Note that it doesn't matter if the view callable invoked via a subrequest
+actually returns a literal Response object. Any view callable that uses a
+renderer or which returns an object that can be interpreted by a response
+adapter will work too:
+
+.. code-block:: python
+
+ from wsgiref.simple_server import make_server
+ from pyramid.config import Configurator
+ from pyramid.request import Request
+
+ def view_one(request):
+ subreq = Request.blank('/view_two')
+ response = request.invoke_subrequest(subreq)
+ return response
+
+ def view_two(request):
+ return 'This came from view_two'
+
+ if __name__ == '__main__':
+ config = Configurator()
+ config.add_route('one', '/view_one')
+ config.add_route('two', '/view_two')
+ config.add_view(view_one, route_name='one')
+ config.add_view(view_two, route_name='two', renderer='string')
+ app = config.make_wsgi_app()
+ server = make_server('0.0.0.0', 8080, app)
+ server.serve_forever()
+
+Even though the ``view_two`` view callable returned a string, it was invoked
+in such a way that the ``string`` renderer associated with the view
+registration that was found turned it into a "real" response object for
+consumption by ``view_one``.
+
+Being able to unconditionally obtain a response object by invoking a view
+callable indirectly is the main advantage to using
+:meth:`pyramid.request.Request.invoke_subrequest` instead of simply importing
+the view callable and executing it directly. Note that there's not much
+advantage to invoking a view using a subrequest if you *can* invoke a view
+callable directly. Subrequests are slower and are less convenient if you
+actually do want just the literal information returned by a function that
+happens to be a view callable.
+
+Note that if a view callable invoked by a subrequest raises an exception, the
+exception will usually bubble up to the invoking code:
+
+.. code-block:: python
+
+ from wsgiref.simple_server import make_server
+ from pyramid.config import Configurator
+ from pyramid.request import Request
+
+ def view_one(request):
+ subreq = Request.blank('/view_two')
+ response = request.invoke_subrequest(subreq)
+ return response
+
+ def view_two(request):
+ raise ValueError('foo')
+
+ if __name__ == '__main__':
+ config = Configurator()
+ config.add_route('one', '/view_one')
+ config.add_route('two', '/view_two')
+ config.add_view(view_one, route_name='one')
+ config.add_view(view_two, route_name='two', renderer='string')
+ app = config.make_wsgi_app()
+ server = make_server('0.0.0.0', 8080, app)
+ server.serve_forever()
+
+In the above application, the call to ``request.invoke_subrequest(subreq)``
+will actually raise a :exc:`ValueError` exception instead of retrieving a
+"500" response from the attempted invocation of ``view_two``.
+
+The :meth:`pyramid.request.Request.invoke_subrequest` API accepts two
+arguments: a positional argument ``request`` that must be provided, and and
+``use_tweens`` keyword argument that is optional; it defaults to ``False``.
+
+The ``request`` object passed to the API must be an object that implements
+the Pyramid request interface (such as a :class:`pyramid.request.Request`
+instance). If ``use_tweens`` is ``True``, the request will be sent to the
+:term:`tween` in the tween stack closest to the request ingress. If
+``use_tweens`` is ``False``, the request will be sent to the main router
+handler, and no tweens will be invoked. It's usually best to not invoke any
+tweens when executing a subrequest, because the original request will invoke
+any tween logic as necessary. The
+:meth:`pyramid.request.Request.invoke_subrequest` function also:
+
+- manages the threadlocal stack so that
+ :func:`~pyramid.threadlocal.get_current_request` and
+ :func:`~pyramid.threadlocal.get_current_registry` work during a request
+ (they will return the subrequest instead of the original request)
+
+- Adds a ``registry`` attribute and a ``invoke_subrequest`` attribute (a
+ callable) to the request object it's handed.
+
+- sets request extensions (such as those added via
+ :meth:`~pyramid.config.Configurator.add_request_method` or
+ :meth:`~pyramid.config.Configurator.set_request_property`) on the subrequest
+ object passed as ``request``
+
+- causes a :class:`~pyramid.event.NewRequest` event to be sent at the
+ beginning of request processing.
+
+- causes a :class:`~pyramid.event.ContextFound` event to be sent when a
+ context resource is found.
+
+- causes a :class:`~pyramid.event.NewResponse` event to be sent when the
+ Pyramid application returns a response.
+
+- Calls any :term:`response callback` functions defined within the subrequest's
+ lifetime if a response is obtained from the Pyramid application.
+
+- Calls any :term:`finished callback` functions defined within the subrequest's
+ lifetime.
+
+It's a poor idea to use the original ``request`` object as an argument to
+:meth:`~pyramid.request.Request.invoke_subrequest`. You should construct a
+new request instead as demonstrated in the above example, using
+:meth:`pyramid.request.Request.blank`. Once you've constructed a request
+object, you'll need to massage the it to match the view callable you'd like
+to be executed during the subrequest. This can be done by adjusting the
+subrequest's URL, its headers, its request method, and other attributes. See
+the documentation for :class:`pyramid.request.Request` to understand how to
+massage your new request object into something that will match the view you'd
+like to call via a subrequest.
+
+We've demonstrated use of a subrequest from within a view callable, but you
+can use the :meth:`~pyramid.request.Request.invoke_subrequest` API from
+within a tween or an event handler as well.
diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst
index 58a44202c..8e333bf5d 100644
--- a/docs/whatsnew-1.4.rst
+++ b/docs/whatsnew-1.4.rst
@@ -65,6 +65,14 @@ Partial Mako and Chameleon Template Renderings
defined as ``macroname`` within the ``template.pt`` template instead of the
entire templae.
+Subrequest Support
+~~~~~~~~~~~~~~~~~~
+
+- Developers may invoke a subrequest by using the
+ :meth:`pyramid.request.Request.invoke_subrequest` API. This allows a
+ developer to obtain a response from one view callable by issuing a subrequest
+ from within a different view callable.
+
Minor Feature Additions
-----------------------
@@ -222,6 +230,12 @@ Backwards Incompatibilities
* ``registerSettings``, use
:meth:`pyramid.config.Configurator.add_settings` instead.
+- In Pyramid 1.3 and previous, the ``__call__`` method of a Response object
+ returned by a view was invoked before any finished callbacks were executed.
+ As of this release, the ``__call__`` method of a Response object is invoked
+ *after* finished callbacks are executed. This is in support of the
+ :meth:`pyramid.request.Request.invoke_subrequest` feature.
+
Deprecations
------------
@@ -239,6 +253,8 @@ Documentation Enhancements
how to show Pyramid-generated deprecation warnings while running tests and
while running a server.
+- Added a :ref:`subrequest_chapter` chapter to the narrative documentation.
+
- Many cleanups and improvements to narrative and API docs.
Dependency Changes
diff --git a/pyramid/router.py b/pyramid/router.py
index 0cbe00f3a..18624376c 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -53,6 +53,7 @@ class Router(object):
tweens = q(ITweens)
if tweens is None:
tweens = excview_tween_factory
+ self.orig_handle_request = self.handle_request
self.handle_request = tweens(self.handle_request, registry)
self.root_policy = self.root_factory # b/w compat
self.registry = registry
@@ -161,35 +162,73 @@ class Router(object):
return response
- def __call__(self, environ, start_response):
+ def invoke_subrequest(self, request, use_tweens=False):
"""
- Accept ``environ`` and ``start_response``; create a
- :term:`request` and route the request to a :app:`Pyramid`
- view based on introspection of :term:`view configuration`
- within the application registry; call ``start_response`` and
- return an iterable.
+ Obtain a response object from the Pyramid application based on
+ information in the ``request`` object provided. The ``request``
+ object must be an object that implements the Pyramid request
+ interface (such as a :class:`pyramid.request.Request` instance). If
+ ``use_tweens`` is ``True``, the request will be sent to the
+ :term:`tween` in the tween stack closest to the request ingress. If
+ ``use_tweens`` is ``False``, the request will be sent to the main
+ router handler, and no tweens will be invoked. This function also:
+
+ - manages the threadlocal stack (so that
+ :func:`~pyramid.threadlocal.get_current_request` and
+ :func:`~pyramid.threadlocal.get_current_registry` work during a
+ request)
+
+ - Adds a ``registry`` attribute and a ``invoke_subrequest`` attribute
+ (a callable) to the request object it's handed.
+
+ - sets request extensions (such as those added via
+ :meth:`~pyramid.config.Configurator.add_request_method` or
+ :meth:`~pyramid.config.Configurator.set_request_property`) on the
+ request it's passed.
+
+ - causes a :class:`~pyramid.event.NewRequest` event to be sent at the
+ beginning of request processing.
+
+ - causes a :class:`~pyramid.event.ContextFound` event to be sent
+ when a context resource is found.
+
+ - causes a :class:`~pyramid.event.NewResponse` event to be sent when
+ the Pyramid application returns a response.
+
+ - Calls any :term:`response callback` functions defined within the
+ request's lifetime if a response is obtained from the Pyramid
+ application.
+
+ - Calls any :term:`finished callback` functions defined within the
+ request's lifetime.
+
+ See also :ref:`subrequest_chapter`.
"""
registry = self.registry
has_listeners = self.registry.has_listeners
notify = self.registry.notify
- request = self.request_factory(environ)
threadlocals = {'registry':registry, 'request':request}
manager = self.threadlocal_manager
manager.push(threadlocals)
request.registry = registry
+ request.invoke_subrequest = self.invoke_subrequest
+ if use_tweens:
+ handle_request = self.handle_request
+ else:
+ handle_request = self.orig_handle_request
try:
try:
extensions = self.request_extensions
if extensions is not None:
request._set_extensions(extensions)
- response = self.handle_request(request)
+ response = handle_request(request)
has_listeners and notify(NewResponse(request, response))
if request.response_callbacks:
request._process_response_callbacks(response)
- return response(request.environ, start_response)
+ return response
finally:
if request.finished_callbacks:
@@ -197,3 +236,16 @@ class Router(object):
finally:
manager.pop()
+
+ def __call__(self, environ, start_response):
+ """
+ Accept ``environ`` and ``start_response``; create a
+ :term:`request` and route the request to a :app:`Pyramid`
+ view based on introspection of :term:`view configuration`
+ within the application registry; call ``start_response`` and
+ return an iterable.
+ """
+ request = self.request_factory(environ)
+ response = self.invoke_subrequest(request, use_tweens=True)
+ return response(request.environ, start_response)
+
diff --git a/pyramid/tests/pkgs/subrequestapp/__init__.py b/pyramid/tests/pkgs/subrequestapp/__init__.py
new file mode 100644
index 000000000..06a4d9d16
--- /dev/null
+++ b/pyramid/tests/pkgs/subrequestapp/__init__.py
@@ -0,0 +1,20 @@
+from pyramid.config import Configurator
+from pyramid.request import Request
+
+def view_one(request):
+ subreq = Request.blank('/view_two')
+ response = request.invoke_subrequest(subreq)
+ return response
+
+def view_two(request):
+ request.response.body = 'This came from view_two'
+ return request.response
+
+def main():
+ config = Configurator()
+ config.add_route('one', '/view_one')
+ config.add_route('two', '/view_two')
+ config.add_view(view_one, route_name='one')
+ config.add_view(view_two, route_name='two')
+ return config
+
diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py
index 590ba0760..3f26791de 100644
--- a/pyramid/tests/test_integration.py
+++ b/pyramid/tests/test_integration.py
@@ -578,26 +578,41 @@ class WSGIApp2AppTest(unittest.TestCase):
res = self.testapp.get('/hello', status=200)
self.assertTrue(b'Hello' in res.body)
-if os.name != 'java': # uses chameleon
- class RendererScanAppTest(IntegrationBase, unittest.TestCase):
- package = 'pyramid.tests.pkgs.rendererscanapp'
- def test_root(self):
- res = self.testapp.get('/one', status=200)
- self.assertTrue(b'One!' in res.body)
-
- def test_two(self):
- res = self.testapp.get('/two', status=200)
- self.assertTrue(b'Two!' in res.body)
-
- def test_rescan(self):
- self.config.scan('pyramid.tests.pkgs.rendererscanapp')
- app = self.config.make_wsgi_app()
- from webtest import TestApp
- testapp = TestApp(app)
- res = testapp.get('/one', status=200)
- self.assertTrue(b'One!' in res.body)
- res = testapp.get('/two', status=200)
- self.assertTrue(b'Two!' in res.body)
+class SubrequestAppTest(unittest.TestCase):
+ def setUp(self):
+ from pyramid.tests.pkgs.subrequestapp import main
+ config = main()
+ app = config.make_wsgi_app()
+ from webtest import TestApp
+ self.testapp = TestApp(app)
+ self.config = config
+
+ def tearDown(self):
+ self.config.end()
+
+ def test_it(self):
+ res = self.testapp.get('/view_one', status=200)
+ self.assertTrue(b'This came from view_two' in res.body)
+
+class RendererScanAppTest(IntegrationBase, unittest.TestCase):
+ package = 'pyramid.tests.pkgs.rendererscanapp'
+ def test_root(self):
+ res = self.testapp.get('/one', status=200)
+ self.assertTrue(b'One!' in res.body)
+
+ def test_two(self):
+ res = self.testapp.get('/two', status=200)
+ self.assertTrue(b'Two!' in res.body)
+
+ def test_rescan(self):
+ self.config.scan('pyramid.tests.pkgs.rendererscanapp')
+ app = self.config.make_wsgi_app()
+ from webtest import TestApp
+ testapp = TestApp(app)
+ res = testapp.get('/one', status=200)
+ self.assertTrue(b'One!' in res.body)
+ res = testapp.get('/two', status=200)
+ self.assertTrue(b'Two!' in res.body)
class DummyContext(object):
pass