From c570a9806513784341a7cb7caa72f4a8957d4c78 Mon Sep 17 00:00:00 2001 From: ZyX Date: Sat, 28 Jun 2014 19:01:12 +0400 Subject: [PATCH] Add watcher option Ref #818 --- docs/source/configuration.rst | 13 +++++ powerline/__init__.py | 46 +++++++++++----- powerline/lib/config.py | 41 +++++++++++++- powerline/lib/file_watcher.py | 52 ++++++++++++------ powerline/lib/vcs/__init__.py | 36 +++++++------ powerline/lib/vcs/bzr.py | 20 +++++-- powerline/lib/vcs/git.py | 97 ++++++++++++++++------------------ powerline/lib/vcs/mercurial.py | 22 ++++++-- powerline/lint/__init__.py | 5 +- powerline/lint/inspect.py | 2 + powerline/segment.py | 14 +++-- powerline/segments/common.py | 7 +-- powerline/segments/vim.py | 12 +++-- powerline/theme.py | 7 ++- tests/lib/config_mock.py | 7 ++- tests/test_config_reload.py | 1 + tests/test_configuration.py | 3 +- tests/test_lib.py | 23 ++++---- tests/test_segments.py | 60 +++++++++++++-------- 19 files changed, 313 insertions(+), 155 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 0ab55695..b8e70ce6 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -128,6 +128,19 @@ Common configuration is a subdictionary that is a value of ``common`` key in letters, Cyrillic letters). Valid values: any positive integer; it is suggested that you only set it to 1 (default) or 2. +``watcher`` + Select filesystem watcher. Variants are + + ======= =================================== + Variant Description + ======= =================================== + auto Selects most performant watcher. + inotify Select inotify watcher. Linux only. + stat Select stat-based polling watcher. + ======= =================================== + + Default is ``auto``. + .. _config-common-additional_escapes: ``additional_escapes`` diff --git a/powerline/__init__.py b/powerline/__init__.py index bc023f61..83678be4 100644 --- a/powerline/__init__.py +++ b/powerline/__init__.py @@ -63,7 +63,7 @@ class PowerlineLogger(object): _fallback_logger = None -def _get_fallback_logger(): +def get_fallback_logger(): global _fallback_logger if _fallback_logger: return _fallback_logger @@ -179,15 +179,31 @@ class Powerline(object): self.common_config = config['common'] if self.common_config != self.prev_common_config: common_config_differs = True + self.prev_common_config = self.common_config - self.common_config['paths'] = [os.path.expanduser(path) for path in self.common_config.get('paths', [])] + + self.common_config.setdefault('paths', []) + self.common_config.setdefault('watcher', 'auto') + self.common_config.setdefault('log_level', 'WARNING') + self.common_config.setdefault('log_format', '%(asctime)s:%(levelname)s:%(message)s') + self.common_config.setdefault('term_truecolor', False) + self.common_config.setdefault('ambiwidth', 1) + self.common_config.setdefault('additional_escapes', None) + self.common_config.setdefault('reload_config', True) + self.common_config.setdefault('interval', None) + self.common_config.setdefault('log_file', None) + + self.common_config['paths'] = [ + os.path.expanduser(path) for path in self.common_config['paths'] + ] + self.import_paths = self.common_config['paths'] if not self.logger: - log_format = self.common_config.get('log_format', '%(asctime)s:%(levelname)s:%(message)s') + log_format = self.common_config['log_format'] formatter = logging.Formatter(log_format) - level = getattr(logging, self.common_config.get('log_level', 'WARNING')) + level = getattr(logging, self.common_config['log_level']) handler = self.get_log_handler() handler.setLevel(level) handler.setFormatter(formatter) @@ -198,15 +214,17 @@ class Powerline(object): if not self.pl: self.pl = PowerlineLogger(self.use_daemon_threads, self.logger, self.ext) - if not self.config_loader.pl: - self.config_loader.pl = self.pl + self.config_loader.pl = self.pl + + if not self.run_once: + self.config_loader.set_watcher(self.common_config['watcher']) self.renderer_options.update( pl=self.pl, - term_truecolor=self.common_config.get('term_truecolor', False), - ambiwidth=self.common_config.get('ambiwidth', 1), - tmux_escape=self.common_config.get('additional_escapes') == 'tmux', - screen_escape=self.common_config.get('additional_escapes') == 'screen', + term_truecolor=self.common_config['term_truecolor'], + ambiwidth=self.common_config['ambiwidth'], + tmux_escape=self.common_config['additional_escapes'] == 'tmux', + screen_escape=self.common_config['additional_escapes'] == 'screen', theme_kwargs={ 'ext': self.ext, 'common_config': self.common_config, @@ -215,8 +233,8 @@ class Powerline(object): }, ) - if not self.run_once and self.common_config.get('reload_config', True): - interval = self.common_config.get('interval', None) + if not self.run_once and self.common_config['reload_config']: + interval = self.common_config['interval'] self.config_loader.set_interval(interval) self.run_loader_update = (interval is None) if interval is not None and not self.config_loader.is_alive(): @@ -278,7 +296,7 @@ class Powerline(object): :return: logging.Handler subclass. ''' - log_file = self.common_config.get('log_file', None) + log_file = self.common_config['log_file'] if log_file: log_file = os.path.expanduser(log_file) log_dir = os.path.dirname(log_file) @@ -473,5 +491,5 @@ class Powerline(object): def exception(self, msg, *args, **kwargs): if 'prefix' not in kwargs: kwargs['prefix'] = 'powerline' - pl = getattr(self, 'pl', None) or _get_fallback_logger() + pl = getattr(self, 'pl', None) or get_fallback_logger() return pl.exception(msg, *args, **kwargs) diff --git a/powerline/lib/config.py b/powerline/lib/config.py index de2d9902..8dd50afe 100644 --- a/powerline/lib/config.py +++ b/powerline/lib/config.py @@ -28,14 +28,41 @@ class DummyWatcher(object): pass +class DeferredWatcher(object): + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.calls = [] + + def __call__(self, *args, **kwargs): + self.calls.append(('__call__', args, kwargs)) + + def watch(self, *args, **kwargs): + self.calls.append(('watch', args, kwargs)) + + def unwatch(self, *args, **kwargs): + self.calls.append(('unwatch', args, kwargs)) + + def transfer_calls(self, watcher): + for attr, args, kwargs in self.calls: + getattr(watcher, attr)(*args, **kwargs) + + class ConfigLoader(MultiRunnedThread): - def __init__(self, shutdown_event=None, watcher=None, load=load_json_config, run_once=False): + def __init__(self, shutdown_event=None, watcher=None, watcher_type=None, load=load_json_config, run_once=False): super(ConfigLoader, self).__init__() self.shutdown_event = shutdown_event or Event() if run_once: self.watcher = DummyWatcher() + self.watcher_type = 'dummy' else: - self.watcher = watcher or create_file_watcher() + self.watcher = watcher or DeferredWatcher() + if watcher: + if not watcher_type: + raise ValueError('When specifying watcher you must also specify watcher type') + self.watcher_type = watcher_type + else: + self.watcher_type = 'deferred' self._load = load self.pl = None @@ -47,6 +74,16 @@ class ConfigLoader(MultiRunnedThread): self.missing = defaultdict(set) self.loaded = {} + def set_watcher(self, watcher_type, force=False): + if watcher_type == self.watcher_type: + return + watcher = create_file_watcher(self.pl, watcher_type) + with self.lock: + if self.watcher_type == 'deferred': + self.watcher.transfer_calls(watcher) + self.watcher = watcher + self.watcher_type = watcher_type + def set_pl(self, pl): self.pl = pl diff --git a/powerline/lib/file_watcher.py b/powerline/lib/file_watcher.py index 21524e3b..ffb337f2 100644 --- a/powerline/lib/file_watcher.py +++ b/powerline/lib/file_watcher.py @@ -19,8 +19,6 @@ def realpath(path): class INotifyWatch(INotify): - is_stat_based = False - def __init__(self, expire_time=10): super(INotifyWatch, self).__init__() self.watches = {} @@ -147,8 +145,6 @@ class INotifyWatch(INotify): class StatWatch(object): - is_stat_based = True - def __init__(self): self.watches = {} self.lock = RLock() @@ -184,29 +180,51 @@ class StatWatch(object): self.watches.clear() -def create_file_watcher(use_stat=False, expire_time=10): +def create_file_watcher(pl, watcher_type='auto', expire_time=10): ''' - Create an object that can watch for changes to specified files. To use: + Create an object that can watch for changes to specified files - watcher = create_file_watcher() - watcher(path1) # Will return True if path1 has changed since the last time this was called. Always returns True the first time. - watcher.unwatch(path1) + Use ``.__call__()`` method of the returned object to start watching the file + or check whether file has changed since last call. - Uses inotify if available, otherwise tracks mtimes. expire_time is the - number of minutes after the last query for a given path for the inotify - watch for that path to be automatically removed. This conserves kernel + Use ``.unwatch()`` method of the returned object to stop watching the file. + + Uses inotify if available, otherwise tracks mtimes. expire_time is the + number of minutes after the last query for a given path for the inotify + watch for that path to be automatically removed. This conserves kernel resources. + + :param PowerlineLogger pl: + Logger. + :param str watcher_type: + One of ``inotify`` (linux only), ``stat``, ``auto``. Determines what + watcher will be used. ``auto`` will use ``inotify`` if available. + :param int expire_time: + Number of minutes since last ``.__call__()`` before inotify watcher will + stop watching given file. ''' - if use_stat: + if watcher_type == 'stat': + pl.debug('Using requested stat-based watcher', prefix='watcher') return StatWatch() - try: + if watcher_type == 'inotify': + # Explicitly selected inotify watcher: do not catch INotifyError then. + pl.debug('Using requested inotify watcher', prefix='watcher') return INotifyWatch(expire_time=expire_time) - except INotifyError: - pass + + if sys.platform.startswith('linux'): + try: + pl.debug('Trying to use inotify watcher', prefix='watcher') + return INotifyWatch(expire_time=expire_time) + except INotifyError: + pl.info('Failed to create inotify watcher', prefix='watcher') + + pl.debug('Using stat-based watcher') return StatWatch() + if __name__ == '__main__': - watcher = create_file_watcher() + from powerline import get_fallback_logger + watcher = create_file_watcher(get_fallback_logger()) print ('Using watcher: %s' % watcher.__class__.__name__) print ('Watching %s, press Ctrl-C to quit' % sys.argv[-1]) watcher.watch(sys.argv[-1]) diff --git a/powerline/lib/vcs/__init__.py b/powerline/lib/vcs/__init__.py index ff971c5d..1029bc3f 100644 --- a/powerline/lib/vcs/__init__.py +++ b/powerline/lib/vcs/__init__.py @@ -23,22 +23,20 @@ def generate_directories(path): _file_watcher = None -def file_watcher(): +def file_watcher(create_watcher): global _file_watcher if _file_watcher is None: - from powerline.lib.file_watcher import create_file_watcher - _file_watcher = create_file_watcher() + _file_watcher = create_watcher() return _file_watcher _branch_watcher = None -def branch_watcher(): +def branch_watcher(create_watcher): global _branch_watcher if _branch_watcher is None: - from powerline.lib.file_watcher import create_file_watcher - _branch_watcher = create_file_watcher() + _branch_watcher = create_watcher() return _branch_watcher @@ -47,11 +45,11 @@ branch_lock = Lock() file_status_lock = Lock() -def get_branch_name(directory, config_file, get_func): +def get_branch_name(directory, config_file, get_func, create_watcher): global branch_name_cache with branch_lock: # Check if the repo directory was moved/deleted - fw = branch_watcher() + fw = branch_watcher(create_watcher) is_watched = fw.is_watched(directory) try: changed = fw(directory) @@ -117,7 +115,7 @@ class FileStatusCache(dict): file_status_cache = FileStatusCache() -def get_file_status(directory, dirstate_file, file_path, ignore_file_name, get_func, extra_ignore_files=()): +def get_file_status(directory, dirstate_file, file_path, ignore_file_name, get_func, create_watcher, extra_ignore_files=()): global file_status_cache keypath = file_path if os.path.isabs(file_path) else os.path.join(directory, file_path) file_status_cache.update_maps(keypath, directory, dirstate_file, ignore_file_name, extra_ignore_files) @@ -129,7 +127,7 @@ def get_file_status(directory, dirstate_file, file_path, ignore_file_name, get_f return ans # Check if any relevant files have changed - file_changed = file_watcher() + file_changed = file_watcher(create_watcher) changed = False # Check if dirstate has changed try: @@ -217,7 +215,7 @@ vcs_props = ( ) -def guess(path): +def guess(path, create_watcher): for directory in generate_directories(path): for vcs, vcs_dir, check in vcs_props: repo_dir = os.path.join(directory, vcs_dir) @@ -227,20 +225,28 @@ def guess(path): try: if vcs not in globals(): globals()[vcs] = getattr(__import__('powerline.lib.vcs', fromlist=[vcs]), vcs) - return globals()[vcs].Repository(directory) + return globals()[vcs].Repository(directory, create_watcher) except: pass return None +def get_fallback_create_watcher(): + from powerline.lib.file_watcher import create_file_watcher + from powerline import get_fallback_logger + from functools import partial + return partial(create_file_watcher, get_fallback_logger(), 'auto') + + def debug(): '''Test run guess(), repo.branch() and repo.status() - To use run python -c "from powerline.lib.vcs import debug; debug()" - some_file_to_watch ''' + To use:: + python -c "from powerline.lib.vcs import debug; debug()" some_file_to_watch. + ''' import sys dest = sys.argv[-1] - repo = guess(os.path.abspath(dest)) + repo = guess(os.path.abspath(dest), get_fallback_create_watcher) if repo is None: print ('%s is not a recognized vcs repo' % dest) raise SystemExit(1) diff --git a/powerline/lib/vcs/bzr.py b/powerline/lib/vcs/bzr.py index 9734572b..f8818bdc 100644 --- a/powerline/lib/vcs/bzr.py +++ b/powerline/lib/vcs/bzr.py @@ -39,10 +39,11 @@ state = None class Repository(object): - def __init__(self, directory): + def __init__(self, directory, create_watcher): if isinstance(directory, bytes): directory = directory.decode(sys.getfilesystemencoding() or sys.getdefaultencoding() or 'utf-8') self.directory = os.path.abspath(directory) + self.create_watcher = create_watcher def status(self, path=None): '''Return status of repository or file. @@ -57,8 +58,14 @@ class Repository(object): those returned by bzr status -S ''' if path is not None: - return get_file_status(self.directory, os.path.join(self.directory, '.bzr', 'checkout', 'dirstate'), - path, '.bzrignore', self.do_status) + return get_file_status( + directory=self.directory, + dirstate_file=os.path.join(self.directory, '.bzr', 'checkout', 'dirstate'), + file_path=path, + ignore_file_name='.bzrignore', + get_func=self.do_status, + create_watcher=self.create_watcher, + ) return self.do_status(self.directory, path) def do_status(self, directory, path): @@ -93,4 +100,9 @@ class Repository(object): def branch(self): config_file = os.path.join(self.directory, '.bzr', 'branch', 'branch.conf') - return get_branch_name(self.directory, config_file, branch_name_from_config_file) + return get_branch_name( + directory=self.directory, + config_file=config_file, + get_func=branch_name_from_config_file, + create_watcher=self.create_watcher, + ) diff --git a/powerline/lib/vcs/git.py b/powerline/lib/vcs/git.py index 5339ca4e..3bf60562 100644 --- a/powerline/lib/vcs/git.py +++ b/powerline/lib/vcs/git.py @@ -6,7 +6,7 @@ import os import sys import re -from powerline.lib.vcs import get_branch_name as _get_branch_name, get_file_status +from powerline.lib.vcs import get_branch_name, get_file_status from powerline.lib.shell import readlines @@ -40,32 +40,57 @@ def git_directory(directory): return path -def get_branch_name(base_dir): - head = os.path.join(git_directory(base_dir), 'HEAD') - return _get_branch_name(base_dir, head, branch_name_from_config_file) +class GitRepository(object): + __slots__ = ('directory', 'create_watcher') + def __init__(self, directory, create_watcher): + self.directory = os.path.abspath(directory) + self.create_watcher = create_watcher -def do_status(directory, path, func): - if path: - gitd = git_directory(directory) - # We need HEAD as without it using fugitive to commit causes the - # current file's status (and only the current file) to not be updated - # for some reason I cannot be bothered to figure out. - return get_file_status( - directory, os.path.join(gitd, 'index'), - path, '.gitignore', func, extra_ignore_files=tuple(os.path.join(gitd, x) for x in ('logs/HEAD', 'info/exclude'))) - return func(directory, path) + def status(self, path=None): + '''Return status of repository or file. + + Without file argument: returns status of the repository: + + :First column: working directory status (D: dirty / space) + :Second column: index status (I: index dirty / space) + :Third column: presence of untracked files (U: untracked files / space) + :None: repository clean + + With file argument: returns status of this file. Output is + equivalent to the first two columns of "git status --porcelain" + (except for merge statuses as they are not supported by libgit2). + ''' + if path: + gitd = git_directory(self.directory) + # We need HEAD as without it using fugitive to commit causes the + # current file's status (and only the current file) to not be updated + # for some reason I cannot be bothered to figure out. + return get_file_status( + directory=self.directory, + dirstate_file=os.path.join(gitd, 'index'), + file_path=path, + ignore_file_name='.gitignore', + get_func=self.do_status, + create_watcher=self.create_watcher, + extra_ignore_files=tuple(os.path.join(gitd, x) for x in ('logs/HEAD', 'info/exclude')), + ) + return self.do_status(self.directory, path) + + def branch(self): + head = os.path.join(git_directory(self.directory), 'HEAD') + return get_branch_name( + directory=self.directory, + config_file=head, + get_func=branch_name_from_config_file, + create_watcher=self.create_watcher, + ) try: import pygit2 as git - class Repository(object): - __slots__ = ('directory',) - - def __init__(self, directory): - self.directory = os.path.abspath(directory) - + class Repository(GitRepository): @staticmethod def ignore_event(path, name): return False @@ -121,32 +146,8 @@ try: index_column = 'I' r = wt_column + index_column + untracked_column return r if r != ' ' else None - - def status(self, path=None): - '''Return status of repository or file. - - Without file argument: returns status of the repository: - - :First column: working directory status (D: dirty / space) - :Second column: index status (I: index dirty / space) - :Third column: presence of untracked files (U: untracked files / space) - :None: repository clean - - With file argument: returns status of this file. Output is - equivalent to the first two columns of "git status --porcelain" - (except for merge statuses as they are not supported by libgit2). - ''' - return do_status(self.directory, path, self.do_status) - - def branch(self): - return get_branch_name(self.directory) except ImportError: - class Repository(object): - __slots__ = ('directory',) - - def __init__(self, directory): - self.directory = os.path.abspath(directory) - + class Repository(GitRepository): @staticmethod def ignore_event(path, name): # Ignore changes to the index.lock file, since they happen @@ -182,9 +183,3 @@ except ImportError: r = wt_column + index_column + untracked_column return r if r != ' ' else None - - def status(self, path=None): - return do_status(self.directory, path, self.do_status) - - def branch(self): - return get_branch_name(self.directory) diff --git a/powerline/lib/vcs/mercurial.py b/powerline/lib/vcs/mercurial.py index 2aa890cd..cebd7960 100644 --- a/powerline/lib/vcs/mercurial.py +++ b/powerline/lib/vcs/mercurial.py @@ -18,15 +18,16 @@ def branch_name_from_config_file(directory, config_file): class Repository(object): - __slots__ = ('directory', 'ui') + __slots__ = ('directory', 'ui', 'create_watcher') statuses = 'MARDUI' repo_statuses = (1, 1, 1, 1, 2) repo_statuses_str = (None, 'D ', ' U', 'DU') - def __init__(self, directory): + def __init__(self, directory, create_watcher): self.directory = os.path.abspath(directory) self.ui = ui.ui() + self.create_watcher = create_watcher def _repo(self, directory): # Cannot create this object once and use always: when repository updates @@ -47,8 +48,14 @@ class Repository(object): "U"nknown, "I"gnored, (None)Clean. ''' if path: - return get_file_status(self.directory, os.path.join(self.directory, '.hg', 'dirstate'), - path, '.hgignore', self.do_status) + return get_file_status( + directory=self.directory, + dirstate_file=os.path.join(self.directory, '.hg', 'dirstate'), + file_path=path, + ignore_file_name='.hgignore', + get_func=self.do_status, + create_watcher=self.create_watcher, + ) return self.do_status(self.directory, path) def do_status(self, directory, path): @@ -69,4 +76,9 @@ class Repository(object): def branch(self): config_file = os.path.join(self.directory, '.hg', 'branch') - return get_branch_name(self.directory, config_file, branch_name_from_config_file) + return get_branch_name( + directory=self.directory, + config_file=config_file, + get_func=branch_name_from_config_file, + create_watcher=self.create_watcher, + ) diff --git a/powerline/lint/__init__.py b/powerline/lint/__init__.py index 9f73b9a8..928f144e 100644 --- a/powerline/lint/__init__.py +++ b/powerline/lint/__init__.py @@ -460,13 +460,14 @@ main_spec = (Spec( # only for existence of the path, not for it being a directory paths=Spec().list((lambda value, *args: (True, True, not os.path.exists(os.path.expanduser(value.value)))), lambda value: 'path does not exist: {0}'.format(value)).optional(), - log_file=Spec().type(str).func(lambda value, *args: (True, True, not os.path.isdir(os.path.dirname(os.path.expanduser(value)))), + log_file=Spec().type(unicode).func(lambda value, *args: (True, True, not os.path.isdir(os.path.dirname(os.path.expanduser(value)))), lambda value: 'directory does not exist: {0}'.format(os.path.dirname(value))).optional(), log_level=Spec().re('^[A-Z]+$').func(lambda value, *args: (True, True, not hasattr(logging, value)), lambda value: 'unknown debugging level {0}'.format(value)).optional(), - log_format=Spec().type(str).optional(), + log_format=Spec().type(unicode).optional(), interval=Spec().either(Spec().cmp('gt', 0.0), Spec().type(type(None))).optional(), reload_config=Spec().type(bool).optional(), + watcher=Spec().type(unicode).oneof(set(('auto', 'inotify', 'stat'))).optional(), ).context_message('Error while loading common configuration (key {key})'), ext=Spec( vim=Spec( diff --git a/powerline/lint/inspect.py b/powerline/lint/inspect.py index 294f2f59..345f1a5c 100644 --- a/powerline/lint/inspect.py +++ b/powerline/lint/inspect.py @@ -25,6 +25,8 @@ def getconfigargspec(obj): if (arg == 'self' or (arg == 'segment_info' and getattr(obj, 'powerline_requires_segment_info', None)) or + (arg == 'create_watcher' and + getattr(obj, 'powerline_requires_filesystem_watcher', None)) or (arg == 'pl') or (method.startswith('render') and (1 if argspec.args[0] == 'self' else 0) + i == len(argspec.args)) or arg in args): diff --git a/powerline/segment.py b/powerline/segment.py index b842c62a..cf495c87 100644 --- a/powerline/segment.py +++ b/powerline/segment.py @@ -1,6 +1,8 @@ # vim:fileencoding=utf-8:noet -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals, division, print_function + +from powerline.lib.file_watcher import create_file_watcher import sys @@ -52,15 +54,15 @@ def get_attr_func(contents_func, key, args): return None else: if args is None: - return lambda : func() + return lambda: func() else: return lambda pl, shutdown_event: func(pl=pl, shutdown_event=shutdown_event, **args) -def gen_segment_getter(pl, ext, path, theme_configs, default_module=None): +def gen_segment_getter(pl, ext, common_config, theme_configs, default_module=None): data = { 'default_module': default_module or 'powerline.segments.' + ext, - 'path': path, + 'path': common_config['paths'], } def get_key(segment, module, key, default=None): @@ -90,6 +92,10 @@ def gen_segment_getter(pl, ext, path, theme_configs, default_module=None): startup_func = get_attr_func(_contents_func, 'startup', args) shutdown_func = get_attr_func(_contents_func, 'shutdown', None) + if hasattr(_contents_func, 'powerline_requires_filesystem_watcher'): + create_watcher = lambda: create_file_watcher(pl, common_config['watcher']) + args['create_watcher'] = create_watcher + if hasattr(_contents_func, 'powerline_requires_segment_info'): contents_func = lambda pl, segment_info: _contents_func(pl=pl, segment_info=segment_info, **args) else: diff --git a/powerline/segments/common.py b/powerline/segments/common.py index 5c22416c..775b38e2 100644 --- a/powerline/segments/common.py +++ b/powerline/segments/common.py @@ -18,7 +18,7 @@ from powerline.lib.threaded import ThreadedSegment, KwThreadedSegment, with_docs from powerline.lib.monotonic import monotonic from powerline.lib.humanize_bytes import humanize_bytes from powerline.lib.unicode import u -from powerline.theme import requires_segment_info +from powerline.theme import requires_segment_info, requires_filesystem_watcher from collections import namedtuple @@ -51,8 +51,9 @@ def hostname(pl, segment_info, only_if_ssh=False, exclude_domain=False): return socket.gethostname() +@requires_filesystem_watcher @requires_segment_info -def branch(pl, segment_info, status_colors=False): +def branch(pl, segment_info, create_watcher, status_colors=False): '''Return the current VCS branch. :param bool status_colors: @@ -61,7 +62,7 @@ def branch(pl, segment_info, status_colors=False): Highlight groups used: ``branch_clean``, ``branch_dirty``, ``branch``. ''' name = segment_info['getcwd']() - repo = guess(path=name) + repo = guess(path=name, create_watcher=create_watcher) if repo is not None: branch = repo.branch() scol = ['branch'] diff --git a/powerline/segments/vim.py b/powerline/segments/vim.py index 9161c729..f1647dee 100644 --- a/powerline/segments/vim.py +++ b/powerline/segments/vim.py @@ -10,7 +10,7 @@ except ImportError: from powerline.bindings.vim import (vim_get_func, getbufvar, vim_getbufoption, buffer_name, vim_getwinvar) -from powerline.theme import requires_segment_info +from powerline.theme import requires_segment_info, requires_filesystem_watcher from powerline.lib import add_divider_highlight_group from powerline.lib.vcs import guess, tree_status from powerline.lib.humanize_bytes import humanize_bytes @@ -368,8 +368,9 @@ def modified_buffers(pl, text='+ ', join_str=','): return None +@requires_filesystem_watcher @requires_segment_info -def branch(pl, segment_info, status_colors=False): +def branch(pl, segment_info, create_watcher, status_colors=False): '''Return the current working branch. :param bool status_colors: @@ -382,7 +383,7 @@ def branch(pl, segment_info, status_colors=False): name = segment_info['buffer'].name skip = not (name and (not vim_getbufoption(segment_info, 'buftype'))) if not skip: - repo = guess(path=name) + repo = guess(path=name, create_watcher=create_watcher) if repo is not None: branch = repo.branch() scol = ['branch'] @@ -396,8 +397,9 @@ def branch(pl, segment_info, status_colors=False): }] +@requires_filesystem_watcher @requires_segment_info -def file_vcs_status(pl, segment_info): +def file_vcs_status(pl, segment_info, create_watcher): '''Return the VCS status for this buffer. Highlight groups used: ``file_vcs_status``. @@ -405,7 +407,7 @@ def file_vcs_status(pl, segment_info): name = segment_info['buffer'].name skip = not (name and (not vim_getbufoption(segment_info, 'buftype'))) if not skip: - repo = guess(path=name) + repo = guess(path=name, create_watcher=create_watcher) if repo is not None: status = repo.status(os.path.relpath(name, repo.directory)) if not status: diff --git a/powerline/theme.py b/powerline/theme.py index c6157e8f..6450fab3 100644 --- a/powerline/theme.py +++ b/powerline/theme.py @@ -10,6 +10,11 @@ def requires_segment_info(func): return func +def requires_filesystem_watcher(func): + func.powerline_requires_filesystem_watcher = True + return func + + def new_empty_segment_line(): return { 'left': [], @@ -37,7 +42,7 @@ class Theme(object): theme_configs = [theme_config] if top_theme_config: theme_configs.append(top_theme_config) - get_segment = gen_segment_getter(pl, ext, common_config['paths'], theme_configs, theme_config.get('default_module')) + get_segment = gen_segment_getter(pl, ext, common_config, theme_configs, theme_config.get('default_module')) for segdict in itertools.chain((theme_config['segments'],), theme_config['segments'].get('above', ())): self.segments.append(new_empty_segment_line()) diff --git a/tests/lib/config_mock.py b/tests/lib/config_mock.py index 2ad78853..f4f50624 100644 --- a/tests/lib/config_mock.py +++ b/tests/lib/config_mock.py @@ -138,7 +138,12 @@ def get_powerline_raw(PowerlineClass, **kwargs): else: renderer = SimpleRenderer pl = PowerlineClass( - config_loader=ConfigLoader(load=load_json_config, watcher=watcher, run_once=kwargs.get('run_once')), + config_loader=ConfigLoader( + load=load_json_config, + watcher=watcher, + watcher_type='test', + run_once=kwargs.get('run_once') + ), **kwargs ) pl._watcher = watcher diff --git a/tests/test_config_reload.py b/tests/test_config_reload.py index f3ce58aa..4d33128e 100644 --- a/tests/test_config_reload.py +++ b/tests/test_config_reload.py @@ -23,6 +23,7 @@ config = { }, 'spaces': 0, 'interval': 0, + 'watcher': 'test', }, 'ext': { 'test': { diff --git a/tests/test_configuration.py b/tests/test_configuration.py index d6e64f40..343f90ba 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -4,7 +4,7 @@ import tests.vim as vim_module import powerline as powerline_module from tests import TestCase from tests.lib import replace_item -from tests.lib.config_mock import swap_attributes, get_powerline, pop_events +from tests.lib.config_mock import swap_attributes, get_powerline from tests.lib.config_mock import get_powerline_raw from functools import wraps from copy import deepcopy @@ -27,6 +27,7 @@ config = { }, 'spaces': 0, 'interval': 0, + 'watcher': 'test', }, 'ext': { 'test': { diff --git a/tests/test_lib.py b/tests/test_lib.py index 6c306615..0bbae4e4 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -3,9 +3,11 @@ from __future__ import division from powerline.lib import mergedicts, add_divider_highlight_group, REMOVE_THIS_KEY from powerline.lib.humanize_bytes import humanize_bytes -from powerline.lib.vcs import guess +from powerline.lib.vcs import guess, get_fallback_create_watcher from powerline.lib.threaded import ThreadedSegment, KwThreadedSegment from powerline.lib.monotonic import monotonic +from powerline.lib.file_watcher import create_file_watcher, INotifyError +from powerline import get_fallback_logger import threading import os import sys @@ -384,9 +386,9 @@ class TestFilesystemWatchers(TestCase): self.fail('The change to {0} was not detected'.format(path)) def test_file_watcher(self): - from powerline.lib.file_watcher import create_file_watcher - w = create_file_watcher(use_stat=False) - if w.is_stat_based: + try: + w = create_file_watcher(pl=get_fallback_logger(), watcher_type='inotify') + except INotifyError: raise SkipTest('This test is not suitable for a stat based file watcher') f1, f2, f3 = map(lambda x: os.path.join(INOTIFY_DIR, 'file%d' % x), (1, 2, 3)) with open(f1, 'wb'): @@ -485,7 +487,8 @@ class TestVCS(TestCase): self.assertEqual(ans, q) def test_git(self): - repo = guess(path=GIT_REPO) + create_watcher = get_fallback_create_watcher() + repo = guess(path=GIT_REPO, create_watcher=create_watcher) self.assertNotEqual(repo, None) self.assertEqual(repo.branch(), 'master') self.assertEqual(repo.status(), None) @@ -520,7 +523,8 @@ class TestVCS(TestCase): if use_mercurial: def test_mercurial(self): - repo = guess(path=HG_REPO) + create_watcher = get_fallback_create_watcher() + repo = guess(path=HG_REPO, create_watcher=create_watcher) self.assertNotEqual(repo, None) self.assertEqual(repo.branch(), 'default') self.assertEqual(repo.status(), None) @@ -536,7 +540,8 @@ class TestVCS(TestCase): if use_bzr: def test_bzr(self): - repo = guess(path=BZR_REPO) + create_watcher = get_fallback_create_watcher() + repo = guess(path=BZR_REPO, create_watcher=create_watcher) self.assertNotEqual(repo, None, 'No bzr repo found. Do you have bzr installed?') self.assertEqual(repo.branch(), 'test_powerline') self.assertEqual(repo.status(), None) @@ -587,7 +592,7 @@ class TestVCS(TestCase): os.mkdir(d) call(['bzr', 'init', '-q'], cwd=d) call(['bzr', 'nick', '-q', x], cwd=d) - repo = guess(path=d) + repo = guess(path=d, create_watcher=create_watcher) self.assertEqual(repo.branch(), x) self.assertFalse(repo.status()) if x == 'b1': @@ -598,7 +603,7 @@ class TestVCS(TestCase): os.rename(os.path.join(BZR_REPO, 'b'), os.path.join(BZR_REPO, 'b2')) for x, y in (('b1', 'b2'), ('b2', 'b1')): d = os.path.join(BZR_REPO, x) - repo = guess(path=d) + repo = guess(path=d, create_watcher=create_watcher) self.do_branch_rename_test(repo, y) if x == 'b1': self.assertFalse(repo.status()) diff --git a/tests/test_segments.py b/tests/test_segments.py index ddd264f2..497a58ab 100644 --- a/tests/test_segments.py +++ b/tests/test_segments.py @@ -3,9 +3,11 @@ from __future__ import unicode_literals from powerline.segments import shell, common +from powerline.lib.vcs import get_fallback_create_watcher import tests.vim as vim_module import sys import os +from functools import partial from tests.lib import Args, urllib_read, replace_attr, new_module, replace_module_module, replace_env, Pl from tests import TestCase @@ -13,6 +15,16 @@ from tests import TestCase vim = None +def get_dummy_guess(**kwargs): + if 'directory' in kwargs: + def guess(path, create_watcher): + return Args(branch=lambda: os.path.basename(path), **kwargs) + else: + def guess(path, create_watcher): + return Args(branch=lambda: os.path.basename(path), directory=path, **kwargs) + return guess + + class TestShell(TestCase): def test_last_status(self): pl = Pl() @@ -201,23 +213,25 @@ class TestCommon(TestCase): def test_branch(self): pl = Pl() + create_watcher = get_fallback_create_watcher() segment_info = {'getcwd': os.getcwd} - with replace_attr(common, 'guess', lambda path: Args(branch=lambda: os.path.basename(path), status=lambda: None, directory='/tmp/tests')): + branch = partial(common.branch, pl=pl, create_watcher=create_watcher) + with replace_attr(common, 'guess', get_dummy_guess(status=lambda: None, directory='/tmp/tests')): with replace_attr(common, 'tree_status', lambda repo, pl: None): - self.assertEqual(common.branch(pl=pl, segment_info=segment_info, status_colors=False), + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [{'highlight_group': ['branch'], 'contents': 'tests'}]) - self.assertEqual(common.branch(pl=pl, segment_info=segment_info, status_colors=True), + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [{'contents': 'tests', 'highlight_group': ['branch_clean', 'branch']}]) - with replace_attr(common, 'guess', lambda path: Args(branch=lambda: os.path.basename(path), status=lambda: 'D ', directory='/tmp/tests')): + with replace_attr(common, 'guess', get_dummy_guess(status=lambda: 'D ', directory='/tmp/tests')): with replace_attr(common, 'tree_status', lambda repo, pl: 'D '): - self.assertEqual(common.branch(pl=pl, segment_info=segment_info, status_colors=False), + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [{'highlight_group': ['branch'], 'contents': 'tests'}]) - self.assertEqual(common.branch(pl=pl, segment_info=segment_info, status_colors=True), + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [{'contents': 'tests', 'highlight_group': ['branch_dirty', 'branch']}]) - self.assertEqual(common.branch(pl=pl, segment_info=segment_info, status_colors=False), + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [{'highlight_group': ['branch'], 'contents': 'tests'}]) - with replace_attr(common, 'guess', lambda path: None): - self.assertEqual(common.branch(pl=pl, segment_info=segment_info, status_colors=False), None) + with replace_attr(common, 'guess', lambda path, create_watcher: None): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), None) def test_cwd(self): new_os = new_module('os', path=os.path, sep='/') @@ -705,32 +719,36 @@ class TestVim(TestCase): def test_branch(self): pl = Pl() + create_watcher = get_fallback_create_watcher() + branch = partial(vim.branch, pl=pl, create_watcher=create_watcher) with vim_module._with('buffer', '/foo') as segment_info: - with replace_attr(vim, 'guess', lambda path: Args(branch=lambda: os.path.basename(path), status=lambda: None, directory=path)): + with replace_attr(vim, 'guess', get_dummy_guess(status=lambda: None)): with replace_attr(vim, 'tree_status', lambda repo, pl: None): - self.assertEqual(vim.branch(pl=pl, segment_info=segment_info, status_colors=False), + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [{'divider_highlight_group': 'branch:divider', 'highlight_group': ['branch'], 'contents': 'foo'}]) - self.assertEqual(vim.branch(pl=pl, segment_info=segment_info, status_colors=True), + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [{'divider_highlight_group': 'branch:divider', 'highlight_group': ['branch_clean', 'branch'], 'contents': 'foo'}]) - with replace_attr(vim, 'guess', lambda path: Args(branch=lambda: os.path.basename(path), status=lambda: 'DU', directory=path)): + with replace_attr(vim, 'guess', get_dummy_guess(status=lambda: 'DU')): with replace_attr(vim, 'tree_status', lambda repo, pl: 'DU'): - self.assertEqual(vim.branch(pl=pl, segment_info=segment_info, status_colors=False), + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [{'divider_highlight_group': 'branch:divider', 'highlight_group': ['branch'], 'contents': 'foo'}]) - self.assertEqual(vim.branch(pl=pl, segment_info=segment_info, status_colors=True), + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [{'divider_highlight_group': 'branch:divider', 'highlight_group': ['branch_dirty', 'branch'], 'contents': 'foo'}]) def test_file_vcs_status(self): pl = Pl() + create_watcher = get_fallback_create_watcher() + file_vcs_status = partial(vim.file_vcs_status, pl=pl, create_watcher=create_watcher) with vim_module._with('buffer', '/foo') as segment_info: - with replace_attr(vim, 'guess', lambda path: Args(branch=lambda: os.path.basename(path), status=lambda file: 'M', directory=path)): - self.assertEqual(vim.file_vcs_status(pl=pl, segment_info=segment_info), + with replace_attr(vim, 'guess', get_dummy_guess(status=lambda file: 'M')): + self.assertEqual(file_vcs_status(segment_info=segment_info), [{'highlight_group': ['file_vcs_status_M', 'file_vcs_status'], 'contents': 'M'}]) - with replace_attr(vim, 'guess', lambda path: Args(branch=lambda: os.path.basename(path), status=lambda file: None, directory=path)): - self.assertEqual(vim.file_vcs_status(pl=pl, segment_info=segment_info), None) + with replace_attr(vim, 'guess', get_dummy_guess(status=lambda file: None)): + self.assertEqual(file_vcs_status(segment_info=segment_info), None) with vim_module._with('buffer', '/bar') as segment_info: with vim_module._with('bufoptions', buftype='nofile'): - with replace_attr(vim, 'guess', lambda path: Args(branch=lambda: os.path.basename(path), status=lambda file: 'M', directory=path)): - self.assertEqual(vim.file_vcs_status(pl=pl, segment_info=segment_info), None) + with replace_attr(vim, 'guess', get_dummy_guess(status=lambda file: 'M')): + self.assertEqual(file_vcs_status(segment_info=segment_info), None) old_cwd = None