Adopted AppSettings and moved a few things from utils to the cache module where they belong.

This commit is contained in:
Jannis Leidel
2011-02-09 04:51:36 +01:00
parent fe840cede0
commit 4b916124a2
23 changed files with 279 additions and 281 deletions

View File

@@ -1,13 +1,13 @@
import os import os
from django.conf import settings as django_settings
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from compressor.conf import settings
from compressor import filters from compressor import filters
from compressor.cache import get_hexdigest, get_mtime
from compressor.conf import settings
from compressor.exceptions import UncompressableFileError from compressor.exceptions import UncompressableFileError
from compressor.utils import get_hexdigest, get_mtime, get_class from compressor.utils import get_class
class Compressor(object): class Compressor(object):
@@ -25,12 +25,12 @@ class Compressor(object):
try: try:
base_url = self.storage.base_url base_url = self.storage.base_url
except AttributeError: except AttributeError:
base_url = settings.URL base_url = settings.COMPRESS_URL
if not url.startswith(base_url): if not url.startswith(base_url):
raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, base_url)) raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, base_url))
basename = url.replace(base_url, "", 1) basename = url.replace(base_url, "", 1)
filename = os.path.join(settings.ROOT, basename) filename = os.path.join(settings.COMPRESS_ROOT, basename)
if not os.path.exists(filename): if not os.path.exists(filename):
raise UncompressableFileError('"%s" does not exist' % filename) raise UncompressableFileError('"%s" does not exist' % filename)
return filename return filename
@@ -38,7 +38,7 @@ class Compressor(object):
def _get_parser(self): def _get_parser(self):
if self._parser: if self._parser:
return self._parser return self._parser
parser_cls = get_class(settings.PARSER) parser_cls = get_class(settings.COMPRESS_PARSER)
self._parser = parser_cls(self.content) self._parser = parser_cls(self.content)
return self._parser return self._parser
@@ -54,7 +54,7 @@ class Compressor(object):
def cachekey(self): def cachekey(self):
cachebits = [self.content] cachebits = [self.content]
cachebits.extend([str(m) for m in self.mtimes]) cachebits.extend([str(m) for m in self.mtimes])
cachestr = "".join(cachebits).encode(django_settings.DEFAULT_CHARSET) cachestr = "".join(cachebits).encode(settings.DEFAULT_CHARSET)
return "django_compressor.%s" % get_hexdigest(cachestr)[:12] return "django_compressor.%s" % get_hexdigest(cachestr)[:12]
@property @property
@@ -82,7 +82,7 @@ class Compressor(object):
input = fd.read() input = fd.read()
if self.filters: if self.filters:
input = self.filter(input, 'input', filename=v, elem=elem) input = self.filter(input, 'input', filename=v, elem=elem)
charset = attribs.get('charset', django_settings.DEFAULT_CHARSET) charset = attribs.get('charset', settings.DEFAULT_CHARSET)
self._hunks.append(unicode(input, charset)) self._hunks.append(unicode(input, charset))
fd.close() fd.close()
return self._hunks return self._hunks
@@ -91,7 +91,7 @@ class Compressor(object):
# Design decision needed: either everything should be unicode up to # Design decision needed: either everything should be unicode up to
# here or we encode strings as soon as we acquire them. Currently # here or we encode strings as soon as we acquire them. Currently
# concat() expects all hunks to be unicode and does the encoding # concat() expects all hunks to be unicode and does the encoding
return "\n".join([hunk.encode(django_settings.DEFAULT_CHARSET) for hunk in self.hunks]) return "\n".join([hunk.encode(settings.DEFAULT_CHARSET) for hunk in self.hunks])
def filter(self, content, method, **kwargs): def filter(self, content, method, **kwargs):
for f in self.filters: for f in self.filters:
@@ -121,7 +121,7 @@ class Compressor(object):
def new_filepath(self): def new_filepath(self):
filename = "".join([self.hash, self.extension]) filename = "".join([self.hash, self.extension])
return os.path.join( return os.path.join(
settings.OUTPUT_DIR.strip(os.sep), self.output_prefix, filename) settings.COMPRESS_OUTPUT_DIR.strip(os.sep), self.output_prefix, filename)
def save_file(self): def save_file(self):
if self.storage.exists(self.new_filepath): if self.storage.exists(self.new_filepath):
@@ -130,7 +130,7 @@ class Compressor(object):
return True return True
def output(self): def output(self):
if not settings.ENABLED: if not settings.COMPRESS_ENABLED:
return self.content return self.content
self.save_file() self.save_file()
context = getattr(self, 'extra_context', {}) context = getattr(self, 'extra_context', {})
@@ -138,7 +138,7 @@ class Compressor(object):
return render_to_string(self.template_name, context) return render_to_string(self.template_name, context)
def output_inline(self): def output_inline(self):
context = {'content': settings.ENABLED and self.combined or self.concat()} context = {'content': settings.COMPRESS_ENABLED and self.combined or self.concat()}
if hasattr(self, 'extra_context'): if hasattr(self, 'extra_context'):
context.update(self.extra_context) context.update(self.extra_context)
return render_to_string(self.template_name_inline, context) return render_to_string(self.template_name_inline, context)

View File

@@ -1,5 +1,35 @@
import os
from django.core.cache import get_cache from django.core.cache import get_cache
from django.utils.encoding import smart_str
from django.utils.hashcompat import sha_constructor
from compressor.conf import settings from compressor.conf import settings
cache = get_cache(settings.CACHE_BACKEND) def get_hexdigest(plaintext):
return sha_constructor(plaintext).hexdigest()
def get_mtime_cachekey(filename):
return "django_compressor.mtime.%s" % filename
def get_offline_cachekey(source):
return ("django_compressor.offline.%s"
% get_hexdigest("".join(smart_str(s) for s in source)))
def get_mtime(filename):
if settings.COMPRESS_MTIME_DELAY:
key = get_mtime_cachekey(filename)
mtime = cache.get(key)
if mtime is None:
mtime = os.path.getmtime(filename)
cache.set(key, mtime, settings.COMPRESS_MTIME_DELAY)
return mtime
return os.path.getmtime(filename)
def get_hashed_mtime(filename, length=12):
filename = os.path.realpath(filename)
mtime = str(int(get_mtime(filename)))
return get_hexdigest(mtime)[:length]
cache = get_cache(settings.COMPRESS_CACHE_BACKEND)

3
compressor/conf.py Normal file
View File

@@ -0,0 +1,3 @@
from compressor.settings import CompressorSettings
settings = CompressorSettings(prefix="COMPRESS")

View File

