diff --git a/powerline/__init__.py b/powerline/__init__.py index bf3e1212..207a24dd 100644 --- a/powerline/__init__.py +++ b/powerline/__init__.py @@ -14,13 +14,25 @@ from powerline.lib import mergedicts from threading import Lock, Event -def _find_config_file(search_paths, config_file): +def _config_loader_condition(path): + return path and os.path.isfile(path) + + +def _find_config_files(search_paths, config_file, config_loader=None, loader_callback=None): config_file += '.json' + found = False for path in search_paths: config_file_path = os.path.join(path, config_file) if os.path.isfile(config_file_path): - return config_file_path - raise IOError('Config file not found in search path: {0}'.format(config_file)) + yield config_file_path + found = True + elif config_loader: + config_loader.register_missing(_config_loader_condition, loader_callback, config_file_path) + if not found: + raise IOError('Config file not found in search paths ({0}): {1}'.format( + ', '.join(search_paths), + config_file + )) class PowerlineLogger(object): @@ -103,14 +115,14 @@ def get_config_paths(): 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(':')]) + config_paths[:0] = reversed([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) + config_paths.insert(0, plugin_path) return config_paths def generate_config_finder(get_config_paths=get_config_paths): - '''Generate find_config_file function + '''Generate find_config_files function This function will find .json file given its path. @@ -123,17 +135,17 @@ def generate_config_finder(get_config_paths=get_config_paths): 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) + return lambda *args: _find_config_files(config_paths, *args) -def load_config(cfg_path, find_config_file, config_loader, loader_callback=None): +def load_config(cfg_path, find_config_files, 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: + :param function find_config_files: Function that finds configuration file. Check out the description of the return value of ``generate_config_finder`` function. :param ConfigLoader config_loader: @@ -144,16 +156,16 @@ def load_config(cfg_path, find_config_file, config_loader, loader_callback=None) :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: + found_files = find_config_files(cfg_path, config_loader, loader_callback) + ret = None + for path in found_files: if loader_callback: config_loader.register(loader_callback, path) - return config_loader.load(path) + if ret is None: + ret = config_loader.load(path) + else: + mergedicts(ret, config_loader.load(path)) + return ret def _get_log_handler(common_config): @@ -286,7 +298,7 @@ class Powerline(object): elif self.renderer_module[-1] == '.': self.renderer_module = self.renderer_module[:-1] - self.find_config_file = generate_config_finder(self.get_config_paths) + self.find_config_files = generate_config_finder(self.get_config_paths) self.cr_kwargs_lock = Lock() self.cr_kwargs = {} @@ -437,7 +449,7 @@ class Powerline(object): '''Load configuration and setup watches.''' return load_config( cfg_path, - self.find_config_file, + self.find_config_files, self.config_loader, self.cr_callbacks[type] ) @@ -445,7 +457,7 @@ class Powerline(object): def _purge_configs(self, type): function = self.cr_callbacks[type] self.config_loader.unregister_functions(set((function,))) - self.config_loader.unregister_missing(set(((self.find_config_file, function),))) + self.config_loader.unregister_missing(set(((self.find_config_files, function),))) def load_theme_config(self, name): '''Get theme configuration. @@ -601,7 +613,7 @@ class Powerline(object): pass functions = tuple(self.cr_callbacks.values()) self.config_loader.unregister_functions(set(functions)) - self.config_loader.unregister_missing(set(((self.find_config_file, function) for function in functions))) + self.config_loader.unregister_missing(set(((self.find_config_files, function) for function in functions))) def __enter__(self): return self diff --git a/powerline/bindings/config.py b/powerline/bindings/config.py index 4f8d9402..e63a6c00 100644 --- a/powerline/bindings/config.py +++ b/powerline/bindings/config.py @@ -119,9 +119,9 @@ def source_tmux_files(pl, args): def create_powerline_logger(args): - find_config_file = generate_config_finder() + find_config_files = generate_config_finder() config_loader = ConfigLoader(run_once=True) - config = load_config('config', find_config_file, config_loader) + config = load_config('config', find_config_files, 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 cae49b79..1417c67f 100644 --- a/powerline/lint/__init__.py +++ b/powerline/lint/__init__.py @@ -1,8 +1,8 @@ # vim:fileencoding=utf-8:noet from powerline.lint.markedjson import load -from powerline import generate_config_finder, get_config_paths -from powerline.lib.config import load_json_config +from powerline import generate_config_finder, get_config_paths, load_config +from powerline.lib.config import ConfigLoader from powerline.lint.markedjson.error import echoerr, MarkedError from powerline.segments.vim import vim_modes from powerline.lint.inspect import getconfigargspec @@ -1261,9 +1261,19 @@ theme_spec = (Spec( ).context_message('Error while loading theme')) +def generate_json_config_loader(lhadproblem): + def load_json_config(config_file_path, load=load, open_file=open_file): + with open_file(config_file_path) as config_file_fp: + r, hadproblem = load(config_file_fp) + if hadproblem: + lhadproblem[0] = True + return r + return load_json_config + + def check(paths=None, debug=False): search_paths = paths or get_config_paths() - find_config_file = generate_config_finder(lambda: search_paths) + find_config_files = generate_config_finder(lambda: search_paths) logger = logging.getLogger('powerline-lint') logger.setLevel(logging.DEBUG if debug else logging.ERROR) @@ -1271,6 +1281,11 @@ def check(paths=None, debug=False): ee = EchoErr(echoerr, logger) + lhadproblem = [False] + load_json_config = generate_json_config_loader(lhadproblem) + + config_loader = ConfigLoader(run_once=True, load=load_json_config) + paths = { 'themes': defaultdict(lambda: []), 'colorschemes': defaultdict(lambda: []), @@ -1326,17 +1341,9 @@ def check(paths=None, debug=False): typ, )) - lhadproblem = [False] - - def load_config(stream): - r, hadproblem = load(stream) - if hadproblem: - lhadproblem[0] = True - return r - hadproblem = False try: - main_config = load_json_config(find_config_file('config'), load=load_config, open_file=open_file) + main_config = load_config('config', find_config_files, config_loader) except IOError: main_config = {} sys.stderr.write('\nConfiguration file not found: config.json\n') @@ -1357,7 +1364,7 @@ def check(paths=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('colors'), load=load_config, open_file=open_file) + colors_config = load_config('colors', find_config_files, config_loader) except IOError: colors_config = {} sys.stderr.write('\nConfiguration file not found: colors.json\n') diff --git a/tests/lib/config_mock.py b/tests/lib/config_mock.py index 30672cba..35eaf0cd 100644 --- a/tests/lib/config_mock.py +++ b/tests/lib/config_mock.py @@ -117,7 +117,7 @@ class TestPowerline(Powerline): return self.cr_kwargs -renderer = SimpleRenderer +renderer = EvenSimplerRenderer def get_powerline(**kwargs): diff --git a/tests/test_config_merging.py b/tests/test_config_merging.py new file mode 100644 index 00000000..4e3aaccb --- /dev/null +++ b/tests/test_config_merging.py @@ -0,0 +1,251 @@ +# vim:fileencoding=utf-8:noet +from __future__ import unicode_literals + +from powerline import Powerline +from tests import TestCase +from shutil import rmtree +import os +import json +from powerline.lib import mergedicts_copy as mdc + + +CONFIG_DIR = 'tests/config' + + +root_config = lambda: { + 'common': { + 'dividers': { + 'left': { + 'hard': '#>', + 'soft': '|>', + }, + 'right': { + 'hard': '<#', + 'soft': '<|', + }, + }, + 'spaces': 0, + 'interval': 0, + 'watcher': 'test', + }, + 'ext': { + 'test': { + 'theme': 'default', + 'colorscheme': 'default', + }, + }, +} + + +colors_config = lambda: { + 'colors': { + 'c1': 1, + 'c2': 2, + }, + 'gradients': { + }, +} + + +colorscheme_config = lambda: { + 'groups': { + 'g': {'fg': 'c1', 'bg': 'c2', 'attr': []}, + } +} + + +theme_config = lambda: { + 'segment_data': { + 's': { + 'before': 'b', + }, + }, + 'segments': { + 'left': [ + { + 'type': 'string', + 'name': 's', + 'contents': 't', + 'highlight_group': ['g'], + }, + ], + 'right': [], + } +} + + +main_tree = lambda: { + '1/config': root_config(), + '1/colors': colors_config(), + '1/colorschemes/default': colorscheme_config(), + '1/themes/test/default': theme_config(), +} + + +def mkdir_recursive(directory): + if os.path.isdir(directory): + return + mkdir_recursive(os.path.dirname(directory)) + os.mkdir(directory) + + +class TestPowerline(Powerline): + def get_config_paths(self): + return tuple(sorted([ + os.path.join(CONFIG_DIR, d) + for d in os.listdir(CONFIG_DIR) + ])) + + +class WithConfigTree(object): + __slots__ = ('tree', 'p', 'p_kwargs') + + def __init__(self, tree, p_kwargs={'run_once': True}): + self.tree = tree + self.p = None + self.p_kwargs = p_kwargs + + def __enter__(self, *args): + os.mkdir(CONFIG_DIR) + for k, v in self.tree.items(): + fname = os.path.join(CONFIG_DIR, k) + '.json' + mkdir_recursive(os.path.dirname(fname)) + with open(fname, 'w') as F: + json.dump(v, F) + self.p = TestPowerline( + ext='test', + renderer_module='tests.lib.config_mock', + **self.p_kwargs + ) + return self.p.__enter__(*args) + + def __exit__(self, *args): + try: + rmtree(CONFIG_DIR) + finally: + if self.p: + self.p.__exit__(*args) + + +class TestMerging(TestCase): + def assertRenderEqual(self, p, output, **kwargs): + self.assertEqual(p.render(**kwargs).replace(' ', ' '), output) + + def test_not_merged_config(self): + with WithConfigTree(main_tree()) as p: + self.assertRenderEqual(p, '{12} bt{2-}#>{--}') + + def test_root_config_merging(self): + with WithConfigTree(mdc(main_tree(), { + '2/config': { + 'common': { + 'dividers': { + 'left': { + 'hard': '!>', + } + } + } + }, + })) as p: + self.assertRenderEqual(p, '{12} bt{2-}!>{--}') + with WithConfigTree(mdc(main_tree(), { + '2/config': { + 'common': { + 'dividers': { + 'left': { + 'hard': '!>', + } + } + } + }, + '3/config': { + 'common': { + 'dividers': { + 'left': { + 'hard': '>>', + } + } + } + }, + })) as p: + self.assertRenderEqual(p, '{12} bt{2-}>>{--}') + with WithConfigTree(mdc(main_tree(), { + '2/config': { + 'common': { + 'spaces': 3, + } + }, + '3/config': { + 'common': { + 'dividers': { + 'left': { + 'hard': '>>', + } + } + } + }, + })) as p: + self.assertRenderEqual(p, '{12} bt {2-}>>{--}') + + def test_colors_config_merging(self): + with WithConfigTree(mdc(main_tree(), { + '2/colors': { + 'colors': { + 'c1': 3, + } + }, + })) as p: + self.assertRenderEqual(p, '{32} bt{2-}#>{--}') + with WithConfigTree(mdc(main_tree(), { + '2/colors': { + 'colors': { + 'c1': 3, + } + }, + '3/colors': { + 'colors': { + 'c1': 4, + } + }, + })) as p: + self.assertRenderEqual(p, '{42} bt{2-}#>{--}') + with WithConfigTree(mdc(main_tree(), { + '2/colors': { + 'colors': { + 'c1': 3, + } + }, + '3/colors': { + 'colors': { + 'c2': 4, + } + }, + })) as p: + self.assertRenderEqual(p, '{34} bt{4-}#>{--}') + + def test_colorschemes_merging(self): + with WithConfigTree(mdc(main_tree(), { + '2/colorschemes/default': { + 'groups': { + 'g': {'fg': 'c2', 'bg': 'c1', 'attr': []}, + } + }, + })) as p: + self.assertRenderEqual(p, '{21} bt{1-}#>{--}') + + def test_theme_merging(self): + with WithConfigTree(mdc(main_tree(), { + '2/themes/test/default': { + 'segment_data': { + 's': { + 'after': 'a', + } + } + }, + })) as p: + self.assertRenderEqual(p, '{12} bta{2-}#>{--}') + + +if __name__ == '__main__': + from tests import main + main()