From 37d2c224b804dfebe9ee217c7a536364eacdee15 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 16 Sep 2012 03:25:17 -0400 Subject: [PATCH] docs and test --- CHANGES.txt | 6 + docs/api/request.rst | 6 + docs/index.rst | 1 + docs/latexindex.rst | 1 + docs/narr/subrequest.rst | 143 +++++++++++++++++++ docs/whatsnew-1.4.rst | 10 ++ pyramid/router.py | 4 +- pyramid/tests/pkgs/subrequestapp/__init__.py | 20 +++ pyramid/tests/test_integration.py | 55 ++++--- 9 files changed, 225 insertions(+), 21 deletions(-) create mode 100644 docs/narr/subrequest.rst create mode 100644 pyramid/tests/pkgs/subrequestapp/__init__.py diff --git a/CHANGES.txt b/CHANGES.txt index 6c210600cd..6736f6d3e4 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 the + ``request.subrequest`` API. + Deprecations ------------ @@ -257,6 +260,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.subrequest`` API. + Dependencies ------------ diff --git a/docs/api/request.rst b/docs/api/request.rst index 5e52cfd505..603807abea 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -163,6 +163,10 @@ .. method:: 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 @@ -204,6 +208,8 @@ - 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 4a9b3951bb..faf8258c1c 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 604e6e7c62..6bb875f73a 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 0000000000..6ed679579b --- /dev/null +++ b/docs/narr/subrequest.rst @@ -0,0 +1,143 @@ +.. 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.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.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.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.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() + +Being able to unconditionally obtain a response object by invoking a view +callable indirectly is the main advantage to using +:meth:`pyramid.request.Request.subrequest` instead of simply importing it 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. It's +much slower to use a subrequest. + +The :meth:`pyramid.request.Request.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.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 ``subrequest`` attribute 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.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. diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst index 58a44202cd..70d5f12a7f 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.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 ----------------------- @@ -239,6 +247,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 380c9fe300..75bf70f074 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -201,7 +201,9 @@ def subrequest(self, request, use_tweens=False): - 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 diff --git a/pyramid/tests/pkgs/subrequestapp/__init__.py b/pyramid/tests/pkgs/subrequestapp/__init__.py new file mode 100644 index 0000000000..c59993fb7d --- /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.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 590ba07606..3f26791de1 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -578,26 +578,41 @@ def test_hello(self): 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