@@ -1,119 +0,0 @@
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
# Main switch
ENABLED = getattr(settings, 'COMPRESS', not settings.DEBUG)
# Uses the 1.3 STATIC_URL setting by default
URL = getattr(settings, 'COMPRESS_URL', getattr(settings, 'STATIC_URL', None))
# Check for emptyness since STATIC_URL can be None and ''
if URL:
# Then on to extensive testing
ROOT = getattr(settings, 'COMPRESS_ROOT', getattr(settings, 'STATIC_ROOT', None))
if not ROOT:
raise ImproperlyConfigured('The COMPRESS_ROOT setting (or its '
'fallback STATIC_ROOT) must be set.')
# In case staticfiles is used, make sure COMPRESS_URL can be used
# by checking if the the FileSystemFinder is installed, and if is
# checking if COMPRESS_ROOT is in STATICFILES_DIRS to allow finding
# compressed files.
if ("staticfiles" in settings.INSTALLED_APPS or
"django.contrib.staticfiles" in settings.INSTALLED_APPS):
try:
from staticfiles.settings import FINDERS as finders
except ImportError:
finders = []
if not finders:
finders = getattr(settings, 'STATICFILES_FINDERS', [])
if ("django.contrib.staticfiles.finders.FileSystemFinder" not in finders and
"staticfiles.finders.FileSystemFinder" not in finders):
raise ImproperlyConfigured('Please enable the FileSystemFinder '
'finder of the staticfiles app to '
'use it with django_compressor.')
abs_paths = []
for path in getattr(settings, 'STATICFILES_DIRS', []):
if isinstance(path, tuple) or isinstance(path, list): # stupid Python 2.4
path = path[1] # in case the STATICFILES_DIRS setting has a prefix
abs_paths.append(os.path.abspath(path))
if os.path.abspath(ROOT) not in abs_paths:
raise ImproperlyConfigured('Please add COMPRESS_ROOT to the '
'STATICFILES_DIRS setting when using the '
'staticfiles app.')
else:
# Fallback to good ol' time of double meaning
URL, ROOT = settings.MEDIA_URL, settings.MEDIA_ROOT
if not URL.endswith('/'):
raise ImproperlyConfigured('The URL settings (e.g. COMPRESS_URL) '
'must have a trailing slash.')
OUTPUT_DIR = getattr(settings, 'COMPRESS_OUTPUT_DIR', 'cache')
STORAGE = getattr(settings, 'COMPRESS_STORAGE', 'compressor.storage.CompressorFileStorage')
CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', ['compressor.filters.css_default.CssAbsoluteFilter'])
JS_FILTERS = getattr(settings, 'COMPRESS_JS_FILTERS', ['compressor.filters.jsmin.JSMinFilter'])
if CSS_FILTERS is None:
CSS_FILTERS = []
if JS_FILTERS is None:
JS_FILTERS = []
LESSC_BINARY = LESSC_BINARY = getattr(settings, 'COMPRESS_LESSC_BINARY', 'lessc')
CLOSURE_COMPILER_BINARY = getattr(settings, 'COMPRESS_CLOSURE_COMPILER_BINARY', 'java -jar compiler.jar')
CLOSURE_COMPILER_ARGUMENTS = getattr(settings, 'COMPRESS_CLOSURE_COMPILER_ARGUMENTS', '')
CSSTIDY_BINARY = getattr(settings, 'CSSTIDY_BINARY',
getattr(settings, 'COMPRESS_CSSTIDY_BINARY', 'csstidy'))
CSSTIDY_ARGUMENTS = getattr(settings, 'CSSTIDY_ARGUMENTS',
getattr(settings, 'COMPRESS_CSSTIDY_ARGUMENTS', '--template=highest'))
YUI_BINARY = getattr(settings, 'COMPRESS_YUI_BINARY', 'java -jar yuicompressor.jar')
YUI_CSS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_CSS_ARGUMENTS', '')
YUI_JS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_JS_ARGUMENTS', '')
DATA_URI_MIN_SIZE = getattr(settings, 'COMPRESS_DATA_URI_MIN_SIZE', 1024)
# rebuilds the cache every 30 days if nothing has changed.
REBUILD_TIMEOUT = getattr(settings, 'COMPRESS_REBUILD_TIMEOUT', 60 * 60 * 24 * 30) # 30 days
# the upper bound on how long any compression should take to be generated
# (used against dog piling, should be a lot smaller than REBUILD_TIMEOUT
MINT_DELAY = getattr(settings, 'COMPRESS_MINT_DELAY', 30) # 30 seconds
# check for file changes only after a delay (in seconds, disabled by default)
MTIME_DELAY = getattr(settings, 'COMPRESS_MTIME_DELAY', None)
# the backend to use when parsing the JavaScript or Stylesheet files
PARSER = getattr(settings, 'COMPRESS_PARSER', 'compressor.parser.BeautifulSoupParser')
# Allows changing verbosity from the settings.
VERBOSE = getattr(settings, "COMPRESS_VERBOSE", False)
# the cache backend to use
CACHE_BACKEND = getattr(settings, 'COMPRESS_CACHE_BACKEND', None)
if CACHE_BACKEND is None:
# If we are on Django 1.3 AND using the new CACHES setting...
if getattr(settings, "CACHES", None):
CACHE_BACKEND = "default"
else:
# fallback for people still using the old CACHE_BACKEND setting
CACHE_BACKEND = settings.CACHE_BACKEND
# enables the offline cache -- a cache that is filled by the compress management command
OFFLINE = getattr(settings, 'COMPRESS_OFFLINE', False)
# invalidates the offline cache after one year
OFFLINE_TIMEOUT = getattr(settings, 'COMPRESS_OFFLINE_TIMEOUT', 60 * 60 * 24 * 365) # 1 year
# The context to be used when compressing the files "offline"
OFFLINE_CONTEXT = getattr(settings, 'COMPRESS_OFFLINE_CONTEXT', {})
if not OFFLINE_CONTEXT:
OFFLINE_CONTEXT = {
'MEDIA_URL': settings.MEDIA_URL,
}
# Adds the 1.3 STATIC_URL setting to the context if available
if getattr(settings, 'STATIC_URL', None):
OFFLINE_CONTEXT['STATIC_URL'] = settings.STATIC_URL

View File

@@ -1,5 +1,3 @@
from django.conf import settings as django_settings
from compressor.conf import settings from compressor.conf import settings
from compressor.base import Compressor from compressor.base import Compressor
from compressor.exceptions import UncompressableFileError from compressor.exceptions import UncompressableFileError
@@ -11,7 +9,7 @@ class CssCompressor(Compressor):
self.extension = ".css" self.extension = ".css"
self.template_name = "compressor/css.html" self.template_name = "compressor/css.html"
self.template_name_inline = "compressor/css_inline.html" self.template_name_inline = "compressor/css_inline.html"
self.filters = list(settings.CSS_FILTERS) self.filters = list(settings.COMPRESS_CSS_FILTERS)
self.type = 'css' self.type = 'css'
def split_contents(self): def split_contents(self):
@@ -27,7 +25,7 @@ class CssCompressor(Compressor):
content = self.parser.elem_content(elem) content = self.parser.elem_content(elem)
data = ('file', self.get_filename(elem_attribs['href']), elem) data = ('file', self.get_filename(elem_attribs['href']), elem)
except UncompressableFileError: except UncompressableFileError:
if django_settings.DEBUG: if settings.DEBUG:
raise raise
elif elem_name == 'style': elif elem_name == 'style':
data = ('hunk', self.parser.elem_content(elem), elem) data = ('hunk', self.parser.elem_content(elem), elem)
@@ -48,7 +46,7 @@ class CssCompressor(Compressor):
self.split_contents() self.split_contents()
if not hasattr(self, 'media_nodes'): if not hasattr(self, 'media_nodes'):
return super(CssCompressor, self).output() return super(CssCompressor, self).output()
if not settings.ENABLED: if not settings.COMPRESS_ENABLED:
return self.content return self.content
ret = [] ret = []
for media, subnode in self.media_nodes: for media, subnode in self.media_nodes:

View File

@@ -1,12 +1,11 @@
from compressor.exceptions import FilterError
from compressor.utils import get_class, get_mod_func
from compressor.conf import settings from compressor.conf import settings
from compressor.exceptions import FilterError
class FilterBase(object): class FilterBase(object):
def __init__(self, content, filter_type=None, verbose=0): def __init__(self, content, filter_type=None, verbose=0):
self.type = filter_type self.type = filter_type
self.content = content self.content = content
self.verbose = verbose or settings.VERBOSE self.verbose = verbose or settings.COMPRESS_VERBOSE
def input(self, **kwargs): def input(self, **kwargs):
raise NotImplementedError raise NotImplementedError

