From 4d19b84cb8134a0e7f030064e5d944defaa6970a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Dec 2015 00:17:39 -0600 Subject: new default behavior matches virtual specs, old behavior hidden behind explicit=True --- pyramid/config/views.py | 67 ++++++++++++++++++++++---------- pyramid/interfaces.py | 26 +++++++++---- pyramid/static.py | 10 ++--- pyramid/tests/test_config/test_views.py | 69 ++++++++++++++++++++++++--------- pyramid/tests/test_static.py | 2 +- 5 files changed, 123 insertions(+), 51 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1fcdcb136..759276351 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,4 +1,3 @@ -import bisect import inspect import posixpath import operator @@ -1941,7 +1940,7 @@ class ViewsConfiguratorMixin(object): info = self._get_static_info() info.add(self, name, spec, **kw) - def add_cache_buster(self, path, cachebust): + def add_cache_buster(self, path, cachebust, explicit=False): """ Add a cache buster to a set of files on disk. @@ -1956,10 +1955,16 @@ class ViewsConfiguratorMixin(object): be an object which implements :class:`~pyramid.interfaces.ICacheBuster`. + If ``explicit`` is set to ``True`` then the ``path`` for the cache + buster will be matched based on the ``rawspec`` instead of the + ``pathspec`` as defined in the + :class:`~pyramid.interfaces.ICacheBuster` interface. + Default: ``False``. + """ spec = self._make_spec(path) info = self._get_static_info() - info.add_cache_buster(self, spec, cachebust) + info.add_cache_buster(self, spec, cachebust, explicit=explicit) def _get_static_info(self): info = self.registry.queryUtility(IStaticURLInfo) @@ -1992,7 +1997,7 @@ class StaticURLInfo(object): subpath = subpath.replace('\\', '/') # windows if self.cache_busters: subpath, kw = self._bust_asset_path( - request.registry, spec, subpath, kw) + request, spec, subpath, kw) if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) @@ -2096,7 +2101,7 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) - def add_cache_buster(self, config, spec, cachebust): + def add_cache_buster(self, config, spec, cachebust, explicit=False): if config.registry.settings.get('pyramid.prevent_cachebust'): return @@ -2112,29 +2117,46 @@ class StaticURLInfo(object): def register(): cache_busters = self.cache_busters - specs = [t[0] for t in cache_busters] - if spec in specs: - idx = specs.index(spec) - cache_busters.pop(idx) + # find duplicate cache buster (old_idx) + # and insertion location (new_idx) + new_idx, old_idx = len(cache_busters), None + for idx, (spec_, cb_, explicit_) in enumerate(cache_busters): + # if we find an identical (spec, explicit) then use it + if spec == spec_ and explicit == explicit_: + old_idx = new_idx = idx + break + + # past all explicit==False specs then add to the end + elif not explicit and explicit_: + new_idx = idx + break + + # explicit matches and spec is shorter + elif explicit == explicit_ and len(spec) < len(spec_): + new_idx = idx + break - lengths = [len(t[0]) for t in cache_busters] - new_idx = bisect.bisect_left(lengths, len(spec)) - cache_busters.insert(new_idx, (spec, cachebust)) + if old_idx is not None: + cache_busters.pop(old_idx) + cache_busters.insert(new_idx, (spec, cachebust, explicit)) intr = config.introspectable('cache busters', spec, 'cache buster for %r' % spec, 'cache buster') intr['cachebust'] = cachebust - intr['spec'] = spec + intr['path'] = spec + intr['explicit'] = explicit config.action(None, callable=register, introspectables=(intr,)) - def _bust_asset_path(self, registry, spec, subpath, kw): + def _bust_asset_path(self, request, spec, subpath, kw): + registry = request.registry pkg_name, pkg_subpath = resolve_asset_spec(spec) rawspec = None if pkg_name is not None: + pathspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) overrides = registry.queryUtility(IPackageOverrides, name=pkg_name) if overrides is not None: resource_name = posixpath.join(pkg_subpath, subpath) @@ -2145,14 +2167,19 @@ class StaticURLInfo(object): rawspec = '{0}:{1}'.format(source.pkg_name, rawspec) break - if rawspec is None: - rawspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) + else: + pathspec = pkg_subpath + subpath if rawspec is None: - rawspec = pkg_subpath + subpath + rawspec = pathspec - for base_spec, cachebust in reversed(self.cache_busters): - if rawspec.startswith(base_spec): - subpath, kw = cachebust(rawspec, subpath, kw) + kw['pathspec'] = pathspec + kw['rawspec'] = rawspec + for spec_, cachebust, explicit in reversed(self.cache_busters): + if ( + (explicit and rawspec.startswith(spec_)) or + (not explicit and pathspec.startswith(spec_)) + ): + subpath, kw = cachebust(request, subpath, kw) break return subpath, kw diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 153fdad03..bbdc5121d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1194,11 +1194,11 @@ class ICacheBuster(Interface): .. versionadded:: 1.6 """ - def __call__(pathspec, subpath, kw): + def __call__(request, subpath, kw): """ Modifies a subpath and/or keyword arguments from which a static asset - URL will be computed during URL generation. The ``pathspec`` argument - is the path specification for the resource to be cache busted. + URL will be computed during URL generation. + The ``subpath`` argument is a path of ``/``-delimited segments that represent the portion of the asset URL which is used to find the asset. The ``kw`` argument is a dict of keywords that are to be passed @@ -1209,10 +1209,22 @@ class ICacheBuster(Interface): should be modified to include the cache bust token in the generated URL. - The ``pathspec`` refers to original location of the file, ignoring any - calls to :meth:`pyramid.config.Configurator.override_asset`. For - example, with a call ``request.static_url('myapp:static/foo.png'), the - ``pathspec`` may be ``themepkg:bar.png``, assuming a call to + The ``kw`` dictionary contains extra arguments passed to + :meth:`~pyramid.request.Request.static_url` as well as some extra + items that may be usful including: + + - ``pathspec`` is the path specification for the resource + to be cache busted. + + - ``rawspec`` is the original location of the file, ignoring + any calls to :meth:`pyramid.config.Configurator.override_asset`. + + The ``pathspec`` and ``rawspec`` values are only different in cases + where an asset has been mounted into a virtual location using + :meth:`pyramid.config.Configurator.override_asset`. For example, with + a call to ``request.static_url('myapp:static/foo.png'), the + ``pathspec`` is ``myapp:static/foo.png`` whereas the ``rawspec`` may + be ``themepkg:bar.png``, assuming a call to ``config.override_asset('myapp:static/foo.png', 'themepkg:bar.png')``. """ diff --git a/pyramid/static.py b/pyramid/static.py index 9559cd881..4054d5be0 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -172,15 +172,15 @@ class QueryStringCacheBuster(object): to the query string and defaults to ``'x'``. To use this class, subclass it and provide a ``tokenize`` method which - accepts a ``pathspec`` and returns a token. + accepts ``request, pathspec, kw`` and returns a token. .. versionadded:: 1.6 """ def __init__(self, param='x'): self.param = param - def __call__(self, pathspec, subpath, kw): - token = self.tokenize(pathspec) + def __call__(self, request, subpath, kw): + token = self.tokenize(request, subpath, kw) query = kw.setdefault('_query', {}) if isinstance(query, dict): query[self.param] = token @@ -205,7 +205,7 @@ class QueryStringConstantCacheBuster(QueryStringCacheBuster): super(QueryStringConstantCacheBuster, self).__init__(param=param) self._token = token - def tokenize(self, pathspec): + def tokenize(self, request, subpath, kw): return self._token class ManifestCacheBuster(object): @@ -290,6 +290,6 @@ class ManifestCacheBuster(object): self._mtime = mtime return self._manifest - def __call__(self, pathspec, subpath, kw): + def __call__(self, request, subpath, kw): subpath = self.manifest.get(subpath, subpath) return (subpath, kw) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index eda8d8b05..e89d43c9a 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3978,13 +3978,15 @@ class TestStaticURLInfo(unittest.TestCase): return 'foo' + '/' + subpath, kw inst = self._makeOne() inst.registrations = [(None, 'package:path/', '__viewname')] - inst.cache_busters = [('package:path/', cachebust)] + inst.cache_busters = [('package:path/', cachebust, False)] request = self._makeRequest() called = [False] def route_url(n, **kw): called[0] = True self.assertEqual(n, '__viewname') - self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar'}) + self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar', + 'pathspec': 'package:path/abc', + 'rawspec': 'package:path/abc'}) request.route_url = route_url inst.generate('package:path/abc', request) self.assertTrue(called[0]) @@ -3996,13 +3998,15 @@ class TestStaticURLInfo(unittest.TestCase): return 'foo' + '/' + subpath, kw inst = self._makeOne() inst.registrations = [(None, here, '__viewname')] - inst.cache_busters = [(here, cachebust)] + inst.cache_busters = [(here, cachebust, False)] request = self._makeRequest() called = [False] def route_url(n, **kw): called[0] = True self.assertEqual(n, '__viewname') - self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar'}) + self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar', + 'pathspec': here + 'abc', + 'rawspec': here + 'abc'}) request.route_url = route_url inst.generate(here + 'abc', request) self.assertTrue(called[0]) @@ -4011,13 +4015,15 @@ class TestStaticURLInfo(unittest.TestCase): def fake_cb(*a, **kw): raise AssertionError inst = self._makeOne() inst.registrations = [(None, 'package:path/', '__viewname')] - inst.cache_busters = [('package:path2/', fake_cb)] + inst.cache_busters = [('package:path2/', fake_cb, False)] request = self._makeRequest() called = [False] def route_url(n, **kw): called[0] = True self.assertEqual(n, '__viewname') - self.assertEqual(kw, {'subpath': 'abc'}) + self.assertEqual(kw, {'subpath': 'abc', + 'pathspec': 'package:path/abc', + 'rawspec': 'package:path/abc'}) request.route_url = route_url inst.generate('package:path/abc', request) self.assertTrue(called[0]) @@ -4025,17 +4031,22 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_url_cachebust_with_overrides(self): config = testing.setUp() try: + request = testing.DummyRequest() config.add_static_view('static', 'path') config.override_asset( 'pyramid.tests.test_config:path/', 'pyramid.tests.test_config:other_path/') - def cb(pathspec, subpath, kw): - kw['_query'] = {'x': 'foo'} - return subpath, kw - config.add_cache_buster('other_path', cb) - request = testing.DummyRequest() + def cb(val): + def cb_(request, subpath, kw): + kw['_query'] = {'x': val} + return subpath, kw + return cb_ + config.add_cache_buster('path', cb('foo')) result = request.static_url('path/foo.png') self.assertEqual(result, 'http://example.com/static/foo.png?x=foo') + config.add_cache_buster('other_path', cb('bar'), explicit=True) + result = request.static_url('path/foo.png') + self.assertEqual(result, 'http://example.com/static/foo.png?x=bar') finally: testing.tearDown() @@ -4138,7 +4149,7 @@ class TestStaticURLInfo(unittest.TestCase): inst = self._makeOne() inst.add_cache_buster(config, 'mypackage:path', DummyCacheBuster('foo')) cachebust = inst.cache_busters[-1][1] - subpath, kw = cachebust('mypackage:some/path', 'some/path', {}) + subpath, kw = cachebust(None, 'some/path', {}) self.assertEqual(subpath, 'some/path') self.assertEqual(kw['x'], 'foo') @@ -4148,7 +4159,7 @@ class TestStaticURLInfo(unittest.TestCase): inst = self._makeOne() cb = DummyCacheBuster('foo') inst.add_cache_buster(config, here, cb) - self.assertEqual(inst.cache_busters, [(here + '/', cb)]) + self.assertEqual(inst.cache_busters, [(here + '/', cb, False)]) def test_add_cachebuster_overwrite(self): config = DummyConfig() @@ -4158,17 +4169,39 @@ class TestStaticURLInfo(unittest.TestCase): inst.add_cache_buster(config, 'mypackage:path/', cb1) inst.add_cache_buster(config, 'mypackage:path', cb2) self.assertEqual(inst.cache_busters, - [('mypackage:path/', cb2)]) + [('mypackage:path/', cb2, False)]) + + def test_add_cachebuster_overwrite_explicit(self): + config = DummyConfig() + inst = self._makeOne() + cb1 = DummyCacheBuster('foo') + cb2 = DummyCacheBuster('bar') + inst.add_cache_buster(config, 'mypackage:path/', cb1) + inst.add_cache_buster(config, 'mypackage:path', cb2, True) + self.assertEqual(inst.cache_busters, + [('mypackage:path/', cb1, False), + ('mypackage:path/', cb2, True)]) def test_add_cachebuster_for_more_specific_path(self): config = DummyConfig() inst = self._makeOne() cb1 = DummyCacheBuster('foo') cb2 = DummyCacheBuster('bar') + cb3 = DummyCacheBuster('baz') + cb4 = DummyCacheBuster('xyz') + cb5 = DummyCacheBuster('w') inst.add_cache_buster(config, 'mypackage:path', cb1) - inst.add_cache_buster(config, 'mypackage:path/sub', cb2) - self.assertEqual(inst.cache_busters, - [('mypackage:path/', cb1), ('mypackage:path/sub/', cb2)]) + inst.add_cache_buster(config, 'mypackage:path/sub', cb2, True) + inst.add_cache_buster(config, 'mypackage:path/sub/other', cb3) + inst.add_cache_buster(config, 'mypackage:path/sub/other', cb4, True) + inst.add_cache_buster(config, 'mypackage:path/sub/less', cb5, True) + self.assertEqual( + inst.cache_busters, + [('mypackage:path/', cb1, False), + ('mypackage:path/sub/other/', cb3, False), + ('mypackage:path/sub/', cb2, True), + ('mypackage:path/sub/less/', cb5, True), + ('mypackage:path/sub/other/', cb4, True)]) class Test_view_description(unittest.TestCase): def _callFUT(self, view): @@ -4293,7 +4326,7 @@ class DummyCacheBuster(object): def __init__(self, token): self.token = token - def __call__(self, pathspec, subpath, kw): + def __call__(self, request, subpath, kw): kw['x'] = self.token return subpath, kw diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 73f242add..2ca86bc44 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -383,7 +383,7 @@ class TestQueryStringConstantCacheBuster(unittest.TestCase): def test_token(self): fut = self._makeOne().tokenize - self.assertEqual(fut('whatever'), 'foo') + self.assertEqual(fut(None, 'whatever', None), 'foo') def test_it(self): fut = self._makeOne() -- cgit v1.2.3