Merge pull request #2186 from mmerickel/backport/2171
backport config.add_cache_buster
| | |
| | | 1.6b3 (unreleased) |
| | | ================== |
| | | |
| | | Backward Incompatibilities |
| | | -------------------------- |
| | | |
| | | - Remove the ``cachebust`` option from ``config.add_static_view``. See |
| | | ``config.add_cache_buster`` for the new way to attach cache busters to |
| | | static assets. |
| | | See https://github.com/Pylons/pyramid/pull/2186 |
| | | |
| | | - Modify the ``pyramid.interfaces.ICacheBuster`` API to be a simple callable |
| | | instead of an object with ``match`` and ``pregenerate`` methods. Cache |
| | | busters are now focused solely on generation. Matching has been dropped. |
| | | |
| | | Note this affects usage of ``pyramid.static.QueryStringCacheBuster`` and |
| | | ``pyramid.static.ManifestCacheBuster``. |
| | | |
| | | See https://github.com/Pylons/pyramid/pull/2186 |
| | | |
| | | Features |
| | | -------- |
| | | |
| | | - Add a new ``config.add_cache_buster`` API for attaching cache busters to |
| | | static assets. See https://github.com/Pylons/pyramid/pull/2186 |
| | | |
| | | 1.6b2 (2015-10-15) |
| | | ================== |
| | | |
| | |
| | | resource's old URL. |
| | | |
| | | :app:`Pyramid` can be configured to produce cache busting URLs for static |
| | | assets by passing the optional argument, ``cachebust`` to |
| | | :meth:`~pyramid.config.Configurator.add_static_view`: |
| | | assets using :meth:`~pyramid.config.Configurator.add_cache_buster`: |
| | | |
| | | .. code-block:: python |
| | | :linenos: |
| | |
| | | from pyramid.static import QueryStringConstantCacheBuster |
| | | |
| | | # config is an instance of pyramid.config.Configurator |
| | | config.add_static_view( |
| | | name='static', path='mypackage:folder/static', |
| | | cachebust=QueryStringConstantCacheBuster(str(int(time.time()))), |
| | | ) |
| | | config.add_static_view(name='static', path='mypackage:folder/static/') |
| | | config.add_cache_buster( |
| | | 'mypackage:folder/static/', |
| | | QueryStringConstantCacheBuster(str(int(time.time())))) |
| | | |
| | | Setting the ``cachebust`` argument instructs :app:`Pyramid` to use a cache |
| | | busting scheme which adds the curent time for a static asset to the query |
| | | string in the asset's URL: |
| | | Adding the cachebuster instructs :app:`Pyramid` to add the current time for |
| | | a static asset to the query string in the asset's URL: |
| | | |
| | | .. code-block:: python |
| | | :linenos: |
| | |
| | | # Returns: 'http://www.example.com/static/js/myapp.js?x=1445318121' |
| | | |
| | | 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. |
| | | will its URL. |
| | | |
| | | .. note:: |
| | | |
| | |
| | | |
| | | It can be useful in some situations (e.g., development) to globally disable all |
| | | configured cache busters without changing calls to |
| | | :meth:`~pyramid.config.Configurator.add_static_view`. To do this set the |
| | | :meth:`~pyramid.config.Configurator.add_cache_buster`. To do this set the |
| | | ``PYRAMID_PREVENT_CACHEBUST`` environment variable or the |
| | | ``pyramid.prevent_cachebust`` configuration value to a true value. |
| | | |
| | |
| | | Customizing the Cache Buster |
| | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| | | |
| | | 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`. |
| | | Calls to :meth:`~pyramid.config.Configurator.add_cache_buster` may use |
| | | any object that implements the interface |
| | | :class:`~pyramid.interfaces.ICacheBuster`. |
| | | |
| | | :app:`Pyramid` ships with a very simplistic |
| | | :class:`~pyramid.static.QueryStringConstantCacheBuster`, which adds an |
| | |
| | | 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, param='x'): |
| | | def __init__(self, param='x', repo_path=None): |
| | | super(GitCacheBuster, self).__init__(param=param) |
| | | here = os.path.dirname(os.path.abspath(__file__)) |
| | | if repo_path is None: |
| | | repo_path = os.path.dirname(os.path.abspath(__file__)) |
| | | self.sha1 = subprocess.check_output( |
| | | ['git', 'rev-parse', 'HEAD'], |
| | | cwd=here).strip() |
| | | cwd=repo_path).strip() |
| | | |
| | | def tokenize(self, pathspec): |
| | | return self.sha1 |
| | | |
| | | Choosing a Cache Buster |
| | | ~~~~~~~~~~~~~~~~~~~~~~~ |
| | | A simple cache buster that modifies the path segment can be constructed as |
| | | well: |
| | | |
| | | .. code-block:: python |
| | | :linenos: |
| | | |
| | | import posixpath |
| | | |
| | | class PathConstantCacheBuster(object): |
| | | def __init__(self, token): |
| | | self.token = token |
| | | |
| | | def __call__(self, request, subpath, kw): |
| | | base_subpath, ext = posixpath.splitext(subpath) |
| | | new_subpath = base_subpath + self.token + ext |
| | | return new_subpath, kw |
| | | |
| | | The caveat with this approach is that modifying the path segment |
| | | changes the file name, and thus must match what is actually on the |
| | | filesystem in order for :meth:`~pyramid.config.Configurator.add_static_view` |
| | | to find the file. It's better to use the |
| | | :class:`~pyramid.static.ManifestCacheBuster` for these situations, as |
| | | described in the next section. |
| | | |
| | | .. _path_segment_cache_busters: |
| | | |
| | | Path Segments and Choosing a Cache Buster |
| | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| | | |
| | | Many caching HTTP proxies will fail to cache a resource if the URL contains |
| | | a query string. Therefore, in general, you should prefer a cache busting |
| | |
| | | |
| | | config.add_static_view( |
| | | name='http://mycdn.example.com/', |
| | | path='mypackage:static', |
| | | cachebust=ManifestCacheBuster('myapp:static/manifest.json')) |
| | | path='mypackage:static') |
| | | |
| | | A simpler approach is to use the |
| | | :class:`~pyramid.static.QueryStringConstantCacheBuster` to generate a global |
| | | token that will bust all of the assets at once. The advantage of this strategy |
| | | is that it is simple and by using the query string there doesn't need to be |
| | | any shared information between your application and the static assets. |
| | | |
| | | 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(int(time.time())))) |
| | | config.add_cache_buster( |
| | | 'mypackage:static/', |
| | | ManifestCacheBuster('myapp:static/manifest.json')) |
| | | |
| | | .. index:: |
| | | single: static assets view |
| | |
| | | Often one needs to refer to images and other static assets inside CSS and |
| | | JavaScript files. If cache busting is active, the final static asset URL is not |
| | | available until the static assets have been assembled. These URLs cannot be |
| | | handwritten. Thus, when having static asset references in CSS and JavaScript, |
| | | one needs to perform one of the following tasks: |
| | | handwritten. Below is an example of how to integrate the cache buster into |
| | | the entire stack. Remember, it is just an example and should be modified to |
| | | fit your specific tools. |
| | | |
| | | * Process the files by using a precompiler which rewrites URLs to their final |
| | | cache busted form. Then, you can use the |
| | | * First, process the files by using a precompiler which rewrites URLs to their |
| | | final cache-busted form. Then, you can use the |
| | | :class:`~pyramid.static.ManifestCacheBuster` to synchronize your asset |
| | | pipeline with :app:`Pyramid`, allowing the pipeline to have full control |
| | | over the final URLs of your assets. |
| | | |
| | | * Templatize JS and CSS, and call ``request.static_url()`` inside their |
| | | template code. |
| | | Now that you are able to generate static URLs within :app:`Pyramid`, |
| | | you'll need to handle URLs that are out of our control. To do this you may |
| | | use some of the following options to get started: |
| | | |
| | | * Pass static URL references to CSS and JavaScript via other means. |
| | | * Configure your asset pipeline to rewrite URL references inline in |
| | | CSS and JavaScript. This is the best approach because then the files |
| | | may be hosted by :app:`Pyramid` or an external CDN without having to |
| | | change anything. They really are static. |
| | | |
| | | * Templatize JS and CSS, and call ``request.static_url()`` inside their |
| | | template code. While this approach may work in certain scenarios, it is not |
| | | recommended because your static assets will not really be static and are now |
| | | dependent on :app:`Pyramid` to be served correctly. See |
| | | :ref:`advanced_static` for more information on this approach. |
| | | |
| | | If your CSS and JavaScript assets use URLs to reference other assets it is |
| | | recommended that you implement an external asset pipeline that can rewrite the |
| | | generated static files with new URLs containing cache busting tokens. The |
| | | machinery inside :app:`Pyramid` will not help with this step as it has very |
| | | little knowledge of the asset types your application may use. |
| | | little knowledge of the asset types your application may use. The integration |
| | | into :app:`Pyramid` is simply for linking those assets into your HTML and |
| | | other dynamic content. |
| | | |
| | | .. _advanced_static: |
| | | |
| | |
| | | As of Pyramid 1.6, it is also possible to override an asset by supplying an |
| | | absolute path to a file or directory. This may be useful if the assets are |
| | | not distributed as part of a Python package. |
| | | |
| | | Cache Busting and Asset Overrides |
| | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| | | |
| | | Overriding static assets that are being hosted using |
| | | :meth:`pyramid.config.Configurator.add_static_view` can affect your cache |
| | | busting strategy when using any cache busters that are asset-aware such as |
| | | :class:`pyramid.static.ManifestCacheBuster`. What sets asset-aware cache |
| | | busters apart is that they have logic tied to specific assets. For example, |
| | | a manifest is only generated for a specific set of pre-defined assets. Now, |
| | | imagine you have overridden an asset defined in this manifest with a new, |
| | | unknown version. By default, the cache buster will be invoked for an asset |
| | | it has never seen before and will likely end up returning a cache busting |
| | | token for the original asset rather than the asset that will actually end up |
| | | being served! In order to get around this issue, it's possible to attach a |
| | | different :class:`pyramid.interfaces.ICacheBuster` implementation to the |
| | | new assets. This would cause the original assets to be served by their |
| | | manifest, and the new assets served by their own cache buster. To do this, |
| | | :meth:`pyramid.config.Configurator.add_cache_buster` supports an ``explicit`` |
| | | option. For example: |
| | | |
| | | .. code-block:: python |
| | | :linenos: |
| | | |
| | | from pyramid.static import ManifestCacheBuster |
| | | |
| | | # define a static view for myapp:static assets |
| | | config.add_static_view('static', 'myapp:static') |
| | | |
| | | # setup a cache buster for your app based on the myapp:static assets |
| | | my_cb = ManifestCacheBuster('myapp:static/manifest.json') |
| | | config.add_cache_buster('myapp:static', my_cb) |
| | | |
| | | # override an asset |
| | | config.override_asset( |
| | | to_override='myapp:static/background.png', |
| | | override_with='theme:static/background.png') |
| | | |
| | | # override the cache buster for theme:static assets |
| | | theme_cb = ManifestCacheBuster('theme:static/manifest.json') |
| | | config.add_cache_buster('theme:static', theme_cb, explicit=True) |
| | | |
| | | In the above example there is a default cache buster, ``my_cb``, for all |
| | | assets served from the ``myapp:static`` folder. This would also affect |
| | | ``theme:static/background.png`` when generating URLs via |
| | | ``request.static_url('myapp:static/background.png')``. |
| | | |
| | | The ``theme_cb`` is defined explicitly for any assets loaded from the |
| | | ``theme:static`` folder. Explicit cache busters have priority and thus |
| | | ``theme_cb`` would be invoked for |
| | | ``request.static_url('myapp:static/background.png')``, but ``my_cb`` would |
| | | be used for any other assets like |
| | | ``request.static_url('myapp:static/favicon.ico')``. |
| | |
| | | def __init__(self, prefix): |
| | | self.prefix = prefix |
| | | |
| | | def get_filename(self, resource_name): |
| | | def get_path(self, resource_name): |
| | | if resource_name: |
| | | path = os.path.join(self.prefix, resource_name) |
| | | else: |
| | | path = self.prefix |
| | | return path |
| | | |
| | | def get_filename(self, resource_name): |
| | | path = self.get_path(resource_name) |
| | | if os.path.exists(path): |
| | | return path |
| | | |
| | |
| | | import inspect |
| | | import posixpath |
| | | import operator |
| | | import os |
| | | import warnings |
| | |
| | | IException, |
| | | IExceptionViewClassifier, |
| | | IMultiView, |
| | | IPackageOverrides, |
| | | IRendererFactory, |
| | | IRequest, |
| | | IResponse, |
| | |
| | | |
| | | from pyramid import renderers |
| | | |
| | | from pyramid.asset import resolve_asset_spec |
| | | from pyramid.compat import ( |
| | | string_types, |
| | | urlparse, |
| | |
| | | |
| | | from pyramid.security import NO_PERMISSION_REQUIRED |
| | | from pyramid.static import static_view |
| | | from pyramid.threadlocal import get_current_registry |
| | | |
| | | from pyramid.url import parse_url_overrides |
| | | |
| | |
| | | ``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 |
| | |
| | | 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, explicit=False): |
| | | """ |
| | | Add a cache buster to a set of files on disk. |
| | | |
| | | The ``path`` should be the path on disk where the static files |
| | | reside. This can be an absolute path, a package-relative path, or a |
| | | :term:`asset specification`. |
| | | |
| | | The ``cachebust`` 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 ``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, explicit=explicit) |
| | | |
| | | 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): |
| | |
| | | |
| | | @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) |
| | | if self.cache_busters: |
| | | subpath, kw = self._bust_asset_path( |
| | | request, spec, subpath, kw) |
| | | if url is None: |
| | | kw['subpath'] = subpath |
| | | return request.route_url(route_name, **kw) |
| | |
| | | # 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 |
| | |
| | | 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 |
| | |
| | | ) |
| | | |
| | | def register(): |
| | | registrations = self._get_registrations(config.registry) |
| | | registrations = self.registrations |
| | | |
| | | names = [ t[0] for t in registrations ] |
| | | |
| | |
| | | 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, |
| | |
| | | |
| | | config.action(None, callable=register, introspectables=(intr,)) |
| | | |
| | | def add_cache_buster(self, config, spec, cachebust, explicit=False): |
| | | # ensure the spec always has a trailing slash as we only support |
| | | # adding cache busters to folders, not files |
| | | if os.path.isabs(spec): # FBO windows |
| | | sep = os.sep |
| | | else: |
| | | sep = '/' |
| | | if not spec.endswith(sep) and not spec.endswith(':'): |
| | | spec = spec + sep |
| | | |
| | | def register(): |
| | | if config.registry.settings.get('pyramid.prevent_cachebust'): |
| | | return |
| | | |
| | | cache_busters = self.cache_busters |
| | | |
| | | # 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 |
| | | |
| | | 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['path'] = spec |
| | | intr['explicit'] = explicit |
| | | |
| | | config.action(None, callable=register, introspectables=(intr,)) |
| | | |
| | | 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) |
| | | sources = overrides.filtered_sources(resource_name) |
| | | for source, filtered_path in sources: |
| | | rawspec = source.get_path(filtered_path) |
| | | if hasattr(source, 'pkg_name'): |
| | | rawspec = '{0}:{1}'.format(source.pkg_name, rawspec) |
| | | break |
| | | |
| | | else: |
| | | pathspec = pkg_subpath + subpath |
| | | |
| | | if rawspec is None: |
| | | rawspec = pathspec |
| | | |
| | | 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 |
| | |
| | | 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): |
| | |
| | | |
| | | 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__(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. |
| | | 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. |
| | | """ |
| | | URL will be computed during URL generation. |
| | | |
| | | 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. |
| | | 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)`` where ``subpath`` is the relative URL from where the |
| | | file is served and ``kw`` is the same input argument. The return value |
| | | should be modified to include the cache bust token in the generated |
| | | URL. |
| | | |
| | | ``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. |
| | | 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: |
| | | |
| | | 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. |
| | | - ``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')``. |
| | | """ |
| | | |
| | | # configuration phases: a lower phase number means the actions associated |
| | |
| | | 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 pregenerate(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 |
| | |
| | | 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): |
| | |
| | | "images/background.png": "images/background-a8169106.png", |
| | | } |
| | | |
| | | Specifically, it is a JSON-serialized dictionary where the keys are the |
| | | By default, 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:: |
| | | |
| | |
| | | >>> request.static_url('myapp:static/css/main.css') |
| | | "http://www.example.com/static/css/main-678b7c80.css" |
| | | |
| | | The file format and location can be changed by subclassing and overriding |
| | | :meth:`.parse_manifest`. |
| | | |
| | | 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 |
| | |
| | | |
| | | 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 |
| | | """ |
| | |
| | | |
| | | self._mtime = None |
| | | if not reload: |
| | | self._manifest = self.parse_manifest() |
| | | self._manifest = self.get_manifest() |
| | | |
| | | def parse_manifest(self): |
| | | def get_manifest(self): |
| | | with open(self.manifest_path, 'rb') as fp: |
| | | return self.parse_manifest(fp.read()) |
| | | |
| | | def parse_manifest(self, content): |
| | | """ |
| | | Return a mapping parsed from the ``manifest_path``. |
| | | Parse the ``content`` read from the ``manifest_path`` into a |
| | | dictionary mapping. |
| | | |
| | | Subclasses may override this method to use something other than |
| | | ``json.loads`` to load any type of file format and return a conforming |
| | | dictionary. |
| | | |
| | | """ |
| | | with open(self.manifest_path, 'rb') as fp: |
| | | content = fp.read().decode('utf-8') |
| | | return json.loads(content) |
| | | return json.loads(content.decode('utf-8')) |
| | | |
| | | @property |
| | | def manifest(self): |
| | |
| | | return {} |
| | | mtime = self.getmtime(self.manifest_path) |
| | | if self._mtime is None or mtime > self._mtime: |
| | | self._manifest = self.parse_manifest() |
| | | self._manifest = self.get_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) |
| | | def __call__(self, request, subpath, kw): |
| | | subpath = self.manifest.get(subpath, subpath) |
| | | return (subpath, kw) |
| | |
| | | import os |
| | | import unittest |
| | | from pyramid import testing |
| | | |
| | |
| | | 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 |
| | |
| | | |
| | | def test_generate_registration_miss(self): |
| | | inst = self._makeOne() |
| | | registrations = [ |
| | | (None, 'spec', 'route_name', None), |
| | | ('http://example.com/foo/', 'package:path/', None, None)] |
| | | inst._get_registrations = lambda *x: registrations |
| | | inst.registrations = [ |
| | | (None, 'spec', 'route_name'), |
| | | ('http://example.com/foo/', 'package:path/', None)] |
| | | request = self._makeRequest() |
| | | result = inst.generate('package:path/abc', request) |
| | | self.assertEqual(result, 'http://example.com/foo/abc') |
| | | |
| | | def test_generate_registration_no_registry_on_request(self): |
| | | inst = self._makeOne() |
| | | registrations = [ |
| | | ('http://example.com/foo/', 'package:path/', None, None)] |
| | | inst._get_registrations = lambda *x: registrations |
| | | request = self._makeRequest() |
| | | del request.registry |
| | | result = inst.generate('package:path/abc', request) |
| | | self.assertEqual(result, 'http://example.com/foo/abc') |
| | | |
| | | def test_generate_slash_in_name1(self): |
| | | inst = self._makeOne() |
| | | registrations = [ |
| | | ('http://example.com/foo/', 'package:path/', None, None)] |
| | | inst._get_registrations = lambda *x: registrations |
| | | inst.registrations = [('http://example.com/foo/', 'package:path/', None)] |
| | | request = self._makeRequest() |
| | | result = inst.generate('package:path/abc', request) |
| | | self.assertEqual(result, 'http://example.com/foo/abc') |
| | | |
| | | def test_generate_slash_in_name2(self): |
| | | inst = self._makeOne() |
| | | registrations = [ |
| | | ('http://example.com/foo/', 'package:path/', None, None)] |
| | | inst._get_registrations = lambda *x: registrations |
| | | inst.registrations = [('http://example.com/foo/', 'package:path/', None)] |
| | | request = self._makeRequest() |
| | | result = inst.generate('package:path/', request) |
| | | self.assertEqual(result, 'http://example.com/foo/') |
| | | |
| | | def test_generate_quoting(self): |
| | | from pyramid.interfaces import IStaticURLInfo |
| | | config = testing.setUp() |
| | | try: |
| | | config.add_static_view('images', path='mypkg:templates') |
| | | inst = self._makeOne() |
| | | request = testing.DummyRequest() |
| | | request.registry = config.registry |
| | | inst = config.registry.getUtility(IStaticURLInfo) |
| | | result = inst.generate('mypkg:templates/foo%2Fbar', request) |
| | | self.assertEqual(result, 'http://example.com/images/foo%252Fbar') |
| | | finally: |
| | |
| | | |
| | | def test_generate_route_url(self): |
| | | inst = self._makeOne() |
| | | registrations = [(None, 'package:path/', '__viewname/', None)] |
| | | inst._get_registrations = lambda *x: registrations |
| | | inst.registrations = [(None, 'package:path/', '__viewname/')] |
| | | def route_url(n, **kw): |
| | | self.assertEqual(n, '__viewname/') |
| | | self.assertEqual(kw, {'subpath':'abc', 'a':1}) |
| | |
| | | |
| | | def test_generate_url_unquoted_local(self): |
| | | inst = self._makeOne() |
| | | registrations = [(None, 'package:path/', '__viewname/', None)] |
| | | inst._get_registrations = lambda *x: registrations |
| | | inst.registrations = [(None, 'package:path/', '__viewname/')] |
| | | def route_url(n, **kw): |
| | | self.assertEqual(n, '__viewname/') |
| | | self.assertEqual(kw, {'subpath':'abc def', 'a':1}) |
| | |
| | | |
| | | def test_generate_url_quoted_remote(self): |
| | | inst = self._makeOne() |
| | | registrations = [('http://example.com/', 'package:path/', None, None)] |
| | | inst._get_registrations = lambda *x: registrations |
| | | inst.registrations = [('http://example.com/', 'package:path/', None)] |
| | | request = self._makeRequest() |
| | | result = inst.generate('package:path/abc def', request, a=1) |
| | | self.assertEqual(result, 'http://example.com/abc%20def') |
| | | |
| | | def test_generate_url_with_custom_query(self): |
| | | inst = self._makeOne() |
| | | registrations = [('http://example.com/', 'package:path/', None, None)] |
| | | inst._get_registrations = lambda *x: registrations |
| | | registrations = [('http://example.com/', 'package:path/', None)] |
| | | inst.registrations = registrations |
| | | request = self._makeRequest() |
| | | result = inst.generate('package:path/abc def', request, a=1, |
| | | _query='(openlayers)') |
| | |
| | | |
| | | def test_generate_url_with_custom_anchor(self): |
| | | inst = self._makeOne() |
| | | registrations = [('http://example.com/', 'package:path/', None, None)] |
| | | inst._get_registrations = lambda *x: registrations |
| | | inst.registrations = [('http://example.com/', 'package:path/', None)] |
| | | request = self._makeRequest() |
| | | uc = text_(b'La Pe\xc3\xb1a', 'utf-8') |
| | | result = inst.generate('package:path/abc def', request, a=1, |
| | | _anchor=uc) |
| | | result = inst.generate('package:path/abc def', request, a=1, _anchor=uc) |
| | | self.assertEqual(result, |
| | | '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')] |
| | | 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]) |
| | | |
| | | def test_generate_url_cachebust_abspath(self): |
| | | here = os.path.dirname(__file__) + os.sep |
| | | def cachebust(pathspec, subpath, kw): |
| | | kw['foo'] = 'bar' |
| | | return 'foo' + '/' + subpath, kw |
| | | inst = self._makeOne() |
| | | inst.registrations = [(None, here, '__viewname')] |
| | | 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', |
| | | 'pathspec': here + 'abc', |
| | | 'rawspec': here + 'abc'}) |
| | | request.route_url = route_url |
| | | inst.generate(here + 'abc', request) |
| | | self.assertTrue(called[0]) |
| | | |
| | | def test_generate_url_cachebust_nomatch(self): |
| | | def fake_cb(*a, **kw): raise AssertionError |
| | | inst = self._makeOne() |
| | | inst.registrations = [(None, 'package:path/', '__viewname')] |
| | | 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', |
| | | 'pathspec': 'package:path/abc', |
| | | 'rawspec': 'package:path/abc'}) |
| | | request.route_url = route_url |
| | | inst.generate('package:path/abc', request) |
| | | self.assertTrue(called[0]) |
| | | |
| | | 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(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() |
| | | |
| | | 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) |
| | | expected = [('http://example.com/', 'anotherpackage:path/', 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)] |
| | | 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) |
| | | expected = [('http://example.com/', 'anotherpackage:path/', None)] |
| | | 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) |
| | | expected = [('http://example.com/', 'anotherpackage:path/', None)] |
| | | 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) |
| | | expected = [('//example.com/', 'anotherpackage:path/', None)] |
| | | 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) |
| | | expected = [(None, 'anotherpackage:path/', '__view/')] |
| | | 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) |
| | | expected = [(None, 'anotherpackage:path/', '__/abc/view/')] |
| | | 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') |
| | |
| | | '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) |
| | | cachebust = config.registry._static_url_registrations[0][3] |
| | | self.assertEqual(cachebust, None) |
| | | cachebust = DummyCacheBuster('foo') |
| | | inst.add_cache_buster(config, 'mypackage:path', cachebust) |
| | | self.assertEqual(inst.cache_busters, []) |
| | | |
| | | def test_add_cachebust_custom(self): |
| | | config = self._makeConfig() |
| | | def test_add_cachebuster(self): |
| | | config = DummyConfig() |
| | | inst = self._makeOne() |
| | | inst.add(config, 'view', 'mypackage:path', |
| | | cachebust=DummyCacheBuster('foo')) |
| | | cachebust = config.registry._static_url_registrations[0][3] |
| | | subpath, kw = cachebust('some/path', {}) |
| | | inst.add_cache_buster(config, 'mypackage:path', DummyCacheBuster('foo')) |
| | | cachebust = inst.cache_busters[-1][1] |
| | | subpath, kw = cachebust(None, 'some/path', {}) |
| | | self.assertEqual(subpath, 'some/path') |
| | | self.assertEqual(kw['x'], 'foo') |
| | | |
| | | def test_add_cachebuster_abspath(self): |
| | | here = os.path.dirname(__file__) |
| | | config = DummyConfig() |
| | | inst = self._makeOne() |
| | | cb = DummyCacheBuster('foo') |
| | | inst.add_cache_buster(config, here, cb) |
| | | self.assertEqual(inst.cache_busters, [(here + '/', cb, False)]) |
| | | |
| | | def test_add_cachebuster_overwrite(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) |
| | | self.assertEqual(inst.cache_busters, |
| | | [('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, 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): |
| | |
| | | |
| | | |
| | | class DummyRegistry: |
| | | utility = None |
| | | |
| | | def __init__(self): |
| | | self.settings = {} |
| | | |
| | | def queryUtility(self, type_or_iface, name=None, default=None): |
| | | return self.utility or default |
| | | |
| | | from zope.interface import implementer |
| | | from pyramid.interfaces import ( |
| | |
| | | return self.permitted |
| | | |
| | | class DummyConfig: |
| | | def __init__(self): |
| | | self.registry = DummyRegistry() |
| | | |
| | | route_prefix = '' |
| | | def add_route(self, *args, **kw): |
| | | self.route_args = args |
| | |
| | | class DummyCacheBuster(object): |
| | | def __init__(self, token): |
| | | self.token = token |
| | | def pregenerate(self, pathspec, subpath, kw): |
| | | |
| | | def __call__(self, request, subpath, kw): |
| | | kw['x'] = self.token |
| | | return subpath, kw |
| | | |
| | |
| | | |
| | | def test_token(self): |
| | | fut = self._makeOne().tokenize |
| | | self.assertEqual(fut('whatever'), 'foo') |
| | | self.assertEqual(fut(None, 'whatever', None), 'foo') |
| | | |
| | | def test_pregenerate(self): |
| | | fut = self._makeOne().pregenerate |
| | | def test_it(self): |
| | | fut = self._makeOne() |
| | | self.assertEqual( |
| | | fut('foo', ('bar',), {}), |
| | | (('bar',), {'_query': {'x': 'foo'}})) |
| | | fut('foo', 'bar', {}), |
| | | ('bar', {'_query': {'x': 'foo'}})) |
| | | |
| | | def test_pregenerate_change_param(self): |
| | | fut = self._makeOne('y').pregenerate |
| | | def test_change_param(self): |
| | | fut = self._makeOne('y') |
| | | self.assertEqual( |
| | | fut('foo', ('bar',), {}), |
| | | (('bar',), {'_query': {'y': 'foo'}})) |
| | | fut('foo', 'bar', {}), |
| | | ('bar', {'_query': {'y': 'foo'}})) |
| | | |
| | | def test_pregenerate_query_is_already_tuples(self): |
| | | fut = self._makeOne().pregenerate |
| | | def test_query_is_already_tuples(self): |
| | | fut = self._makeOne() |
| | | self.assertEqual( |
| | | fut('foo', ('bar',), {'_query': [('a', 'b')]}), |
| | | (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) |
| | | 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 |
| | | def test_query_is_tuple_of_tuples(self): |
| | | fut = self._makeOne() |
| | | self.assertEqual( |
| | | fut('foo', ('bar',), {'_query': (('a', 'b'),)}), |
| | | (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) |
| | | fut('foo', 'bar', {'_query': (('a', 'b'),)}), |
| | | ('bar', {'_query': (('a', 'b'), ('x', 'foo'))})) |
| | | |
| | | class TestManifestCacheBuster(unittest.TestCase): |
| | | |
| | |
| | | |
| | | 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'], {})) |
| | | fut = self._makeOne(manifest_path) |
| | | self.assertEqual(fut('foo', 'bar', {}), ('bar', {})) |
| | | self.assertEqual( |
| | | fut('foo', ('css', 'main.css'), {}), |
| | | (['css', 'main-test.css'], {})) |
| | | fut('foo', 'css/main.css', {}), |
| | | ('css/main-test.css', {})) |
| | | |
| | | def test_it_with_relspec(self): |
| | | fut = self._makeOne('fixtures/manifest.json').pregenerate |
| | | self.assertEqual(fut('foo', ('bar',), {}), (['bar'], {})) |
| | | fut = self._makeOne('fixtures/manifest.json') |
| | | self.assertEqual(fut('foo', 'bar', {}), ('bar', {})) |
| | | self.assertEqual( |
| | | fut('foo', ('css', 'main.css'), {}), |
| | | (['css', 'main-test.css'], {})) |
| | | fut('foo', 'css/main.css', {}), |
| | | ('css/main-test.css', {})) |
| | | |
| | | def test_it_with_absspec(self): |
| | | fut = self._makeOne('pyramid.tests:fixtures/manifest.json').pregenerate |
| | | self.assertEqual(fut('foo', ('bar',), {}), (['bar'], {})) |
| | | fut = self._makeOne('pyramid.tests:fixtures/manifest.json') |
| | | self.assertEqual(fut('foo', 'bar', {}), ('bar', {})) |
| | | self.assertEqual( |
| | | fut('foo', ('css', 'main.css'), {}), |
| | | (['css', 'main-test.css'], {})) |
| | | 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 |
| | | fut = inst |
| | | |
| | | # test without a valid manifest |
| | | self.assertEqual( |
| | | fut('foo', ('css', 'main.css'), {}), |
| | | (['css', 'main.css'], {})) |
| | | 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'], {})) |
| | | 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'], {})) |
| | | 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'], {})) |
| | | fut('foo', 'css/main.css', {}), |
| | | ('css/main-678b7c80.css', {})) |
| | | |
| | | def test_invalid_manifest(self): |
| | | self.assertRaises(IOError, lambda: self._makeOne('foo')) |