diff --git a/src/hupper/__init__.py b/src/hupper/__init__.py index 2511abb..42de293 100644 --- a/src/hupper/__init__.py +++ b/src/hupper/__init__.py @@ -1,7 +1,7 @@ # public api # flake8: noqa -from .compat import is_watchdog_supported +from .utils import is_watchdog_supported from .reloader import ( start_reloader, diff --git a/src/hupper/compat.py b/src/hupper/compat.py index a87021e..648fb01 100644 --- a/src/hupper/compat.py +++ b/src/hupper/compat.py @@ -39,15 +39,6 @@ import pickle -def is_watchdog_supported(): - """ Return ``True`` if watchdog is available.""" - try: - import watchdog - except ImportError: - return False - return True - - ################################################ # cross-compatible metaclass implementation # Copyright (c) 2010-2012 Benjamin Peterson diff --git a/src/hupper/interfaces.py b/src/hupper/interfaces.py index eca0a64..b586ca8 100644 --- a/src/hupper/interfaces.py +++ b/src/hupper/interfaces.py @@ -14,7 +14,7 @@ def trigger_reload(self): class IFileMonitorFactory(with_metaclass(abc.ABCMeta)): - def __call__(self, callback): + def __call__(self, callback, **kw): """ Return an :class:`.IFileMonitor` instance. ``callback`` is a callable to be invoked by the ``IFileMonitor`` diff --git a/src/hupper/ipc.py b/src/hupper/ipc.py index dcd7e82..dc62f12 100644 --- a/src/hupper/ipc.py +++ b/src/hupper/ipc.py @@ -1,6 +1,5 @@ import io import imp -import importlib import os import struct import sys @@ -10,13 +9,7 @@ from .compat import WIN from .compat import pickle from .compat import queue - - -def resolve_spec(spec): - modname, funcname = spec.rsplit('.', 1) - module = importlib.import_module(modname) - func = getattr(module, funcname) - return func +from .utils import resolve_spec if WIN: # pragma: no cover diff --git a/src/hupper/polling.py b/src/hupper/polling.py index c9413ef..9554e02 100644 --- a/src/hupper/polling.py +++ b/src/hupper/polling.py @@ -16,10 +16,10 @@ class PollingFileMonitor(threading.Thread, IFileMonitor): disk. Do not set this too low or it will eat your CPU and kill your drive. """ - def __init__(self, callback, poll_interval=1): + def __init__(self, callback, interval=1, **kw): super(PollingFileMonitor, self).__init__() self.callback = callback - self.poll_interval = poll_interval + self.poll_interval = interval self.paths = set() self.mtimes = {} self.lock = threading.Lock() diff --git a/src/hupper/reloader.py b/src/hupper/reloader.py index b9c1551..19ca3a0 100644 --- a/src/hupper/reloader.py +++ b/src/hupper/reloader.py @@ -1,14 +1,16 @@ from __future__ import print_function from glob import glob +import os import signal import sys import threading import time -from .compat import is_watchdog_supported from .compat import queue from .ipc import ProcessGroup +from .utils import resolve_spec +from .utils import is_watchdog_supported from .worker import ( Worker, is_active, @@ -23,8 +25,9 @@ class FileMonitorProxy(object): when it should reload. """ - def __init__(self, monitor_factory, verbose=1): - self.monitor = monitor_factory(self.file_changed) + monitor = None + + def __init__(self, verbose=1): self.verbose = verbose self.change_event = threading.Event() self.changed_paths = set() @@ -197,7 +200,12 @@ def _wait_for_changes(self): self.monitor.clear_changes() def _start_monitor(self): - self.monitor = FileMonitorProxy(self.monitor_factory, self.verbose) + proxy = FileMonitorProxy(self.verbose) + proxy.monitor = self.monitor_factory( + proxy.file_changed, + interval=self.reload_interval, + ) + self.monitor = proxy self.monitor.start() def _stop_monitor(self): @@ -219,6 +227,29 @@ def _restore_signals(self): signal.signal(signal.SIGHUP, signal.SIG_DFL) +def find_default_monitor_factory(verbose): + spec = os.environ.get('HUPPER_DEFAULT_MONITOR') + if spec: + monitor_factory = resolve_spec(spec) + + if verbose > 1: + print('File monitor backend: ' + spec) + + elif is_watchdog_supported(): + from .watchdog import WatchdogFileMonitor as monitor_factory + + if verbose > 1: + print('File monitor backend: watchdog') + + else: + from .polling import PollingFileMonitor as monitor_factory + + if verbose > 1: + print('File monitor backend: polling') + + return monitor_factory + + def start_reloader( worker_path, reload_interval=1, @@ -254,28 +285,17 @@ def start_reloader( fallback to the less efficient :class:`hupper.polling.PollingFileMonitor` otherwise. + If ``monitor_factory`` is ``None`` it can be overridden by the + ``HUPPER_DEFAULT_MONITOR`` environment variable. It should be a dotted + python path pointing at an object implementing + :class:`hupper.interfaces.IFileMonitorFactory`. + """ if is_active(): return get_reloader() if monitor_factory is None: - if is_watchdog_supported(): - from .watchdog import WatchdogFileMonitor - - def monitor_factory(callback): - return WatchdogFileMonitor(callback) - - if verbose > 1: - print('File monitor backend: watchdog') - - else: - from .polling import PollingFileMonitor - - def monitor_factory(callback): - return PollingFileMonitor(callback, reload_interval) - - if verbose > 1: - print('File monitor backend: polling') + monitor_factory = find_default_monitor_factory(verbose) reloader = Reloader( worker_path=worker_path, diff --git a/src/hupper/utils.py b/src/hupper/utils.py new file mode 100644 index 0000000..dde74f1 --- /dev/null +++ b/src/hupper/utils.py @@ -0,0 +1,17 @@ +import importlib + + +def resolve_spec(spec): + modname, funcname = spec.rsplit('.', 1) + module = importlib.import_module(modname) + func = getattr(module, funcname) + return func + + +def is_watchdog_supported(): + """ Return ``True`` if watchdog is available.""" + try: + import watchdog # noqa: F401 + except ImportError: + return False + return True diff --git a/src/hupper/watchdog.py b/src/hupper/watchdog.py index d153cd3..9174be9 100644 --- a/src/hupper/watchdog.py +++ b/src/hupper/watchdog.py @@ -17,7 +17,7 @@ class WatchdogFileMonitor(FileSystemEventHandler, Observer, IFileMonitor): ``callback`` is a callable that accepts a path to a changed file. """ - def __init__(self, callback): + def __init__(self, callback, **kw): super(WatchdogFileMonitor, self).__init__() self.callback = callback self.paths = set() diff --git a/tests/test_reloader.py b/tests/test_reloader.py index 3bb3bd7..463894a 100644 --- a/tests/test_reloader.py +++ b/tests/test_reloader.py @@ -2,15 +2,17 @@ here = os.path.abspath(os.path.dirname(__file__)) -def make_proxy(*args, **kwargs): +def make_proxy(monitor_factory): from hupper.reloader import FileMonitorProxy - return FileMonitorProxy(*args, **kwargs) + proxy = FileMonitorProxy() + proxy.monitor = monitor_factory(proxy.file_changed) + return proxy def test_proxy_proxies(): class DummyMonitor(object): started = stopped = joined = False - def __call__(self, cb): + def __call__(self, cb, **kw): self.cb = cb return self @@ -34,7 +36,7 @@ def join(self): def test_proxy_expands_paths(tmpdir): class DummyMonitor(object): - def __call__(self, cb): + def __call__(self, cb, **kw): self.cb = cb self.paths = [] return self @@ -59,7 +61,7 @@ def add_path(self, path): def test_proxy_tracks_changes(capsys): class DummyMonitor(object): - def __call__(self, cb): + def __call__(self, cb, **kw): self.cb = cb return self