From 53642579ad98875d6561a5059366916da3bcab45 Mon Sep 17 00:00:00 2001 From: Omar Khan <omar@opencraft.com> Date: Fri, 18 Mar 2016 12:42:49 +0700 Subject: [PATCH] Fix paver watch_assets - Update to latest version of watchdog. - inotify doesn't work on nfs share in devstack. Use polling instead. - Some editors (vim) do not trigger modify events in their default configuration. Rebuild assets on all filesystem changes, and debounce when changes happen too quickly. --- pavelib/assets.py | 41 +++++++++++++++++++++++++----- pavelib/paver_tests/test_assets.py | 10 ++++---- requirements/edx/base.txt | 2 +- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/pavelib/assets.py b/pavelib/assets.py index 4670a196fe6..2dad9029cd5 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -4,13 +4,15 @@ Asset compilation and collection. from __future__ import print_function from datetime import datetime +from functools import wraps +from threading import Timer import argparse import glob import traceback from paver import tasks from paver.easy import sh, path, task, cmdopts, needs, consume_args, call_task, no_help -from watchdog.observers import Observer +from watchdog.observers.polling import PollingObserver from watchdog.events import PatternMatchingEventHandler from .utils.envs import Env @@ -238,6 +240,29 @@ def get_watcher_dirs(themes_base_dir=None, themes=None): return dirs +def debounce(seconds=1): + """ + Prevents the decorated function from being called more than every `seconds` + seconds. Waits until calls stop coming in before calling the decorated + function. + """ + def decorator(func): # pylint: disable=missing-docstring + func.timer = None + + @wraps(func) + def wrapper(*args, **kwargs): # pylint: disable=missing-docstring + def call(): # pylint: disable=missing-docstring + func(*args, **kwargs) + func.timer = None + if func.timer: + func.timer.cancel() + func.timer = Timer(seconds, call) + func.timer.start() + + return wrapper + return decorator + + class CoffeeScriptWatcher(PatternMatchingEventHandler): """ Watches for coffeescript changes @@ -255,7 +280,8 @@ class CoffeeScriptWatcher(PatternMatchingEventHandler): for dirname in dirnames: observer.schedule(self, dirname) - def on_modified(self, event): + @debounce() + def on_any_event(self, event): print('\tCHANGED:', event.src_path) try: compile_coffeescript(event.src_path) @@ -288,7 +314,8 @@ class SassWatcher(PatternMatchingEventHandler): for dirname in paths: observer.schedule(self, dirname, recursive=True) - def on_modified(self, event): + @debounce() + def on_any_event(self, event): print('\tCHANGED:', event.src_path) try: compile_sass() # pylint: disable=no-value-for-parameter @@ -303,7 +330,8 @@ class XModuleSassWatcher(SassWatcher): ignore_directories = True ignore_patterns = [] - def on_modified(self, event): + @debounce() + def on_any_event(self, event): print('\tCHANGED:', event.src_path) try: process_xmodule_assets() @@ -324,7 +352,8 @@ class XModuleAssetsWatcher(PatternMatchingEventHandler): """ observer.schedule(self, 'common/lib/xmodule/', recursive=True) - def on_modified(self, event): + @debounce() + def on_any_event(self, event): print('\tCHANGED:', event.src_path) try: process_xmodule_assets() @@ -634,7 +663,7 @@ def watch_assets(options): themes = themes if isinstance(themes, list) else [themes] sass_directories = get_watcher_dirs(theme_base_dir, themes) - observer = Observer() + observer = PollingObserver() CoffeeScriptWatcher().register(observer) SassWatcher().register(observer, sass_directories) diff --git a/pavelib/paver_tests/test_assets.py b/pavelib/paver_tests/test_assets.py index d94bf75e45b..4f6f2116158 100644 --- a/pavelib/paver_tests/test_assets.py +++ b/pavelib/paver_tests/test_assets.py @@ -6,7 +6,7 @@ from unittest import TestCase from paver.easy import call_task from paver.easy import path from mock import patch -from watchdog.observers import Observer +from watchdog.observers.polling import PollingObserver from .utils import PaverTestCase ROOT_PATH = path(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) @@ -161,7 +161,7 @@ class TestPaverWatchAssetTasks(TestCase): Test the "compile_sass" task. """ with patch('pavelib.assets.SassWatcher.register') as mock_register: - with patch('pavelib.assets.Observer.start'): + with patch('pavelib.assets.PollingObserver.start'): call_task( 'pavelib.assets.watch_assets', options={"background": True}, @@ -170,7 +170,7 @@ class TestPaverWatchAssetTasks(TestCase): sass_watcher_args = mock_register.call_args_list[0][0] - self.assertIsInstance(sass_watcher_args[0], Observer) + self.assertIsInstance(sass_watcher_args[0], PollingObserver) self.assertIsInstance(sass_watcher_args[1], list) self.assertItemsEqual(sass_watcher_args[1], self.expected_sass_directories) @@ -186,7 +186,7 @@ class TestPaverWatchAssetTasks(TestCase): ]) with patch('pavelib.assets.SassWatcher.register') as mock_register: - with patch('pavelib.assets.Observer.start'): + with patch('pavelib.assets.PollingObserver.start'): call_task( 'pavelib.assets.watch_assets', options={"background": True, "themes_dir": TEST_THEME.dirname(), @@ -195,7 +195,7 @@ class TestPaverWatchAssetTasks(TestCase): self.assertEqual(mock_register.call_count, 2) sass_watcher_args = mock_register.call_args_list[0][0] - self.assertIsInstance(sass_watcher_args[0], Observer) + self.assertIsInstance(sass_watcher_args[0], PollingObserver) self.assertIsInstance(sass_watcher_args[1], list) self.assertItemsEqual(sass_watcher_args[1], self.expected_sass_directories) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index aba4398d942..4a9b621b12f 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -114,7 +114,7 @@ reportlab==3.1.44 pdfminer==20140328 # Used for development operation -watchdog==0.7.1 +watchdog==0.8.3 # Metrics gathering and monitoring dogapi==1.2.1 -- GitLab