From 0fa1993d2abe87e197374f6abd3e45e62afb8a19 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 4 Jul 2011 01:07:45 -0400 Subject: - 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})``. --- CHANGES.txt | 57 ++++++++++++++++++++ docs/narr/viewconfig.rst | 54 ++++++++++++++++++- docs/whatsnew-1.1.rst | 51 ++++++++++++++++++ pyramid/config.py | 95 +++++++++++++++++++++++++++++--- pyramid/tests/test_config.py | 125 +++++++++++++++++++++++++++++++++++++++++++ pyramid/view.py | 5 +- 6 files changed, 376 insertions(+), 11 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 224860b47..67163d3e7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,60 @@ +Next release +============ + +Features +-------- + +- 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) ================== 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/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index d83582dee..783f2caaa 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -94,6 +94,57 @@ Default HTTP Exception View Minor Feature Additions ----------------------- +- 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 `_ renderer. See :ref:`jsonp_renderer` for more details. diff --git a/pyramid/config.py b/pyramid/config.py index bf3793c26..be0e425c8 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -373,7 +373,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 +398,8 @@ class Configurator(object): phash=phash, package=self.package, mapper=mapper, - decorator=decorator) + decorator=decorator, + http_cache=http_cache) return deriver(view) @@ -996,7 +997,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* @@ -1086,6 +1087,59 @@ class Configurator(object): performed and the value is passed back to the upstream :app:`Pyramid` machinery unmolested). + 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 @@ -1301,7 +1355,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) @@ -1351,7 +1405,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 @@ -2875,8 +2930,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): @@ -2911,6 +2967,31 @@ class ViewDeriver(object): return wrapped_response 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') diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 20fdd93e8..fa1ad2b88 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): @@ -4175,6 +4196,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() @@ -5278,3 +5394,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/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() -- cgit v1.2.3