summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pyramid/static.py82
-rw-r--r--pyramid/tests/fixtures/manifest.json4
-rw-r--r--pyramid/tests/fixtures/manifest2.json4
-rw-r--r--pyramid/tests/test_static.py54
4 files changed, 144 insertions, 0 deletions
diff --git a/pyramid/static.py b/pyramid/static.py
index 2aff02c0c..59f440c82 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
+import json
import os
from os.path import (
+ getmtime,
normcase,
normpath,
join,
@@ -202,3 +204,83 @@ class QueryStringConstantCacheBuster(QueryStringCacheBuster):
def tokenize(self, pathspec):
return self._token
+
+class ManifestCacheBuster(object):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which
+ uses a supplied manifest file to map an asset path to a cache-busted
+ version of the path.
+
+ The file is expected to conform to the following simple JSON format:
+
+ .. code-block:: json
+
+ {
+ "css/main.css": "css/main-678b7c80.css",
+ "images/background.png": "images/background-a8169106.png",
+ }
+
+ Specifically, it is a JSON-serialized dictionary where the keys are the
+ source asset paths used in calls to
+ :meth:`~pyramid.request.Request.static_url. For example::
+
+ .. code-block:: python
+
+ >>> request.static_url('myapp:static/css/main.css')
+ "http://www.example.com/static/css/main-678b7c80.css"
+
+ If a path is not found in the manifest it will pass through unchanged.
+
+ If ``reload`` is ``True`` then the manifest file will be reloaded when
+ changed. It is not recommended to leave this enabled in production.
+
+ If the manifest file cannot be found on disk it will be treated as
+ an empty mapping unless ``reload`` is ``False``.
+
+ The default implementation assumes the requested (possibly cache-busted)
+ path is the actual filename on disk. Subclasses may override the ``match``
+ method to alter this behavior. For example, to strip the cache busting
+ token from the path.
+
+ .. versionadded:: 1.6
+ """
+ exists = staticmethod(exists) # testing
+ getmtime = staticmethod(getmtime) # testing
+
+ def __init__(self, manifest_path, reload=False):
+ self.manifest_path = manifest_path
+ self.reload = reload
+
+ self._mtime = None
+ if not reload:
+ self._manifest = self.parse_manifest()
+
+ def parse_manifest(self):
+ """
+ Return a mapping parsed from the ``manifest_path``.
+
+ Subclasses may override this method to use something other than
+ ``json.loads``.
+
+ """
+ with open(self.manifest_path, 'rb') as fp:
+ content = fp.read().decode('utf-8')
+ return json.loads(content)
+
+ @property
+ def manifest(self):
+ """ The current manifest dictionary."""
+ if self.reload:
+ if not self.exists(self.manifest_path):
+ return {}
+ mtime = self.getmtime(self.manifest_path)
+ if self._mtime is None or mtime > self._mtime:
+ self._manifest = self.parse_manifest()
+ self._mtime = mtime
+ return self._manifest
+
+ def pregenerate(self, pathspec, subpath, kw):
+ path = '/'.join(subpath)
+ path = self.manifest.get(path, path)
+ new_subpath = path.split('/')
+ return (new_subpath, kw)
diff --git a/pyramid/tests/fixtures/manifest.json b/pyramid/tests/fixtures/manifest.json
new file mode 100644
index 000000000..0a43bc5e3
--- /dev/null
+++ b/pyramid/tests/fixtures/manifest.json
@@ -0,0 +1,4 @@
+{
+ "css/main.css": "css/main-test.css",
+ "images/background.png": "images/background-a8169106.png"
+}
diff --git a/pyramid/tests/fixtures/manifest2.json b/pyramid/tests/fixtures/manifest2.json
new file mode 100644
index 000000000..fd6b9a7bb
--- /dev/null
+++ b/pyramid/tests/fixtures/manifest2.json
@@ -0,0 +1,4 @@
+{
+ "css/main.css": "css/main-678b7c80.css",
+ "images/background.png": "images/background-a8169106.png"
+}
diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py
index 7f50a0e43..ac30e9e50 100644
--- a/pyramid/tests/test_static.py
+++ b/pyramid/tests/test_static.py
@@ -1,6 +1,9 @@
import datetime
+import os.path
import unittest
+here = os.path.dirname(__file__)
+
# 5 years from now (more or less)
fiveyrsfuture = datetime.datetime.utcnow() + datetime.timedelta(5*365)
@@ -406,6 +409,57 @@ class TestQueryStringConstantCacheBuster(unittest.TestCase):
fut('foo', ('bar',), {'_query': (('a', 'b'),)}),
(('bar',), {'_query': (('a', 'b'), ('x', 'foo'))}))
+class TestManifestCacheBuster(unittest.TestCase):
+
+ def _makeOne(self, path, **kw):
+ from pyramid.static import ManifestCacheBuster as cls
+ return cls(path, **kw)
+
+ def test_it(self):
+ manifest_path = os.path.join(here, 'fixtures', 'manifest.json')
+ fut = self._makeOne(manifest_path).pregenerate
+ self.assertEqual(fut('foo', ('bar',), {}), (['bar'], {}))
+ self.assertEqual(
+ fut('foo', ('css', 'main.css'), {}),
+ (['css', 'main-test.css'], {}))
+
+ def test_reload(self):
+ manifest_path = os.path.join(here, 'fixtures', 'manifest.json')
+ new_manifest_path = os.path.join(here, 'fixtures', 'manifest2.json')
+ inst = self._makeOne('foo', reload=True)
+ inst.getmtime = lambda *args, **kwargs: 0
+ fut = inst.pregenerate
+
+ # test without a valid manifest
+ self.assertEqual(
+ fut('foo', ('css', 'main.css'), {}),
+ (['css', 'main.css'], {}))
+
+ # swap to a real manifest, setting mtime to 0
+ inst.manifest_path = manifest_path
+ self.assertEqual(
+ fut('foo', ('css', 'main.css'), {}),
+ (['css', 'main-test.css'], {}))
+
+ # ensure switching the path doesn't change the result
+ inst.manifest_path = new_manifest_path
+ self.assertEqual(
+ fut('foo', ('css', 'main.css'), {}),
+ (['css', 'main-test.css'], {}))
+
+ # update mtime, should cause a reload
+ inst.getmtime = lambda *args, **kwargs: 1
+ self.assertEqual(
+ fut('foo', ('css', 'main.css'), {}),
+ (['css', 'main-678b7c80.css'], {}))
+
+ def test_invalid_manifest(self):
+ self.assertRaises(IOError, lambda: self._makeOne('foo'))
+
+ def test_invalid_manifest_with_reload(self):
+ inst = self._makeOne('foo', reload=True)
+ self.assertEqual(inst.manifest, {})
+
class DummyContext:
pass