Chris McDonough
2011-07-04 0fa1993d2abe87e197374f6abd3e45e62afb8a19
- 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})``.
6 files modified
387 ■■■■■ changed files
CHANGES.txt 57 ●●●●● patch | view | raw | blame | history
docs/narr/viewconfig.rst 54 ●●●●● patch | view | raw | blame | history
docs/whatsnew-1.1.rst 51 ●●●●● patch | view | raw | blame | history
pyramid/config.py 95 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_config.py 125 ●●●●● patch | view | raw | blame | history
pyramid/view.py 5 ●●●●● patch | view | raw | blame | history
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)
==================
docs/narr/viewconfig.rst
@@ -160,6 +160,55 @@
  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 @@
.. 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:
docs/whatsnew-1.1.rst
@@ -94,6 +94,57 @@
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 <http://en.wikipedia.org/wiki/JSONP>`_ renderer.  See
  :ref:`jsonp_renderer` for more details.
pyramid/config.py
@@ -373,7 +373,7 @@
                     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 @@
                              phash=phash,
                              package=self.package,
                              mapper=mapper,
                              decorator=decorator)
                              decorator=decorator,
                              http_cache=http_cache)
        
        return deriver(view)
@@ -996,7 +997,7 @@
                 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*
@@ -1085,6 +1086,59 @@
          defined, the "null" renderer is assumed (no rendering is
          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
@@ -1301,7 +1355,7 @@
                    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 @@
                                  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 @@
                    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):
@@ -2912,6 +2968,31 @@
        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__':
pyramid/tests/test_config.py
@@ -823,6 +823,27 @@
        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 @@
        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 @@
        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))
pyramid/view.py
@@ -166,7 +166,7 @@
    :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 @@
                 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 @@
        self.custom_predicates = custom_predicates
        self.decorator = decorator
        self.mapper = mapper
        self.http_cache = http_cache
    def __call__(self, wrapped):
        settings = self.__dict__.copy()