View File

@@ -8,9 +8,9 @@ from compressor.utils import cmd_split
class ClosureCompilerFilter(FilterBase): class ClosureCompilerFilter(FilterBase):
def output(self, **kwargs): def output(self, **kwargs):
arguments = settings.CLOSURE_COMPILER_ARGUMENTS arguments = settings.COMPRESS_CLOSURE_COMPILER_ARGUMENTS
command = '%s %s' % (settings.CLOSURE_COMPILER_BINARY, arguments) command = '%s %s' % (settings.COMPRESS_CLOSURE_COMPILER_BINARY, arguments)
try: try:
p = Popen(cmd_split(command), stdout=PIPE, stdin=PIPE, stderr=PIPE) p = Popen(cmd_split(command), stdout=PIPE, stdin=PIPE, stderr=PIPE)

View File

@@ -2,23 +2,23 @@ import os
import re import re
import posixpath import posixpath
from compressor.filters import FilterBase, FilterError from compressor.cache import get_hexdigest, get_mtime
from compressor.conf import settings from compressor.conf import settings
from compressor.utils import get_hexdigest, get_mtime from compressor.filters import FilterBase
URL_PATTERN = re.compile(r'url\(([^\)]+)\)') URL_PATTERN = re.compile(r'url\(([^\)]+)\)')
class CssAbsoluteFilter(FilterBase): class CssAbsoluteFilter(FilterBase):
def input(self, filename=None, **kwargs): def input(self, filename=None, **kwargs):
media_root = os.path.normcase(os.path.abspath(settings.ROOT)) media_root = os.path.normcase(os.path.abspath(settings.COMPRESS_ROOT))
if filename is not None: if filename is not None:
filename = os.path.normcase(os.path.abspath(filename)) filename = os.path.normcase(os.path.abspath(filename))
if not filename or not filename.startswith(media_root): if not filename or not filename.startswith(media_root):
return self.content return self.content
self.media_path = filename[len(media_root):].replace(os.sep, '/') self.media_path = filename[len(media_root):].replace(os.sep, '/')
self.media_path = self.media_path.lstrip('/') self.media_path = self.media_path.lstrip('/')
self.media_url = settings.URL.rstrip('/') self.media_url = settings.COMPRESS_URL.rstrip('/')
try: try:
mtime = get_mtime(filename) mtime = get_mtime(filename)
self.mtime = get_hexdigest(str(int(mtime)))[:12] self.mtime = get_hexdigest(str(int(mtime)))[:12]

View File

@@ -1,4 +1,4 @@
from compressor.filters import FilterBase, FilterError from compressor.filters import FilterBase
from compressor.filters.cssmin.cssmin import cssmin from compressor.filters.cssmin.cssmin import cssmin
class CSSMinFilter(FilterBase): class CSSMinFilter(FilterBase):

View File

@@ -16,7 +16,7 @@ class CSSTidyFilter(FilterBase):
output_file = tempfile.NamedTemporaryFile(mode='w+b') output_file = tempfile.NamedTemporaryFile(mode='w+b')
command = '%s %s %s %s' % (settings.CSSTIDY_BINARY, tmp_file.name, settings.CSSTIDY_ARGUMENTS, output_file.name) command = '%s %s %s %s' % (settings.COMPRESS_CSSTIDY_BINARY, tmp_file.name, settings.COMPRESS_CSSTIDY_ARGUMENTS, output_file.name)
command_output = Popen(command, shell=True, command_output = Popen(command, shell=True,
stdout=PIPE, stdin=PIPE, stderr=PIPE).communicate() stdout=PIPE, stdin=PIPE, stderr=PIPE).communicate()

View File

@@ -1,11 +1,10 @@
import os import os
import re import re
import mimetypes import mimetypes
import urlparse
from base64 import b64encode from base64 import b64encode
from compressor.filters import FilterBase
from compressor.conf import settings from compressor.conf import settings
from compressor.filters import FilterBase
class DataUriFilter(FilterBase): class DataUriFilter(FilterBase):
"""Filter for embedding media as data: URIs. """Filter for embedding media as data: URIs.
@@ -18,7 +17,7 @@ class DataUriFilter(FilterBase):
Don't use this class directly. Use a subclass. Don't use this class directly. Use a subclass.
""" """
def input(self, filename=None, **kwargs): def input(self, filename=None, **kwargs):
if not filename or not filename.startswith(settings.ROOT): if not filename or not filename.startswith(settings.COMPRESS_ROOT):
return self.content return self.content
output = self.content output = self.content
for url_pattern in self.url_patterns: for url_pattern in self.url_patterns:
@@ -29,13 +28,13 @@ class DataUriFilter(FilterBase):
# strip query string of file paths # strip query string of file paths
if "?" in url: if "?" in url:
url = url.split("?")[0] url = url.split("?")[0]
return os.path.join(settings.ROOT, url[len(settings.URL):]) return os.path.join(settings.COMPRESS_ROOT, url[len(settings.COMPRESS_URL):])
def data_uri_converter(self, matchobj): def data_uri_converter(self, matchobj):
url = matchobj.group(1).strip(' \'"') url = matchobj.group(1).strip(' \'"')
if not url.startswith('data:'): if not url.startswith('data:'):
path = self.get_file_path(url) path = self.get_file_path(url)
if os.stat(path).st_size <= settings.DATA_URI_MIN_SIZE: if os.stat(path).st_size <= settings.COMPRESS_DATA_URI_MIN_SIZE:
data = b64encode(open(path, 'rb').read()) data = b64encode(open(path, 'rb').read())
return 'url("data:%s;base64,%s")' % (mimetypes.guess_type(path)[0], data) return 'url("data:%s;base64,%s")' % (mimetypes.guess_type(path)[0], data)
return 'url("%s")' % url return 'url("%s")' % url

View File

@@ -3,7 +3,7 @@ import warnings
import tempfile import tempfile
from compressor.conf import settings from compressor.conf import settings
from compressor.filters import FilterBase, FilterError from compressor.filters import FilterBase
warnings.simplefilter('ignore', RuntimeWarning) warnings.simplefilter('ignore', RuntimeWarning)
@@ -17,7 +17,7 @@ class LessFilter(FilterBase):
output_file = tempfile.NamedTemporaryFile(mode='w+b') output_file = tempfile.NamedTemporaryFile(mode='w+b')
command = '%s %s %s' % (settings.LESSC_BINARY, tmp_file.name, output_file.name) command = '%s %s %s' % (settings.COMPRESS_LESSC_BINARY, tmp_file.name, output_file.name)
command_output = os.popen(command).read() command_output = os.popen(command).read()

View File

@@ -10,11 +10,11 @@ class YUICompressorFilter(FilterBase):
def output(self, **kwargs): def output(self, **kwargs):
arguments = '' arguments = ''
if self.type == 'js': if self.type == 'js':
arguments = settings.YUI_JS_ARGUMENTS arguments = settings.COMPRESS_YUI_JS_ARGUMENTS
if self.type == 'css': if self.type == 'css':
arguments = settings.YUI_CSS_ARGUMENTS arguments = settings.COMPRESS_YUI_CSS_ARGUMENTS
command = '%s --type=%s %s' % (settings.YUI_BINARY, self.type, arguments) command = '%s --type=%s %s' % (settings.COMPRESS_YUI_BINARY, self.type, arguments)
if self.verbose: if self.verbose:
command += ' --verbose' command += ' --verbose'

View File

