diff --git a/powerline/__init__.py b/powerline/__init__.py index 83678be4..2d4fba9c 100644 --- a/powerline/__init__.py +++ b/powerline/__init__.py @@ -13,7 +13,7 @@ from powerline.config import DEFAULT_SYSTEM_CONFIG_DIR from threading import Lock, Event -def find_config_file(search_paths, config_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) @@ -83,6 +83,143 @@ def get_fallback_logger(): return _fallback_logger +def _generate_change_callback(lock, key, dictionary): + def on_file_change(path): + with lock: + dictionary[key] = True + return on_file_change + + +def get_config_paths(): + '''Get configuration paths from environment variables. + + Uses $XDG_CONFIG_HOME and $XDG_CONFIG_DIRS according to the XDG specification. + + :return: list of paths + ''' + config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) + config_path = os.path.join(config_home, 'powerline') + config_paths = [config_path] + config_dirs = os.environ.get('XDG_CONFIG_DIRS', DEFAULT_SYSTEM_CONFIG_DIR) + if config_dirs is not None: + config_paths.extend([os.path.join(d, 'powerline') for d in config_dirs.split(':')]) + plugin_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'config_files') + config_paths.append(plugin_path) + return config_paths + + +def generate_config_finder(get_config_paths=get_config_paths): + '''Generate find_config_file function + + This function will find .json file given its path. + + :param function get_config_paths: + Function that being called with no arguments will return a list of paths + that should be searched for configuration files. + + :return: + Function that being given configuration file name will return full path + to it or raise IOError if it failed to find the file. + ''' + config_paths = get_config_paths() + return lambda cfg_path: _find_config_file(config_paths, cfg_path) + + +def load_config(cfg_path, find_config_file, config_loader, loader_callback=None): + '''Load configuration file and setup watches + + Watches are only set up if loader_callback is not None. + + :param str cfg_path: + Path for configuration file that should be loaded. + :param function find_config_file: + Function that finds configuration file. Check out the description of + the return value of ``generate_config_finder`` function. + :param ConfigLoader config_loader: + Configuration file loader class instance. + :param function loader_callback: + Function that will be called by config_loader when change to + configuration file is detected. + + :return: Configuration file contents. + ''' + try: + path = find_config_file(cfg_path) + except IOError: + if loader_callback: + config_loader.register_missing(find_config_file, loader_callback, cfg_path) + raise + else: + if loader_callback: + config_loader.register(loader_callback, path) + return config_loader.load(path) + + +def _get_log_handler(common_config): + '''Get log handler. + + :param dict common_config: + Configuration dictionary used to create handler. + + :return: logging.Handler subclass. + ''' + log_file = common_config['log_file'] + if log_file: + log_file = os.path.expanduser(log_file) + log_dir = os.path.dirname(log_file) + if not os.path.isdir(log_dir): + os.mkdir(log_dir) + return logging.FileHandler(log_file) + else: + return logging.StreamHandler() + + +def create_logger(common_config): + '''Create logger according to provided configuration + ''' + log_format = common_config['log_format'] + formatter = logging.Formatter(log_format) + + level = getattr(logging, common_config['log_level']) + handler = _get_log_handler(common_config) + handler.setLevel(level) + handler.setFormatter(formatter) + + logger = logging.getLogger('powerline') + logger.setLevel(level) + logger.addHandler(handler) + return logger + + +def finish_common_config(common_config): + '''Add default values to common config and expand ~ in paths + + :param dict common_config: + Common configuration, as it was just loaded. + + :return: + Copy of common configuration with all configuration keys and expanded + paths. + ''' + common_config = common_config.copy() + common_config.setdefault('paths', []) + common_config.setdefault('watcher', 'auto') + common_config.setdefault('log_level', 'WARNING') + common_config.setdefault('log_format', '%(asctime)s:%(levelname)s:%(message)s') + common_config.setdefault('term_truecolor', False) + common_config.setdefault('ambiwidth', 1) + common_config.setdefault('additional_escapes', None) + common_config.setdefault('reload_config', True) + common_config.setdefault('interval', None) + common_config.setdefault('log_file', None) + + common_config['paths'] = [ + os.path.expanduser(path) for path in common_config['paths'] + ] + + return common_config + + class Powerline(object): '''Main powerline class, entrance point for all powerline uses. Sets powerline up and loads the configuration. @@ -132,16 +269,19 @@ class Powerline(object): elif self.renderer_module[-1] == '.': self.renderer_module = self.renderer_module[:-1] - config_paths = self.get_config_paths() - self.find_config_file = lambda cfg_path: find_config_file(config_paths, cfg_path) + self.find_config_file = generate_config_finder(self.get_config_paths) self.cr_kwargs_lock = Lock() - self.create_renderer_kwargs = { - 'load_main': True, - 'load_colors': True, - 'load_colorscheme': True, - 'load_theme': True, - } + self.cr_kwargs = {} + self.cr_callbacks = {} + for key in ('main', 'colors', 'colorscheme', 'theme'): + self.cr_kwargs['load_' + key] = True + self.cr_callbacks[key] = _generate_change_callback( + self.cr_kwargs_lock, + 'load_' + key, + self.cr_kwargs + ) + self.shutdown_event = shutdown_event or Event() self.config_loader = config_loader or ConfigLoader(shutdown_event=self.shutdown_event, run_once=run_once) self.run_loader_update = False @@ -182,35 +322,12 @@ class Powerline(object): self.prev_common_config = self.common_config - 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.common_config = finish_common_config(self.common_config) self.import_paths = self.common_config['paths'] if not self.logger: - log_format = self.common_config['log_format'] - formatter = logging.Formatter(log_format) - - level = getattr(logging, self.common_config['log_level']) - handler = self.get_log_handler() - handler.setLevel(level) - handler.setFormatter(formatter) - - self.logger = logging.getLogger('powerline') - self.logger.setLevel(level) - self.logger.addHandler(handler) + self.logger = create_logger(self.common_config) if not self.pl: self.pl = PowerlineLogger(self.use_daemon_threads, self.logger, self.ext) @@ -288,53 +405,28 @@ class Powerline(object): else: self.renderer = renderer - def get_log_handler(self): - '''Get log handler. - - :param dict common_config: - Common configuration. - - :return: logging.Handler subclass. - ''' - log_file = self.common_config['log_file'] - if log_file: - log_file = os.path.expanduser(log_file) - log_dir = os.path.dirname(log_file) - if not os.path.isdir(log_dir): - os.mkdir(log_dir) - return logging.FileHandler(log_file) - else: - return logging.StreamHandler() - @staticmethod def get_config_paths(): '''Get configuration paths. + Should be overridden in subclasses in order to provide a way to override + used paths. + :return: list of paths ''' - config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) - config_path = os.path.join(config_home, 'powerline') - config_paths = [config_path] - config_dirs = os.environ.get('XDG_CONFIG_DIRS', DEFAULT_SYSTEM_CONFIG_DIR) - if config_dirs is not None: - config_paths.extend([os.path.join(d, 'powerline') for d in config_dirs.split(':')]) - plugin_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'config_files') - config_paths.append(plugin_path) - return config_paths + return get_config_paths() def _load_config(self, cfg_path, type): '''Load configuration and setup watches.''' - function = getattr(self, 'on_' + type + '_change') - try: - path = self.find_config_file(cfg_path) - except IOError: - self.config_loader.register_missing(self.find_config_file, function, cfg_path) - raise - self.config_loader.register(function, path) - return self.config_loader.load(path) + return load_config( + cfg_path, + self.find_config_file, + self.config_loader, + self.cr_callbacks[type] + ) def _purge_configs(self, type): - function = getattr(self, 'on_' + type + '_change') + function = self.cr_callbacks[type] self.config_loader.unregister_functions(set((function,))) self.config_loader.unregister_missing(set(((self.find_config_file, function),))) @@ -392,23 +484,23 @@ class Powerline(object): '''Updates/creates a renderer if needed.''' if self.run_loader_update: self.config_loader.update() - create_renderer_kwargs = None + cr_kwargs = None with self.cr_kwargs_lock: - if self.create_renderer_kwargs: - create_renderer_kwargs = self.create_renderer_kwargs.copy() - if create_renderer_kwargs: + if self.cr_kwargs: + cr_kwargs = self.cr_kwargs.copy() + if cr_kwargs: try: - self.create_renderer(**create_renderer_kwargs) + self.create_renderer(**cr_kwargs) except Exception as e: self.exception('Failed to create renderer: {0}', str(e)) if hasattr(self, 'renderer'): with self.cr_kwargs_lock: - self.create_renderer_kwargs.clear() + self.cr_kwargs.clear() else: raise else: with self.cr_kwargs_lock: - self.create_renderer_kwargs.clear() + self.cr_kwargs.clear() def render(self, *args, **kwargs): '''Update/create renderer if needed and pass all arguments further to @@ -457,30 +549,9 @@ class Powerline(object): self.renderer.shutdown() except AttributeError: pass - functions = ( - self.on_main_change, - self.on_colors_change, - self.on_colorscheme_change, - self.on_theme_change, - ) + functions = tuple(self.cr_callbacks.values()) self.config_loader.unregister_functions(set(functions)) - self.config_loader.unregister_missing(set(((find_config_file, function) for function in functions))) - - def on_main_change(self, path): - with self.cr_kwargs_lock: - self.create_renderer_kwargs['load_main'] = True - - def on_colors_change(self, path): - with self.cr_kwargs_lock: - self.create_renderer_kwargs['load_colors'] = True - - def on_colorscheme_change(self, path): - with self.cr_kwargs_lock: - self.create_renderer_kwargs['load_colorscheme'] = True - - def on_theme_change(self, path): - with self.cr_kwargs_lock: - self.create_renderer_kwargs['load_theme'] = True + self.config_loader.unregister_missing(set(((self.find_config_file, function) for function in functions))) def __enter__(self): return self diff --git a/powerline/bindings/config.py b/powerline/bindings/config.py index 16722a98..3591903a 100644 --- a/powerline/bindings/config.py +++ b/powerline/bindings/config.py @@ -8,6 +8,9 @@ import subprocess import re from powerline.config import TMUX_CONFIG_DIRECTORY +from powerline.lib.config import ConfigLoader +from powerline import generate_config_finder, load_config, create_logger, PowerlineLogger, finish_common_config +from powerline.lib.shell import run_cmd TmuxVersionInfo = namedtuple('TmuxVersionInfo', ('major', 'minor', 'suffix')) @@ -32,9 +35,9 @@ def run_tmux_command(*args): _run_tmux(subprocess.check_call, args) -def get_tmux_output(*args): +def get_tmux_output(pl, *args): '''Run tmux command and return its output''' - return _run_tmux(subprocess.check_output, args) + return _run_tmux(lambda cmd: run_cmd(pl, cmd), args) NON_DIGITS = re.compile('[^0-9]+') @@ -42,8 +45,8 @@ DIGITS = re.compile('[0-9]+') NON_LETTERS = re.compile('[^a-z]+') -def get_tmux_version(): - version_string = get_tmux_output('-V') +def get_tmux_version(pl): + version_string = get_tmux_output(pl, '-V') _, version_string = version_string.split(' ') version_string = version_string.strip() major, minor = version_string.split('.') @@ -96,7 +99,7 @@ def get_tmux_configs(version): yield (fname, priority + file_version.minor * 10 + file_version.major * 10000) -def source_tmux_files(): +def source_tmux_files(pl, args): '''Source relevant version-specific tmux configuration files Files are sourced in the following order: @@ -104,6 +107,15 @@ def source_tmux_files(): * If files for same versions are to be sourced then first _minus files are sourced, then _plus files and then files without _minus or _plus suffixes. ''' - version = get_tmux_version() + version = get_tmux_version(pl) for fname, priority in sorted(get_tmux_configs(version), key=(lambda v: v[1])): run_tmux_command('source', fname) + + +def create_powerline_logger(args): + find_config_file = generate_config_finder() + config_loader = ConfigLoader(run_once=True) + config = load_config('config', find_config_file, config_loader) + common_config = finish_common_config(config['common']) + logger = create_logger(common_config) + return PowerlineLogger(use_daemon_threads=True, logger=logger, ext='config') diff --git a/powerline/lint/__init__.py b/powerline/lint/__init__.py index 928f144e..611f0606 100644 --- a/powerline/lint/__init__.py +++ b/powerline/lint/__init__.py @@ -1,7 +1,7 @@ # vim:fileencoding=utf-8:noet from powerline.lint.markedjson import load -from powerline import find_config_file, Powerline +from powerline import generate_config_finder, get_config_paths from powerline.lib.config import load_json_config from powerline.lint.markedjson.error import echoerr, MarkedError from powerline.segments.vim import vim_modes @@ -1035,7 +1035,8 @@ theme_spec = (Spec( def check(path=None, debug=False): - search_paths = [path] if path else Powerline.get_config_paths() + search_paths = [path] if path else get_config_paths() + find_config_file = generate_config_finder(lambda: search_paths) logger = logging.getLogger('powerline-lint') logger.setLevel(logging.DEBUG if debug else logging.ERROR) @@ -1092,7 +1093,7 @@ def check(path=None, debug=False): hadproblem = False try: - main_config = load_json_config(find_config_file(search_paths, 'config'), load=load_config, open_file=open_file) + main_config = load_json_config(find_config_file('config'), load=load_config, open_file=open_file) except IOError: main_config = {} sys.stderr.write('\nConfiguration file not found: config.json\n') @@ -1108,7 +1109,7 @@ def check(path=None, debug=False): import_paths = [os.path.expanduser(path) for path in main_config.get('common', {}).get('paths', [])] try: - colors_config = load_json_config(find_config_file(search_paths, 'colors'), load=load_config, open_file=open_file) + colors_config = load_json_config(find_config_file('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/scripts/powerline-config b/scripts/powerline-config index 151dc1d5..028ab783 100755 --- a/scripts/powerline-config +++ b/scripts/powerline-config @@ -12,7 +12,7 @@ except ImportError: TMUX_ACTIONS = { - 'source': (lambda args: config.source_tmux_files()), + 'source': config.source_tmux_files, } @@ -30,4 +30,6 @@ if __name__ == '__main__': args = parser.parse_args() - args.function(args) + pl = config.create_powerline_logger(args) + + args.function(pl, args) diff --git a/tests/lib/config_mock.py b/tests/lib/config_mock.py index f4f50624..30672cba 100644 --- a/tests/lib/config_mock.py +++ b/tests/lib/config_mock.py @@ -22,7 +22,7 @@ def load_json_config(config_file_path, *args, **kwargs): raise IOError(config_file_path) -def find_config_file(config, search_paths, config_file): +def _find_config_file(config, search_paths, config_file): if config_file.endswith('raise') and config_file not in config: raise IOError('fcf:' + config_file) return config_file @@ -114,7 +114,7 @@ class TestPowerline(Powerline): return local_themes def _will_create_renderer(self): - return self.create_renderer_kwargs + return self.cr_kwargs renderer = SimpleRenderer @@ -158,7 +158,7 @@ def swap_attributes(cfg_container, powerline_module, replaces): config_container = cfg_container if not replaces: replaces = { - 'find_config_file': lambda *args: find_config_file(config_container['config'], *args), + '_find_config_file': lambda *args: _find_config_file(config_container['config'], *args), } for attr, val in replaces.items(): old_val = getattr(powerline_module, attr)