diff options
| -rw-r--r-- | CHANGES.txt | 30 | ||||
| -rw-r--r-- | docs/api/request.rst | 42 | ||||
| -rw-r--r-- | docs/narr/renderers.rst | 74 | ||||
| -rw-r--r-- | docs/narr/templates.rst | 14 | ||||
| -rw-r--r-- | pyramid/renderers.py | 46 | ||||
| -rw-r--r-- | pyramid/request.py | 22 | ||||
| -rw-r--r-- | pyramid/testing.py | 24 | ||||
| -rw-r--r-- | pyramid/tests/test_renderers.py | 95 | ||||
| -rw-r--r-- | pyramid/tests/test_testing.py | 45 |
9 files changed, 335 insertions, 57 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 3ae834d93..e613e021d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,36 @@ Next release ============ +Requestresponse branch +---------------------- + +- Deprecated all assignments to ``request.response_*`` attributes such as + ``request.response_content_type = 'foo'``. Assignments and mutations of + the following request attributes that were considered by the framework for + response influence are now deprecated: ``response_content_type``, + ``response_headerlist``, ``response_status``, ``response_charset``, and + ``response_cache_for``. Instead of assigning these to the request object + for detection by the rendering machinery, users should use the appropriate + API of the Response object created by accessing ``request.response`` + (e.g. ``request.response_content_type = 'abc'`` -> + ``request.response.content_type = 'abc'``). + +- Custom request objects are now required to have a ``response`` attribute + (or reified property) if they are meant to be used with renderers. This + ``response`` attribute should be an instance of the class + ``pyramid.response.Response``. + +- The JSON and string renderer factories now use + ``request.response.content_type`` rather than + ``request.response_content_type``. They determine whether they should set + the content type of the response by comparing the response's content type + against the default (usually ``text/html``); if the content type is not the + default, the renderer changes the content type (to ``application/json`` or + ``text/plain`` for JSON and string renderers respectively). + +- Made it possible to assign to and delete + ``pyramid.testing.DummyRequest.registry`` (bugfix). + Documentation ------------- diff --git a/docs/api/request.rst b/docs/api/request.rst index d17441c0a..80b41badb 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -85,6 +85,38 @@ of ``request.exception`` will be ``None`` within response and finished callbacks. + .. attribute:: response + + This attribute is actually a "reified" property which returns an + instance of the :class:`pyramid.response.Response` class. The response + object returned does not exist until this attribute is accessed. Once + it is accessed, subsequent accesses to this request object will return + the same :class:`~pyramid.response.Response` object. + + The ``request.response`` API is used by renderers. A render obtains the + response object it will return from a view that uses that renderer by + accessing ``request.response``. Therefore, it's possible to use the + ``request.response`` API to set up a response object with "the right" + attributes (e.g. by calling ``request.response.set_cookie(...)`` or + ``request.response.content_type = 'text/plain'``, etc) within a view + that uses a renderer. For example, within a view that uses a + :term:`renderer`: + + response = request.response + response.set_cookie('mycookie', 'mine, all mine!') + return {'text':'Value that will be used by the renderer'} + + Mutations to this response object will be preserved in the response sent + to the client after rendering. + + Non-renderer code can also make use of request.response instead of + creating a response "by hand". For example, in view code:: + + response = request.response + response.body = 'Hello!' + response.content_type = 'text/plain' + return response + .. attribute:: session If a :term:`session factory` has been configured, this attribute @@ -127,10 +159,18 @@ .. attribute:: response_* + .. warning:: As of Pyramid 1.1, assignment to ``response_*`` attrs are + deprecated. Assigning to one will cause a deprecation warning to be + emitted. Instead of assigning ``response_*`` attributes to the + request, use the API of the :class:`pyramid.response.Response` + object exposed as ``request.response`` to influence response + behavior. + You can set attributes on a :class:`pyramid.request.Request` which will influence the behavor of *rendered* responses (views which use a :term:`renderer` and which don't directly return a response). These attributes begin with ``response_``, such as ``response_headerlist``. If you need to influence response values from a view that uses a renderer (such as the status code, a header, the content type, etc) see, - :ref:`response_request_attrs`. + :ref:`response_prefixed_attrs`. + diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 0b7cdb834..c3533648b 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -92,8 +92,8 @@ will be employed. return HTTPFound(location='http://example.com') # any renderer avoided Views which use a renderer can vary non-body response attributes (such as -headers and the HTTP status code) by attaching properties to the request. -See :ref:`response_request_attrs`. +headers and the HTTP status code) by attaching a property to the +``request.response`` attribute See :ref:`request_response_attr`. Additional renderers can be added by developers to the system as necessary (see :ref:`adding_and_overriding_renderers`). @@ -147,7 +147,8 @@ representing the ``str()`` serialization of the return value: {'content': 'Hello!'} Views which use the string renderer can vary non-body response attributes by -attaching properties to the request. See :ref:`response_request_attrs`. +using the API of the ``request.response`` attribute. See +:ref:`request_response_attr`. .. index:: pair: renderer; JSON @@ -199,7 +200,8 @@ You can configure a view to use the JSON renderer by naming ``json`` as the Views which use the JSON renderer can vary non-body response attributes by -attaching properties to the request. See :ref:`response_request_attrs`. +using the api of the ``request.response`` attribute. See +:ref:`request_response_attr`. .. index:: pair: renderer; chameleon @@ -269,8 +271,9 @@ Here's an example view configuration which uses a Chameleon text renderer: context='myproject.resources.Hello', renderer='myproject:templates/foo.txt') -Views which use a Chameleon renderer can vary response attributes by -attaching properties to the request. See :ref:`response_request_attrs`. +Views which use a Chameleon renderer can vary response attributes by using +the API of the ``request.response`` attribute. See +:ref:`request_response_attr`. .. index:: pair: renderer; mako @@ -333,7 +336,7 @@ additional :ref:`mako_template_renderer_settings`. single: response headers (from a renderer) single: renderer response headers -.. _response_request_attrs: +.. _request_response_attr: Varying Attributes of Rendered Responses ---------------------------------------- @@ -342,9 +345,43 @@ Before a response constructed by a :term:`renderer` is returned to :app:`Pyramid`, several attributes of the request are examined which have the potential to influence response behavior. -View callables that don't directly return a response should set these -attributes on the ``request`` object via ``setattr`` during their execution, -to influence associated response attributes. +View callables that don't directly return a response should use the API of +the :class:`pyramid.response.Response` attribute available as +``request.response`` during their execution, to influence associated response +behavior. + +For example, if you need to change the response status from within a view +callable that uses a renderer, assign the ``status`` attribute to the +``response`` attribute of the request before returning a result: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(name='gone', renderer='templates/gone.pt') + def myview(request): + request.response.status = '404 Not Found' + return {'URL':request.URL} + +For more information on attributes of the request, see the API documentation +in :ref:`request_module`. For more information on the API of +``request.response``, see :class:`pyramid.response.Response`. + +.. _response_prefixed_attrs: + +Deprecated Mechanism to Vary Attributes of Rendered Responses +------------------------------------------------------------- + +.. warning:: This section describes behavior deprecated in Pyramid 1.1. + +In previous releases of Pyramid (1.0 and before), the ``request.response`` +attribute did not exist. Instead, Pyramid required users to set special +``response_`` -prefixed attributes of the request to influence response +behavior. As of Pyramid 1.1, those request attributes are deprecated and +their use will cause a deprecation warning to be issued when used. Until +their existence is removed completely, we document them below, for benefit of +people with older code bases. ``response_content_type`` Defines the content-type of the resulting response, @@ -367,23 +404,6 @@ to influence associated response attributes. returning various values in the ``response_headerlist``, this is purely a convenience. -For example, if you need to change the response status from within a view -callable that uses a renderer, assign the ``response_status`` attribute to -the request before returning a result: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - - @view_config(name='gone', renderer='templates/gone.pt') - def myview(request): - request.response_status = '404 Not Found' - return {'URL':request.URL} - -For more information on attributes of the request, see the API -documentation in :ref:`request_module`. - .. index:: single: renderer (adding) diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 426ec229b..150b173e3 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -367,13 +367,13 @@ templates as renderers. See :ref:`available_template_system_bindings`. render a view without needing to fork your code to do so. See :ref:`extending_chapter` for more information. -By default, views rendered via a template renderer return a -:term:`Response` object which has a *status code* of ``200 OK``, and a -*content-type* of ``text/html``. To vary attributes of the response -of a view that uses a renderer, such as the content-type, headers, or -status attributes, you must set attributes on the *request* object -within the view before returning the dictionary. See -:ref:`response_request_attrs` for more information. +By default, views rendered via a template renderer return a :term:`Response` +object which has a *status code* of ``200 OK``, and a *content-type* of +``text/html``. To vary attributes of the response of a view that uses a +renderer, such as the content-type, headers, or status attributes, you must +use the API of the :class:`pyramid.response.Response` object exposed as +``request.response`` within the view before returning the dictionary. See +:ref:`request_response_attr` for more information. The same set of system values are provided to templates rendered via a renderer view configuration as those provided to templates rendered diff --git a/pyramid/renderers.py b/pyramid/renderers.py index c8771709a..a6dce9b3a 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -1,6 +1,7 @@ import os import pkg_resources import threading +import warnings from zope.interface import implements @@ -134,8 +135,10 @@ def json_renderer_factory(info): def _render(value, system): request = system.get('request') if request is not None: - if not hasattr(request, 'response_content_type'): - request.response_content_type = 'application/json' + response = request.response + ct = response.content_type + if ct == response.default_content_type: + response.content_type = 'application/json' return json.dumps(value) return _render @@ -145,8 +148,10 @@ def string_renderer_factory(info): value = str(value) request = system.get('request') if request is not None: - if not hasattr(request, 'response_content_type'): - request.response_content_type = 'text/plain' + response = request.response + ct = response.content_type + if ct == response.default_content_type: + response.content_type = 'text/plain' return value return _render @@ -344,29 +349,54 @@ class RendererHelper(object): return self._make_response(result, request) def _make_response(self, result, request): - registry = self.registry - response_factory = registry.queryUtility(IResponseFactory, - default=Response) + response = getattr(request, 'response', None) + if response is None: + # request is None or request is not a pyramid.response.Response + registry = self.registry + response_factory = registry.queryUtility(IResponseFactory, + default=Response) + + response = response_factory() - response = response_factory(result) + if result is None: + result = '' + + if isinstance(result, unicode): + response.unicode_body = result + else: + response.body = result if request is not None: + # deprecated mechanism to set up request.response_* attrs attrs = request.__dict__ content_type = attrs.get('response_content_type', None) if content_type is not None: response.content_type = content_type + deprecate_req_attr('Setting', 'content_type', + 'set', 'content_type') headerlist = attrs.get('response_headerlist', None) if headerlist is not None: for k, v in headerlist: response.headers.add(k, v) + deprecate_req_attr('Setting or mutating', 'headerlist', + 'set or mutate', 'headerlist') status = attrs.get('response_status', None) if status is not None: response.status = status + deprecate_req_attr('Setting', 'status', 'set', 'status') charset = attrs.get('response_charset', None) if charset is not None: response.charset = charset + deprecate_req_attr('Setting', 'charset', 'set', 'charset') cache_for = attrs.get('response_cache_for', None) if cache_for is not None: response.cache_expires = cache_for + deprecate_req_attr('Setting', 'cache_for', + 'set', 'cache_expires') return response + +def deprecate_req_attr(*args): + depwarn = ('%s "request.response_%s" is deprecated as of Pyramid 1.1; %s ' + '"request.response.%s" instead.') + warnings.warn(depwarn % args, DeprecationWarning, 3) diff --git a/pyramid/request.py b/pyramid/request.py index b607f159f..9d2b9344b 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -5,9 +5,11 @@ from webob import Request as WebobRequest from pyramid.interfaces import IRequest from pyramid.interfaces import ISessionFactory +from pyramid.interfaces import IResponseFactory from pyramid.exceptions import ConfigurationError from pyramid.decorator import reify +from pyramid.response import Response from pyramid.url import resource_url from pyramid.url import route_url from pyramid.url import static_url @@ -298,6 +300,26 @@ class Request(WebobRequest): """ return route_path(route_name, self, *elements, **kw) + @reify + def response(self): + """This attribute is actually a "reified" property which returns an + instance of the :class:`pyramid.response.Response`. class. The + response object returned does not exist until this attribute is + accessed. Once it is accessed, subsequent accesses will return the + same Response object. + + The ``request.response`` API is used by renderers. A render obtains + the response object it will return from a view that uses that renderer + by accessing ``request.response``. Therefore, it's possible to use the + ``request.response`` API to set up a response object with "the + right" attributes (e.g. by calling ``request.response.set_cookie()``) + within a view that uses a renderer. Mutations to this response object + will be preserved in the response sent to the client.""" + registry = self.registry + response_factory = registry.queryUtility(IResponseFactory, + default=Response) + return response_factory() + # override default WebOb "environ['adhoc_attr']" mutation behavior __getattr__ = object.__getattribute__ __setattr__ = object.__setattr__ diff --git a/pyramid/testing.py b/pyramid/testing.py index a98807a19..36cc38830 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -9,12 +9,14 @@ from zope.interface import Interface from zope.interface import alsoProvides from pyramid.interfaces import IRequest +from pyramid.interfaces import IResponseFactory from pyramid.interfaces import ISecuredView from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier from pyramid.interfaces import ISession from pyramid.config import Configurator +from pyramid.decorator import reify from pyramid.exceptions import Forbidden from pyramid.response import Response from pyramid.registry import Registry @@ -653,6 +655,8 @@ class DummyRequest(object): response_callbacks = () charset = 'UTF-8' script_name = '' + _registry = None + def __init__(self, params=None, environ=None, headers=None, path='/', cookies=None, post=None, **kw): if environ is None: @@ -698,9 +702,23 @@ class DummyRequest(object): self.response_callbacks = [] self.response_callbacks.append(callback) - @property - def registry(self): - return get_current_registry() + def _get_registry(self): + if self._registry is None: + return get_current_registry() + return self._registry + + def _set_registry(self, registry): + self._registry = registry + + def _del_registry(self): + self._registry = None + + registry = property(_get_registry, _set_registry, _del_registry) + + @reify + def response(self): + f = self.registry.queryUtility(IResponseFactory, default=Response) + return f() def setUp(registry=None, request=None, hook_zca=True, autocommit=True, settings=None): diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 70c2c620e..b0baaee0d 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -3,6 +3,18 @@ import unittest from pyramid.testing import cleanUp from pyramid import testing +def hide_warnings(wrapped): + import warnings + def wrapper(*arg, **kw): + warnings.filterwarnings('ignore') + try: + wrapped(*arg, **kw) + finally: + warnings.resetwarnings() + wrapper.__name__ = wrapped.__name__ + wrapper.__doc__ = wrapped.__doc__ + return wrapper + class TestTemplateRendererFactory(unittest.TestCase): def setUp(self): self.config = cleanUp() @@ -394,6 +406,12 @@ class TestRendererFromName(unittest.TestCase): class Test_json_renderer_factory(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + def _callFUT(self, name): from pyramid.renderers import json_renderer_factory return json_renderer_factory(name) @@ -407,14 +425,14 @@ class Test_json_renderer_factory(unittest.TestCase): request = testing.DummyRequest() renderer = self._callFUT(None) renderer({'a':1}, {'request':request}) - self.assertEqual(request.response_content_type, 'application/json') + self.assertEqual(request.response.content_type, 'application/json') def test_with_request_content_type_set(self): request = testing.DummyRequest() - request.response_content_type = 'text/mishmash' + request.response.content_type = 'text/mishmash' renderer = self._callFUT(None) renderer({'a':1}, {'request':request}) - self.assertEqual(request.response_content_type, 'text/mishmash') + self.assertEqual(request.response.content_type, 'text/mishmash') class Test_string_renderer_factory(unittest.TestCase): def _callFUT(self, name): @@ -443,14 +461,14 @@ class Test_string_renderer_factory(unittest.TestCase): request = testing.DummyRequest() renderer = self._callFUT(None) renderer(None, {'request':request}) - self.assertEqual(request.response_content_type, 'text/plain') + self.assertEqual(request.response.content_type, 'text/plain') def test_with_request_content_type_set(self): request = testing.DummyRequest() - request.response_content_type = 'text/mishmash' + request.response.content_type = 'text/mishmash' renderer = self._callFUT(None) renderer(None, {'request':request}) - self.assertEqual(request.response_content_type, 'text/mishmash') + self.assertEqual(request.response.content_type, 'text/mishmash') class TestRendererHelper(unittest.TestCase): @@ -492,8 +510,15 @@ class TestRendererHelper(unittest.TestCase): name='.foo') return renderer + def _registerResponseFactory(self): + from pyramid.interfaces import IResponseFactory + class ResponseFactory(object): + pass + self.config.registry.registerUtility(ResponseFactory, IResponseFactory) + def test_render_to_response(self): self._registerRendererFactory() + self._registerResponseFactory() request = Dummy() helper = self._makeOne('loo.foo') response = helper.render_to_response('values', 'system_values', @@ -502,6 +527,7 @@ class TestRendererHelper(unittest.TestCase): def test_render_view(self): self._registerRendererFactory() + self._registerResponseFactory() request = Dummy() helper = self._makeOne('loo.foo') view = 'view' @@ -561,8 +587,43 @@ class TestRendererHelper(unittest.TestCase): result = helper.render('values', None) self.assertEqual(result[1]['a'], 1) + def test__make_response_request_is_None(self): + request = None + helper = self._makeOne('loo.foo') + response = helper._make_response('abc', request) + self.assertEqual(response.body, 'abc') + + def test__make_response_request_is_None_response_factory_exists(self): + self._registerResponseFactory() + request = None + helper = self._makeOne('loo.foo') + response = helper._make_response('abc', request) + self.assertEqual(response.__class__.__name__, 'ResponseFactory') + self.assertEqual(response.body, 'abc') + + def test__make_response_result_is_unicode(self): + from pyramid.response import Response + request = testing.DummyRequest() + request.response = Response() + helper = self._makeOne('loo.foo') + la = unicode('/La Pe\xc3\xb1a', 'utf-8') + response = helper._make_response(la, request) + self.assertEqual(response.body, la.encode('utf-8')) + + def test__make_response_result_is_str(self): + from pyramid.response import Response + request = testing.DummyRequest() + request.response = Response() + helper = self._makeOne('loo.foo') + la = unicode('/La Pe\xc3\xb1a', 'utf-8') + response = helper._make_response(la.encode('utf-8'), request) + self.assertEqual(response.body, la.encode('utf-8')) + + @hide_warnings def test__make_response_with_content_type(self): + from pyramid.response import Response request = testing.DummyRequest() + request.response = Response() attrs = {'response_content_type':'text/nonsense'} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') @@ -570,8 +631,11 @@ class TestRendererHelper(unittest.TestCase): self.assertEqual(response.content_type, 'text/nonsense') self.assertEqual(response.body, 'abc') + @hide_warnings def test__make_response_with_headerlist(self): + from pyramid.response import Response request = testing.DummyRequest() + request.response = Response() attrs = {'response_headerlist':[('a', '1'), ('b', '2')]} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') @@ -583,8 +647,11 @@ class TestRendererHelper(unittest.TestCase): ('b', '2')]) self.assertEqual(response.body, 'abc') + @hide_warnings def test__make_response_with_status(self): + from pyramid.response import Response request = testing.DummyRequest() + request.response = Response() attrs = {'response_status':'406 You Lose'} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') @@ -592,16 +659,22 @@ class TestRendererHelper(unittest.TestCase): self.assertEqual(response.status, '406 You Lose') self.assertEqual(response.body, 'abc') + @hide_warnings def test__make_response_with_charset(self): + from pyramid.response import Response request = testing.DummyRequest() + request.response = Response() attrs = {'response_charset':'UTF-16'} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') response = helper._make_response('abc', request) self.assertEqual(response.charset, 'UTF-16') + @hide_warnings def test__make_response_with_cache_for(self): + from pyramid.response import Response request = testing.DummyRequest() + request.response = Response() attrs = {'response_cache_for':100} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') @@ -611,21 +684,21 @@ class TestRendererHelper(unittest.TestCase): def test_with_alternate_response_factory(self): from pyramid.interfaces import IResponseFactory class ResponseFactory(object): - def __init__(self, result): - self.result = result + def __init__(self): + pass self.config.registry.registerUtility(ResponseFactory, IResponseFactory) request = testing.DummyRequest() helper = self._makeOne('loo.foo') response = helper._make_response('abc', request) self.assertEqual(response.__class__, ResponseFactory) - self.assertEqual(response.result, 'abc') + self.assertEqual(response.body, 'abc') def test__make_response_with_real_request(self): # functional from pyramid.request import Request request = Request({}) - attrs = {'response_status':'406 You Lose'} - request.__dict__.update(attrs) + request.registry = self.config.registry + request.response.status = '406 You Lose' helper = self._makeOne('loo.foo') response = helper._make_response('abc', request) self.assertEqual(response.status, '406 You Lose') diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index 99b0a1911..ccf4edbc5 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -502,6 +502,51 @@ class TestDummyRequest(unittest.TestCase): finally: config.end() + def test_set_registry(self): + request = self._makeOne() + request.registry = 'abc' + self.assertEqual(request.registry, 'abc') + + def test_del_registry(self): + # see https://github.com/Pylons/pyramid/issues/165 + from pyramid.registry import Registry + from pyramid.config import Configurator + request = self._makeOne() + request.registry = 'abc' + self.assertEqual(request.registry, 'abc') + del request.registry + try: + registry = Registry('this_test') + config = Configurator(registry=registry) + config.begin() + self.failUnless(request.registry is registry) + finally: + config.end() + + def test_response_with_responsefactory(self): + from pyramid.registry import Registry + from pyramid.interfaces import IResponseFactory + registry = Registry('this_test') + class ResponseFactory(object): + pass + registry.registerUtility(ResponseFactory, IResponseFactory) + request = self._makeOne() + request.registry = registry + resp = request.response + self.assertEqual(resp.__class__, ResponseFactory) + self.failUnless(request.response is resp) # reified + + def test_response_without_responsefactory(self): + from pyramid.registry import Registry + from pyramid.response import Response + registry = Registry('this_test') + request = self._makeOne() + request.registry = registry + resp = request.response + self.assertEqual(resp.__class__, Response) + self.failUnless(request.response is resp) # reified + + class TestDummyTemplateRenderer(unittest.TestCase): def _getTargetClass(self, ): from pyramid.testing import DummyTemplateRenderer |