@@ -1,5 +1,3 @@
from django.conf import settings as django_settings
from compressor.conf import settings from compressor.conf import settings
from compressor.base import Compressor from compressor.base import Compressor
from compressor.exceptions import UncompressableFileError from compressor.exceptions import UncompressableFileError
@@ -12,7 +10,7 @@ class JsCompressor(Compressor):
self.extension = ".js" self.extension = ".js"
self.template_name = "compressor/js.html" self.template_name = "compressor/js.html"
self.template_name_inline = "compressor/js_inline.html" self.template_name_inline = "compressor/js_inline.html"
self.filters = settings.JS_FILTERS self.filters = settings.COMPRESS_JS_FILTERS
self.type = 'js' self.type = 'js'
def split_contents(self): def split_contents(self):
@@ -24,7 +22,7 @@ class JsCompressor(Compressor):
try: try:
self.split_content.append(('file', self.get_filename(attribs['src']), elem)) self.split_content.append(('file', self.get_filename(attribs['src']), elem))
except UncompressableFileError: except UncompressableFileError:
if django_settings.DEBUG: if settings.DEBUG:
raise raise
else: else:
content = self.parser.elem_content(elem) content = self.parser.elem_content(elem)

View File

@@ -9,16 +9,15 @@ try:
except ImportError: except ImportError:
from StringIO import StringIO from StringIO import StringIO
from django.conf import settings as django_settings
from django.core.management.base import NoArgsCommand, CommandError from django.core.management.base import NoArgsCommand, CommandError
from django.template import Context, Template, TemplateDoesNotExist, TemplateSyntaxError from django.template import Context, Template, TemplateDoesNotExist, TemplateSyntaxError
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from compressor.cache import cache from compressor.cache import cache, get_offline_cachekey
from compressor.conf import settings from compressor.conf import settings
from compressor.exceptions import OfflineGenerationError from compressor.exceptions import OfflineGenerationError
from compressor.templatetags.compress import CompressorNode from compressor.templatetags.compress import CompressorNode
from compressor.utils import get_offline_cachekey, walk, any, import_module from compressor.utils import walk, any, import_module
class Command(NoArgsCommand): class Command(NoArgsCommand):
@@ -46,7 +45,7 @@ class Command(NoArgsCommand):
from django.template.loader import find_template_source as finder_func from django.template.loader import find_template_source as finder_func
try: try:
source, name = finder_func('test') source, name = finder_func('test')
except TemplateDoesNotExist, e: except TemplateDoesNotExist:
pass pass
from django.template.loader import template_source_loaders from django.template.loader import template_source_loaders
return template_source_loaders or [] return template_source_loaders or []
@@ -64,7 +63,7 @@ class Command(NoArgsCommand):
verbosity = int(options.get("verbosity", 0)) verbosity = int(options.get("verbosity", 0))
if not log: if not log:
log = StringIO() log = StringIO()
if not django_settings.TEMPLATE_LOADERS: if not settings.TEMPLATE_LOADERS:
raise OfflineGenerationError("No template loaders defined. You " raise OfflineGenerationError("No template loaders defined. You "
"must set TEMPLATE_LOADERS in your " "must set TEMPLATE_LOADERS in your "
"settings.") "settings.")
@@ -76,7 +75,7 @@ class Command(NoArgsCommand):
if get_template_sources is None: if get_template_sources is None:
get_template_sources = loader.get_template_sources get_template_sources = loader.get_template_sources
paths.update(list(get_template_sources(''))) paths.update(list(get_template_sources('')))
except (ImportError, AttributeError), e: except (ImportError, AttributeError):
# Yeah, this didn't work out so well, let's move on # Yeah, this didn't work out so well, let's move on
pass pass
if not paths: if not paths:
@@ -107,7 +106,7 @@ class Command(NoArgsCommand):
template_file = open(template_name) template_file = open(template_name)
try: try:
template = Template(template_file.read().decode( template = Template(template_file.read().decode(
django_settings.FILE_CHARSET)) settings.FILE_CHARSET))
finally: finally:
template_file.close() template_file.close()
except IOError: # unreadable file -> ignore except IOError: # unreadable file -> ignore
@@ -136,12 +135,12 @@ class Command(NoArgsCommand):
log.write("Compressing... ") log.write("Compressing... ")
count = 0 count = 0
results = [] results = []
context = Context(settings.OFFLINE_CONTEXT) context = Context(settings.COMPRESS_OFFLINE_CONTEXT)
for nodes in compressor_nodes.values(): for nodes in compressor_nodes.values():
for node in nodes: for node in nodes:
key = get_offline_cachekey(node.nodelist) key = get_offline_cachekey(node.nodelist)
result = node.render(context, compress=True, offline=False) result = node.render(context, compress=True, offline=False)
cache.set(key, result, settings.OFFLINE_TIMEOUT) cache.set(key, result, settings.COMPRESS_OFFLINE_TIMEOUT)
results.append(result) results.append(result)
count += 1 count += 1
log.write("done\nCompressed %d block(s) from %d template(s).\n" log.write("done\nCompressed %d block(s) from %d template(s).\n"
@@ -183,11 +182,11 @@ class Command(NoArgsCommand):
return set([x for x in ext_list if x != '.py']) return set([x for x in ext_list if x != '.py'])
def handle_noargs(self, **options): def handle_noargs(self, **options):
if not settings.ENABLED and not options.get("force"): if not settings.COMPRESS_ENABLED and not options.get("force"):
raise CommandError("Compressor is disabled. Set COMPRESS " raise CommandError("Compressor is disabled. Set COMPRESS "
"settting to True to enable it " "settting to True to enable it "
"(Use -f/--force to override).") "(Use -f/--force to override).")
if not settings.OFFLINE: if not settings.COMPRESS_OFFLINE:
if not options.get("force"): if not options.get("force"):
raise CommandError("Aborting; COMPRESS_OFFLINE is not set. " raise CommandError("Aborting; COMPRESS_OFFLINE is not set. "
"(Use -f/--force to override)") "(Use -f/--force to override)")

View File

@@ -4,9 +4,9 @@ from optparse import make_option
from django.core.management.base import NoArgsCommand, CommandError from django.core.management.base import NoArgsCommand, CommandError
from compressor.cache import cache from compressor.cache import cache, get_mtime, get_mtime_cachekey
from compressor.conf import settings from compressor.conf import settings
from compressor.utils import get_mtime, get_mtime_cachekey, walk from compressor.utils import walk
class Command(NoArgsCommand): class Command(NoArgsCommand):
help = "Add or remove all mtime values from the cache" help = "Add or remove all mtime values from the cache"
@@ -50,19 +50,19 @@ class Command(NoArgsCommand):
if (options['add'] and options['clean']) or (not options['add'] and not options['clean']): if (options['add'] and options['clean']) or (not options['add'] and not options['clean']):
raise CommandError('Please specify either "--add" or "--clean"') raise CommandError('Please specify either "--add" or "--clean"')
if not settings.MTIME_DELAY: if not settings.COMPRESS_MTIME_DELAY:
raise CommandError('mtime caching is currently disabled. Please ' raise CommandError('mtime caching is currently disabled. Please '
'set the COMPRESS_MTIME_DELAY setting to a number of seconds.') 'set the COMPRESS_MTIME_DELAY setting to a number of seconds.')
files_to_add = set() files_to_add = set()
keys_to_delete = set() keys_to_delete = set()
for root, dirs, files in walk(settings.ROOT, followlinks=options['follow_links']): for root, dirs, files in walk(settings.COMPRESS_ROOT, followlinks=options['follow_links']):
for dir_ in dirs: for dir_ in dirs:
if self.is_ignored(dir_): if self.is_ignored(dir_):
dirs.remove(dir_) dirs.remove(dir_)
for filename in files: for filename in files:
common = "".join(root.split(settings.ROOT)) common = "".join(root.split(settings.COMPRESS_ROOT))
if common.startswith(os.sep): if common.startswith(os.sep):
common = common[len(os.sep):] common = common[len(os.sep):]
if self.is_ignored(os.path.join(common, filename)): if self.is_ignored(os.path.join(common, filename)):

View File

@@ -1,9 +1,6 @@
from django.conf import settings as django_settings
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode
from compressor.conf import settings
from compressor.exceptions import ParserError from compressor.exceptions import ParserError
from compressor.utils import get_class
class ParserBase(object): class ParserBase(object):

125
compressor/settings.py Normal file
View File

@@ -0,0 +1,125 @@
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from compressor.utils import AppSettings
class CompressorSettings(AppSettings):
# Main switch
ENABLED = False
# Allows changing verbosity from the settings.
VERBOSE = False
# the backend to use when parsing the JavaScript or Stylesheet files
PARSER = 'compressor.parser.BeautifulSoupParser'
OUTPUT_DIR = 'cache'
STORAGE = 'compressor.storage.CompressorFileStorage'
URL = None
ROOT = None
CSS_FILTERS = ['compressor.filters.css_default.CssAbsoluteFilter']
JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter']
LESSC_BINARY = LESSC_BINARY = 'lessc'
CLOSURE_COMPILER_BINARY = 'java -jar compiler.jar'
CLOSURE_COMPILER_ARGUMENTS = ''
CSSTIDY_BINARY = 'csstidy'
CSSTIDY_ARGUMENTS = '--template=highest'
YUI_BINARY = 'java -jar yuicompressor.jar'
YUI_CSS_ARGUMENTS = ''
YUI_JS_ARGUMENTS = 'COMPRESS_YUI_JS_ARGUMENTS'
DATA_URI_MIN_SIZE = 1024
# the cache backend to use
CACHE_BACKEND = None
# rebuilds the cache every 30 days if nothing has changed.
REBUILD_TIMEOUT = 60 * 60 * 24 * 30 # 30 days
# the upper bound on how long any compression should take to be generated
# (used against dog piling, should be a lot smaller than REBUILD_TIMEOUT
MINT_DELAY = 30 # 30 seconds
# check for file changes only after a delay (in seconds, disabled by default)
MTIME_DELAY = None
# enables the offline cache -- a cache that is filled by the compress management command
OFFLINE = False
# invalidates the offline cache after one year
OFFLINE_TIMEOUT = 60 * 60 * 24 * 365 # 1 year
# The context to be used when compressing the files "offline"
OFFLINE_CONTEXT = {}
def configure_enabled(self, value):
return value or getattr(settings, 'COMPRESS', not self.DEBUG)
def configure_url(self, value):
# Uses the 1.3 STATIC_URL setting by default
url = getattr(settings, 'STATIC_URL', value)
# Check for emptyness since STATIC_URL can be None and ''
if url:
# Then on to extensive testing
root = getattr(settings, 'STATIC_ROOT', None)
if not root:
raise ImproperlyConfigured('The COMPRESS_ROOT setting (or its '
'fallback STATIC_ROOT) must be set.')
# In case staticfiles is used, make sure COMPRESS_URL can be used
# by checking if the the FileSystemFinder is installed, and if is
# checking if COMPRESS_ROOT is in STATICFILES_DIRS to allow finding
# compressed files.
if ("staticfiles" in self.INSTALLED_APPS or
"django.contrib.staticfiles" in self.INSTALLED_APPS):
try:
from staticfiles.conf import settings as staticfiles_settings
finders = staticfiles_settings.STATICFILES_FINDERS
standalone = True
except ImportError:
finders = []
standalone = False
if not finders:
finders = getattr(settings, 'STATICFILES_FINDERS', [])
if ("django.contrib.staticfiles.finders.FileSystemFinder" not in finders and
"staticfiles.finders.FileSystemFinder" not in finders):
raise ImproperlyConfigured(
'Please enable the FileSystemFinder finder of the '
'staticfiles app to use it with django_compressor.')
abs_paths = []
output_path = os.path.join(root, self.COMPRESS_OUTPUT_DIR)
for path in getattr(settings, 'STATICFILES_DIRS', []):
if isinstance(path, tuple) or isinstance(path, list): # stupid Python 2.4
path = path[1] # in case the STATICFILES_DIRS setting has a prefix
abs_paths.append(os.path.abspath(path))
if os.path.abspath(output_path) not in abs_paths:
extension = ((self.COMPRESS_OUTPUT_DIR, output_path),)
if standalone:
from staticfiles.conf import settings as staticfiles_settings
staticfiles_settings.STATICFILES_DIRS += extension
else:
settings.STATICFILES_DIRS += extension
else:
# Fallback to good ol' times of ambiguity
url, root = settings.MEDIA_URL, settings.MEDIA_ROOT
if not url.endswith('/'):
raise ImproperlyConfigured('The URL settings (e.g. COMPRESS_URL) '
'must have a trailing slash.')
self.COMPRESS_ROOT = root
return url
def configure_cache_backend(self, value):
if value is None:
# If we are on Django 1.3 AND using the new CACHES setting...
if getattr(settings, "CACHES", None):
return "default"
# fallback for people still using the old CACHE_BACKEND setting
return settings.CACHE_BACKEND
return value
def configure_offline_context(self, value):
if value:
value = {
'MEDIA_URL': settings.MEDIA_URL,
}
# Adds the 1.3 STATIC_URL setting to the context if available
if getattr(settings, 'STATIC_URL', None):
value['STATIC_URL'] = settings.STATIC_URL
return value

View File

@@ -13,14 +13,14 @@ class CompressorFileStorage(FileSystemStorage):
""" """
def __init__(self, location=None, base_url=None, *args, **kwargs): def __init__(self, location=None, base_url=None, *args, **kwargs):
if location is None: if location is None:
location = settings.ROOT location = settings.COMPRESS_ROOT
if base_url is None: if base_url is None:
base_url = settings.URL base_url = settings.COMPRESS_URL
super(CompressorFileStorage, self).__init__(location, base_url, super(CompressorFileStorage, self).__init__(location, base_url,
*args, **kwargs) *args, **kwargs)
class DefaultStorage(LazyObject): class DefaultStorage(LazyObject):
def _setup(self): def _setup(self):
self._wrapped = get_storage_class(settings.STORAGE)() self._wrapped = get_storage_class(settings.COMPRESS_STORAGE)()
default_storage = DefaultStorage() default_storage = DefaultStorage()

View File

@@ -2,11 +2,10 @@ import time
from django import template from django import template
from compressor.cache import cache, get_offline_cachekey
from compressor.conf import settings
from compressor.css import CssCompressor from compressor.css import CssCompressor
from compressor.js import JsCompressor from compressor.js import JsCompressor
from compressor.cache import cache
from compressor.conf import settings
from compressor.utils import get_offline_cachekey
OUTPUT_FILE = 'file' OUTPUT_FILE = 'file'
@@ -28,17 +27,17 @@ class CompressorNode(template.Node):
if (time.time() > refresh_time) and not refreshed: if (time.time() > refresh_time) and not refreshed:
# Store the stale value while the cache # Store the stale value while the cache
# revalidates for another MINT_DELAY seconds. # revalidates for another MINT_DELAY seconds.
self.cache_set(key, val, timeout=settings.MINT_DELAY, refreshed=True) self.cache_set(key, val, timeout=settings.COMPRESS_MINT_DELAY, refreshed=True)
return None return None
return val return val
def cache_set(self, key, val, timeout=settings.REBUILD_TIMEOUT, refreshed=False): def cache_set(self, key, val, timeout=settings.COMPRESS_REBUILD_TIMEOUT, refreshed=False):
refresh_time = timeout + time.time() refresh_time = timeout + time.time()
real_timeout = timeout + settings.MINT_DELAY real_timeout = timeout + settings.COMPRESS_MINT_DELAY
packed_val = (val, refresh_time, refreshed) packed_val = (val, refresh_time, refreshed)
return cache.set(key, packed_val, real_timeout) return cache.set(key, packed_val, real_timeout)
def render(self, context, compress=settings.ENABLED, offline=settings.OFFLINE): def render(self, context, compress=settings.COMPRESS_ENABLED, offline=settings.COMPRESS_OFFLINE):
if compress and offline: if compress and offline:
key = get_offline_cachekey(self.nodelist) key = get_offline_cachekey(self.nodelist)
content = cache.get(key) content = cache.get(key)

View File

@@ -2,24 +2,23 @@ import os
import re import re
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
from django.conf import settings as django_settings
from django.core.cache.backends import dummy from django.core.cache.backends import dummy
from django.core.files.storage import get_storage_class from django.core.files.storage import get_storage_class
from django.template import Template, Context, TemplateSyntaxError from django.template import Template, Context, TemplateSyntaxError
from django.test import TestCase from django.test import TestCase
from compressor import storage from compressor import storage
from compressor.cache import get_hashed_mtime
from compressor.conf import settings from compressor.conf import settings
from compressor.css import CssCompressor from compressor.css import CssCompressor
from compressor.js import JsCompressor from compressor.js import JsCompressor
from compressor.management.commands.compress import Command as CompressCommand from compressor.management.commands.compress import Command as CompressCommand
from compressor.utils import get_hashed_mtime
class CompressorTestCase(TestCase): class CompressorTestCase(TestCase):
def setUp(self): def setUp(self):
settings.ENABLED = True settings.COMPRESS_ENABLED = True
self.css = """ self.css = """
<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8"> <link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8">
<style type="text/css">p { border:5px solid green;}</style> <style type="text/css">p { border:5px solid green;}</style>
@@ -35,9 +34,9 @@ class CompressorTestCase(TestCase):
def test_css_split(self): def test_css_split(self):
out = [ out = [
('file', os.path.join(settings.ROOT, u'css/one.css'), u'<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8" />'), ('file', os.path.join(settings.COMPRESS_ROOT, u'css/one.css'), u'<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8" />'),
('hunk', u'p { border:5px solid green;}', u'<style type="text/css">p { border:5px solid green;}</style>'), ('hunk', u'p { border:5px solid green;}', u'<style type="text/css">p { border:5px solid green;}</style>'),
('file', os.path.join(settings.ROOT, u'css/two.css'), u'<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8" />'), ('file', os.path.join(settings.COMPRESS_ROOT, u'css/two.css'), u'<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8" />'),
] ]
split = self.cssNode.split_contents() split = self.cssNode.split_contents()
split = [(x[0], x[1], self.cssNode.parser.elem_str(x[2])) for x in split] split = [(x[0], x[1], self.cssNode.parser.elem_str(x[2])) for x in split]
@@ -57,7 +56,7 @@ class CompressorTestCase(TestCase):
self.assert_(is_date.match(str(float(date))), "mtimes is returning something that doesn't look like a date: %s" % date) self.assert_(is_date.match(str(float(date))), "mtimes is returning something that doesn't look like a date: %s" % date)
def test_css_return_if_off(self): def test_css_return_if_off(self):
settings.ENABLED = False settings.COMPRESS_ENABLED = False
self.assertEqual(self.css, self.cssNode.output()) self.assertEqual(self.css, self.cssNode.output())
def test_cachekey(self): def test_cachekey(self):
@@ -72,7 +71,7 @@ class CompressorTestCase(TestCase):
self.assertEqual(output, self.cssNode.output().strip()) self.assertEqual(output, self.cssNode.output().strip())
def test_js_split(self): def test_js_split(self):
out = [('file', os.path.join(settings.ROOT, u'js/one.js'), '<script src="/media/js/one.js" type="text/javascript" charset="utf-8"></script>'), out = [('file', os.path.join(settings.COMPRESS_ROOT, u'js/one.js'), '<script src="/media/js/one.js" type="text/javascript" charset="utf-8"></script>'),
('hunk', u'obj.value = "value";', '<script type="text/javascript" charset="utf-8">obj.value = "value";</script>') ('hunk', u'obj.value = "value";', '<script type="text/javascript" charset="utf-8">obj.value = "value";</script>')
] ]
split = self.jsNode.split_contents() split = self.jsNode.split_contents()
@@ -92,7 +91,7 @@ class CompressorTestCase(TestCase):
self.assertEqual(out, self.jsNode.combined) self.assertEqual(out, self.jsNode.combined)
def test_js_return_if_off(self): def test_js_return_if_off(self):
settings.ENABLED = False settings.COMPRESS_ENABLED = False
self.assertEqual(self.js, self.jsNode.output()) self.assertEqual(self.js, self.jsNode.output())
def test_js_return_if_on(self): def test_js_return_if_on(self):
@@ -100,17 +99,17 @@ class CompressorTestCase(TestCase):
self.assertEqual(output, self.jsNode.output()) self.assertEqual(output, self.jsNode.output())
def test_custom_output_dir(self): def test_custom_output_dir(self):
old_output_dir = settings.OUTPUT_DIR old_output_dir = settings.COMPRESS_OUTPUT_DIR
settings.OUTPUT_DIR = 'custom' settings.COMPRESS_OUTPUT_DIR = 'custom'
output = u'<script type="text/javascript" src="/media/custom/js/3f33b9146e12.js" charset="utf-8"></script>' output = u'<script type="text/javascript" src="/media/custom/js/3f33b9146e12.js" charset="utf-8"></script>'
self.assertEqual(output, JsCompressor(self.js).output()) self.assertEqual(output, JsCompressor(self.js).output())
settings.OUTPUT_DIR = '' settings.COMPRESS_OUTPUT_DIR = ''
output = u'<script type="text/javascript" src="/media/js/3f33b9146e12.js" charset="utf-8"></script>' output = u'<script type="text/javascript" src="/media/js/3f33b9146e12.js" charset="utf-8"></script>'
self.assertEqual(output, JsCompressor(self.js).output()) self.assertEqual(output, JsCompressor(self.js).output())
settings.OUTPUT_DIR = '/custom/nested/' settings.COMPRESS_OUTPUT_DIR = '/custom/nested/'
output = u'<script type="text/javascript" src="/media/custom/nested/js/3f33b9146e12.js" charset="utf-8"></script>' output = u'<script type="text/javascript" src="/media/custom/nested/js/3f33b9146e12.js" charset="utf-8"></script>'
self.assertEqual(output, JsCompressor(self.js).output()) self.assertEqual(output, JsCompressor(self.js).output())
settings.OUTPUT_DIR = old_output_dir settings.COMPRESS_OUTPUT_DIR = old_output_dir
try: try:
@@ -123,27 +122,27 @@ else:
def test_css_split(self): def test_css_split(self):
out = [ out = [
('file', os.path.join(settings.ROOT, u'css/one.css'), u'<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8">'), ('file', os.path.join(settings.COMPRESS_ROOT, u'css/one.css'), u'<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8">'),
('hunk', u'p { border:5px solid green;}', u'<style type="text/css">p { border:5px solid green;}</style>'), ('hunk', u'p { border:5px solid green;}', u'<style type="text/css">p { border:5px solid green;}</style>'),
('file', os.path.join(settings.ROOT, u'css/two.css'), u'<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8">'), ('file', os.path.join(settings.COMPRESS_ROOT, u'css/two.css'), u'<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8">'),
] ]
split = self.cssNode.split_contents() split = self.cssNode.split_contents()
split = [(x[0], x[1], self.cssNode.parser.elem_str(x[2])) for x in split] split = [(x[0], x[1], self.cssNode.parser.elem_str(x[2])) for x in split]
self.assertEqual(out, split) self.assertEqual(out, split)
def setUp(self): def setUp(self):
self.old_parser = settings.PARSER self.old_parser = settings.COMPRESS_PARSER
settings.PARSER = 'compressor.parser.LxmlParser' settings.COMPRESS_PARSER = 'compressor.parser.LxmlParser'
super(LxmlCompressorTestCase, self).setUp() super(LxmlCompressorTestCase, self).setUp()
def tearDown(self): def tearDown(self):
settings.PARSER = self.old_parser settings.COMPRESS_PARSER = self.old_parser
class CssAbsolutizingTestCase(TestCase): class CssAbsolutizingTestCase(TestCase):
def setUp(self): def setUp(self):
settings.ENABLED = True settings.COMPRESS_ENABLED = True
settings.URL = '/media/' settings.COMPRESS_URL = '/media/'
self.css = """ self.css = """
<link rel="stylesheet" href="/media/css/url/url1.css" type="text/css" charset="utf-8"> <link rel="stylesheet" href="/media/css/url/url1.css" type="text/css" charset="utf-8">
<link rel="stylesheet" href="/media/css/url/2/url2.css" type="text/css" charset="utf-8"> <link rel="stylesheet" href="/media/css/url/2/url2.css" type="text/css" charset="utf-8">
@@ -152,43 +151,43 @@ class CssAbsolutizingTestCase(TestCase):
def test_css_absolute_filter(self): def test_css_absolute_filter(self):
from compressor.filters.css_default import CssAbsoluteFilter from compressor.filters.css_default import CssAbsoluteFilter
filename = os.path.join(settings.ROOT, 'css/url/test.css') filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
content = "p { background: url('../../images/image.gif') }" content = "p { background: url('../../images/image.gif') }"
output = "p { background: url('%simages/image.gif?%s') }" % (settings.URL, get_hashed_mtime(filename)) output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
filter = CssAbsoluteFilter(content) filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename)) self.assertEqual(output, filter.input(filename=filename))
settings.URL = 'http://media.example.com/' settings.COMPRESS_URL = 'http://media.example.com/'
filename = os.path.join(settings.ROOT, 'css/url/test.css') filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
output = "p { background: url('%simages/image.gif?%s') }" % (settings.URL, get_hashed_mtime(filename)) output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
self.assertEqual(output, filter.input(filename=filename)) self.assertEqual(output, filter.input(filename=filename))
def test_css_absolute_filter_https(self): def test_css_absolute_filter_https(self):
from compressor.filters.css_default import CssAbsoluteFilter from compressor.filters.css_default import CssAbsoluteFilter
filename = os.path.join(settings.ROOT, 'css/url/test.css') filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
content = "p { background: url('../../images/image.gif') }" content = "p { background: url('../../images/image.gif') }"
output = "p { background: url('%simages/image.gif?%s') }" % (settings.URL, get_hashed_mtime(filename)) output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
filter = CssAbsoluteFilter(content) filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename)) self.assertEqual(output, filter.input(filename=filename))
settings.URL = 'https://media.example.com/' settings.COMPRESS_URL = 'https://media.example.com/'
filename = os.path.join(settings.ROOT, 'css/url/test.css') filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
output = "p { background: url('%simages/image.gif?%s') }" % (settings.URL, get_hashed_mtime(filename)) output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
self.assertEqual(output, filter.input(filename=filename)) self.assertEqual(output, filter.input(filename=filename))
def test_css_absolute_filter_relative_path(self): def test_css_absolute_filter_relative_path(self):
from compressor.filters.css_default import CssAbsoluteFilter from compressor.filters.css_default import CssAbsoluteFilter
filename = os.path.join(django_settings.TEST_DIR, 'whatever', '..', 'media', 'whatever/../css/url/test.css') filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'media', 'whatever/../css/url/test.css')
content = "p { background: url('../../images/image.gif') }" content = "p { background: url('../../images/image.gif') }"
output = "p { background: url('%simages/image.gif?%s') }" % (settings.URL, get_hashed_mtime(filename)) output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
filter = CssAbsoluteFilter(content) filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename)) self.assertEqual(output, filter.input(filename=filename))
settings.URL = 'https://media.example.com/' settings.COMPRESS_URL = 'https://media.example.com/'
output = "p { background: url('%simages/image.gif?%s') }" % (settings.URL, get_hashed_mtime(filename)) output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
self.assertEqual(output, filter.input(filename=filename)) self.assertEqual(output, filter.input(filename=filename))
def test_css_hunks(self): def test_css_hunks(self):
hash_dict = { hash_dict = {
'hash1': get_hashed_mtime(os.path.join(settings.ROOT, 'css/url/url1.css')), 'hash1': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/url1.css')),
'hash2': get_hashed_mtime(os.path.join(settings.ROOT, 'css/url/2/url2.css')), 'hash2': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/2/url2.css')),
} }
out = [u"p { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\n" % hash_dict, out = [u"p { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\n" % hash_dict,
u"p { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\n" % hash_dict] u"p { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\n" % hash_dict]
@@ -197,19 +196,19 @@ class CssAbsolutizingTestCase(TestCase):
class CssDataUriTestCase(TestCase): class CssDataUriTestCase(TestCase):
def setUp(self): def setUp(self):
settings.ENABLED = True settings.COMPRESS_ENABLED = True
settings.CSS_FILTERS = [ settings.COMPRESS_CSS_FILTERS = [
'compressor.filters.css_default.CssAbsoluteFilter', 'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.datauri.CssDataUriFilter', 'compressor.filters.datauri.CssDataUriFilter',
] ]
settings.URL = '/media/' settings.COMPRESS_URL = '/media/'
self.css = """ self.css = """
<link rel="stylesheet" href="/media/css/datauri.css" type="text/css" charset="utf-8"> <link rel="stylesheet" href="/media/css/datauri.css" type="text/css" charset="utf-8">
""" """
self.cssNode = CssCompressor(self.css) self.cssNode = CssCompressor(self.css)
def test_data_uris(self): def test_data_uris(self):
datauri_hash = get_hashed_mtime(os.path.join(settings.ROOT, 'css/datauri.css')) datauri_hash = get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/datauri.css'))
out = [u'.add { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }\n.python { background-image: url("/media/img/python.png?%s"); }\n.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); }\n' % datauri_hash] out = [u'.add { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }\n.python { background-image: url("/media/img/python.png?%s"); }\n.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); }\n' % datauri_hash]
self.assertEqual(out, self.cssNode.hunks) self.assertEqual(out, self.cssNode.hunks)
@@ -264,12 +263,12 @@ def render(template_string, context_dict=None):
class TemplatetagTestCase(TestCase): class TemplatetagTestCase(TestCase):
def setUp(self): def setUp(self):
settings.ENABLED = True settings.COMPRESS_ENABLED = True
def test_empty_tag(self): def test_empty_tag(self):
template = u"""{% load compress %}{% compress js %}{% block js %} template = u"""{% load compress %}{% compress js %}{% block js %}
{% endblock %}{% endcompress %}""" {% endblock %}{% endcompress %}"""
context = { 'MEDIA_URL': settings.URL } context = { 'MEDIA_URL': settings.COMPRESS_URL }
self.assertEqual(u'', render(template, context)) self.assertEqual(u'', render(template, context))
def test_css_tag(self): def test_css_tag(self):
@@ -279,7 +278,7 @@ class TemplatetagTestCase(TestCase):
<link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css" charset="utf-8"> <link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css" charset="utf-8">
{% endcompress %} {% endcompress %}
""" """
context = { 'MEDIA_URL': settings.URL } context = { 'MEDIA_URL': settings.COMPRESS_URL }
out = u'<link rel="stylesheet" href="/media/cache/css/f7c661b7a124.css" type="text/css">' out = u'<link rel="stylesheet" href="/media/cache/css/f7c661b7a124.css" type="text/css">'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))
@@ -289,7 +288,7 @@ class TemplatetagTestCase(TestCase):
<style type="text/css">p { border:5px solid green;}</style> <style type="text/css">p { border:5px solid green;}</style>
{% endcompress %} {% endcompress %}
""" """
context = { 'MEDIA_URL': settings.URL } context = { 'MEDIA_URL': settings.COMPRESS_URL }
out = '<link rel="stylesheet" href="/media/cache/css/1c1c0855907b.css" type="text/css">' out = '<link rel="stylesheet" href="/media/cache/css/1c1c0855907b.css" type="text/css">'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))
@@ -299,7 +298,7 @@ class TemplatetagTestCase(TestCase):
<script type="text/javascript" charset="utf-8">obj.value = "value";</script> <script type="text/javascript" charset="utf-8">obj.value = "value";</script>
{% endcompress %} {% endcompress %}
""" """
context = { 'MEDIA_URL': settings.URL } context = { 'MEDIA_URL': settings.COMPRESS_URL }
out = u'<script type="text/javascript" src="/media/cache/js/3f33b9146e12.js" charset="utf-8"></script>' out = u'<script type="text/javascript" src="/media/cache/js/3f33b9146e12.js" charset="utf-8"></script>'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))
@@ -309,7 +308,7 @@ class TemplatetagTestCase(TestCase):
<script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script> <script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script>
{% endcompress %} {% endcompress %}
""" """
context = { 'MEDIA_URL': settings.URL } context = { 'MEDIA_URL': settings.COMPRESS_URL }
out = u'<script type="text/javascript" src="/media/cache/js/5d5c0e1cb25f.js" charset="utf-8"></script>' out = u'<script type="text/javascript" src="/media/cache/js/5d5c0e1cb25f.js" charset="utf-8"></script>'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))
@@ -319,7 +318,7 @@ class TemplatetagTestCase(TestCase):
<script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script> <script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script>
{% endcompress %} {% endcompress %}
""" """
context = { 'MEDIA_URL': settings.URL } context = { 'MEDIA_URL': settings.COMPRESS_URL }
out = u'<script type="text/javascript" src="/media/cache/js/40a8e9ffb476.js" charset="utf-8"></script>' out = u'<script type="text/javascript" src="/media/cache/js/40a8e9ffb476.js" charset="utf-8"></script>'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))
@@ -334,7 +333,7 @@ class StorageTestCase(TestCase):
def setUp(self): def setUp(self):
self._storage = storage.default_storage self._storage = storage.default_storage
storage.default_storage = get_storage_class('compressor.tests.storage.TestStorage')() storage.default_storage = get_storage_class('compressor.tests.storage.TestStorage')()
settings.ENABLED = True settings.COMPRESS_ENABLED = True
def tearDown(self): def tearDown(self):
storage.default_storage = self._storage storage.default_storage = self._storage
@@ -346,7 +345,7 @@ class StorageTestCase(TestCase):
<link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css" charset="utf-8"> <link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css" charset="utf-8">
{% endcompress %} {% endcompress %}
""" """
context = { 'MEDIA_URL': settings.URL } context = { 'MEDIA_URL': settings.COMPRESS_URL }
out = u'<link rel="stylesheet" href="/media/cache/css/5b231a62e9a6.css.gz" type="text/css">' out = u'<link rel="stylesheet" href="/media/cache/css/5b231a62e9a6.css.gz" type="text/css">'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))
@@ -355,7 +354,7 @@ class VerboseTestCase(CompressorTestCase):
def setUp(self): def setUp(self):
super(VerboseTestCase, self).setUp() super(VerboseTestCase, self).setUp()
settings.VERBOSE = True settings.COMPRESS_VERBOSE = True
class CacheBackendTestCase(CompressorTestCase): class CacheBackendTestCase(CompressorTestCase):
@@ -369,11 +368,11 @@ class OfflineGenerationTestCase(TestCase):
"""Uses templates/test_compressor_offline.html""" """Uses templates/test_compressor_offline.html"""
def setUp(self): def setUp(self):
self._old_compress = settings.ENABLED self._old_compress = settings.COMPRESS_ENABLED
settings.ENABLED = True settings.COMPRESS_ENABLED = True
def tearDown(self): def tearDown(self):
settings.ENABLED = self._old_compress settings.COMPRESS_ENABLED = self._old_compress
def test_offline(self): def test_offline(self):
count, result = CompressCommand().compress() count, result = CompressCommand().compress()
@@ -384,8 +383,8 @@ class OfflineGenerationTestCase(TestCase):
]) ])
def test_offline_with_context(self): def test_offline_with_context(self):
self._old_offline_context = settings.OFFLINE_CONTEXT self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
settings.OFFLINE_CONTEXT = { settings.COMPRESS_OFFLINE_CONTEXT = {
'color': 'blue', 'color': 'blue',
} }
count, result = CompressCommand().compress() count, result = CompressCommand().compress()
@@ -394,4 +393,4 @@ class OfflineGenerationTestCase(TestCase):
u'<link rel="stylesheet" href="/media/cache/css/8a2405e029de.css" type="text/css">\n', u'<link rel="stylesheet" href="/media/cache/css/8a2405e029de.css" type="text/css">\n',
u'<script type="text/javascript" src="/media/cache/js/bf53fa5b13e2.js" charset="utf-8"></script>', u'<script type="text/javascript" src="/media/cache/js/bf53fa5b13e2.js" charset="utf-8"></script>',
]) ])
settings.OFFLINE_CONTEXT = self._old_offline_context settings.COMPRESS_OFFLINE_CONTEXT = self._old_offline_context

