Permalink
Browse files

- Add support for lookup in STATICFILES_DIRS

- Some refactoring and cleanup
  • Loading branch information...
1 parent ab01bf4 commit 86fae7ea143c3be24a06dcee0c312be190c86523 @andreyfedoseev committed Nov 3, 2012
View
@@ -5,4 +5,5 @@ dist
*.egg
.idea
virtualenv
-less/tests/media/LESS_CACHE
+less/tests/media/LESS_CACHE
+/less/tests/static/LESS_CACHE/
View
@@ -0,0 +1,73 @@
+from less.utils import compile_less, logger
+from less.settings import LESS_DEVMODE_WATCH_DIRS, LESS_OUTPUT_DIR, LESS_DEVMODE_EXCLUDE
+from django.conf import settings
+import os
+import re
+import sys
+import time
+import threading
+
+
+try:
+ STATIC_ROOT = settings.STATIC_ROOT
+except AttributeError:
+ STATIC_ROOT = settings.MEDIA_ROOT
+
+
+WATCHED_FILES = {}
+LESS_IMPORT_RE = re.compile(r"""@import\s+['"](.+?\.less)['"]\s*;""")
+
+
+def daemon():
+
+ while True:
+ to_be_compiled = set()
+ for watched_dir in LESS_DEVMODE_WATCH_DIRS:
+ for root, dirs, files in os.walk(watched_dir):
+ for filename in filter(lambda f: f.endswith(".less"), files):
+ filename = os.path.join(root, filename)
+ f = os.path.relpath(filename, STATIC_ROOT)
+ if f in LESS_DEVMODE_EXCLUDE:
+ continue
+ mtime = os.path.getmtime(filename)
+
+ if f not in WATCHED_FILES:
+ WATCHED_FILES[f] = [None, set()]
+
+ if WATCHED_FILES[f][0] != mtime:
+ WATCHED_FILES[f][0] = mtime
+ # Look for @import statements to update dependecies
+ for line in open(filename):
+ for imported in LESS_IMPORT_RE.findall(line):
+ imported = os.path.relpath(os.path.join(os.path.dirname(filename), imported), STATIC_ROOT)
+ if imported not in WATCHED_FILES:
+ WATCHED_FILES[imported] = [None, set([f])]
+ else:
+ WATCHED_FILES[imported][1].add(f)
+
+ to_be_compiled.add(f)
+ importers = WATCHED_FILES[f][1]
+ while importers:
+ for importer in importers:
+ to_be_compiled.add(importer)
+ importers = WATCHED_FILES[importer][1]
+
+ for less_path in to_be_compiled:
+ full_path = os.path.join(STATIC_ROOT, less_path)
+ base_filename = os.path.split(less_path)[-1][:-5]
+ output_directory = os.path.join(STATIC_ROOT, LESS_OUTPUT_DIR, os.path.dirname(less_path))
+ output_path = os.path.join(output_directory, "%s.css" % base_filename)
+ if isinstance(full_path, unicode):
+ filesystem_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
+ full_path = full_path.encode(filesystem_encoding)
+
+ compile_less(full_path, output_path, less_path)
+ logger.debug("Compiled: %s" % less_path)
+
+ time.sleep(1)
+
+
+def start_daemon():
+ thread = threading.Thread(target=daemon)
+ thread.daemon = True
+ thread.start()
View
@@ -0,0 +1,9 @@
+from less.settings import LESS_DEVMODE
+
+
+if LESS_DEVMODE:
+ # Run the devmode daemon if it's enabled.
+ # We start it here because this file is auto imported by Django when
+ # devserver is started.
+ from less.devmode import start_daemon
+ start_daemon()
View
@@ -5,4 +5,7 @@
LESS_USE_CACHE = getattr(settings, "LESS_USE_CACHE", True)
LESS_CACHE_TIMEOUT = getattr(settings, "LESS_CACHE_TIMEOUT", 60 * 60 * 24 * 30) # 30 days
LESS_MTIME_DELAY = getattr(settings, "LESS_MTIME_DELAY", 10) # 10 seconds
-LESS_OUTPUT_DIR = getattr(settings, "LESS_OUTPUT_DIR", "LESS_CACHE")
+LESS_OUTPUT_DIR = getattr(settings, "LESS_OUTPUT_DIR", "LESS_CACHE")
+LESS_DEVMODE = getattr(settings, "LESS_DEVMODE", False)
+LESS_DEVMODE_WATCH_DIRS = getattr(settings, "LESS_DEVMODE_WATCH_DIRS", [settings.STATIC_ROOT])
+LESS_DEVMODE_EXCLUDE = getattr(settings, "LESS_DEVMODE_EXCLUDE", ())
@@ -1,19 +1,20 @@
from tempfile import NamedTemporaryFile
from ..cache import get_cache_key, get_hexdigest, get_hashed_mtime
+from ..utils import compile_less, STATIC_ROOT
from ..settings import LESS_EXECUTABLE, LESS_USE_CACHE,\
- LESS_CACHE_TIMEOUT, LESS_OUTPUT_DIR
-from ..utils import URLConverter
+ LESS_CACHE_TIMEOUT, LESS_OUTPUT_DIR, LESS_DEVMODE, LESS_DEVMODE_WATCH_DIRS
from django.conf import settings
from django.core.cache import cache
from django.template.base import Library, Node
import logging
-import shlex
import subprocess
import os
import sys
logger = logging.getLogger("less")
+
+
register = Library()
@@ -28,7 +29,7 @@ def compile(self, source):
source_file = NamedTemporaryFile(delete=False)
source_file.write(source)
source_file.close()
- args = shlex.split("%s %s" % (LESS_EXECUTABLE, source_file.name))
+ args = [LESS_EXECUTABLE, source_file.name]
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, errors = p.communicate()
@@ -62,56 +63,63 @@ def do_inlineless(parser, token):
return InlineLessNode(nodelist)
-@register.simple_tag
-def less(path):
+def less_paths(path):
- try:
- STATIC_ROOT = settings.STATIC_ROOT
- except AttributeError:
- STATIC_ROOT = settings.MEDIA_ROOT
+ # while developing it is more confortable
+ # searching for the less files rather then
+ # doing collectstatics all the time
+ if settings.DEBUG:
+ for sfdir in settings.STATICFILES_DIRS:
+ prefix = None
+ if isinstance(sfdir, (tuple, list)):
+ prefix, sfdir = sfdir
+ if prefix:
+ if not path.startswith(prefix):
+ continue
+ input_file = os.path.join(sfdir, path[len(prefix):].lstrip(os.sep))
+ else:
+ input_file = os.path.join(sfdir, path)
+ if os.path.exists(input_file):
+ output_dir = os.path.join(STATIC_ROOT, LESS_OUTPUT_DIR, os.path.dirname(path))
+ file_name = os.path.basename(path)
+ return input_file, file_name, output_dir
- try:
- STATIC_URL = settings.STATIC_URL
- except AttributeError:
- STATIC_URL = settings.MEDIA_URL
+ full_path = os.path.join(STATIC_ROOT, path)
+ file_name = os.path.split(path)[-1]
- encoded_full_path = full_path = os.path.join(STATIC_ROOT, path)
- if isinstance(full_path, unicode):
- filesystem_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
- encoded_full_path = full_path.encode(filesystem_encoding)
+ output_dir = os.path.join(STATIC_ROOT, LESS_OUTPUT_DIR, os.path.dirname(path))
- filename = os.path.split(path)[-1]
+ return full_path, file_name, output_dir
- output_directory = os.path.join(STATIC_ROOT, LESS_OUTPUT_DIR, os.path.dirname(path))
- hashed_mtime = get_hashed_mtime(full_path)
+@register.simple_tag
+def less(path):
+
+ logger.info("processing file %s" % path)
- if filename.endswith(".less"):
- base_filename = filename[:-5]
- else:
- base_filename = filename
+ full_path, file_name, output_dir = less_paths(path)
+ base_file_name = os.path.splitext(file_name)[0]
- output_path = os.path.join(output_directory, "%s-%s.css" % (base_filename, hashed_mtime))
+ if LESS_DEVMODE and any(map(lambda watched_dir: full_path.startswith(watched_dir), LESS_DEVMODE_WATCH_DIRS)):
+ return os.path.join(os.path.dirname(path), "%s.css" % base_file_name)
+
+ hashed_mtime = get_hashed_mtime(full_path)
+ output_file = "%s-%s.css" % (base_file_name, hashed_mtime)
+ output_path = os.path.join(output_dir, output_file)
+
+ encoded_full_path = full_path
+ if isinstance(full_path, unicode):
+ filesystem_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
+ encoded_full_path = full_path.encode(filesystem_encoding)
if not os.path.exists(output_path):
- command = "%s %s" % (LESS_EXECUTABLE, encoded_full_path)
- args = shlex.split(command)
- p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- out, errors = p.communicate()
- if out:
- if not os.path.exists(output_directory):
- os.makedirs(output_directory)
- compiled_file = open(output_path, "w+")
- compiled_file.write(URLConverter(out, os.path.join(STATIC_URL, path)).convert())
- compiled_file.close()
-
- # Remove old files
- compiled_filename = os.path.split(output_path)[-1]
- for filename in os.listdir(output_directory):
- if filename.startswith(base_filename) and filename != compiled_filename:
- os.remove(os.path.join(output_directory, filename))
- elif errors:
- logger.error(errors)
+ if not compile_less(encoded_full_path, output_path, path):
return path
- return output_path[len(STATIC_ROOT):].replace(os.sep, '/').lstrip("/")
+ # Remove old files
+ compiled_filename = os.path.split(output_path)[-1]
+ for filename in os.listdir(output_dir):
+ if filename.startswith(base_file_name) and filename != compiled_filename:
+ os.remove(os.path.join(output_dir, filename))
+
+ return os.path.join(LESS_OUTPUT_DIR, os.path.dirname(path), output_file)
@@ -1,12 +1,20 @@
from django.conf.global_settings import *
import os
+DEBUG = True
+
+STATIC_ROOT = MEDIA_ROOT = os.path.join(os.path.dirname(__file__), 'static')
+STATIC_URL = MEDIA_URL = "/static/"
+
+STATICFILES_DIRS = (
+ os.path.join(os.path.dirname(__file__), 'staticfiles_dir'),
+ ("prefix", os.path.join(os.path.dirname(__file__), 'staticfiles_dir_with_prefix')),
+)
-STATIC_ROOT = MEDIA_ROOT = os.path.join(os.path.dirname(__file__), 'media')
-STATIC_URL = MEDIA_URL = "/media/"
INSTALLED_APPS = (
"less",
)
+
LESS_MTIME_DELAY = 2
LESS_OUTPUT_DIR = "LESS_CACHE"
@@ -0,0 +1,5 @@
+#header-from-staticfiles-dir {
+ h1 {
+ color: red;
+ }
+}
@@ -0,0 +1,5 @@
+#header-from-staticfiles-dir-with-prefix {
+ h1 {
+ color: red;
+ }
+}
View
@@ -1,4 +1,5 @@
from unittest import main, TestCase
+from django.http import HttpRequest
from django.template.base import Template
from django.template.context import RequestContext
import os
@@ -23,6 +24,9 @@ def setUp(self):
if os.path.exists(output_dir):
shutil.rmtree(output_dir)
+ def _get_request_context(self):
+ return RequestContext(HttpRequest())
+
def test_inline_less(self):
template = Template("""
{% load less %}
@@ -36,7 +40,7 @@ def test_inline_less(self):
rendered = """#bordered {
border: 2px;
}"""
- self.assertEqual(template.render(RequestContext({})).strip(), rendered)
+ self.assertEqual(template.render(self._get_request_context()).strip(), rendered)
def test_external_less(self):
@@ -45,13 +49,13 @@ def test_external_less(self):
{% less "styles/test.less" %}
""")
compiled_filename_re = re.compile(r"LESS_CACHE/styles/test-[a-f0-9]{12}.css")
- compiled_filename = template.render(RequestContext({})).strip()
+ compiled_filename = template.render(self._get_request_context()).strip()
self.assertTrue(bool(compiled_filename_re.match(compiled_filename)))
compiled_path = os.path.join(self.django_settings.MEDIA_ROOT, compiled_filename)
compiled_content = open(compiled_path).read().strip()
compiled = """#header h1 {
- background-image: url('/media/images/header.png');
+ background-image: url('/static/images/header.png');
}"""
self.assertEquals(compiled_content, compiled)
@@ -60,15 +64,15 @@ def test_external_less(self):
os.utime(source_path, None)
# The modification time is cached so the compiled file is not updated
- compiled_filename_2 = template.render(RequestContext({})).strip()
+ compiled_filename_2 = template.render(self._get_request_context()).strip()
self.assertTrue(bool(compiled_filename_re.match(compiled_filename_2)))
self.assertEquals(compiled_filename, compiled_filename_2)
# Wait to invalidate the cached modification time
time.sleep(self.django_settings.LESS_MTIME_DELAY)
# Now the file is re-compiled
- compiled_filename_3 = template.render(RequestContext({})).strip()
+ compiled_filename_3 = template.render(self._get_request_context()).strip()
self.assertTrue(bool(compiled_filename_re.match(compiled_filename_3)))
self.assertNotEquals(compiled_filename, compiled_filename_3)
@@ -78,6 +82,37 @@ def test_external_less(self):
compiled_filename_3))
self.assertEquals(len(os.listdir(compiled_file_dir)), 1)
+ def test_lookup_in_staticfiles_dirs(self):
+
+ template = Template("""
+ {% load less %}
+ {% less "another_test.less" %}
+ """)
+ compiled_filename_re = re.compile(r"LESS_CACHE/another_test-[a-f0-9]{12}.css")
+ compiled_filename = template.render(self._get_request_context()).strip()
+ self.assertTrue(bool(compiled_filename_re.match(compiled_filename)))
+
+ compiled_path = os.path.join(self.django_settings.STATIC_ROOT, compiled_filename)
+ compiled_content = open(compiled_path).read().strip()
+ compiled = """#header-from-staticfiles-dir h1 {
+ color: red;
+}"""
+ self.assertEquals(compiled_content, compiled)
+
+ template = Template("""
+ {% load less %}
+ {% less "prefix/another_test.less" %}
+ """)
+ compiled_filename_re = re.compile(r"LESS_CACHE/prefix/another_test-[a-f0-9]{12}.css")
+ compiled_filename = template.render(self._get_request_context()).strip()
+ self.assertTrue(bool(compiled_filename_re.match(compiled_filename)))
+
+ compiled_path = os.path.join(self.django_settings.STATIC_ROOT, compiled_filename)
+ compiled_content = open(compiled_path).read().strip()
+ compiled = """#header-from-staticfiles-dir-with-prefix h1 {
+ color: red;
+}"""
+ self.assertEquals(compiled_content, compiled)
if __name__ == '__main__':
- main()
+ main()
Oops, something went wrong.

0 comments on commit 86fae7e

Please sign in to comment.