diff options
| -rw-r--r-- | docs/api/static.rst | 7 | ||||
| -rw-r--r-- | docs/narr/assets.rst | 87 | ||||
| -rw-r--r-- | pyramid/config/views.py | 4 | ||||
| -rw-r--r-- | pyramid/static.py | 30 | ||||
| -rw-r--r-- | pyramid/tests/test_static.py | 46 |
5 files changed, 135 insertions, 39 deletions
diff --git a/docs/api/static.rst b/docs/api/static.rst index de5bcabda..543e526ad 100644 --- a/docs/api/static.rst +++ b/docs/api/static.rst @@ -9,8 +9,11 @@ :members: :inherited-members: - .. autoclass:: PathSegmentCacheBuster + .. autoclass:: PathSegmentMd5CacheBuster :members: - .. autoclass:: QueryStringCacheBuster + .. autoclass:: QueryStringMd5CacheBuster + :members: + + .. autoclass:: QueryStringConstantCacheBuster :members: diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 7fb0ec40b..33677988d 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -379,25 +379,19 @@ equivalent to: .. code-block:: python :linenos: - from pyramid.static import PathSegmentCacheBuster + from pyramid.static import PathSegmentMd5CacheBuster # config is an instance of pyramid.config.Configurator config.add_static_view(name='static', path='mypackage:folder/static', - cachebust=PathSegmentCacheBuster()) + cachebust=PathSegmentMd5CacheBuster()) -:app:`Pyramid` includes two ready to use cache buster implementations: -:class:`~pyramid.static.PathSegmentCacheBuster`, which inserts an asset token -in the path portion of the asset's URL, and -:class:`~pyramid.static.QueryStringCacheBuster`, which adds an asset token to -the query string of the asset's URL. Both of these classes generate md5 -checksums as asset tokens. - -.. note:: - - Many HTTP caching proxy implementations will fail to cache any URL which - has a query string. For this reason, you should probably prefer - :class:`~pyramid.static.PathSegmentCacheBuster` to - :class:`~pyramid.static.QueryStringCacheBuster`. +:app:`Pyramid` includes a handful of ready to use cache buster implementations: +:class:`~pyramid.static.PathSegmentMd5CacheBuster`, which inserts an md5 +checksum token in the path portion of the asset's URL, +:class:`~pyramid.static.QueryStringMd5CacheBuster`, which adds an md5 checksum +token to the query string of the asset's URL, and +:class:`~pyramid.static.QueryStringConstantCacheBuster`, which adds an +arbitrary token you provide to the query string of the asset's URL. In order to implement your own cache buster, you can write your own class from scratch which implements the :class:`~pyramid.interfaces.ICacheBuster` @@ -405,22 +399,65 @@ interface. Alternatively you may choose to subclass one of the existing implementations. One of the most likely scenarios is you'd want to change the way the asset token is generated. To do this just subclass an existing implementation and replace the :meth:`~pyramid.interfaces.ICacheBuster.token` -method. Here is an example which just uses a global setting for the asset -token: +method. Here is an example which just uses Git to get the hash of the +currently checked out code: .. code-block:: python :linenos: - - from pyramid.static import PathSegmentCacheBuster - class MyCacheBuster(PathSegmentCacheBuster): - - def __init__(self, config): - # config is an instance of pyramid.config.Configurator - self._token = config.registry.settings['myapp.cachebust_token'] + import os + import subprocess + from pyramid.static import PathSegmentMd5CacheBuster + + class GitCacheBuster(PathSegmentMd5CacheBuster): + """ + Assuming your code is installed as a Git checkout, as opposed to as an + egg from an egg repository like PYPI, you can use this cachebuster to + get the current commit's SHA1 to use as the cache bust token. + """ + def __init__(self): + here = os.path.dirname(os.path.abspath(__file__)) + self.sha1 = subprocess.check_output( + ['git', 'rev-parse', 'HEAD'], + cwd=here).strip() def token(self, pathspec): - return self._token + return self.sha1 + +Choosing a Cache Buster +~~~~~~~~~~~~~~~~~~~~~~~ + +The default cache buster implementation, +:class:`~pyramid.static.PathSegmentMd5CacheBuster`, works very well assuming +that you're using :app:`Pyramid` to serve your static assets. The md5 checksum +is fine grained enough that browsers should only request new versions of +specific assets that have changed. Many caching HTTP proxies will fail to +cache a resource if the URL contains a query string. In general, therefore, +you should prefer a cache busting strategy which modifies the path segment to +a strategy which adds a query string. + +It is possible, however, that your static assets are being served by another +web server or externally on a CDN. In these cases modifying the path segment +for a static asset URL would cause the external service to fail to find the +asset, causing your customer to get a 404. In these cases you would need to +fall back to a cache buster which adds a query string. It is even possible +that there isn't a copy of your static assets available to the :app:`Pyramid` +application, so a cache busting implementation that generates md5 checksums +would fail since it can't access the assets. In such a case, +:class:`~pyramid.static.QueryStringConstantCacheBuster` is a reasonable +fallback. The following code would set up a cachebuster that just uses the +time at start up as a cachebust token: + +.. code-block:: python + :linenos: + + import time + from pyramid.static import QueryStringConstantCacheBuster + + config.add_static_view( + name='http://mycdn.example.com/', + path='mypackage:static', + cachebust=QueryStringConstantCacheBuster(str(time.time()))) .. index:: single: static assets view diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e6c5baf58..5ca696069 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -34,7 +34,7 @@ from pyramid.interfaces import ( ) from pyramid import renderers -from pyramid.static import PathSegmentCacheBuster +from pyramid.static import PathSegmentMd5CacheBuster from pyramid.compat import ( string_types, @@ -1894,7 +1894,7 @@ def isexception(o): @implementer(IStaticURLInfo) class StaticURLInfo(object): # Indirection for testing - _default_cachebust = PathSegmentCacheBuster + _default_cachebust = PathSegmentMd5CacheBuster def _get_registrations(self, registry): try: diff --git a/pyramid/static.py b/pyramid/static.py index 0cbb5533f..5e017e1cd 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -192,7 +192,7 @@ class Md5AssetTokenGenerator(object): self.token_cache[pathspec] = token = _generate_md5(pathspec) return token -class PathSegmentCacheBuster(Md5AssetTokenGenerator): +class PathSegmentMd5CacheBuster(Md5AssetTokenGenerator): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which inserts an md5 checksum token for cache busting in the path portion of an @@ -205,17 +205,18 @@ class PathSegmentCacheBuster(Md5AssetTokenGenerator): def match(self, subpath): return subpath[1:] -class QueryStringCacheBuster(Md5AssetTokenGenerator): +class QueryStringMd5CacheBuster(Md5AssetTokenGenerator): """ - An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds a - token for cache busting in the query string of an asset URL. Generated md5 - checksums are cached in order to speed up subsequent calls. + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds + an md5 checksum token for cache busting in the query string of an asset + URL. Generated md5 checksums are cached in order to speed up subsequent + calls. The optional ``param`` argument determines the name of the parameter added to the query string and defaults to ``'x'``. """ def __init__(self, param='x'): - super(QueryStringCacheBuster, self).__init__() + super(QueryStringMd5CacheBuster, self).__init__() self.param = param def pregenerate(self, token, subpath, kw): @@ -226,4 +227,21 @@ class QueryStringCacheBuster(Md5AssetTokenGenerator): kw['_query'] = tuple(query) + ((self.param, token),) return subpath, kw +class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds + an arbitrary token for cache busting in the query string of an asset URL. + + The ``token`` parameter is the token string to use for cache busting and + will be the same for every request. + + The optional ``param`` argument determines the name of the parameter added + to the query string and defaults to ``'x'``. + """ + def __init__(self, token, param='x'): + self._token = token + self.param = param + + def token(self, pathspec): + return self._token diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 134bea25e..2f4de249e 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -420,10 +420,10 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): token = fut(self.fspath) self.assertEqual(token, expected) -class TestPathSegmentCacheBuster(unittest.TestCase): +class TestPathSegmentMd5CacheBuster(unittest.TestCase): def _makeOne(self): - from pyramid.static import PathSegmentCacheBuster as cls + from pyramid.static import PathSegmentMd5CacheBuster as cls inst = cls() inst.token = lambda pathspec: 'foo' return inst @@ -440,10 +440,10 @@ class TestPathSegmentCacheBuster(unittest.TestCase): fut = self._makeOne().match self.assertEqual(fut(('foo', 'bar')), ('bar',)) -class TestQueryStringCacheBuster(unittest.TestCase): +class TestQueryStringMd5CacheBuster(unittest.TestCase): def _makeOne(self, param=None): - from pyramid.static import QueryStringCacheBuster as cls + from pyramid.static import QueryStringMd5CacheBuster as cls if param: inst = cls(param) else: @@ -479,6 +479,44 @@ class TestQueryStringCacheBuster(unittest.TestCase): fut('foo', ('bar',), {'_query': (('a', 'b'),)}), (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) +class TestQueryStringConstantCacheBuster(TestQueryStringMd5CacheBuster): + + def _makeOne(self, param=None): + from pyramid.static import QueryStringConstantCacheBuster as cls + if param: + inst = cls('foo', param) + else: + inst = cls('foo') + return inst + + def test_token(self): + fut = self._makeOne().token + self.assertEqual(fut('whatever'), 'foo') + + def test_pregenerate(self): + fut = self._makeOne().pregenerate + self.assertEqual( + fut('foo', ('bar',), {}), + (('bar',), {'_query': {'x': 'foo'}})) + + def test_pregenerate_change_param(self): + fut = self._makeOne('y').pregenerate + self.assertEqual( + fut('foo', ('bar',), {}), + (('bar',), {'_query': {'y': 'foo'}})) + + def test_pregenerate_query_is_already_tuples(self): + fut = self._makeOne().pregenerate + self.assertEqual( + fut('foo', ('bar',), {'_query': [('a', 'b')]}), + (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) + + def test_pregenerate_query_is_tuple_of_tuples(self): + fut = self._makeOne().pregenerate + self.assertEqual( + fut('foo', ('bar',), {'_query': (('a', 'b'),)}), + (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) + class DummyContext: pass |
