diff options
| -rw-r--r-- | CHANGES.txt | 62 | ||||
| -rw-r--r-- | TODO.txt | 3 | ||||
| -rw-r--r-- | docs/api/request.rst | 7 | ||||
| -rw-r--r-- | docs/narr/hooks.rst | 86 | ||||
| -rw-r--r-- | docs/narr/viewconfig.rst | 54 | ||||
| -rw-r--r-- | docs/tutorials/wiki/background.rst | 2 | ||||
| -rw-r--r-- | docs/tutorials/wiki2/background.rst | 2 | ||||
| -rw-r--r-- | docs/whatsnew-1.1.rst | 56 | ||||
| -rw-r--r-- | pyramid/config.py | 99 | ||||
| -rw-r--r-- | pyramid/events.py | 3 | ||||
| -rw-r--r-- | pyramid/request.py | 7 | ||||
| -rw-r--r-- | pyramid/tests/test_config.py | 125 | ||||
| -rw-r--r-- | pyramid/tests/test_request.py | 20 | ||||
| -rw-r--r-- | pyramid/view.py | 5 |
14 files changed, 474 insertions, 57 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 224860b47..0eb02baad 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,65 @@ +Next release +============ + +Features +-------- + +- New request attribute: ``json``. If the request's ``content_type`` is + ``application/json``, this attribute will contain the JSON-decoded + variant of the request body. If the request's ``content_type`` is not + ``application/json``, this attribute will be ``None``. + +- A new value ``http_cache`` can be used as a view configuration + parameter. + + When you supply an ``http_cache`` value to a view configuration, the + ``Expires`` and ``Cache-Control`` headers of a response generated by the + associated view callable are modified. The value for ``http_cache`` may be + one of the following: + + - A nonzero integer. If it's a nonzero integer, it's treated as a number + of seconds. This number of seconds will be used to compute the + ``Expires`` header and the ``Cache-Control: max-age`` parameter of + responses to requests which call this view. For example: + ``http_cache=3600`` instructs the requesting browser to 'cache this + response for an hour, please'. + + - A ``datetime.timedelta`` instance. If it's a ``datetime.timedelta`` + instance, it will be converted into a number of seconds, and that number + of seconds will be used to compute the ``Expires`` header and the + ``Cache-Control: max-age`` parameter of responses to requests which call + this view. For example: ``http_cache=datetime.timedelta(days=1)`` + instructs the requesting browser to 'cache this response for a day, + please'. + + - Zero (``0``). If the value is zero, the ``Cache-Control`` and + ``Expires`` headers present in all responses from this view will be + composed such that client browser cache (and any intermediate caches) are + instructed to never cache the response. + + - A two-tuple. If it's a two tuple (e.g. ``http_cache=(1, + {'public':True})``), the first value in the tuple may be a nonzero + integer or a ``datetime.timedelta`` instance; in either case this value + will be used as the number of seconds to cache the response. The second + value in the tuple must be a dictionary. The values present in the + dictionary will be used as input to the ``Cache-Control`` response + header. For example: ``http_cache=(3600, {'public':True})`` means 'cache + for an hour, and add ``public`` to the Cache-Control header of the + response'. All keys and values supported by the + ``webob.cachecontrol.CacheControl`` interface may be added to the + dictionary. Supplying ``{'public':True}`` is equivalent to calling + ``response.cache_control.public = True``. + + Providing a non-tuple value as ``http_cache`` is equivalent to calling + ``response.cache_expires(value)`` within your view's body. + + Providing a two-tuple value as ``http_cache`` is equivalent to calling + ``response.cache_expires(value[0], **value[1])`` within your view's body. + + If you wish to avoid influencing, the ``Expires`` header, and instead wish + to only influence ``Cache-Control`` headers, pass a tuple as ``http_cache`` + with the first element of ``None``, e.g.: ``(None, {'public':True})``. + 1.1a4 (2011-07-01) ================== @@ -9,6 +9,9 @@ Must-Have Should-Have ----------- +- Make "localizer" a property of request (instead of requiring + "get_localizer(request)"? + - Investigate mod_wsgi tutorial to make sure it still works (2 reports say no; application package not found). diff --git a/docs/api/request.rst b/docs/api/request.rst index 27ce395ac..5dfb2ae9a 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -180,6 +180,13 @@ object (exposed to view code as ``request.response``) to influence rendered response behavior. + .. attribute:: json + + If the request's ``content_type`` is ``application/json``, this + attribute will contain the JSON-decoded variant of the request body. + If the request's ``content_type`` is not ``application/json``, this + attribute will be ``None``. + .. note:: For information about the API of a :term:`multidict` structure (such as diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 56c566a4c..94701c9f9 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -176,12 +176,54 @@ already constructed a :term:`configurator` it can also be registered via the config.set_request_factory(MyRequest) .. index:: + single: before render event + single: adding renderer globals + +.. _beforerender_event: + +Using The Before Render Event +----------------------------- + +Subscribers to the :class:`pyramid.events.BeforeRender` event may introspect +and modify the set of :term:`renderer globals` before they are passed to a +:term:`renderer`. This event object iself has a dictionary-like interface +that can be used for this purpose. For example: + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def add_global(event): + event['mykey'] = 'foo' + +An object of this type is sent as an event just before a :term:`renderer` is +invoked (but *after* the application-level renderer globals factory added via +:class:`~pyramid.config.Configurator.set_renderer_globals_factory`, if any, +has injected its own keys into the renderer globals dictionary). + +If a subscriber attempts to add a key that already exist in the renderer +globals dictionary, a :exc:`KeyError` is raised. This limitation is enforced +because event subscribers do not possess any relative ordering. The set of +keys added to the renderer globals dictionary by all +:class:`pyramid.events.BeforeRender` subscribers and renderer globals +factories must be unique. + +See the API documentation for the :class:`~pyramid.events.BeforeRender` event +interface at :class:`pyramid.interfaces.IBeforeRender`. + +Another (deprecated) mechanism which allows event subscribers more control +when adding renderer global values exists in :ref:`adding_renderer_globals`. + +.. index:: single: renderer globals .. _adding_renderer_globals: -Adding Renderer Globals ------------------------ +Adding Renderer Globals (Deprecated) +------------------------------------ .. warning:: this feature is deprecated as of Pyramid 1.1. A non-deprecated mechanism which allows event subscribers to add renderer global values @@ -231,46 +273,6 @@ already constructed a :term:`configurator` it can also be registered via the config = Configurator() config.set_renderer_globals_factory(renderer_globals_factory) -.. index:: - single: before render event - -.. _beforerender_event: - -Using The Before Render Event ------------------------------ - -Subscribers to the :class:`pyramid.events.BeforeRender` event may introspect -and modify the set of :term:`renderer globals` before they are passed to a -:term:`renderer`. This event object iself has a dictionary-like interface -that can be used for this purpose. For example: - -.. code-block:: python - :linenos: - - from pyramid.events import subscriber - from pyramid.events import BeforeRender - - @subscriber(BeforeRender) - def add_global(event): - event['mykey'] = 'foo' - -An object of this type is sent as an event just before a :term:`renderer` is -invoked (but *after* the application-level renderer globals factory added via -:class:`~pyramid.config.Configurator.set_renderer_globals_factory`, if any, -has injected its own keys into the renderer globals dictionary). - -If a subscriber attempts to add a key that already exist in the renderer -globals dictionary, a :exc:`KeyError` is raised. This limitation is enforced -because event subscribers do not possess any relative ordering. The set of -keys added to the renderer globals dictionary by all -:class:`pyramid.events.BeforeRender` subscribers and renderer globals -factories must be unique. - -See the API documentation for the :class:`~pyramid.events.BeforeRender` event -interface at :class:`pyramid.interfaces.IBeforeRender`. - -Another mechanism which allows event subscribers more control when adding -renderer global values exists in :ref:`adding_renderer_globals`. .. index:: single: response callback diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 5640800a2..ec42446ff 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -160,6 +160,55 @@ Non-Predicate Arguments view callable itself returns a :term:`response` (see :ref:`the_response`), the specified renderer implementation is never called. +``http_cache`` + When you supply an ``http_cache`` value to a view configuration, the + ``Expires`` and ``Cache-Control`` headers of a response generated by the + associated view callable are modified. The value for ``http_cache`` may be + one of the following: + + - A nonzero integer. If it's a nonzero integer, it's treated as a number + of seconds. This number of seconds will be used to compute the + ``Expires`` header and the ``Cache-Control: max-age`` parameter of + responses to requests which call this view. For example: + ``http_cache=3600`` instructs the requesting browser to 'cache this + response for an hour, please'. + + - A ``datetime.timedelta`` instance. If it's a ``datetime.timedelta`` + instance, it will be converted into a number of seconds, and that number + of seconds will be used to compute the ``Expires`` header and the + ``Cache-Control: max-age`` parameter of responses to requests which call + this view. For example: ``http_cache=datetime.timedelta(days=1)`` + instructs the requesting browser to 'cache this response for a day, + please'. + + - Zero (``0``). If the value is zero, the ``Cache-Control`` and + ``Expires`` headers present in all responses from this view will be + composed such that client browser cache (and any intermediate caches) are + instructed to never cache the response. + + - A two-tuple. If it's a two tuple (e.g. ``http_cache=(1, + {'public':True})``), the first value in the tuple may be a nonzero + integer or a ``datetime.timedelta`` instance; in either case this value + will be used as the number of seconds to cache the response. The second + value in the tuple must be a dictionary. The values present in the + dictionary will be used as input to the ``Cache-Control`` response + header. For example: ``http_cache=(3600, {'public':True})`` means 'cache + for an hour, and add ``public`` to the Cache-Control header of the + response'. All keys and values supported by the + ``webob.cachecontrol.CacheControl`` interface may be added to the + dictionary. Supplying ``{'public':True}`` is equivalent to calling + ``response.cache_control.public = True``. + + Providing a non-tuple value as ``http_cache`` is equivalent to calling + ``response.cache_expires(value)`` within your view's body. + + Providing a two-tuple value as ``http_cache`` is equivalent to calling + ``response.cache_expires(value[0], **value[1])`` within your view's body. + + If you wish to avoid influencing, the ``Expires`` header, and instead wish + to only influence ``Cache-Control`` headers, pass a tuple as ``http_cache`` + with the first element of ``None``, e.g.: ``(None, {'public':True})``. + ``wrapper`` The :term:`view name` of a different :term:`view configuration` which will receive the response body of this view as the ``request.wrapped_body`` @@ -400,8 +449,9 @@ configuration stanza: .. code-block:: python :linenos: - config.add_view('mypackage.views.my_view', name='my_view', request_method='POST', - context=MyResource, permission='read') + config.add_view('mypackage.views.my_view', name='my_view', + request_method='POST', context=MyResource, + permission='read') All arguments to ``view_config`` may be omitted. For example: diff --git a/docs/tutorials/wiki/background.rst b/docs/tutorials/wiki/background.rst index e49407b70..eda2fe825 100644 --- a/docs/tutorials/wiki/background.rst +++ b/docs/tutorials/wiki/background.rst @@ -11,7 +11,7 @@ Python web framework experience. To code along with this tutorial, the developer will need a UNIX machine with development tools (Mac OS X with XCode, any Linux or BSD -variant, etc) *or* he will need a Windows system of any kind. +variant, etc) *or* a Windows system of any kind. This tutorial targets :app:`Pyramid` version 1.0. diff --git a/docs/tutorials/wiki2/background.rst b/docs/tutorials/wiki2/background.rst index 880b5b219..79b39914b 100644 --- a/docs/tutorials/wiki2/background.rst +++ b/docs/tutorials/wiki2/background.rst @@ -10,7 +10,7 @@ people without any prior Python web framework experience. To code along with this tutorial, the developer will need a UNIX machine with development tools (Mac OS X with XCode, any Linux or BSD -variant, etc) *or* he will need a Windows system of any kind. +variant, etc) *or* a Windows system of any kind. This tutorial is targeted at :app:`Pyramid` version 1.0. diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index d83582dee..fdf3b1c74 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -94,6 +94,62 @@ Default HTTP Exception View Minor Feature Additions ----------------------- +- New request attribute: ``json``. If the request's ``content_type`` is + ``application/json``, this attribute will contain the JSON-decoded + variant of the request body. If the request's ``content_type`` is not + ``application/json``, this attribute will be ``None``. + +- A new value ``http_cache`` can be used as a :term:`view configuration` + parameter. + + When you supply an ``http_cache`` value to a view configuration, the + ``Expires`` and ``Cache-Control`` headers of a response generated by the + associated view callable are modified. The value for ``http_cache`` may be + one of the following: + + - A nonzero integer. If it's a nonzero integer, it's treated as a number + of seconds. This number of seconds will be used to compute the + ``Expires`` header and the ``Cache-Control: max-age`` parameter of + responses to requests which call this view. For example: + ``http_cache=3600`` instructs the requesting browser to 'cache this + response for an hour, please'. + + - A ``datetime.timedelta`` instance. If it's a ``datetime.timedelta`` + instance, it will be converted into a number of seconds, and that number + of seconds will be used to compute the ``Expires`` header and the + ``Cache-Control: max-age`` parameter of responses to requests which call + this view. For example: ``http_cache=datetime.timedelta(days=1)`` + instructs the requesting browser to 'cache this response for a day, + please'. + + - Zero (``0``). If the value is zero, the ``Cache-Control`` and + ``Expires`` headers present in all responses from this view will be + composed such that client browser cache (and any intermediate caches) are + instructed to never cache the response. + + - A two-tuple. If it's a two tuple (e.g. ``http_cache=(1, + {'public':True})``), the first value in the tuple may be a nonzero + integer or a ``datetime.timedelta`` instance; in either case this value + will be used as the number of seconds to cache the response. The second + value in the tuple must be a dictionary. The values present in the + dictionary will be used as input to the ``Cache-Control`` response + header. For example: ``http_cache=(3600, {'public':True})`` means 'cache + for an hour, and add ``public`` to the Cache-Control header of the + response'. All keys and values supported by the + ``webob.cachecontrol.CacheControl`` interface may be added to the + dictionary. Supplying ``{'public':True}`` is equivalent to calling + ``response.cache_control.public = True``. + + Providing a non-tuple value as ``http_cache`` is equivalent to calling + ``response.cache_expires(value)`` within your view's body. + + Providing a two-tuple value as ``http_cache`` is equivalent to calling + ``response.cache_expires(value[0], **value[1])`` within your view's body. + + If you wish to avoid influencing, the ``Expires`` header, and instead wish + to only influence ``Cache-Control`` headers, pass a tuple as ``http_cache`` + with the first element of ``None``, e.g.: ``(None, {'public':True})``. + - A `JSONP <http://en.wikipedia.org/wiki/JSONP>`_ renderer. See :ref:`jsonp_renderer` for more details. diff --git a/pyramid/config.py b/pyramid/config.py index 732d2c521..61463eb86 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -293,6 +293,7 @@ class Configurator(object): default_view_mapper=None, autocommit=False, exceptionresponse_view=default_exceptionresponse_view, + root_route_name=None, ): if package is None: package = caller_package() @@ -302,6 +303,7 @@ class Configurator(object): self.package = name_resolver.package self.registry = registry self.autocommit = autocommit + self.root_route_name = root_route_name if registry is None: registry = Registry(self.package_name) self.registry = registry @@ -373,7 +375,7 @@ class Configurator(object): attr=None, renderer=None, wrapper_viewname=None, viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH, decorator=None, - mapper=None): + mapper=None, http_cache=None): view = self.maybe_dotted(view) mapper = self.maybe_dotted(mapper) if isinstance(renderer, basestring): @@ -398,7 +400,8 @@ class Configurator(object): phash=phash, package=self.package, mapper=mapper, - decorator=decorator) + decorator=decorator, + http_cache=http_cache) return deriver(view) @@ -1035,7 +1038,7 @@ class Configurator(object): request_param=None, containment=None, attr=None, renderer=None, wrapper=None, xhr=False, accept=None, header=None, path_info=None, custom_predicates=(), - context=None, decorator=None, mapper=None): + context=None, decorator=None, mapper=None, http_cache=None): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken down below into *predicate* arguments and *non-predicate* @@ -1123,7 +1126,60 @@ class Configurator(object): The ``renderer`` attribute is optional. If it is not defined, the "null" renderer is assumed (no rendering is performed and the value is passed back to the upstream - :app:`Pyramid` machinery unmolested). + :app:`Pyramid` machinery unmodified). + + http_cache + + When you supply an ``http_cache`` value to a view configuration, + the ``Expires`` and ``Cache-Control`` headers of a response + generated by the associated view callable are modified. The value + for ``http_cache`` may be one of the following: + + - A nonzero integer. If it's a nonzero integer, it's treated as a + number of seconds. This number of seconds will be used to + compute the ``Expires`` header and the ``Cache-Control: + max-age`` parameter of responses to requests which call this view. + For example: ``http_cache=3600`` instructs the requesting browser + to 'cache this response for an hour, please'. + + - A ``datetime.timedelta`` instance. If it's a + ``datetime.timedelta`` instance, it will be converted into a + number of seconds, and that number of seconds will be used to + compute the ``Expires`` header and the ``Cache-Control: + max-age`` parameter of responses to requests which call this view. + For example: ``http_cache=datetime.timedelta(days=1)`` instructs + the requesting browser to 'cache this response for a day, please'. + + - Zero (``0``). If the value is zero, the ``Cache-Control`` and + ``Expires`` headers present in all responses from this view will + be composed such that client browser cache (and any intermediate + caches) are instructed to never cache the response. + + - A two-tuple. If it's a two tuple (e.g. ``http_cache=(1, + {'public':True})``), the first value in the tuple may be a + nonzero integer or a ``datetime.timedelta`` instance; in either + case this value will be used as the number of seconds to cache + the response. The second value in the tuple must be a + dictionary. The values present in the dictionary will be used as + input to the ``Cache-Control`` response header. For example: + ``http_cache=(3600, {'public':True})`` means 'cache for an hour, + and add ``public`` to the Cache-Control header of the response'. + All keys and values supported by the + ``webob.cachecontrol.CacheControl`` interface may be added to the + dictionary. Supplying ``{'public':True}`` is equivalent to + calling ``response.cache_control.public = True``. + + Providing a non-tuple value as ``http_cache`` is equivalent to + calling ``response.cache_expires(value)`` within your view's body. + + Providing a two-tuple value as ``http_cache`` is equivalent to + calling ``response.cache_expires(value[0], **value[1])`` within your + view's body. + + If you wish to avoid influencing, the ``Expires`` header, and + instead wish to only influence ``Cache-Control`` headers, pass a + tuple as ``http_cache`` with the first element of ``None``, e.g.: + ``(None, {'public':True})``. wrapper @@ -1340,7 +1396,7 @@ class Configurator(object): renderer=renderer, wrapper=wrapper, xhr=xhr, accept=accept, header=header, path_info=path_info, custom_predicates=custom_predicates, context=context, - mapper = mapper, + mapper = mapper, http_cache = http_cache, ) view_info = deferred_views.setdefault(route_name, []) view_info.append(info) @@ -1390,7 +1446,8 @@ class Configurator(object): phash=phash, package=self.package, mapper=mapper, - decorator=decorator) + decorator=decorator, + http_cache=http_cache) derived_view = deriver(view) registered = self.registry.adapters.registered @@ -2918,8 +2975,9 @@ class ViewDeriver(object): self.secured_view( self.owrapped_view( self.decorated_view( - self.rendered_view( - self.mapped_view(view)))))))) + self.http_cached_view( + self.rendered_view( + self.mapped_view(view))))))))) @wraps_view def mapped_view(self, view): @@ -2955,6 +3013,31 @@ class ViewDeriver(object): return _owrapped_view @wraps_view + def http_cached_view(self, view): + seconds = self.kw.get('http_cache') + options = {} + + if seconds is None: + return view + + if isinstance(seconds, (tuple, list)): + try: + seconds, options = seconds + except ValueError: + raise ConfigurationError( + 'If http_cache parameter is a tuple or list, it must be ' + 'in the form (seconds, options); not %s' % (seconds,)) + + def wrapper(context, request): + response = view(context, request) + cache_expires = getattr(response, 'cache_expires', None) + if cache_expires is not None: + cache_expires(seconds, **options) + return response + + return wrapper + + @wraps_view def secured_view(self, view): permission = self.kw.get('permission') if permission == '__no_permission_required__': diff --git a/pyramid/events.py b/pyramid/events.py index 68e400550..22cbf0cb2 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -24,7 +24,8 @@ class subscriber(object): def mysubscriber(event): event.request.foo = 1 - More than one event type can be passed as a construtor argument: + More than one event type can be passed as a construtor argument. The + decorated subscriber will be called for each event type. .. code-block:: python diff --git a/pyramid/request.py b/pyramid/request.py index cc5137869..a3848461f 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -10,6 +10,7 @@ from pyramid.interfaces import IResponse from pyramid.interfaces import ISessionFactory from pyramid.interfaces import IResponseFactory +from pyramid.compat import json from pyramid.exceptions import ConfigurationError from pyramid.decorator import reify from pyramid.response import Response @@ -489,6 +490,12 @@ class Request(BaseRequest, DeprecatedRequestMethods): return False return adapted is ob + @property + def json(self): + if self.content_type == 'application/json': + return json.loads(self.body, encoding=self.charset) + + def route_request_iface(name, bases=()): iface = InterfaceClass('%s_IRequest' % name, bases=bases) # for exception view lookups diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 63644da2d..3423b9784 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -823,6 +823,27 @@ class ConfiguratorTests(unittest.TestCase): result = wrapper(None, None) self.assertEqual(result, 'OK') + def test_add_view_with_http_cache(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def view(request): + """ ABC """ + return response + config = self._makeOne(autocommit=True) + config.add_view(view=view, http_cache=(86400, {'public':True})) + wrapper = self._getViewCallable(config) + self.assertFalse(wrapper is view) + self.assertEqual(wrapper.__doc__, view.__doc__) + request = testing.DummyRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(days=1) + result = wrapper(None, request) + self.assertEqual(result, response) + headers = dict(response.headerlist) + self.assertEqual(headers['Cache-Control'], 'max-age=86400, public') + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + def test_add_view_as_instance(self): class AView: def __call__(self, context, request): @@ -4199,6 +4220,101 @@ class TestViewDeriver(unittest.TestCase): result = deriver(view) self.assertNotEqual(result, view) + def test_http_cached_view_integer(self): + import datetime + from webob import Response + response = Response('OK') + def inner_view(context, request): + return response + deriver = self._makeOne(http_cache=3600) + result = deriver(inner_view) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_timedelta(self): + import datetime + from webob import Response + response = Response('OK') + def inner_view(context, request): + return response + deriver = self._makeOne(http_cache=datetime.timedelta(hours=1)) + result = deriver(inner_view) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_tuple(self): + import datetime + from webob import Response + response = Response('OK') + def inner_view(context, request): + return response + deriver = self._makeOne(http_cache=(3600, {'public':True})) + result = deriver(inner_view) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600, public') + + def test_http_cached_view_tuple_seconds_None(self): + from webob import Response + response = Response('OK') + def inner_view(context, request): + return response + deriver = self._makeOne(http_cache=(None, {'public':True})) + result = deriver(inner_view) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertEqual(headers['Cache-Control'], 'public') + + def test_http_cached_view_nonresponse_object_returned_downstream(self): + def inner_view(context, request): + return None + deriver = self._makeOne(http_cache=3600) + result = deriver(inner_view) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, None) # doesn't blow up + + def test_http_cached_view_bad_tuple(self): + from pyramid.exceptions import ConfigurationError + deriver = self._makeOne(http_cache=(None,)) + def view(request): pass + self.assertRaises(ConfigurationError, deriver, view) + class TestDefaultViewMapper(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -5302,3 +5418,12 @@ class DummyRegistry(object): self.adapters.append((arg, kw)) def queryAdapter(self, *arg, **kw): return self.adaptation + +def parse_httpdate(s): + import datetime + return datetime.datetime.strptime(s, "%a, %d %b %Y %H:%M:%S %Z") + +def assert_similar_datetime(one, two): + for attr in ('year', 'month', 'day', 'hour', 'minute'): + assert(getattr(one, attr) == getattr(two, attr)) + diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 76426b8a8..0c1c78721 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -233,6 +233,26 @@ class TestRequest(unittest.TestCase): request.registry.registerAdapter(adapter, (Foo,), IResponse) self.assertEqual(request.is_response(foo), True) + def test_json_incorrect_mimetype(self): + request = self._makeOne({}) + self.assertEqual(request.json, None) + + def test_json_correct_mimetype(self): + request = self._makeOne({}) + request.content_type = 'application/json' + request.body = '{"a":1}' + self.assertEqual(request.json, {'a':1}) + + def test_json_alternate_charset(self): + from pyramid.compat import json + request = self._makeOne({}) + request.content_type = 'application/json' + request.charset = 'latin-1' + la = unicode('La Pe\xc3\xb1a', 'utf-8') + body = json.dumps({'a':la}, encoding='latin-1') + request.body = body + self.assertEqual(request.json, {'a':la}) + class TestRequestDeprecatedMethods(unittest.TestCase): def setUp(self): self.config = testing.setUp() diff --git a/pyramid/view.py b/pyramid/view.py index afa10fd0f..ea20a19c2 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -166,7 +166,7 @@ class view_config(object): :class:`pyramid.view.view_config`: ``context``, ``permission``, ``name``, ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, - ``custom_predicates``, ``decorator``, and ``mapper``. + ``custom_predicates``, ``decorator``, ``mapper``, and ``http_cache``. The meanings of these arguments are the same as the arguments passed to :meth:`pyramid.config.Configurator.add_view`. @@ -181,7 +181,7 @@ class view_config(object): containment=None, attr=None, renderer=None, wrapper=None, xhr=False, accept=None, header=None, path_info=None, custom_predicates=(), context=None, decorator=None, - mapper=None): + mapper=None, http_cache=None): self.name = name self.request_type = request_type self.context = context or for_ @@ -200,6 +200,7 @@ class view_config(object): self.custom_predicates = custom_predicates self.decorator = decorator self.mapper = mapper + self.http_cache = http_cache def __call__(self, wrapped): settings = self.__dict__.copy() |
