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