From f886c9b7c524f86025e07d804655811209cbe225 Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Thu, 27 Mar 2014 23:55:11 -0400 Subject: [PATCH] Watch for asset changes. --- pavelib/__init__.py | 4 ++ pavelib/assets.py | 134 +++++++++++++++++++++++++++++++++++--- pavelib/servers.py | 4 +- pavelib/utils/cmd.py | 1 + requirements/edx/base.txt | 2 +- 5 files changed, 134 insertions(+), 11 deletions(-) diff --git a/pavelib/__init__.py b/pavelib/__init__.py index 9b405a2d0b..f70eb31bd2 100644 --- a/pavelib/__init__.py +++ b/pavelib/__init__.py @@ -1 +1,5 @@ +""" +paver commands +""" __all__ = ["assets", "servers", "docs", "prereqs"] +from . import assets, servers, docs, prereqs diff --git a/pavelib/assets.py b/pavelib/assets.py index 41513cc153..a9acc69980 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -1,18 +1,97 @@ """ Asset compilation and collection. """ +from __future__ import print_function import argparse -from paver.easy import * +from paver.easy import sh, path, task, cmdopts, needs, consume_args, call_task +from watchdog.observers import Observer +from watchdog.events import PatternMatchingEventHandler +import glob +import traceback from .utils.envs import Env from .utils.cmd import cmd, django_cmd - COFFEE_DIRS = ['lms', 'cms', 'common'] SASS_LOAD_PATHS = ['./common/static/sass'] SASS_UPDATE_DIRS = ['*/static'] SASS_CACHE_PATH = '/tmp/sass-cache' +class CoffeeScriptWatcher(PatternMatchingEventHandler): + """ + Watches for coffeescript changes + """ + ignore_directories = True + patterns = ['*.coffee'] + + def register(self, observer): + """ + register files with observer + """ + dirnames = set() + for filename in sh(coffeescript_files(), capture=True).splitlines(): + dirnames.add(path(filename).dirname()) + for dirname in dirnames: + observer.schedule(self, dirname) + + def on_modified(self, event): + print('\tCHANGED:', event.src_path) + try: + compile_coffeescript(event.src_path) + except Exception: # pylint: disable=W0703 + traceback.print_exc() + + +class SassWatcher(PatternMatchingEventHandler): + """ + Watches for sass file changes + """ + ignore_directories = True + patterns = ['*.scss'] + ignore_patterns = ['common/static/xmodule/*'] + + def register(self, observer): + """ + register files with observer + """ + for dirname in SASS_LOAD_PATHS + SASS_UPDATE_DIRS + theme_sass_paths(): + paths = [] + if '*' in dirname: + paths.extend(glob.glob(dirname)) + else: + paths.append(dirname) + for dirname in paths: + observer.schedule(self, dirname, recursive=True) + + def on_modified(self, event): + print('\tCHANGED:', event.src_path) + try: + compile_sass() + except Exception: # pylint: disable=W0703 + traceback.print_exc() + + +class XModuleSassWatcher(SassWatcher): + """ + Watches for sass file changes + """ + ignore_directories = True + ignore_patterns = [] + + def register(self, observer): + """ + register files with observer + """ + observer.schedule(self, 'common/lib/xmodule/', recursive=True) + + def on_modified(self, event): + print('\tCHANGED:', event.src_path) + try: + process_xmodule_assets() + except Exception: # pylint: disable=W0703 + traceback.print_exc() + + def theme_sass_paths(): """ Return the a list of paths to the theme's sass assets, @@ -25,23 +104,30 @@ def theme_sass_paths(): parent_dir = path(edxapp_env.REPO_ROOT).abspath().parent theme_root = parent_dir / "themes" / theme_name return [theme_root / "static" / "sass"] - else: return [] -def compile_coffeescript(): +def coffeescript_files(): + """ + return find command for paths containing coffee files + """ + dirs = " ".join([Env.REPO_ROOT / coffee_dir for coffee_dir in COFFEE_DIRS]) + return cmd('find', dirs, '-type f', '-name \"*.coffee\"') + + +def compile_coffeescript(*files): """ Compile CoffeeScript to JavaScript. """ - dirs = " ".join([Env.REPO_ROOT / coffee_dir for coffee_dir in COFFEE_DIRS]) + if not files: + files = ["`{}`".format(coffeescript_files())] sh(cmd( - "node_modules/.bin/coffee", "--compile", - " `find {dirs} -type f -name \"*.coffee\"`".format(dirs=dirs) + "node_modules/.bin/coffee", "--compile", *files )) -def compile_sass(debug): +def compile_sass(debug=False): """ Compile Sass to CSS. """ @@ -81,6 +167,31 @@ def collect_assets(systems, settings): sh(django_cmd(sys, settings, "collectstatic --noinput > /dev/null")) +@task +@cmdopts([('background', 'b', 'Background mode')]) +def watch_assets(options): + """ + Watch for changes to asset files, and regenerate js/css + """ + observer = Observer() + + CoffeeScriptWatcher().register(observer) + SassWatcher().register(observer) + XModuleSassWatcher().register(observer) + + print("Starting asset watcher...") + observer.start() + if not getattr(options, 'background', False): + # when running as a separate process, the main thread needs to loop + # in order to allow for shutdown by contrl-c + try: + while True: + observer.join(2) + except KeyboardInterrupt: + observer.stop() + print("\nStopped asset watcher.") + + @task @needs('pavelib.prereqs.install_prereqs') @consume_args @@ -105,6 +216,10 @@ def update_assets(args): '--skip-collect', dest='collect', action='store_false', default=True, help="Skip collection of static assets", ) + parser.add_argument( + '--watch', action='store_true', default=False, + help="Watch files for changes", + ) args = parser.parse_args(args) compile_templated_sass(args.system, args.settings) @@ -114,3 +229,6 @@ def update_assets(args): if args.collect: collect_assets(args.system, args.settings) + + if args.watch: + call_task('watch_assets', options={'background': not args.debug}) diff --git a/pavelib/servers.py b/pavelib/servers.py index 0b88e4cc26..9d6ed6c88f 100644 --- a/pavelib/servers.py +++ b/pavelib/servers.py @@ -27,7 +27,7 @@ def run_server(system, settings=None, port=None, skip_assets=False): if not skip_assets: # Local dev settings use staticfiles to serve assets, so we can skip the collecstatic step - args = [system, '--settings={}'.format(settings), '--skip-collect'] + args = [system, '--settings={}'.format(settings), '--skip-collect', '--watch'] call_task('pavelib.assets.update_assets', args=args) if port is None: @@ -119,7 +119,7 @@ def run_all_servers(options): if not fast: for system in ['lms', 'studio']: - args = [system, '--settings={}'.format(settings), '--skip-collect'] + args = [system, '--settings={}'.format(settings), '--skip-collect', '--watch'] call_task('pavelib.assets.update_assets', args=args) run_multi_processes([ diff --git a/pavelib/utils/cmd.py b/pavelib/utils/cmd.py index 44e83eb65c..07f9befc47 100644 --- a/pavelib/utils/cmd.py +++ b/pavelib/utils/cmd.py @@ -2,6 +2,7 @@ Helper functions for constructing shell commands. """ + def cmd(*args): """ Concatenate the arguments into a space-separated shell command. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 7906e741a4..80b3184fab 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -77,7 +77,7 @@ django-ratelimit-backend==0.6 unicodecsv==0.9.4 # Used for development operation -watchdog==0.6.0 +watchdog==0.7.1 # Metrics gathering and monitoring dogapi==1.2.1