Commit 19b53f70 by Timothée Peignier

clean up the mess in utils.py and add support for glob selection

parent 8b570fa2
from compress.packager import Packager
__all__ = ['packager']
packager = Packager()
import os
import re
import subprocess
import urlparse
from compress.conf import settings
from compress.utils import to_class
URL_DETECTOR = r'url\([\'"]?([^\s)]+\.[a-z]+)[\'"]?\)'
class Compressor(object):
def __init__(self, verbose=False):
self.verbose = verbose
def js_compressors(self):
return [to_class(compressor) for compressor in settings.COMPRESS_JS_COMPRESSORS]
js_compressors = property(js_compressors)
def css_compressors(self):
return [to_class(compressor) for compressor in settings.COMPRESS_CSS_COMPRESSORS]
css_compressors = property(css_compressors)
def compress_js(self, paths):
"""Concatenate and compress JS files"""
js = self.concatenate(paths)
for compressor in self.js_compressors:
js = getattr(compressor(verbose=self.verbose), 'compress_js')(js)
return js
def compress_css(self, paths):
"""Concatenate and compress CSS files"""
css = self.concatenate_and_rewrite(paths)
for compressor in self.css_compressors:
css = getattr(compressor(verbose=self.verbose), 'compress_css')(css)
return css
def concatenate_and_rewrite(self, paths):
"""Concatenate together files and rewrite urls"""
stylesheets = []
for path in paths:
def reconstruct(match):
asset_path = match.group(1)
asset_url = urlparse.urljoin(
settings.COMPRESS_URL,
self.construct_asset_path(asset_path, path)[1:]
)
return "url(%s)" % asset_url
content = self.read_file(path)
content = re.sub(URL_DETECTOR, reconstruct, content)
stylesheets.append(content)
return '\n'.join(stylesheets)
def concatenate(self, paths):
"""Concatenate together a list of files"""
return '\n'.join([self.read_file(path) for path in paths])
def construct_asset_path(self, asset_path, css_path):
public_path = self.absolute_path(asset_path, css_path)
if os.path.isabs(asset_path):
return asset_path
else:
return self.relative_path(public_path)
def absolute_path(self, asset_path, css_path):
if os.path.isabs(asset_path):
path = os.path.join(settings.COMPRESS_ROOT, asset_path)
else:
path = os.path.join(os.path.dirname(css_path), asset_path)
return os.path.normpath(path)
def relative_path(self, absolute_path):
compress_root = os.path.normpath(settings.COMPRESS_ROOT)
return os.path.join('../', absolute_path.replace(compress_root, ''))
def read_file(self, path):
"""Read file content in binary mode"""
f = open(path, 'rb')
content = f.read()
f.close()
return content
class CompressorBase(object):
def __init__(self, verbose):
self.verbose = verbose
def filter_css(self, css):
raise NotImplementedError
def filter_js(self, js):
raise NotImplementedError
class CompressorError(Exception):
"""This exception is raised when a filter fails"""
pass
class SubProcessCompressor(CompressorBase):
def execute_command(self, command, content):
pipe = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
stdin=subprocess.PIPE, stderr=subprocess.PIPE)
pipe.stdin.write(content)
pipe.stdin.close()
compressed_content = pipe.stdout.read()
pipe.stdout.close()
error = pipe.stderr.read()
pipe.stderr.close()
if pipe.wait() != 0:
if not error:
error = "Unable to apply %s compressor" % self.__class__.__name__
raise CompressorError(error)
if self.verbose:
print error
return compressed_content
from compress.conf import settings
from compress.filters import SubProcessFilter
from compress.compressors import SubProcessCompressor
class ClosureCompressorFilter(SubProcessFilter):
def filter_js(self, js):
class ClosureCompressor(SubProcessCompressor):
def compress_js(self, js):
command = '%s %s' % (settings.COMPRESS_CLOSURE_BINARY, settings.COMPRESS_CLOSURE_ARGUMENTS)
if self.verbose:
command += ' --verbose'
......
......@@ -4,7 +4,7 @@ import tempfile
from django.conf import settings
from compress.filters import FilterBase
from compress.compressors import CompressorBase
BINARY = getattr(settings, 'CSSTIDY_BINARY', 'csstidy')
ARGUMENTS = getattr(settings, 'CSSTIDY_ARGUMENTS', '--template=highest')
......@@ -12,8 +12,8 @@ ARGUMENTS = getattr(settings, 'CSSTIDY_ARGUMENTS', '--template=highest')
warnings.simplefilter('ignore', RuntimeWarning)
class CSSTidyFilter(FilterBase):
def filter_css(self, css):
class CSSTidyCompressor(CompressorBase):
def compress_css(self, css):
tmp_file = tempfile.NamedTemporaryFile(mode='w+b')
tmp_file.write(css)
tmp_file.flush()
......
from compress.compressors import CompressorBase
from compress.compressors.jsmin.jsmin import jsmin
class JSMinCompressor(CompressorBase):
def compress_js(self, js):
return jsmin(js)
from compress.conf import settings
from compress.filters import SubProcessFilter
from compress.compressors import SubProcessCompressor
class UglifyJSCompressorFilter(SubProcessFilter):
def filter_js(self, js):
class UglifyJSCompressor(SubProcessCompressor):
def compress_js(self, js):
command = '%s %s' % (settings.COMPRESS_UGLIFYJS_BINARY, settings.COMPRESS_UGLIFYJS_ARGUMENTS)
if self.verbose:
command += ' --verbose'
......
from compress.conf import settings
from compress.compressors import SubProcessCompressor
class YUICompressor(SubProcessCompressor):
def compress_common(self, content, type_, arguments):
command = '%s --type=%s %s' % (settings.COMPRESS_YUI_BINARY, type_, arguments)
if self.verbose:
command += ' --verbose'
return self.execute_command(command, content)
def compress_js(self, js):
return self.compress_common(js, 'js', settings.COMPRESS_YUI_JS_ARGUMENTS)
def compress_css(self, css):
return self.compress_common(css, 'css', settings.COMPRESS_YUI_CSS_ARGUMENTS)
......@@ -15,8 +15,12 @@ COMPRESS_VERSION_DEFAULT = getattr(settings, 'COMPRESS_VERSION_DEFAULT', '0')
COMPRESS_VERSION_REMOVE_OLD = getattr(settings, 'COMPRESS_VERSION_REMOVE_OLD', True)
COMPRESS_VERSIONING = getattr(settings, 'COMPRESS_VERSIONING', 'compress.versioning.mtime.MTimeVersioning')
COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', ['compress.filters.csstidy.CSSTidyFilter'])
COMPRESS_JS_FILTERS = getattr(settings, 'COMPRESS_JS_FILTERS', ['compress.filters.jsmin.JSMinFilter'])
COMPRESS_CSS_COMPRESSORS = getattr(settings, 'COMPRESS_CSS_COMPRESSORS', [
'compress.compressors.csstidy.YUICompressor']
)
COMPRESS_JS_COMPRESSORS = getattr(settings, 'COMPRESS_JS_COMPRESSORS', [
'compress.compressors.csstidy.YUICompressor']
)
COMPRESS_CSS = getattr(settings, 'COMPRESS_CSS', {})
COMPRESS_JS = getattr(settings, 'COMPRESS_JS', {})
......@@ -30,8 +34,8 @@ COMPRESS_CLOSURE_ARGUMENTS = getattr(settings, 'COMPRESS_CLOSURE_ARGUMENTS', '')
COMPRESS_UGLIFYJS_BINARY = getattr(settings, 'COMPRESS_UGLIFYJS_BINARY', '/usr/local/bin/uglifyjs')
COMPRESS_UGLIFYJS_ARGUMENTS = getattr(settings, 'COMPRESS_UGLIFYJS_ARGUMENTS', '-nc')
if COMPRESS_CSS_FILTERS is None:
COMPRESS_CSS_FILTERS = []
if COMPRESS_CSS_COMPRESSORS is None:
COMPRESS_CSS_COMPRESSORS = []
if COMPRESS_JS_FILTERS is None:
COMPRESS_JS_FILTERS = []
if COMPRESS_JS_COMPRESSORS is None:
COMPRESS_JS_COMPRESSORS = []
import subprocess
class FilterBase:
def __init__(self, verbose):
self.verbose = verbose
def filter_css(self, css):
raise NotImplementedError
def filter_js(self, js):
raise NotImplementedError
class FilterError(Exception):
"""This exception is raised when a filter fails"""
pass
class SubProcessFilter(FilterBase):
def execute_command(self, command, content):
pipe = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
stdin=subprocess.PIPE, stderr=subprocess.PIPE)
pipe.stdin.write(content)
pipe.stdin.close()
compressed_content = pipe.stdout.read()
pipe.stdout.close()
error = pipe.stderr.read()
pipe.stderr.close()
if pipe.wait() != 0:
if not error:
error = "Unable to apply %s filter" % self.__class__.__name__
raise FilterError(error)
if self.verbose:
print error
return compressed_content
from compress.filters.jsmin.jsmin import jsmin
from compress.filters import FilterBase
class JSMinFilter(FilterBase):
def filter_js(self, js):
return jsmin(js)
from compress.conf import settings
from compress.filters import SubProcessFilter
class YUICompressorFilter(SubProcessFilter):
def filter_common(self, content, type_, arguments):
command = '%s --type=%s %s' % (settings.COMPRESS_YUI_BINARY, type_, arguments)
if self.verbose:
command += ' --verbose'
return self.execute_command(command, content)
def filter_js(self, js):
return self.filter_common(js, 'js', settings.COMPRESS_YUI_JS_ARGUMENTS)
def filter_css(self, css):
return self.filter_common(css, 'css', settings.COMPRESS_YUI_CSS_ARGUMENTS)
from django.core.management.base import NoArgsCommand
from optparse import make_option
from django.conf import settings
from django.core.management.base import NoArgsCommand
class Command(NoArgsCommand):
option_list = NoArgsCommand.option_list + (
make_option('--force', action='store_true', default=False, help='Force update of all files, even if the source files are older than the current compressed file.'),
make_option('--force',
action='store_true',
default=False,
help='Force update of all files, even if the source files are older than the current compressed file.'
),
)
help = 'Updates and compresses CSS and JavsScript on-demand, without restarting Django'
help = 'Updates and compresses CSS and JS on-demand, without restarting Django'
args = ''
def handle_noargs(self, **options):
force = options.get('force', False)
verbosity = int(options.get('verbosity', 1))
from compress.utils import needs_update, filter_css, filter_js
for name, css in settings.COMPRESS_CSS.items():
u, version = needs_update(css['output_filename'],
css['source_filenames'])
if (force or u) or verbosity >= 2:
msg = 'CSS Group \'%s\'' % name
print msg
print len(msg) * '-'
print "Version: %s" % version
if force or u:
filter_css(css, verbosity)
if (force or u) or verbosity >= 2:
print
for name, js in settings.COMPRESS_JS.items():
if 'external_urls' in js:
u, version = False, "External"
else:
u, version = needs_update(js['output_filename'],
js['source_filenames'])
if (force or u) or verbosity >= 2:
msg = 'JavaScript Group \'%s\'' % name
print msg
print len(msg) * '-'
print "Version: %s" % version
if (force or u) and 'external_urls' not in js:
filter_js(js, verbosity)
if (force or u) or verbosity >= 2:
print
# Backwards compatibility for Django r9110
if not [opt for opt in Command.option_list if opt.dest == 'verbosity']:
Command.option_list += (
make_option('--verbosity', '-v', action="store", dest="verbosity",
default='1', type='choice', choices=['0', '1', '2'],
help="Verbosity level; 0=minimal output, 1=normal output, 2=all output"),
)
from compress import packager
packager.force = options.get('force', False)
packager.verbose = int(options.get('verbosity', 1)) >= 2
for package_name, package in packager.packages['css'].items():
if packager.verbose or packager.force:
message = "CSS Group '%s'" % package_name
print message
print len(message) * '-'
packager.pack_stylesheets(package)
print
for package_name, package in packager.packages['js'].items():
if packager.verbose or packager.force:
message = "JS Group '%s'" % package_name
print message
print len(message) * '-'
packager.pack_javascripts(package)
print
import glob
import os
import urlparse
from compress.conf import settings
from compress.compressors import Compressor
from compress.versioning import Versioning
from compress.signals import css_filtered, js_filtered
class Packager(object):
def __init__(self, force=False, verbose=False):
self.force = force
self.verbose = verbose
self.compressor = Compressor(verbose)
self.versioning = Versioning(verbose)
self.packages = {
'css': self.create_packages(settings.COMPRESS_CSS),
'js': self.create_packages(settings.COMPRESS_JS),
}
def package_for(self, kind, package_name):
try:
return self.packages[kind][package_name]
except KeyError:
raise PackageNotFound("No corresponding package for %s package name : %s"
% (kind, package_name)
)
def individual_url(self, filename):
print settings.COMPRESS_URL, filename
return urlparse.urljoin(settings.COMPRESS_URL, self.compressor.relative_path(filename)[1:])
def pack_stylesheets(self, package):
css = self.compressor.compress_css(package['paths'])
return self.pack(package, css, css_filtered)
def pack(self, package, content, signal):
if settings.COMPRESS_AUTO or self.force:
need_update, version = self.versioning.need_update(
package['output'], package['paths'])
if need_update or self.force:
output_filename = self.versioning.output_filename(package['output'],
self.versioning.version(package['paths']))
self.versioning.cleanup(package['output'])
if self.verbose or self.force:
print "Saving %s" % self.compressor.relative_path(output_filename)
self.save_file(output_filename, content)
signal.send(sender=self, package=package, version=version)
else:
filename_base, filename = os.path.split(package['output'])
version = self.versioning.version_from_file(filename_base, filename)
return self.versioning.output_filename(package['output'], version)
def pack_javascripts(self, package):
js = self.compressor.compress_js(package['paths'])
return self.pack(package, js, js_filtered)
def save_file(self, filename, content):
dirname = os.path.dirname(filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
fd = open(filename, 'wb+')
fd.write(content)
fd.close()
def create_packages(self, config):
packages = {}
if not config:
return packages
for name in config:
packages[name] = {}
paths = []
for path in config[name]['source_filenames']:
full_path = os.path.join(settings.COMPRESS_ROOT, path)
paths.extend([os.path.normpath(path) for path in glob.glob(full_path)])
packages[name]['paths'] = paths
packages[name]['output'] = config[name]['output_filename']
packages[name]['context'] = {}
if 'extra_context' in config[name]:
packages[name]['context'] = config[name]['extra_context']
if 'template_name' in config[name]:
packages[name]['template'] = config[name]['template_name']
if 'externals_urls' in config[name]:
packages[name]['externals'] = config[name]['externals_urls']
return packages
class PackageNotFound(Exception):
pass
from django.dispatch import Signal
css_filtered = Signal()
js_filtered = Signal()
css_filtered = Signal(providing_args=["package", "version"])
js_filtered = Signal(providing_args=["package", "version"])
import os
from django import template
from django.template.loader import render_to_string
from compress.conf import settings
from compress.utils import compress_root, compress_url, needs_update, filter_css, filter_js, get_output_filename, get_version_from_file
from compress.packager import Packager, PackageNotFound
register = template.Library()
# Preload CSS templates
css_templates = {}
for key in settings.COMPRESS_CSS:
settings.COMPRESS_CSS[key]['template'] = template.loader.get_template(settings.COMPRESS_CSS[key].get('template_name', 'compress/css.html'))
# Preload JS templates
js_templates = {}
for key in settings.COMPRESS_JS:
settings.COMPRESS_JS[key]['template'] = template.loader.get_template(settings.COMPRESS_JS[key].get('template_name', 'compress/js.html'))
def render_common(obj, filename, version):
if settings.COMPRESS:
filename = get_output_filename(filename, version)
context = template.Context(obj.get('extra_context', {}))
prefix = context.get('prefix', None)
if filename.startswith('http://'):
context['url'] = filename
else:
context['url'] = compress_url(filename, prefix)
return obj['template'].render(context)
def render_css(css, filename, version=None):
return render_common(css, filename, version)
def render_js(js, filename, version=None):
return render_common(js, filename, version)
class CompressedCSSNode(template.Node):
def __init__(self, name):
self.name = name
self.packager = Packager()
def render(self, context):
css_name = template.Variable(self.name).resolve(context)
package_name = template.Variable(self.name).resolve(context)
try:
css = settings.COMPRESS_CSS[css_name]
except KeyError:
package = self.packager.package_for('css', package_name)
except PackageNotFound:
return '' # fail silently, do not return anything if an invalid group is specified
if settings.COMPRESS:
version = None
if settings.COMPRESS_AUTO:
u, version = needs_update(css['output_filename'],
css['source_filenames'])
if u:
filter_css(css)
else:
filename_base, filename = os.path.split(css['output_filename'])
path_name = compress_root(filename_base)
version = get_version_from_file(path_name, filename)
return render_css(css, css['output_filename'], version)
compressed_path = self.packager.pack_stylesheets(package)
return self.render_css(package, compressed_path)
else:
# output source files
r = ''
for source_file in css['source_filenames']:
r += render_css(css, source_file)
return self.render_individual(package)
def render_css(self, package, path):
context = {}
if not 'template' in package:
package['template'] = "compress/css.html"
if not 'context' in package:
context = package['context']
context.update({
'url': self.packager.individual_url(path)
})
return render_to_string(package['template'], context)
return r
def render_individual(self, package):
tags = [self.render_css(package, path) for path in package['paths']]
return '\n'.join(tags)
class CompressedJSNode(template.Node):
def __init__(self, name):
self.name = name
self.packager = Packager()
def render(self, context):
js_name = template.Variable(self.name).resolve(context)
package_name = template.Variable(self.name).resolve(context)
try:
js = settings.COMPRESS_JS[js_name]
except KeyError:
package = self.packager.package_for('js', package_name)
except PackageNotFound:
return '' # fail silently, do not return anything if an invalid group is specified
if 'external_urls' in js:
r = ''
for url in js['external_urls']:
r += render_js(js, url)
return r
if 'externals' in package:
return '\n'.joins([self.render_external(package, url) for url in package['externals']])
if settings.COMPRESS:
version = None
if settings.COMPRESS_AUTO:
u, version = needs_update(js['output_filename'],
js['source_filenames'])
if u:
filter_js(js)
else:
filename_base, filename = os.path.split(js['output_filename'])
path_name = compress_root(filename_base)
version = get_version_from_file(path_name, filename) if settings.COMPRESS_VERSION else None
return render_js(js, js['output_filename'], version)
compressed_path = self.packager.pack_javascripts(package)
return self.render_js(package, compressed_path)
else:
# output source files
r = ''
for source_file in js['source_filenames']:
r += render_js(js, source_file)
return r
return self.render_individual(package)
def render_js(self, package, path):
context = {}
if not 'template' in package:
package['template'] = "compress/js.html"
if not 'context' in package:
context = package['context']
context.update({
'url': self.packager.individual_url(path)
})
return render_to_string(package['template'], context)
def render_external(self, package, url):
if not 'template' in package:
package['template'] = "compress/js.html"
return render_to_string(package['template'], {
'url': url
})
def render_individual(self, package):
tags = [self.render_js(package, js) for js in package['paths']]
return '\n'.join(tags)
#@register.tag
def compressed_css(parser, token):
try:
tag_name, name = token.split_contents()
......@@ -128,12 +95,10 @@ def compressed_css(parser, token):
compressed_css = register.tag(compressed_css)
#@register.tag
def compressed_js(parser, token):
try:
tag_name, name = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError, '%r requires exactly one argument: the name of a group in the COMPRESS_JS setting' % token.split_contents()[0]
return CompressedJSNode(name)
compressed_js = register.tag(compressed_js)
import os
import re
from django.utils import importlib
from django.conf import settings as django_settings
from django.utils.http import urlquote
from compress.conf import settings
from compress.signals import css_filtered, js_filtered
def get_class(class_string):
"""
Convert a string version of a function name to the callable object.
"""
if not hasattr(class_string, '__bases__'):
try:
class_string = class_string.encode('ascii')
mod_name, class_name = get_mod_func(class_string)
if class_name != '':
class_string = getattr(__import__(mod_name, {}, {}, ['']), class_name)
except (ImportError, AttributeError):
raise Exception('Failed to import filter %s' % class_string)
return class_string
def get_mod_func(callback):
"""
Converts 'django.views.news.stories.story_detail' to
('django.views.news.stories', 'story_detail')
"""
try:
dot = callback.rindex('.')
except ValueError:
return callback, ''
return callback[:dot], callback[dot + 1:]
def needs_update(output_file, source_files, verbosity=0):
"""
Scan the source files for changes and returns True if the output_file needs to be updated.
"""
version = get_version(source_files)
on = get_output_filename(output_file, version)
compressed_file_full = compress_root(on)
if not os.path.exists(compressed_file_full):
return True, version
update_needed = getattr(get_class(settings.COMPRESS_VERSIONING)(), 'needs_update')(output_file, source_files, version)
return update_needed
def compress_root(filename):
"""
Return the full path to ``filename``. ``filename`` is a relative path name in COMPRESS_ROOT
"""
return os.path.join(settings.COMPRESS_ROOT, filename)
def compress_source(filename):
"""
Return the full path to ``filename``. ``filename`` is a relative path name in COMPRESS_SOURCE
"""
return os.path.join(settings.COMPRESS_SOURCE, filename)
def compress_url(url, prefix=None):
if prefix:
return prefix + urlquote(url)
return settings.COMPRESS_URL + urlquote(url)
def concat(filenames, separator=''):
"""
Concatenate the files from the list of the ``filenames``, output separated with ``separator``.
"""
# find relative paths in css:
# url definition, any spacing, single or double quotes, no starting slash
rel_exp = re.compile(
'(url\s*\(\s*[\'"]?\s*)([^/\'"\s]\S+?)(\s*[\'"]?\s*\))',
flags=re.IGNORECASE)
r = ''
for filename in filenames:
fd = open(compress_source(filename), 'rb')
contents = fd.read()
fd.close()
if filename.lower().endswith('.css') and \
django_settings.COMPRESS_ROOT == settings.COMPRESS_SOURCE:
if django_settings.COMPRESS_URL.endswith('/'):
abspath = os.path.normpath(os.path.dirname(filename))
else:
abspath = os.path.normpath(os.path.join('/',
os.path.dirname(filename)))
abspath = django_settings.COMPRESS_URL + abspath + '/'
contents = rel_exp.sub('\\1' + abspath + '\\2\\3', contents)
r += contents
r += separator
return r
def save_file(filename, contents):
dirname = os.path.dirname(compress_root(filename))
if not os.path.exists(dirname):
os.makedirs(dirname)
fd = open(compress_root(filename), 'wb+')
fd.write(contents)
fd.close()
def get_output_filename(filename, version):
if settings.COMPRESS_VERSION and version is not None:
return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, version)
else:
return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, settings.COMPRESS_VERSION_DEFAULT)
def get_version(source_files, verbosity=0):
version = getattr(get_class(settings.COMPRESS_VERSIONING)(), 'get_version')(source_files)
return version
def get_version_from_file(path, filename):
regex = re.compile(r'^%s$' % (get_output_filename(settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)]), r'([A-Za-z0-9]+)')))
results = []
for f in sorted(os.listdir(path), reverse=True):
result = regex.match(f)
if result and result.groups():
results.append(result.groups()[0])
results.sort()
return results[-1]
def remove_files(path, filename, verbosity=0):
regex = re.compile(r'^%s$' % (os.path.basename(get_output_filename(settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)]), r'[A-Za-z0-9]+'))))
if os.path.exists(path):
for f in os.listdir(path):
if regex.match(f):
if verbosity >= 1:
print "Removing outdated file %s" % f
os.unlink(os.path.join(path, f))
def filter_common(obj, verbosity, filters, attr, separator, signal):
output = concat(obj['source_filenames'], separator)
filename = get_output_filename(obj['output_filename'], get_version(obj['source_filenames']))
if settings.COMPRESS_VERSION and settings.COMPRESS_VERSION_REMOVE_OLD:
remove_files(os.path.dirname(compress_root(filename)), obj['output_filename'], verbosity)
if verbosity >= 1:
print "Saving %s" % filename
for f in filters:
output = getattr(get_class(f)(verbose=(verbosity >= 2)), attr)(output)
save_file(filename, output)
signal.send(None)
def filter_css(css, verbosity=0):
return filter_common(css, verbosity, filters=settings.COMPRESS_CSS_FILTERS, attr='filter_css', separator='', signal=css_filtered)
def filter_js(js, verbosity=0):
return filter_common(js, verbosity, filters=settings.COMPRESS_JS_FILTERS, attr='filter_js', separator='', signal=js_filtered)
def to_class(class_str):
module_bits = class_str.split('.')
module_path, class_name = '.'.join(module_bits[:-1]), module_bits[-1]
module = importlib.import_module(module_path)
return getattr(module, class_name, None)
import os
import re
from compress.conf import settings
from compress.utils import to_class
class Versioning(object):
def __init__(self, verbose=False):
self.verbose = verbose
def versionner(self):
return to_class(settings.COMPRESS_VERSIONING)(self)
versionner = property(versionner)
def version(self, paths):
return getattr(self.versionner, 'version')(paths)
def version_from_file(self, path, filename):
filename = settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)])
regex = re.compile(r'^%s$' % self.output_filename(filename, r'([A-Za-z0-9]+)'))
versions = []
for f in sorted(os.listdir(path), reverse=True):
version = regex.match(f)
if version and version.groups():
versions.append(version.group(1))
versions.sort()
return versions[-1]
def output_filename(self, filename, version):
if settings.COMPRESS_VERSION and version is not None:
output_filename = filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER,
version)
else:
output_filename = filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER,
settings.COMPRESS_VERSION_DEFAULT)
output_path = os.path.join(settings.COMPRESS_ROOT, output_filename)
return os.path.normpath(output_path)
def relative_path(self, filename):
return os.path.join(settings.COMPRESS_ROOT, filename)
def need_update(self, output_file, paths):
version = self.version(paths)
output_file = self.output_filename(output_file, version)
if not os.path.exists(self.relative_path(output_file)):
return True, version
return getattr(self.versionner, 'need_update')(output_file, paths, version)
def cleanup(self, filename):
if not settings.COMPRESS_VERSION and not settings.COMPRESS_VERSION_REMOVE_OLD:
return # Nothing to delete here
filename = settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)])
path = os.path.dirname(filename)
regex = re.compile(r'^%s$' % os.path.basename(self.output_filename(filename, r'([A-Za-z0-9]+)')))
if os.path.exists(path):
for f in os.listdir(path):
if regex.match(f):
if self.verbose:
print "Removing outdated file %s" % f
os.unlink(os.path.join(path, f))
class VersioningBase(object):
def get_version(self, source_files):
def __init__(self, versioning):
self.versioning = versioning
def output_filename(self, filename, version):
return self.versioning.output_filename(filename, version)
def version(self, source_files):
raise NotImplementedError
def needs_update(self, output_file, source_files, version):
......
from compress.conf import settings
from compress.utils import get_output_filename, compress_source
from compress.versioning import VersioningBase, VersioningError
from django.utils.hashcompat import sha_constructor
......@@ -11,16 +10,15 @@ except ImportError:
class GitVersioningBase(VersioningBase):
def needs_update(self, output_file, source_files, version):
output_file_name = get_output_filename(output_file, version)
ph = settings.COMPRESS_VERSION_PLACEHOLDER
of = output_file
def need_update(self, output_file, paths, version):
output_file_name = self.output_filename(output_file, version)
placeholder = settings.COMPRESS_VERSION_PLACEHOLDER
try:
phi = of.index(ph)
old_version = output_file_name[phi:phi + len(ph) - len(of)]
placeholder_index = output_file.index(placeholder)
old_version = output_file_name[placeholder_index:placeholder_index + len(placeholder) - len(output_file)]
return (version != old_version), version
except ValueError:
# no placeholder found, do not update, manual update if needed
# No placeholder found, do not update, manual update if needed
return False, version
def hexdigest(self, plaintext):
......@@ -31,12 +29,12 @@ class GitRevVersioning(GitVersioningBase):
"""
Version as hash of revision of all files in sources_files list.
"""
def get_version(self, source_files):
repo = git.Repo(compress_source(source_files[0]))
def version(self, paths):
repo = git.Repo(paths[0])
kwargs = {'max_count': 1}
commit_revs = []
for f in source_files:
commit = [i for i in repo.iter_commits(paths=compress_source(f), **kwargs)][0]
for f in paths:
commit = [i for i in repo.iter_commits(paths=f, **kwargs)][0]
commit_revs.append(commit.name_rev)
return self.hexdigest(', '.join(commit_revs))[0:16]
......@@ -45,7 +43,7 @@ class GitHeadRevVersioning(GitVersioningBase):
"""
Version as hash of latest revision in HEAD. Assumes all sources_files in same git repo.
"""
def get_version(self, source_files):
f = source_files[0]
repo = git.Repo(compress_source(f))
def version(self, paths):
f = paths[0]
repo = git.Repo(f)
return self.hexdigest(repo.head.commit.name_rev)[0:16]
......@@ -2,7 +2,6 @@ import cStringIO
from hashlib import md5, sha1
from compress.conf import settings
from compress.utils import concat, get_output_filename
from compress.versioning import VersioningBase
......@@ -10,20 +9,30 @@ class HashVersioningBase(VersioningBase):
def __init__(self, hash_method):
self.hash_method = hash_method
def needs_update(self, output_file, source_files, version):
output_file_name = get_output_filename(output_file, version)
ph = settings.COMPRESS_VERSION_PLACEHOLDER
of = output_file
def need_update(self, output_file, paths, version):
output_file_name = self.output_filename(output_file, version)
placeholder = settings.COMPRESS_VERSION_PLACEHOLDER
try:
phi = of.index(ph)
old_version = output_file_name[phi:phi + len(ph) - len(of)]
placeholder_index = output_file.index(placeholder)
old_version = output_file_name[placeholder_index:placeholder_index + len(placeholder) - len(output_file)]
return (version != old_version), version
except ValueError:
# no placeholder found, do not update, manual update if needed
return False, version
def get_version(self, source_files):
buf = concat(source_files)
def concatenate(self, paths):
"""Concatenate together a list of files"""
return '\n'.join([self.read_file(path) for path in paths])
def read_file(self, path):
"""Read file content in binary mode"""
f = open(path, 'rb')
content = f.read()
f.close()
return content
def version(self, paths):
buf = self.concatenate(paths)
s = cStringIO.StringIO(buf)
version = self.get_hash(s)
s.close()
......
import os
from compress.utils import get_output_filename, compress_source, compress_root
from compress.versioning import VersioningBase
class MTimeVersioning(VersioningBase):
def get_version(self, source_files):
def version(self, paths):
# Return the modification time for the newest source file
return str(max(
[int(os.stat(compress_source(f)).st_mtime) for f in source_files]
[int(os.stat(path).st_mtime) for path in paths]
))
def needs_update(self, output_file, source_files, version):
output_file_name = get_output_filename(output_file, version)
compressed_file_full = compress_root(output_file_name)
return (int(os.stat(compressed_file_full).st_mtime) < int(version)), version
def need_update(self, output_file, paths, version):
output_filename = self.output_filename(output_file, version)
return (int(os.stat(output_filename).st_mtime) < int(version)), version
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment