From 7646c949e2c7a0cb1bcc60538e78a0ef3916c8a4 Mon Sep 17 00:00:00 2001 From: ZyX Date: Mon, 25 Mar 2013 08:59:37 +0400 Subject: [PATCH] Automatically reload configuration Needs testing --- powerline/__init__.py | 87 +++++++++++++++++++++++++++------- powerline/lib/threaded.py | 27 +++++------ powerline/lint/__init__.py | 14 ++---- tests/test_configuration.py | 94 +++++++++++++++---------------------- 4 files changed, 124 insertions(+), 98 deletions(-) diff --git a/powerline/__init__.py b/powerline/__init__.py index f48fad1d..04513b8b 100644 --- a/powerline/__init__.py +++ b/powerline/__init__.py @@ -7,8 +7,9 @@ import sys import logging from powerline.colorscheme import Colorscheme +from powerline.lib.file_watcher import create_file_watcher -from threading import Lock +from threading import Lock, Thread, Event DEFAULT_SYSTEM_CONFIG_DIR = None @@ -18,16 +19,20 @@ def open_file(path): return open(path, 'r') -def load_json_config(search_paths, config_file, load=json.load, open_file=open_file): +def find_config_file(search_paths, config_file): config_file += '.json' for path in search_paths: config_file_path = os.path.join(path, config_file) if os.path.isfile(config_file_path): - with open_file(config_file_path) as config_file_fp: - return load(config_file_fp) + return config_file_path raise IOError('Config file not found in search path: {0}'.format(config_file)) +def load_json_config(config_file_path, load=json.load, open_file=open_file): + with open_file(config_file_path) as config_file_fp: + return load(config_file_fp) + + class PowerlineState(object): def __init__(self, logger, environ, getcwd, home): self.environ = environ @@ -110,6 +115,11 @@ class Powerline(object): self.config_paths = self.get_config_paths() self.renderer_lock = Lock() + self.configs_lock = Lock() + self.shutdown_event = Event() + self.watcher = create_file_watcher() + self.configs = {} + self.thread = None self.prev_common_config = None self.prev_ext_config = None @@ -134,8 +144,6 @@ class Powerline(object): Determines whether colorscheme configuration should be (re)loaded. :param bool load_theme: Determines whether theme configuration should be reloaded. - - Note: reloading of local themes should be taken care of in renderer. ''' common_config_differs = False ext_config_differs = False @@ -208,13 +216,24 @@ class Powerline(object): self.pl.exception('Failed to import renderer module: {0}', str(e)) sys.exit(1) + # Renderer updates configuration file via segments’ .startup thus it + # should be locked to prevent state when configuration was updated, + # but .render still uses old renderer. with self.renderer_lock: - self.renderer = Renderer(self.theme_config, - self.local_themes, - self.theme_kwargs, - self.colorscheme, - self.pl, - **self.renderer_options) + try: + renderer = Renderer(self.theme_config, + self.local_themes, + self.theme_kwargs, + self.colorscheme, + self.pl, + **self.renderer_options) + except Exception as e: + self.pl.exception('Failed to construct renderer object: {0}', str(e)) + else: + self.renderer = renderer + + if not self.run_once and not self.is_alive(): + self.start() def get_log_handler(self): '''Get log handler. @@ -250,6 +269,14 @@ class Powerline(object): config_paths.append(plugin_path) return config_paths + def _load_config(self, cfg_path, type): + '''Load configuration and setup watcher.''' + path = find_config_file(self.config_paths, cfg_path) + with self.configs_lock: + self.configs[path] = type + self.watcher.watch(path) + return load_json_config(path) + def load_theme_config(self, name): '''Get theme configuration. @@ -258,14 +285,14 @@ class Powerline(object): :return: dictionary with :ref:`theme configuration ` ''' - return load_json_config(self.config_paths, os.path.join('themes', self.ext, name)) + return self._load_config(os.path.join('themes', self.ext, name), 'theme') def load_main_config(self): '''Get top-level configuration. :return: dictionary with :ref:`top-level configuration `. ''' - return load_json_config(self.config_paths, 'config') + return self._load_config('config', 'main_config') def load_colorscheme_config(self, name): '''Get colorscheme. @@ -275,14 +302,14 @@ class Powerline(object): :return: dictionary with :ref:`colorscheme configuration `. ''' - return load_json_config(self.config_paths, os.path.join('colorschemes', self.ext, name)) + return self._load_config(os.path.join('colorschemes', self.ext, name), 'colorscheme') def load_colors_config(self): '''Get colorscheme. :return: dictionary with :ref:`colors configuration `. ''' - return load_json_config(self.config_paths, 'colors') + return self._load_config('colors', 'colors') @staticmethod def get_local_themes(local_themes): @@ -310,5 +337,33 @@ class Powerline(object): def shutdown(self): '''Lock renderer from modifications and run its ``.shutdown()`` method. ''' + self.shutdown_event.set() with self.renderer_lock: self.renderer.shutdown() + + def is_alive(self): + return self.thread and self.thread.is_alive() + + def start(self): + self.thread = Thread(target=self.run) + self.thread.start() + + def run(self): + while not self.shutdown_event.is_set(): + kwargs = {} + with self.configs_lock: + for path, type in self.configs.items(): + if self.watcher(path): + kwargs['load_' + type] = True + if kwargs: + try: + self.create_renderer(**kwargs) + except Exception as e: + self.pl.exception('Failed to create renderer: {0}', str(e)) + self.shutdown_event.wait(10) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.shutdown() diff --git a/powerline/lib/threaded.py b/powerline/lib/threaded.py index 50d7895e..6ac4ab32 100644 --- a/powerline/lib/threaded.py +++ b/powerline/lib/threaded.py @@ -17,7 +17,6 @@ class ThreadedSegment(object): self.shutdown_event = Event() self.write_lock = Lock() self.run_once = True - self.did_set_interval = False self.thread = None self.skip = False self.crashed_value = None @@ -48,17 +47,12 @@ class ThreadedSegment(object): return self.thread and self.thread.is_alive() def start(self): - self.keep_going = True + self.shutdown_event.clear() self.thread = Thread(target=self.run) self.thread.start() - def sleep(self, adjust_time): - self.shutdown_event.wait(max(self.interval - adjust_time, self.min_sleep_time)) - if self.shutdown_event.is_set(): - self.keep_going = False - def run(self): - while self.keep_going: + while not self.shutdown_event.is_set(): start_time = monotonic() try: self.update() @@ -67,7 +61,7 @@ class ThreadedSegment(object): self.skip = True else: self.skip = False - self.sleep(monotonic() - start_time) + self.shutdown_event.wait(max(self.interval - (monotonic() - start_time), self.min_sleep_time)) def shutdown(self): self.shutdown_event.set() @@ -79,19 +73,18 @@ class ThreadedSegment(object): # .set_interval(). interval = interval or getattr(self, 'interval') self.interval = interval - self.has_set_interval = True def set_state(self, interval=None, update_first=True, **kwargs): - if not self.did_set_interval or interval: - self.set_interval(interval) - self.updated = not (update_first and self.update_first) + self.set_interval(interval) + self.updated = not (update_first and self.update_first) def startup(self, pl, **kwargs): self.run_once = False self.pl = pl + self.set_state(**kwargs) + if not self.is_alive(): - self.set_state(**kwargs) self.start() def error(self, *args, **kwargs): @@ -161,12 +154,14 @@ class KwThreadedSegment(ThreadedSegment): self.queries.pop(key) def set_state(self, interval=None, update_first=True, **kwargs): - if not self.did_set_interval or (interval < self.interval): - self.set_interval(interval) + self.set_interval(interval) if self.update_first: self.update_first = update_first + with self.write_lock: + self.queries.clear() + @staticmethod def render_one(update_state, **kwargs): return update_state diff --git a/powerline/lint/__init__.py b/powerline/lint/__init__.py index 91078e26..31d70798 100644 --- a/powerline/lint/__init__.py +++ b/powerline/lint/__init__.py @@ -1,5 +1,5 @@ from powerline.lint.markedjson import load -from powerline import load_json_config, Powerline +from powerline import load_json_config, find_config_file, Powerline from powerline.lint.markedjson.error import echoerr, MarkedError from powerline.segments.vim import vim_modes import itertools @@ -21,14 +21,6 @@ def open_file(path): return open(path, 'rb') -def find_config(search_paths, config_file): - config_file += '.json' - for path in search_paths: - if os.path.isfile(os.path.join(path, config_file)): - return path - return None - - EMPTYTUPLE = tuple() @@ -893,7 +885,7 @@ def check(path=None): hadproblem = False try: - main_config = load_json_config(search_paths, 'config', load=load_config, open_file=open_file) + main_config = load_json_config(find_config_file(search_paths, 'config'), load=load_config, open_file=open_file) except IOError: main_config = {} sys.stderr.write('\nConfiguration file not found: config.json\n') @@ -909,7 +901,7 @@ def check(path=None): import_paths = [os.path.expanduser(path) for path in main_config.get('common', {}).get('paths', [])] try: - colors_config = load_json_config(search_paths, 'colors', load=load_config, open_file=open_file) + colors_config = load_json_config(find_config_file(search_paths, 'colors'), load=load_config, open_file=open_file) except IOError: colors_config = {} sys.stderr.write('\nConfiguration file not found: colors.json\n') diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 8cb7531c..f5d766c5 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -15,17 +15,6 @@ VBLOCK = chr(ord('V') - 0x40) SBLOCK = chr(ord('S') - 0x40) -def shutdown(powerline): - from powerline.segments import common, vim - try: - powerline.shutdown() - finally: - # After shutdown threads are useless, it is needed to recreate them. - from imp import reload - reload(common) - reload(vim) - - class TestConfig(TestCase): def test_vim(self): from powerline.vim import VimPowerline @@ -36,31 +25,30 @@ class TestConfig(TestCase): outputs = {} i = 0 mode = None - powerline = VimPowerline() - def check_output(*args): - out = powerline.render(*args + (0 if mode == 'nc' else 1,)) - if out in outputs: - self.fail('Duplicate in set #{0} for mode {1!r} (previously defined in set #{2} for mode {3!r})'.format(i, mode, *outputs[out])) - outputs[out] = (i, mode) + with VimPowerline() as powerline: + def check_output(*args): + out = powerline.render(*args + (0 if mode == 'nc' else 1,)) + if out in outputs: + self.fail('Duplicate in set #{0} for mode {1!r} (previously defined in set #{2} for mode {3!r})'.format(i, mode, *outputs[out])) + outputs[out] = (i, mode) - with vim_module._with('buffer', 'foo.txt'): - with vim_module._with('globals', powerline_config_path=cfg_path): - exclude = set(('no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!')) - try: - for mode in ['n', 'nc', 'no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'i', 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!']: - if mode != 'nc': - vim_module._start_mode(mode) - check_output(1, 0) - for args, kwargs in buffers: - i += 1 - if mode in exclude: - continue - with vim_module._with(*args, **kwargs): - check_output(1, 0) - finally: - vim_module._start_mode('n') - shutdown(powerline) + with vim_module._with('buffer', 'foo.txt'): + with vim_module._with('globals', powerline_config_path=cfg_path): + exclude = set(('no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!')) + try: + for mode in ['n', 'nc', 'no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'i', 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!']: + if mode != 'nc': + vim_module._start_mode(mode) + check_output(1, 0) + for args, kwargs in buffers: + i += 1 + if mode in exclude: + continue + with vim_module._with(*args, **kwargs): + check_output(1, 0) + finally: + vim_module._start_mode('n') def test_tmux(self): from powerline.segments import common @@ -68,29 +56,26 @@ class TestConfig(TestCase): reload(common) from powerline.shell import ShellPowerline with replace_attr(common, 'urllib_read', urllib_read): - powerline = ShellPowerline(Args(ext=['tmux']), run_once=False) - powerline.render() - powerline = ShellPowerline(Args(ext=['tmux']), run_once=False) - powerline.render() - shutdown(powerline) + with ShellPowerline(Args(ext=['tmux']), run_once=False) as powerline: + powerline.render() + with ShellPowerline(Args(ext=['tmux']), run_once=False) as powerline: + powerline.render() def test_zsh(self): from powerline.shell import ShellPowerline args = Args(last_pipe_status=[1, 0], ext=['shell'], renderer_module='zsh_prompt') - powerline = ShellPowerline(args, run_once=False) - powerline.render(segment_info=args) - powerline = ShellPowerline(args, run_once=False) - powerline.render(segment_info=args) - shutdown(powerline) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=args) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=args) def test_bash(self): from powerline.shell import ShellPowerline args = Args(last_exit_code=1, ext=['shell'], renderer_module='bash_prompt', config=[('ext', {'shell': {'theme': 'default_leftonly'}})]) - powerline = ShellPowerline(args, run_once=False) - powerline.render(segment_info=args) - powerline = ShellPowerline(args, run_once=False) - powerline.render(segment_info=args) - shutdown(powerline) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=args) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=args) def test_ipython(self): from powerline.ipython import IpythonPowerline @@ -100,12 +85,11 @@ class TestConfig(TestCase): config_overrides = None theme_overrides = {} - powerline = IpyPowerline() - segment_info = Args(prompt_count=1) - for prompt_type in ['in', 'in2', 'out', 'rewrite']: - powerline.render(matcher_info=prompt_type, segment_info=segment_info) - powerline.render(matcher_info=prompt_type, segment_info=segment_info) - shutdown(powerline) + with IpyPowerline() as powerline: + segment_info = Args(prompt_count=1) + for prompt_type in ['in', 'in2', 'out', 'rewrite']: + powerline.render(matcher_info=prompt_type, segment_info=segment_info) + powerline.render(matcher_info=prompt_type, segment_info=segment_info) def test_wm(self): from powerline.segments import common