Source code for coaster.assets

# -*- coding: utf-8 -*-

"""
Assets
======

Coaster provides a simple asset management system for semantically versioned
assets using the semantic_version_ and webassets_ libraries. Many popular
libraries such as jQuery are not semantically versioned, so you will have to
be careful about assumptions you make around them.

.. _semantic_version: http://python-semanticversion.readthedocs.org/en/latest/
.. _webassets: http://elsdoerfer.name/docs/webassets/
"""

from __future__ import absolute_import
import six

from collections import defaultdict
import re

from flask_assets import Bundle
from webassets.filter import Filter, register_filter

# Version is not used here but is made available for others to import from
from semantic_version import SimpleSpec, Version

_VERSION_SPECIFIER_RE = re.compile('[<=>!*]')

__all__ = ['Version', 'SimpleSpec', 'VersionedAssets', 'AssetNotFound']


def split_namespec(namespec):
    find_mark = _VERSION_SPECIFIER_RE.search(namespec)
    if find_mark is None:
        name = namespec
        spec = SimpleSpec('*')
    else:
        name = namespec[: find_mark.start()]
        spec = SimpleSpec(namespec[find_mark.start() :])
    return name, spec


[docs]class AssetNotFound(Exception): """No asset with this name"""
[docs]class VersionedAssets(defaultdict): """ Semantic-versioned assets. To use, initialize a container for your assets:: from coaster.assets import VersionedAssets, Version assets = VersionedAssets() And then populate it with your assets. The simplest way is by specifying the asset name, version number, and path to the file (within your static folder):: assets['jquery.js'][Version('1.8.3')] = 'js/jquery-1.8.3.js' You can also specify one or more *requirements* for an asset by supplying a list or tuple of requirements followed by the actual asset:: assets['jquery.form.js'][Version('2.96.0')] = ( 'jquery.js', 'js/jquery.form-2.96.js') You may have an asset that provides replacement functionality for another asset:: assets['zepto.js'][Version('1.0.0-rc1')] = { 'provides': 'jquery.js', 'bundle': 'js/zepto-1.0rc1.js', } Assets specified as a dictionary can have three keys: :parameter provides: Assets provided by this asset :parameter requires: Assets required by this asset (with optional version specifications) :parameter bundle: The asset itself :type provides: string or list :type requires: string or list :type bundle: string or Bundle To request an asset:: assets.require('jquery.js', 'jquery.form.js==2.96.0', ...) This returns a webassets Bundle of the requested assets and their dependencies. You can also ask for certain assets to not be included even if required if, for example, you are loading them from elsewhere such as a CDN. Prefix the asset name with '!':: assets.require('!jquery.js', 'jquery.form.js', ...) To use these assets in a Flask app, register the assets with an environment:: from flask_assets import Environment appassets = Environment(app) appassets.register('js_all', assets.require('jquery.js', ...)) And include them in your master template: .. sourcecode:: jinja {% assets "js_all" -%} <script type="text/javascript" src="{{ ASSET_URL }}"></script> {%- endassets -%} """ def __init__(self): # Override dict's __init__ to prevent parameters super(VersionedAssets, self).__init__(dict) def _require_recursive(self, *namespecs): asset_versions = {} # Name: version bundles = [] for namespec in namespecs: name, spec = split_namespec(namespec) version = spec.select(list(self[name].keys())) if version: if name in asset_versions: if asset_versions[name] not in spec: raise ValueError( "%s does not match already requested asset %s==%s" % (namespec, name, asset_versions[name]) ) else: asset = self[name][version] if isinstance(asset, (list, tuple)): # We have (requires, bundle). Get requirements requires = asset[:-1] provides = [] bundle = asset[-1] elif isinstance(asset, dict): requires = asset.get('requires', []) if isinstance(requires, six.string_types): requires = [requires] provides = asset.get('provides', []) if isinstance(provides, six.string_types): provides = [provides] bundle = asset.get('bundle') else: provides = [] requires = [] bundle = asset filtered_requires = [] for req in requires: req_name, req_spec = split_namespec(req) if req_name in asset_versions: if asset_versions[req_name] not in req_spec: # The version asked for conflicts with a version # currently used. raise ValueError( "%s is not compatible with already requested " "version %s" % (req, asset_versions[req_name]) ) else: filtered_requires.append(req) # Get these requirements req_bundles = self._require_recursive(*filtered_requires) bundles.extend(req_bundles) # Save list of provided assets for provided in provides: if provided not in asset_versions: asset_versions[provided] = version for req_name, req_version, req_bundle in req_bundles: asset_versions[req_name] = req_version if bundle is not None: bundles.append((name, version, bundle)) else: raise AssetNotFound(namespec) return bundles
[docs] def require(self, *namespecs): """Return a bundle of the requested assets and their dependencies.""" blacklist = {n[1:] for n in namespecs if n.startswith('!')} not_blacklist = [n for n in namespecs if not n.startswith('!')] return Bundle( *[ bundle for name, version, bundle in self._require_recursive(*not_blacklist) if name not in blacklist ] )
class UglipyJS(Filter): """ Minifies Javascript using UgliPyJS, the Python wrapper for UglifyJS. """ name = 'uglipyjs' def setup(self): import uglipyjs self.uglipyjs = uglipyjs # skipcq: PYL-W0201 def output(self, _in, out, **kw): out.write(six.text_type(self.uglipyjs.compile(_in.read()), 'utf-8')) register_filter(UglipyJS)