diff options
| -rw-r--r-- | docs/api/static.rst | 9 | ||||
| -rw-r--r-- | docs/narr/assets.rst | 80 | ||||
| -rw-r--r-- | pyramid/config/views.py | 20 | ||||
| -rw-r--r-- | pyramid/static.py | 84 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 10 | ||||
| -rw-r--r-- | pyramid/tests/test_static.py | 113 |
6 files changed, 43 insertions, 273 deletions
diff --git a/docs/api/static.rst b/docs/api/static.rst index b6b279139..e6d6e4618 100644 --- a/docs/api/static.rst +++ b/docs/api/static.rst @@ -9,17 +9,8 @@ :members: :inherited-members: - .. autoclass:: PathSegmentCacheBuster - :members: - .. autoclass:: QueryStringCacheBuster :members: - .. autoclass:: PathSegmentMd5CacheBuster - :members: - - .. autoclass:: QueryStringMd5CacheBuster - :members: - .. autoclass:: QueryStringConstantCacheBuster :members: diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 020794062..397e0258d 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -372,30 +372,38 @@ assets by passing the optional argument, ``cachebust`` to .. code-block:: python :linenos: + import time + from pyramid.static import QueryStringConstantCacheBuster + # config is an instance of pyramid.config.Configurator - config.add_static_view(name='static', path='mypackage:folder/static', - cachebust=True) + config.add_static_view( + name='static', path='mypackage:folder/static', + cachebust=QueryStringConstantCacheBuster(str(int(time.time()))), + ) Setting the ``cachebust`` argument instructs :app:`Pyramid` to use a cache -busting scheme which adds the md5 checksum for a static asset as a path segment -in the asset's URL: +busting scheme which adds the curent time for a static asset to the query +string in the asset's URL: .. code-block:: python :linenos: js_url = request.static_url('mypackage:folder/static/js/myapp.js') - # Returns: 'http://www.example.com/static/c9658b3c0a314a1ca21e5988e662a09e/js/myapp.js' + # Returns: 'http://www.example.com/static/js/myapp.js?x=1445318121' -When the asset changes, so will its md5 checksum, and therefore so will its -URL. Supplying the ``cachebust`` argument also causes the static view to set -headers instructing clients to cache the asset for ten years, unless the -``cache_max_age`` argument is also passed, in which case that value is used. +When the web server restarts, the time constant will change and therefore so +will its URL. Supplying the ``cachebust`` argument also causes the static +view to set headers instructing clients to cache the asset for ten years, +unless the ``cache_max_age`` argument is also passed, in which case that +value is used. -.. note:: +.. warning:: - md5 checksums are cached in RAM, so if you change a static resource without - restarting your application, you may still generate URLs with a stale md5 - checksum. + Cache busting is an inherently complex topic as it integrates the asset + pipeline and the web application. It is expected and desired that + application authors will write their own cache buster implementations + conforming to the properties of their own asset pipelines. See + :ref:`custom_cache_busters` for information on writing your own. Disabling the Cache Buster ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -406,40 +414,21 @@ configured cache busters without changing calls to ``PYRAMID_PREVENT_CACHEBUST`` environment variable or the ``pyramid.prevent_cachebust`` configuration value to a true value. +.. _custom_cache_busters: + Customizing the Cache Buster ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Revisiting from the previous section: - -.. code-block:: python - :linenos: +The ``cachebust`` option to +:meth:`~pyramid.config.Configurator.add_static_view` may be set to any object +that implements the interface :class:`~pyramid.interfaces.ICacheBuster`. - # config is an instance of pyramid.config.Configurator - config.add_static_view(name='static', path='mypackage:folder/static', - cachebust=True) - -Setting ``cachebust`` to ``True`` instructs :app:`Pyramid` to use a default -cache busting implementation that should work for many situations. The -``cachebust`` may be set to any object that implements the interface -:class:`~pyramid.interfaces.ICacheBuster`. The above configuration is exactly -equivalent to: - -.. code-block:: python - :linenos: - - from pyramid.static import PathSegmentMd5CacheBuster - - # config is an instance of pyramid.config.Configurator - config.add_static_view(name='static', path='mypackage:folder/static', - cachebust=PathSegmentMd5CacheBuster()) - -: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 +:app:`Pyramid` ships with a very simplistic :class:`~pyramid.static.QueryStringConstantCacheBuster`, which adds an -arbitrary token you provide to the query string of the asset's URL. +arbitrary token you provide to the query string of the asset's URL. This +is almost never what you want in production as it does not allow fine-grained +busting of individual assets. + In order to implement your own cache buster, you can write your own class from scratch which implements the :class:`~pyramid.interfaces.ICacheBuster` @@ -456,15 +445,16 @@ the hash of the currently checked out code: import os import subprocess - from pyramid.static import PathSegmentCacheBuster + from pyramid.static import QueryStringCacheBuster - class GitCacheBuster(PathSegmentCacheBuster): + class GitCacheBuster(QueryStringCacheBuster): """ Assuming your code is installed as a Git checkout, as opposed to 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): + def __init__(self, param='x'): + super(GitCacheBuster, self).__init__(param=param) here = os.path.dirname(os.path.abspath(__file__)) self.sha1 = subprocess.check_output( ['git', 'rev-parse', 'HEAD'], diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 34fc49c00..e386bc4e1 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -34,7 +34,6 @@ from pyramid.interfaces import ( ) from pyramid import renderers -from pyramid.static import PathSegmentMd5CacheBuster from pyramid.compat import ( string_types, @@ -1862,14 +1861,12 @@ class ViewsConfiguratorMixin(object): The ``cachebust`` keyword argument may be set to cause :meth:`~pyramid.request.Request.static_url` to use cache busting when generating URLs. See :ref:`cache_busting` for general information - about cache busting. The value of the ``cachebust`` argument may be - ``True``, in which case a default cache busting implementation is used. - The value of the ``cachebust`` argument may also be an object which - implements :class:`~pyramid.interfaces.ICacheBuster`. See the - :mod:`~pyramid.static` module for some implementations. If the - ``cachebust`` argument is provided, the default for ``cache_max_age`` - is modified to be ten years. ``cache_max_age`` may still be explicitly - provided to override this default. + about cache busting. The value of the ``cachebust`` argument must + be an object which implements + :class:`~pyramid.interfaces.ICacheBuster`. If the ``cachebust`` + argument is provided, the default for ``cache_max_age`` is modified + to be ten years. ``cache_max_age`` may still be explicitly provided + to override this default. The ``permission`` keyword argument is used to specify the :term:`permission` required by a user to execute the static view. By @@ -1967,9 +1964,6 @@ def isexception(o): @implementer(IStaticURLInfo) class StaticURLInfo(object): - # Indirection for testing - _default_cachebust = PathSegmentMd5CacheBuster - def _get_registrations(self, registry): try: reg = registry._static_url_registrations @@ -2033,8 +2027,6 @@ class StaticURLInfo(object): cb = None else: cb = extra.pop('cachebust', None) - if cb is True: - cb = self._default_cachebust() if cb: def cachebust(subpath, kw): subpath_tuple = tuple(subpath.split('/')) diff --git a/pyramid/static.py b/pyramid/static.py index cb78feb9b..2aff02c0c 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import hashlib import os from os.path import ( @@ -27,7 +26,7 @@ from pyramid.httpexceptions import ( HTTPMovedPermanently, ) -from pyramid.path import AssetResolver, caller_package +from pyramid.path import caller_package from pyramid.response import FileResponse from pyramid.traversal import traversal_path_info @@ -159,71 +158,6 @@ def _secure_path(path_tuple): encoded = slash.join(path_tuple) # will be unicode return encoded -def _generate_md5(spec): - asset = AssetResolver(None).resolve(spec) - md5 = hashlib.md5() - with asset.stream() as stream: - for block in iter(lambda: stream.read(4096), b''): - md5.update(block) - return md5.hexdigest() - -class Md5AssetTokenGenerator(object): - """ - A mixin class which provides an implementation of - :meth:`~pyramid.interfaces.ICacheBuster.target` which generates an md5 - checksum token for an asset, caching it for subsequent calls. - """ - def __init__(self): - self.token_cache = {} - - def tokenize(self, pathspec): - # An astute observer will notice that this use of token_cache doesn't - # look particularly thread safe. Basic read/write operations on Python - # dicts, however, are atomic, so simply accessing and writing values - # to the dict shouldn't cause a segfault or other catastrophic failure. - # (See: http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm) - # - # We do have a race condition that could result in the same md5 - # checksum getting computed twice or more times in parallel. Since - # the program would still function just fine if this were to occur, - # the extra overhead of using locks to serialize access to the dict - # seems an unnecessary burden. - # - token = self.token_cache.get(pathspec) - if not token: - self.token_cache[pathspec] = token = _generate_md5(pathspec) - return token - -class PathSegmentCacheBuster(object): - """ - An implementation of :class:`~pyramid.interfaces.ICacheBuster` which - inserts a token for cache busting in the path portion of an asset URL. - - To use this class, subclass it and provide a ``tokenize`` method which - accepts a ``pathspec`` and returns a token. - - .. versionadded:: 1.6 - """ - def pregenerate(self, pathspec, subpath, kw): - token = self.tokenize(pathspec) - return (token,) + subpath, kw - - def match(self, subpath): - return subpath[1:] - -class PathSegmentMd5CacheBuster(PathSegmentCacheBuster, - Md5AssetTokenGenerator): - """ - An implementation of :class:`~pyramid.interfaces.ICacheBuster` which - inserts an md5 checksum token for cache busting in the path portion of an - asset URL. Generated md5 checksums are cached in order to speed up - subsequent calls. - - .. versionadded:: 1.6 - """ - def __init__(self): - super(PathSegmentMd5CacheBuster, self).__init__() - class QueryStringCacheBuster(object): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds @@ -249,22 +183,6 @@ class QueryStringCacheBuster(object): kw['_query'] = tuple(query) + ((self.param, token),) return subpath, kw -class QueryStringMd5CacheBuster(QueryStringCacheBuster, - Md5AssetTokenGenerator): - """ - 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'``. - - .. versionadded:: 1.6 - """ - def __init__(self, param='x'): - super(QueryStringMd5CacheBuster, self).__init__(param=param) - class QueryStringConstantCacheBuster(QueryStringCacheBuster): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 1c2d300a1..acfb81962 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -4104,16 +4104,6 @@ class TestStaticURLInfo(unittest.TestCase): self.assertEqual(config.view_kw['renderer'], 'mypackage:templates/index.pt') - def test_add_cachebust_default(self): - config = self._makeConfig() - inst = self._makeOne() - inst._default_cachebust = lambda: DummyCacheBuster('foo') - inst.add(config, 'view', 'mypackage:path', cachebust=True) - cachebust = config.registry._static_url_registrations[0][3] - subpath, kw = cachebust('some/path', {}) - self.assertEqual(subpath, 'some/path') - self.assertEqual(kw['x'], 'foo') - def test_add_cachebust_prevented(self): config = self._makeConfig() config.registry.settings['pyramid.prevent_cachebust'] = True diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index a3df74b44..7f50a0e43 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -368,118 +368,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): from pyramid.httpexceptions import HTTPNotFound self.assertRaises(HTTPNotFound, inst, context, request) -class TestMd5AssetTokenGenerator(unittest.TestCase): - _fspath = None - _tmp = None - - @property - def fspath(self): - if self._fspath: - return self._fspath - - import os - import tempfile - self._tmp = tmp = tempfile.mkdtemp() - self._fspath = os.path.join(tmp, 'test.txt') - return self._fspath - - def tearDown(self): - import shutil - if self._tmp: - shutil.rmtree(self._tmp) - - def _makeOne(self): - from pyramid.static import Md5AssetTokenGenerator as cls - return cls() - - def test_package_resource(self): - fut = self._makeOne().tokenize - expected = '76d653a3a044e2f4b38bb001d283e3d9' - token = fut('pyramid.tests:fixtures/static/index.html') - self.assertEqual(token, expected) - - def test_filesystem_resource(self): - fut = self._makeOne().tokenize - expected = 'd5155f250bef0e9923e894dbc713c5dd' - with open(self.fspath, 'w') as f: - f.write("Are we rich yet?") - token = fut(self.fspath) - self.assertEqual(token, expected) - - def test_cache(self): - fut = self._makeOne().tokenize - expected = 'd5155f250bef0e9923e894dbc713c5dd' - with open(self.fspath, 'w') as f: - f.write("Are we rich yet?") - token = fut(self.fspath) - self.assertEqual(token, expected) - - # md5 shouldn't change because we've cached it - with open(self.fspath, 'w') as f: - f.write("Sorry for the convenience.") - token = fut(self.fspath) - self.assertEqual(token, expected) - -class TestPathSegmentMd5CacheBuster(unittest.TestCase): - - def _makeOne(self): - from pyramid.static import PathSegmentMd5CacheBuster as cls - inst = cls() - inst.tokenize = lambda pathspec: 'foo' - return inst - - def test_token(self): - fut = self._makeOne().tokenize - self.assertEqual(fut('whatever'), 'foo') - - def test_pregenerate(self): - fut = self._makeOne().pregenerate - self.assertEqual(fut('foo', ('bar',), 'kw'), (('foo', 'bar'), 'kw')) - - def test_match(self): - fut = self._makeOne().match - self.assertEqual(fut(('foo', 'bar')), ('bar',)) - -class TestQueryStringMd5CacheBuster(unittest.TestCase): - - def _makeOne(self, param=None): - from pyramid.static import QueryStringMd5CacheBuster as cls - if param: - inst = cls(param) - else: - inst = cls() - inst.tokenize = lambda pathspec: 'foo' - return inst - - def test_token(self): - fut = self._makeOne().tokenize - 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 TestQueryStringConstantCacheBuster(TestQueryStringMd5CacheBuster): +class TestQueryStringConstantCacheBuster(unittest.TestCase): def _makeOne(self, param=None): from pyramid.static import QueryStringConstantCacheBuster as cls |