View File

@@ -1,16 +1,12 @@
import inspect
import os import os
import sys import sys
from django.core.exceptions import ImproperlyConfigured
from django.utils.encoding import smart_str
from django.utils.hashcompat import sha_constructor
from compressor.cache import cache
from compressor.conf import settings
from compressor.exceptions import FilterError
from shlex import split as cmd_split from shlex import split as cmd_split
from django.conf import settings
from compressor.exceptions import FilterError
try: try:
any = any any = any
except NameError: except NameError:
@@ -20,31 +16,6 @@ except NameError:
return True return True
return False return False
def get_hexdigest(plaintext):
return sha_constructor(plaintext).hexdigest()
def get_mtime_cachekey(filename):
return "django_compressor.mtime.%s" % filename
def get_offline_cachekey(source):
return ("django_compressor.offline.%s"
% get_hexdigest("".join(smart_str(s) for s in source)))
def get_mtime(filename):
if settings.MTIME_DELAY:
key = get_mtime_cachekey(filename)
mtime = cache.get(key)
if mtime is None:
mtime = os.path.getmtime(filename)
cache.set(key, mtime, settings.MTIME_DELAY)
return mtime
return os.path.getmtime(filename)
def get_hashed_mtime(filename, length=12):
filename = os.path.realpath(filename)
mtime = str(int(get_mtime(filename)))
return get_hexdigest(mtime)[:length]
def get_class(class_string, exception=FilterError): def get_class(class_string, exception=FilterError):
""" """
Convert a string version of a function name to the callable object. Convert a string version of a function name to the callable object.