diff options
| author | Michael Merickel <michael@merickel.org> | 2015-12-01 01:59:14 -0600 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2015-12-01 01:59:14 -0600 |
| commit | 6e29b425182ccc4abc87fcfb32e20b60b15d4bdf (patch) | |
| tree | 517c9526dbff668b417f3627c50885f254d93a9e | |
| parent | fd404c0dc268fe71d39875e74ea4ff5ee68489ef (diff) | |
| download | pyramid-6e29b425182ccc4abc87fcfb32e20b60b15d4bdf.tar.gz pyramid-6e29b425182ccc4abc87fcfb32e20b60b15d4bdf.tar.bz2 pyramid-6e29b425182ccc4abc87fcfb32e20b60b15d4bdf.zip | |
initial work on config.add_cache_buster
| -rw-r--r-- | pyramid/config/views.py | 108 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 45 | ||||
| -rw-r--r-- | pyramid/static.py | 10 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 66 |
4 files changed, 106 insertions, 123 deletions
diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e386bc4e1..67a70145c 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,3 +1,4 @@ +import bisect import inspect import operator import os @@ -1855,18 +1856,7 @@ class ViewsConfiguratorMixin(object): ``Expires`` and ``Cache-Control`` headers for static assets served. Note that this argument has no effect when the ``name`` is a *url prefix*. By default, this argument is ``None``, meaning that no - particular Expires or Cache-Control headers are set in the response, - unless ``cachebust`` is specified. - - 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 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. + particular Expires or Cache-Control headers are set in the response. The ``permission`` keyword argument is used to specify the :term:`permission` required by a user to execute the static view. By @@ -1946,11 +1936,32 @@ class ViewsConfiguratorMixin(object): See :ref:`static_assets_section` for more information. """ spec = self._make_spec(path) + info = self._get_static_info() + info.add(self, name, spec, **kw) + + def add_cache_buster(self, path, cachebust): + """ + 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 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. + + """ + spec = self._make_spec(path) + info = self._get_static_info() + info.add_cache_buster(self, spec, cachebust) + + def _get_static_info(self): info = self.registry.queryUtility(IStaticURLInfo) if info is None: info = StaticURLInfo() self.registry.registerUtility(info, IStaticURLInfo) - info.add(self, name, spec, **kw) + return info def isexception(o): if IInterface.providedBy(o): @@ -1964,26 +1975,18 @@ def isexception(o): @implementer(IStaticURLInfo) class StaticURLInfo(object): - def _get_registrations(self, registry): - try: - reg = registry._static_url_registrations - except AttributeError: - reg = registry._static_url_registrations = [] - return reg + def __init__(self): + self.registrations = [] + self.cache_busters = [] def generate(self, path, request, **kw): - try: - registry = request.registry - except AttributeError: # bw compat (for tests) - registry = get_current_registry() - registrations = self._get_registrations(registry) - for (url, spec, route_name, cachebust) in registrations: + for (url, spec, route_name) in self.registrations: if path.startswith(spec): subpath = path[len(spec):] if WIN: # pragma: no cover subpath = subpath.replace('\\', '/') # windows - if cachebust: - subpath, kw = cachebust(subpath, kw) + # translate spec into overridden spec and lookup cache buster + # to modify subpath, kw if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) @@ -2023,19 +2026,6 @@ class StaticURLInfo(object): # make sure it ends with a slash name = name + '/' - if config.registry.settings.get('pyramid.prevent_cachebust'): - cb = None - else: - cb = extra.pop('cachebust', None) - if cb: - def cachebust(subpath, kw): - subpath_tuple = tuple(subpath.split('/')) - subpath_tuple, kw = cb.pregenerate( - spec + subpath, subpath_tuple, kw) - return '/'.join(subpath_tuple), kw - else: - cachebust = None - if url_parse(name).netloc: # it's a URL # url, spec, route_name @@ -2044,14 +2034,11 @@ class StaticURLInfo(object): else: # it's a view name url = None - ten_years = 10 * 365 * 24 * 60 * 60 # more or less - default = ten_years if cb else None - cache_max_age = extra.pop('cache_max_age', default) + cache_max_age = extra.pop('cache_max_age', None) # create a view - cb_match = getattr(cb, 'match', None) view = static_view(spec, cache_max_age=cache_max_age, - use_subpath=True, cachebust_match=cb_match) + use_subpath=True) # Mutate extra to allow factory, etc to be passed through here. # Treat permission specially because we'd like to default to @@ -2083,7 +2070,7 @@ class StaticURLInfo(object): ) def register(): - registrations = self._get_registrations(config.registry) + registrations = self.registrations names = [t[0] for t in registrations] @@ -2092,7 +2079,7 @@ class StaticURLInfo(object): registrations.pop(idx) # url, spec, route_name - registrations.append((url, spec, route_name, cachebust)) + registrations.append((url, spec, route_name)) intr = config.introspectable('static views', name, @@ -2102,3 +2089,30 @@ class StaticURLInfo(object): intr['spec'] = spec config.action(None, callable=register, introspectables=(intr,)) + + def add_cache_buster(self, config, spec, cachebust): + 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) + + lengths = [len(t[0]) for t in cache_busters] + new_idx = bisect.bisect_left(lengths, len(spec)) + cache_busters.insert(new_idx, (spec, cachebust)) + + intr = config.introspectable('cache busters', + spec, + 'cache buster for %r' % spec, + 'cache buster') + intr['cachebust'] = cachebust + intr['spec'] = spec + + config.action(None, callable=register, introspectables=(intr,)) + + def _find_cache_buster(self, registry, spec): + for base_spec, cachebust in self.cache_busters: + if base_spec.startswith(spec): + pass diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 90534593c..bdf5bdfbe 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -584,6 +584,9 @@ class IStaticURLInfo(Interface): def generate(path, request, **kw): """ Generate a URL for the given path """ + def add_cache_buster(config, spec, cache_buster): + """ Add a new cache buster to a particular set of assets """ + class IResponseFactory(Interface): """ A utility which generates a response """ def __call__(request): @@ -1186,45 +1189,23 @@ class IPredicateList(Interface): class ICacheBuster(Interface): """ - Instances of ``ICacheBuster`` may be provided as arguments to - :meth:`~pyramid.config.Configurator.add_static_view`. Instances of - ``ICacheBuster`` provide mechanisms for generating a cache bust token for - a static asset, modifying a static asset URL to include a cache bust token, - and, optionally, unmodifying a static asset URL in order to look up an - asset. See :ref:`cache_busting`. + A cache buster modifies the URL generation machinery for + :meth:`~pyramid.request.Request.static_url`. See :ref:`cache_busting`. .. versionadded:: 1.6 """ - def pregenerate(pathspec, subpath, kw): + def __call__(pathspec, 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. - The ``subpath`` argument is a tuple of path elements 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 eventually to - :meth:`~pyramid.request.Request.route_url` for URL generation. The - return value should be a two-tuple of ``(subpath, kw)`` which are - versions of the same arguments modified to include the cache bust token - in the generated URL. - """ - - def match(subpath): - """ - Performs the logical inverse of - :meth:`~pyramid.interfaces.ICacheBuster.pregenerate` by taking a - subpath from a cache busted URL and removing the cache bust token, so - that :app:`Pyramid` can find the underlying asset. - - ``subpath`` is the subpath portion of the URL for an incoming request - for a static asset. The return value should be the same tuple with the - cache busting token elided. - - If the cache busting scheme in use doesn't specifically modify the path - portion of the generated URL (e.g. it adds a query string), a method - which implements this interface may not be necessary. It is - permissible for an instance of - :class:`~pyramid.interfaces.ICacheBuster` to omit this method. + 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 + eventually to :meth:`~pyramid.request.Request.static_url` for URL + generation. The return value should be a two-tuple of + ``(subpath, kw)`` which are versions of the same arguments modified + to include the cache bust token in the generated URL. """ # configuration phases: a lower phase number means the actions associated diff --git a/pyramid/static.py b/pyramid/static.py index c7a5c7ba5..cda98bea4 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -179,7 +179,7 @@ class QueryStringCacheBuster(object): def __init__(self, param='x'): self.param = param - def pregenerate(self, pathspec, subpath, kw): + def __call__(self, pathspec, subpath, kw): token = self.tokenize(pathspec) query = kw.setdefault('_query', {}) if isinstance(query, dict): @@ -289,8 +289,6 @@ class ManifestCacheBuster(object): 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) + def __call__(self, pathspec, 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 acfb81962..020ed131d 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3865,22 +3865,11 @@ class TestStaticURLInfo(unittest.TestCase): def _makeOne(self): return self._getTargetClass()() - def _makeConfig(self, registrations=None): - config = DummyConfig() - registry = DummyRegistry() - if registrations is not None: - registry._static_url_registrations = registrations - config.registry = registry - return config - def _makeRequest(self): request = DummyRequest() request.registry = DummyRegistry() return request - def _assertRegistrations(self, config, expected): - self.assertEqual(config.registry._static_url_registrations, expected) - def test_verifyClass(self): from pyramid.interfaces import IStaticURLInfo from zope.interface.verify import verifyClass @@ -4002,12 +3991,12 @@ class TestStaticURLInfo(unittest.TestCase): 'http://example.com/abc%20def#La%20Pe%C3%B1a') def test_generate_url_cachebust(self): - def cachebust(subpath, kw): + def cachebust(request, subpath, kw): kw['foo'] = 'bar' return 'foo' + '/' + subpath, kw inst = self._makeOne() - registrations = [(None, 'package:path/', '__viewname', cachebust)] - inst._get_registrations = lambda *x: registrations + inst.registrations = [(None, 'package:path/', '__viewname', cachebust)] + inst.cache_busters = [('package:path/', cachebust)] request = self._makeRequest() def route_url(n, **kw): self.assertEqual(n, '__viewname') @@ -4016,88 +4005,88 @@ class TestStaticURLInfo(unittest.TestCase): inst.generate('package:path/abc', request) def test_add_already_exists(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig( - [('http://example.com/', 'package:path/', None)]) + inst.registrations = [('http://example.com/', 'package:path/', None)] inst.add(config, 'http://example.com', 'anotherpackage:path') expected = [ - ('http://example.com/', 'anotherpackage:path/', None, None)] - self._assertRegistrations(config, expected) + ('http://example.com/', 'anotherpackage:path/', None, None)] + self.assertEqual(inst.registrations, expected) def test_add_package_root(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig() inst.add(config, 'http://example.com', 'package:') - expected = [('http://example.com/', 'package:', None, None)] - self._assertRegistrations(config, expected) + expected = [('http://example.com/', 'package:', None, None)] + self.assertEqual(inst.registrations, expected) def test_add_url_withendslash(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig() inst.add(config, 'http://example.com/', 'anotherpackage:path') expected = [ ('http://example.com/', 'anotherpackage:path/', None, None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) def test_add_url_noendslash(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig() inst.add(config, 'http://example.com', 'anotherpackage:path') expected = [ ('http://example.com/', 'anotherpackage:path/', None, None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) def test_add_url_noscheme(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig() inst.add(config, '//example.com', 'anotherpackage:path') expected = [('//example.com/', 'anotherpackage:path/', None, None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) def test_add_viewname(self): from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1) expected = [(None, 'anotherpackage:path/', '__view/', None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) self.assertEqual(config.route_args, ('__view/', 'view/*subpath')) self.assertEqual(config.view_kw['permission'], NO_PERMISSION_REQUIRED) self.assertEqual(config.view_kw['view'].__class__, static_view) def test_add_viewname_with_route_prefix(self): - config = self._makeConfig() + config = DummyConfig() config.route_prefix = '/abc' inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path',) expected = [(None, 'anotherpackage:path/', '__/abc/view/', None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) self.assertEqual(config.route_args, ('__/abc/view/', 'view/*subpath')) def test_add_viewname_with_permission(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1, permission='abc') self.assertEqual(config.view_kw['permission'], 'abc') def test_add_viewname_with_context(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1, context=DummyContext) self.assertEqual(config.view_kw['context'], DummyContext) def test_add_viewname_with_for_(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1, for_=DummyContext) self.assertEqual(config.view_kw['context'], DummyContext) def test_add_viewname_with_renderer(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1, renderer='mypackage:templates/index.pt') @@ -4105,7 +4094,7 @@ class TestStaticURLInfo(unittest.TestCase): 'mypackage:templates/index.pt') def test_add_cachebust_prevented(self): - config = self._makeConfig() + config = DummyConfig() config.registry.settings['pyramid.prevent_cachebust'] = True inst = self._makeOne() inst.add(config, 'view', 'mypackage:path', cachebust=True) @@ -4113,7 +4102,7 @@ class TestStaticURLInfo(unittest.TestCase): self.assertEqual(cachebust, None) def test_add_cachebust_custom(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'mypackage:path', cachebust=DummyCacheBuster('foo')) @@ -4236,7 +4225,8 @@ class DummyMultiView: class DummyCacheBuster(object): def __init__(self, token): self.token = token - def pregenerate(self, pathspec, subpath, kw): + + def __call__(self, pathspec, subpath, kw): kw['x'] = self.token return subpath, kw |
