Implement theme hierarchy

Fixes #783
This commit is contained in:
ZyX 2014-08-05 20:29:03 +04:00
parent 145e1c2050
commit bdde4ae99f
10 changed files with 511 additions and 239 deletions

View File

@ -17,9 +17,11 @@ Powerline provides default configurations in the following locations:
:file:`powerline/config.json`
:ref:`Colorschemes <config-colors>`
:file:`powerline/colorschemes/{name}.json`,
:file:`powerline/colorscheme/__main__.json`,
:file:`powerline/colorscheme/{extension}/__main__.json`,
:file:`powerline/colorschemes/{extension}/{name}.json`
:ref:`Themes <config-themes>`
:file:`powerline/themes/{top_theme}.json`,
:file:`powerline/themes/{extension}/__main__.json`,
:file:`powerline/themes/{extension}/default.json`
The default configuration files are stored in the main package. User
@ -48,6 +50,10 @@ overrides <local-configuration-overrides>`.
corresponding values are both dictionaries in which case these dictionaries
are merged and key is assigned the result of the merge.
.. note:: Some configuration files (i.e. themes and colorschemes) have two level
of merging: first happens merging described above, second theme- or
colorscheme-specific merging happens.
.. _quick-guide:
Quick setup guide

View File

@ -58,15 +58,6 @@ Common configuration is a subdictionary that is a value of ``common`` key in
codes thus rendering powerline prompt colorless. Valid values: ``"tmux"``,
``"screen"``, ``null`` (default).
``dividers``
Defines the dividers used in all Powerline extensions. This option
should usually only be changed if you don't have a patched font, or if
you use a font patched with the legacy font patcher.
The ``hard`` dividers are used to divide segments with different
background colors, while the ``soft`` dividers are used to divide
segments with the same background color.
.. _config-common-paths:
``paths``
@ -95,6 +86,12 @@ Common configuration is a subdictionary that is a value of ``common`` key in
Boolean, determines whether configuration should be reloaded at all.
Defaults to ``True``.
.. _config-common-default_top_theme:
``default_top_theme``
String, determines which top-level theme will be used as the default.
Defaults to ``powerline``. See `Themes`_ section for more details.
Extension-specific configuration
--------------------------------
@ -109,6 +106,12 @@ Common configuration is a subdictionary that is a value of ``ext`` key in
Defines the theme used for this extension.
``top_theme``
.. _config-ext-top_theme:
Defines the top-level theme used for this extension. See `Themes`_ section
for more details.
``local_themes``
.. _config-ext-local_themes:
@ -155,7 +158,7 @@ Colorschemes
============
:Location: :file:`powerline/colorschemes/{name}.json`,
:file:`powerline/colorscheme/__main__.json`,
:file:`powerline/colorschemes/__main__.json`,
:file:`powerline/colorschemes/{extension}/{name}.json`
Colorscheme files are processed in order given: definitions from each next file
@ -213,7 +216,17 @@ override those from each previous file. It is required that either
Themes
======
:Location: :file:`powerline/themes/{extension}/{name}.json`
:Location: :file:`powerline/themes/{top_theme}.json`,
:file:`powerline/themes/__main__.json`,
:file:`powerline/themes/{extension}/{name}.json`
Theme files are processed in order given: definitions from each next file
override those from each previous file. It is required that file
:file:`powerline/themes/{extension}/{name}.json` exists.
`{top_theme}` component of the file name is obtained either from :ref:`top_theme
extension-specific key <config-ext-top_theme>` or from :ref:`default_top_theme
common configuration key <config-common-default_top_theme>`.
``name``
Name of the theme.
@ -223,6 +236,20 @@ Themes
``default_module``
Python module where segments will be looked by default.
``spaces``
Defines number of spaces just before the divider (on the right side) or just
after it (on the left side). These spaces will not be added if divider is
not drawn.
``dividers``
Defines the dividers used in all Powerline extensions. This option
should usually only be changed if you don't have a patched font, or if
you use a font patched with the legacy font patcher.
The ``hard`` dividers are used to divide segments with different
background colors, while the ``soft`` dividers are used to divide
segments with the same background color.
.. _config-themes-segment_data:
``segment_data``
@ -240,6 +267,9 @@ Themes
<config-ext-theme>`. For the :ref:`default theme <config-ext-theme>` itself
step 2 is obviously avoided.
.. note:: Top-level themes are out of equation here: they are merged
before the above merging process happens.
``segments``
A dict with a ``left`` and a ``right`` lists, consisting of segment
dictionaries. Shell themes may also contain ``above`` list of dictionaries.

View File

@ -215,6 +215,7 @@ def finish_common_config(common_config):
paths.
'''
common_config = common_config.copy()
common_config.setdefault('default_top_theme', 'powerline')
common_config.setdefault('paths', [])
common_config.setdefault('watcher', 'auto')
common_config.setdefault('log_level', 'WARNING')
@ -345,13 +346,15 @@ class Powerline(object):
if load_main:
self._purge_configs('main')
config = self.load_main_config()
self.common_config = config['common']
self.common_config = finish_common_config(config['common'])
if self.common_config != self.prev_common_config:
common_config_differs = True
self.prev_common_config = self.common_config
load_theme = (load_theme
or not self.prev_common_config
or self.prev_common_config['default_top_theme'] != self.common_config['default_top_theme'])
self.common_config = finish_common_config(self.common_config)
self.prev_common_config = self.common_config
self.import_paths = self.common_config['paths']
@ -386,6 +389,8 @@ class Powerline(object):
if interval is not None and not self.config_loader.is_alive():
self.config_loader.start()
self.default_top_theme = self.common_config['default_top_theme']
self.ext_config = config['ext'][self.ext]
if self.ext_config != self.prev_ext_config:
ext_config_differs = True
@ -445,30 +450,20 @@ class Powerline(object):
'''
return get_config_paths()
def _load_config(self, cfg_path, type):
def _load_config(self, cfg_path, cfg_type):
'''Load configuration and setup watches.'''
return load_config(
cfg_path,
self.find_config_files,
self.config_loader,
self.cr_callbacks[type]
self.cr_callbacks[cfg_type]
)
def _purge_configs(self, type):
function = self.cr_callbacks[type]
def _purge_configs(self, cfg_type):
function = self.cr_callbacks[cfg_type]
self.config_loader.unregister_functions(set((function,)))
self.config_loader.unregister_missing(set(((self.find_config_files, function),)))
def load_theme_config(self, name):
'''Get theme configuration.
:param str name:
Name of the theme to load.
:return: dictionary with :ref:`theme configuration <config-themes>`
'''
return self._load_config(os.path.join('themes', self.ext, name), 'theme')
def load_main_config(self):
'''Get top-level configuration.
@ -476,6 +471,47 @@ class Powerline(object):
'''
return self._load_config('config', 'main')
def _load_hierarhical_config(self, cfg_type, levels, ignore_levels):
'''Load and merge multiple configuration files
:param str cfg_type:
Type of the loaded configuration files (e.g. ``colorscheme``,
``theme``).
:param list levels:
Configuration names resembling levels in hierarchy, sorted by
priority. Configuration file names with higher priority should go
last.
:param set ignore_levels:
If only files listed in this variable are present then configuration
file is considered not loaded: at least one file on the level not
listed in this variable must be present.
'''
config = {}
loaded = 0
exceptions = []
for i, cfg_path in enumerate(levels):
try:
lvl_config = self._load_config(cfg_path, cfg_type)
except IOError as e:
if sys.version_info < (3,):
tb = sys.exc_info()[2]
exceptions.append((e, tb))
else:
exceptions.append(e)
else:
if i not in ignore_levels:
loaded += 1
mergedicts(config, lvl_config)
if not loaded:
for exception in exceptions:
if type(exception) is tuple:
e = exception[0]
else:
e = exception
self.exception('Failed to load %s: {0}' % cfg_type, e, exception=exception)
raise e
return config
def load_colorscheme_config(self, name):
'''Get colorscheme.
@ -484,40 +520,27 @@ class Powerline(object):
:return: dictionary with :ref:`colorscheme configuration <config-colorschemes>`.
'''
# TODO Make sure no colorscheme name ends with __ (do it in
# powerline-lint)
levels = (
os.path.join('colorschemes', name),
os.path.join('colorschemes', self.ext, '__main__'),
os.path.join('colorschemes', self.ext, name),
)
config = {}
loaded = 0
exceptions = []
for cfg_path in levels:
try:
lvl_config = self._load_config(cfg_path, 'colorscheme')
except IOError as e:
if sys.version_info < (3,):
tb = sys.exc_info()[2]
exceptions.append((e, tb))
else:
exceptions.append(e)
else:
if not cfg_path.endswith('__'):
loaded += 1
# TODO Either make sure `attr` list is always present or make
# mergedicts not merge group definitions.
mergedicts(config, lvl_config)
if not loaded:
for exception in exceptions:
if type(exception) is tuple:
e = exception[0]
else:
e = exception
self.exception('Failed to load colorscheme: {0}', e, exception=exception)
raise e
return config
return self._load_hierarhical_config('colorscheme', levels, (1,))
def load_theme_config(self, name):
'''Get theme configuration.
:param str name:
Name of the theme to load.
:return: dictionary with :ref:`theme configuration <config-themes>`
'''
levels = (
os.path.join('themes', self.ext_config.get('top_theme') or self.default_top_theme),
os.path.join('themes', self.ext, '__main__'),
os.path.join('themes', self.ext, name),
)
return self._load_hierarhical_config('theme', levels, (0, 1,))
def load_colors_config(self):
'''Get colorscheme.

View File

@ -1,17 +1,6 @@
{
"common": {
"term_truecolor": false,
"dividers": {
"left": {
"hard": " ",
"soft": " "
},
"right": {
"hard": " ",
"soft": " "
}
},
"spaces": 1
"term_truecolor": false
},
"ext": {
"ipython": {

View File

@ -0,0 +1,13 @@
{
"dividers": {
"left": {
"hard": " ",
"soft": " "
},
"right": {
"hard": " ",
"soft": " "
}
},
"spaces": 1
}

View File

@ -431,9 +431,6 @@ class WithPath(object):
def check_matcher_func(ext, match_name, data, context, echoerr):
import_paths = [os.path.expanduser(path) for path in context[0][1].get('common', {}).get('paths', [])]
if match_name == '__tabline__':
return True, False
match_module, separator, match_function = match_name.rpartition('.')
if not separator:
match_module = 'powerline.matchers.{0}'.format(ext)
@ -512,23 +509,30 @@ def check_config(d, theme, data, context, echoerr):
return True, False, False
def check_top_theme(theme, data, context, echoerr):
if theme not in data['configs']['top_themes']:
echoerr(context='Error while checking extension configuration (key {key})'.format(key=context_key(context)),
context_mark=context[-2][0].mark,
problem='failed to find top theme {0}'.format(theme),
problem_mark=theme.mark)
return True, False, True
return True, False, False
divider_spec = Spec().type(unicode).len('le', 3,
lambda value: 'Divider {0!r} is too large!'.format(value)).copy
divside_spec = Spec(
hard=divider_spec(),
soft=divider_spec(),
ext_theme_spec = Spec().type(unicode).func(lambda *args: check_config('themes', *args)).copy
top_theme_spec = Spec().type(unicode).func(check_top_theme).copy
ext_spec = Spec(
colorscheme=Spec().type(unicode).func(
(lambda *args: check_config('colorschemes', *args))
),
theme=ext_theme_spec(),
top_theme=top_theme_spec().optional(),
).copy
colorscheme_spec = Spec().type(unicode).func(lambda *args: check_config('colorschemes', *args)).copy
theme_spec = Spec().type(unicode).func(lambda *args: check_config('themes', *args)).copy
main_spec = (Spec(
common=Spec(
dividers=Spec(
left=divside_spec(),
right=divside_spec(),
),
spaces=Spec().unsigned().cmp(
'le', 2, lambda value: 'Are you sure you need such a big ({0}) number of spaces?'.format(value)
),
default_top_theme=top_theme_spec().optional(),
term_truecolor=Spec().type(bool).optional(),
# Python is capable of loading from zip archives. Thus checking path
# only for existence of the path, not for it being a directory
@ -556,36 +560,29 @@ main_spec = (Spec(
watcher=Spec().type(unicode).oneof(set(('auto', 'inotify', 'stat'))).optional(),
).context_message('Error while loading common configuration (key {key})'),
ext=Spec(
vim=Spec(
colorscheme=colorscheme_spec(),
theme=theme_spec(),
local_themes=Spec().unknown_spec(
lambda *args: check_matcher_func('vim', *args), theme_spec()
vim=ext_spec().update(
local_themes=Spec(
__tabline__=ext_theme_spec(),
).unknown_spec(
lambda *args: check_matcher_func('vim', *args), ext_theme_spec()
),
).optional(),
ipython=Spec(
colorscheme=colorscheme_spec(),
theme=theme_spec(),
ipython=ext_spec().update(
local_themes=Spec(
in2=theme_spec(),
out=theme_spec(),
rewrite=theme_spec(),
in2=ext_theme_spec(),
out=ext_theme_spec(),
rewrite=ext_theme_spec(),
),
).optional(),
shell=Spec(
colorscheme=colorscheme_spec(),
theme=theme_spec(),
shell=ext_spec().update(
local_themes=Spec(
continuation=theme_spec(),
select=theme_spec(),
continuation=ext_theme_spec(),
select=ext_theme_spec(),
),
).optional(),
).unknown_spec(
check_ext,
Spec(
colorscheme=colorscheme_spec(),
theme=theme_spec(),
)
ext_spec(),
).context_message('Error while loading extensions configuration (key {key})'),
).context_message('Error while loading main configuration'))
@ -771,7 +768,7 @@ type_keys = {
}
required_keys = {
'function': set(('name',)),
'string': set(('contents',)),
'string': set(()),
'filler': set(),
'segment_list': set(('name', 'segments',)),
}
@ -845,11 +842,11 @@ def check_full_segment_data(segment, data, context, echoerr):
ext = data['ext']
theme_segment_data = context[0][1].get('segment_data', {})
top_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None)
if not top_theme_name or data['theme'] == top_theme_name:
main_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None)
if not main_theme_name or data['theme'] == main_theme_name:
top_segment_data = {}
else:
top_segment_data = data['ext_theme_configs'].get(top_theme_name, {}).get('segment_data', {})
top_segment_data = data['ext_theme_configs'].get(main_theme_name, {}).get('segment_data', {})
names = [segment['name']]
if segment.get('type', 'function') == 'function':
@ -977,12 +974,16 @@ def check_segment_name(name, data, context, echoerr):
return True, False, hadproblem
elif context[-2][1].get('type') != 'segment_list':
if name not in context[0][1].get('segment_data', {}):
top_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None)
if data['theme'] == top_theme_name:
top_theme = {}
main_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None)
if data['theme'] == main_theme_name:
main_theme = {}
else:
top_theme = data['ext_theme_configs'].get(top_theme_name, {})
if name not in top_theme.get('segment_data', {}):
main_theme = data['ext_theme_configs'].get(main_theme_name, {})
if (
name not in main_theme.get('segment_data', {})
and name not in data['ext_theme_configs'].get('__main__', {}).get('segment_data', {})
and not any(((name in theme.get('segment_data', {})) for theme in data['top_themes'].values()))
):
echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
problem='found useless use of name key (such name is not present in theme/segment_data)',
problem_mark=name.mark)
@ -1070,34 +1071,49 @@ def check_highlight_groups(hl_groups, data, context, echoerr):
return True, False, False
def check_segment_data_key(key, data, context, echoerr):
def list_themes(data, context):
theme_type = data['theme_type']
ext = data['ext']
top_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None)
is_top_theme = (data['theme'] == top_theme_name)
if is_top_theme:
themes = data['ext_theme_configs'].values()
main_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None)
is_main_theme = (data['theme'] == main_theme_name)
if theme_type == 'top':
return list(itertools.chain(*[
[(ext, theme) for theme in theme_configs.values()]
for ext, theme_configs in data['theme_configs'].items()
]))
elif theme_type == 'main' or is_main_theme:
return [(ext, theme) for theme in data['ext_theme_configs'].values()]
else:
themes = [context[0][1]]
return [(ext, context[0][1])]
for theme in themes:
def check_segment_data_key(key, data, context, echoerr):
has_module_name = '.' in key
found = False
for ext, theme in list_themes(data, context):
for segments in theme.get('segments', {}).values():
found = False
for segment in segments:
if 'name' in segment:
if key == segment['name']:
found = True
module = segment.get('module', theme.get('default_module', 'powerline.segments.' + ext))
if key == unicode(module) + '.' + unicode(segment['name']):
found = True
if has_module_name:
module = segment.get('module', theme.get('default_module', 'powerline.segments.' + ext))
full_name = unicode(module) + '.' + unicode(segment['name'])
if key == full_name:
found = True
break
else:
if key == segment['name']:
found = True
break
if found:
break
if found:
break
else:
echoerr(context='Error while checking segment data',
problem='found key {0} that cannot be associated with any segment'.format(key),
problem_mark=key.mark)
return True, False, True
if data['theme_type'] != 'top':
echoerr(context='Error while checking segment data',
problem='found key {0} that cannot be associated with any segment'.format(key),
problem_mark=key.mark)
return True, False, True
return True, False, False
@ -1109,8 +1125,8 @@ threaded_args_specs = {
}
def check_args_variant(segment, args, data, context, echoerr):
argspec = getconfigargspec(segment)
def check_args_variant(func, args, data, context, echoerr):
argspec = getconfigargspec(func)
present_args = set(args)
all_args = set(argspec.args)
required_args = set(argspec.args[:-len(argspec.defaults)])
@ -1132,7 +1148,7 @@ def check_args_variant(segment, args, data, context, echoerr):
problem_mark=next(iter(present_args - all_args)).mark)
hadproblem = True
if isinstance(segment, ThreadedSegment):
if isinstance(func, ThreadedSegment):
for key in set(threaded_args_specs) & present_args:
proceed, khadproblem = threaded_args_specs[key].match(
args[key],
@ -1149,13 +1165,13 @@ def check_args_variant(segment, args, data, context, echoerr):
return hadproblem
def check_args(get_segment_variants, args, data, context, echoerr):
def check_args(get_functions, args, data, context, echoerr):
new_echoerr = DelayedEchoErr(echoerr)
count = 0
hadproblem = False
for segment in get_segment_variants(data, context, new_echoerr):
for func in get_functions(data, context, new_echoerr):
count += 1
shadproblem = check_args_variant(segment, args, data, context, echoerr)
shadproblem = check_args_variant(func, args, data, context, echoerr)
if shadproblem:
hadproblem = True
@ -1171,7 +1187,7 @@ def check_args(get_segment_variants, args, data, context, echoerr):
return True, False, hadproblem
def get_one_segment_variant(data, context, echoerr):
def get_one_segment_function(data, context, echoerr):
name = context[-2][1].get('name')
if name:
func = import_segment(name, data, context, echoerr)
@ -1179,7 +1195,7 @@ def get_one_segment_variant(data, context, echoerr):
yield func
def get_all_possible_segments(data, context, echoerr):
def get_all_possible_functions(data, context, echoerr):
name = context[-2][0]
module, name = name.rpartition('.')[::2]
if module:
@ -1187,13 +1203,13 @@ def get_all_possible_segments(data, context, echoerr):
if func:
yield func
else:
for theme_config in data['ext_theme_configs'].values():
for ext, theme_config in list_themes(data, context):
for segments in theme_config.get('segments', {}).values():
for segment in segments:
if segment.get('type', 'function') == 'function':
module = segment.get(
'module',
context[0][1].get('default_module', 'powerline.segments.' + data['ext'])
theme_config.get('default_module', 'powerline.segments.' + data['ext'])
)
func = import_segment(name, data, context, echoerr, module=module)
if func:
@ -1222,7 +1238,7 @@ segment_spec = Spec(
before=Spec().type(unicode).optional(),
width=Spec().either(Spec().unsigned(), Spec().cmp('eq', 'auto')).optional(),
align=Spec().oneof(set('lr')).optional(),
args=args_spec().func(lambda *args, **kwargs: check_args(get_one_segment_variant, *args, **kwargs)),
args=args_spec().func(lambda *args, **kwargs: check_args(get_one_segment_function, *args, **kwargs)),
contents=Spec().type(unicode).optional(),
highlight_group=Spec().list(
highlight_group_spec().re(
@ -1245,20 +1261,52 @@ segdict_spec=Spec(
(lambda value, *args: (True, True, not (('left' in value) or ('right' in value)))),
(lambda value: 'segments dictionary must contain either left, right or both keys')
).context_message('Error while loading segments (key {key})').copy
theme_spec = (Spec(
default_module=segment_module_spec(),
divside_spec = Spec(
hard=divider_spec(),
soft=divider_spec(),
).copy
segment_data_value_spec = Spec(
after=Spec().type(unicode).optional(),
before=Spec().type(unicode).optional(),
display=Spec().type(bool).optional(),
args=args_spec().func(lambda *args, **kwargs: check_args(get_all_possible_functions, *args, **kwargs)),
contents=Spec().type(unicode).optional(),
).copy
dividers_spec = Spec(
left=divside_spec(),
right=divside_spec(),
).copy
spaces_spec = Spec().unsigned().cmp(
'le', 2, (lambda value: 'Are you sure you need such a big ({0}) number of spaces?'.format(value))
).copy
common_theme_spec = Spec(
default_module=segment_module_spec().optional(),
).context_message('Error while loading theme').copy
top_theme_spec = common_theme_spec().update(
dividers=dividers_spec(),
spaces=spaces_spec(),
segment_data=Spec().unknown_spec(
Spec().func(check_segment_data_key),
Spec(
after=Spec().type(unicode).optional(),
before=Spec().type(unicode).optional(),
display=Spec().type(bool).optional(),
args=args_spec().func(lambda *args, **kwargs: check_args(get_all_possible_segments, *args, **kwargs)),
contents=Spec().type(unicode).optional(),
),
segment_data_value_spec(),
).optional().context_message('Error while loading segment data (key {key})'),
)
main_theme_spec = common_theme_spec().update(
dividers=dividers_spec().optional(),
spaces=spaces_spec().optional(),
segment_data=Spec().unknown_spec(
Spec().func(check_segment_data_key),
segment_data_value_spec(),
).optional().context_message('Error while loading segment data (key {key})'),
)
theme_spec = common_theme_spec().update(
dividers=dividers_spec().optional(),
spaces=spaces_spec().optional(),
segment_data=Spec().unknown_spec(
Spec().func(check_segment_data_key),
segment_data_value_spec(),
).optional().context_message('Error while loading segment data (key {key})'),
segments=segdict_spec().update(above=Spec().list(segdict_spec()).optional()),
).context_message('Error while loading theme'))
)
def generate_json_config_loader(lhadproblem):
@ -1315,6 +1363,8 @@ def check(paths=None, debug=False):
hadproblem = True
sys.stderr.write('Path {0} is supposed to be a directory, but it is not\n'.format(d))
hadproblem = False
configs = defaultdict(lambda: defaultdict(lambda: {}))
for typ in ('themes', 'colorschemes'):
for ext in paths[typ]:
@ -1324,6 +1374,11 @@ def check(paths=None, debug=False):
name = subp[:-5]
if name != '__main__':
lists[typ].add(name)
if name.startswith('__') or name.endswith('__'):
hadproblem = True
sys.stderr.write('File name is not supposed to start or end with “__”: {0}'.format(
os.path.join(d, subp)
))
configs[typ][ext][name] = os.path.join(d, subp)
for path in paths['top_' + typ]:
name = os.path.basename(path)[:-5]
@ -1341,7 +1396,6 @@ def check(paths=None, debug=False):
typ,
))
hadproblem = False
try:
main_config = load_config('config', find_config_files, config_loader)
except IOError:
@ -1469,17 +1523,53 @@ def check(paths=None, debug=False):
hadproblem = True
theme_configs[ext][theme] = config
top_theme_configs = {}
for top_theme, top_theme_file in configs['top_themes'].items():
with open_file(top_theme_file) as config_file_fp:
try:
config, lhadproblem = load(config_file_fp)
except MarkedError as e:
sys.stderr.write(str(e) + '\n')
hadproblem = True
continue
if lhadproblem:
hadproblem = True
top_theme_configs[top_theme] = config
for ext, configs in theme_configs.items():
data = {
'ext': ext,
'colorscheme_configs': colorscheme_configs,
'import_paths': import_paths,
'main_config': main_config,
'top_themes': top_theme_configs,
'ext_theme_configs': configs,
'colors_config': colors_config
}
for theme, config in configs.items():
data['theme'] = theme
if theme_spec.match(config, context=(('', config),), data=data, echoerr=ee)[1]:
if theme == '__main__':
data['theme_type'] = 'main'
spec = main_theme_spec
else:
data['theme_type'] = 'regular'
spec = theme_spec
if spec.match(config, context=(('', config),), data=data, echoerr=ee)[1]:
hadproblem = True
for top_theme, config in top_theme_configs.items():
data = {
'ext': ext,
'colorscheme_configs': colorscheme_configs,
'import_paths': import_paths,
'main_config': main_config,
'theme_configs': theme_configs,
'ext_theme_configs': configs,
'colors_config': colors_config
}
data['theme_type'] = 'top'
data['theme'] = top_theme
if top_theme_spec.match(config, context=(('', config),), data=data, echoerr=ee)[1]:
hadproblem = True
return hadproblem

View File

@ -31,13 +31,13 @@ class Theme(object):
top_theme_config=None,
run_once=False,
shutdown_event=None):
self.dividers = theme_config.get('dividers', common_config['dividers'])
self.dividers = theme_config['dividers']
self.dividers = dict((
(key, dict((k, u(v))
for k, v in val.items()))
for key, val in self.dividers.items()
))
self.spaces = theme_config.get('spaces', common_config['spaces'])
self.spaces = theme_config['spaces']
self.segments = []
self.EMPTY_SEGMENT = {
'contents': None,

View File

@ -17,17 +17,6 @@ CONFIG_DIR = 'tests/config'
root_config = lambda: {
'common': {
'dividers': {
'left': {
'hard': '#>',
'soft': '|>',
},
'right': {
'hard': '<#',
'soft': '<|',
},
},
'spaces': 0,
'interval': None,
'watcher': 'auto',
},
@ -76,12 +65,41 @@ theme_config = lambda: {
}
}
top_theme_config = lambda: {
'dividers': {
'left': {
'hard': '#>',
'soft': '|>',
},
'right': {
'hard': '<#',
'soft': '<|',
},
},
'spaces': 0,
}
main_tree = lambda: {
'1/config': root_config(),
'1/colors': colors_config(),
'1/colorschemes/default': colorscheme_config(),
'1/themes/test/default': theme_config(),
'1/themes/powerline': top_theme_config(),
'1/themes/other1': mdc(top_theme_config(), {
'dividers': {
'left': {
'hard': '!>',
}
}
}),
'1/themes/other2': mdc(top_theme_config(), {
'dividers': {
'left': {
'hard': '>>',
}
}
}),
}
@ -151,11 +169,7 @@ class TestMerging(TestCase):
with WithConfigTree(mdc(main_tree(), {
'2/config': {
'common': {
'dividers': {
'left': {
'hard': '!>',
}
}
'default_top_theme': 'other1',
}
},
})) as p:
@ -163,36 +177,26 @@ class TestMerging(TestCase):
with WithConfigTree(mdc(main_tree(), {
'2/config': {
'common': {
'dividers': {
'left': {
'hard': '!>',
}
}
'default_top_theme': 'other1',
}
},
'3/config': {
'common': {
'dividers': {
'left': {
'hard': '>>',
}
}
'default_top_theme': 'other2',
}
},
})) as p:
self.assertRenderEqual(p, '{12} bt{2-}>>{--}')
def test_top_theme_merging(self):
with WithConfigTree(mdc(main_tree(), {
'2/config': {
'common': {
'spaces': 1,
}
'2/themes/powerline': {
'spaces': 1,
},
'3/config': {
'common': {
'dividers': {
'left': {
'hard': '>>',
}
'3/themes/powerline': {
'dividers': {
'left': {
'hard': '>>',
}
}
},

View File

@ -12,17 +12,6 @@ from tests.lib.config_mock import get_powerline, add_watcher_events
config = {
'config': {
'common': {
'dividers': {
"left": {
"hard": ">>",
"soft": ">",
},
"right": {
"hard": "<<",
"soft": "<",
},
},
'spaces': 0,
'interval': 0,
'watcher': 'test',
},
@ -73,6 +62,32 @@ config = {
],
},
},
'themes/powerline': {
'dividers': {
"left": {
"hard": ">>",
"soft": ">",
},
"right": {
"hard": "<<",
"soft": "<",
},
},
'spaces': 0,
},
'themes/other': {
'dividers': {
"left": {
"hard": ">>",
"soft": ">",
},
"right": {
"hard": "<<",
"soft": "<",
},
},
'spaces': 1,
},
'themes/test/2': {
'segments': {
"left": [
@ -116,7 +131,7 @@ class TestConfigReload(TestCase):
def test_noreload(self, config):
with get_powerline(config, run_once=True) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['config']['common']['spaces'] = 1
add_watcher_events(p, 'config', wait=False, interval=0.05)
# When running once thread should not start
@ -128,25 +143,30 @@ class TestConfigReload(TestCase):
def test_reload_main(self, config):
with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['config']['common']['spaces'] = 1
config['config']['common']['default_top_theme'] = 'other'
add_watcher_events(p, 'config')
p.render()
self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>')
self.assertAccessEvents(p, 'config')
self.assertAccessEvents(p, 'config', 'themes/other', 'check:themes/test/__main__', 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [])
config['config']['ext']['test']['theme'] = 'nonexistent'
add_watcher_events(p, 'config')
self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'check:themes/test/nonexistent')
self.assertAccessEvents(p, 'config', 'check:themes/test/nonexistent', 'themes/other', 'check:themes/test/__main__')
# It should normally handle file missing error
self.assertEqual(p.logger._pop_msgs(), ['exception:test:powerline:Failed to create renderer: themes/test/nonexistent'])
self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/test/__main__',
'exception:test:powerline:Failed to load theme: themes/test/nonexistent',
'exception:test:powerline:Failed to create renderer: themes/test/nonexistent'
])
config['config']['ext']['test']['theme'] = 'default'
add_watcher_events(p, 'config')
self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'themes/test/default')
self.assertAccessEvents(p, 'config', 'themes/test/default', 'themes/other', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), [])
config['config']['ext']['test']['colorscheme'] = 'nonexistent'
@ -170,7 +190,7 @@ class TestConfigReload(TestCase):
config['config']['ext']['test']['theme'] = '2'
add_watcher_events(p, 'config')
self.assertEqual(p.render(), '<2 3 1> t <3 4 False>>><1 4 4>b <4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'themes/test/2')
self.assertAccessEvents(p, 'config', 'themes/test/2', 'themes/other', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), [])
self.assertEqual(p.renderer.local_themes, None)
@ -185,7 +205,7 @@ class TestConfigReload(TestCase):
def test_reload_unexistent(self, config):
with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['config']['ext']['test']['colorscheme'] = 'nonexistentraise'
add_watcher_events(p, 'config')
@ -222,7 +242,7 @@ class TestConfigReload(TestCase):
def test_reload_colors(self, config):
with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['colors']['colors']['col1'] = 5
add_watcher_events(p, 'colors')
@ -234,7 +254,7 @@ class TestConfigReload(TestCase):
def test_reload_colorscheme(self, config):
with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['colorschemes/test/default']['groups']['str1']['bg'] = 'col3'
add_watcher_events(p, 'colorschemes/test/default')
@ -246,12 +266,24 @@ class TestConfigReload(TestCase):
def test_reload_theme(self, config):
with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['themes/test/default']['segments']['left'][0]['contents'] = 'col3'
add_watcher_events(p, 'themes/test/default')
self.assertEqual(p.render(), '<1 2 1> col3<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'themes/test/default')
self.assertAccessEvents(p, 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), [])
@with_new_config
def test_reload_top_theme(self, config):
with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['themes/powerline']['dividers']['left']['hard'] = '|>'
add_watcher_events(p, 'themes/powerline')
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>|><3 4 4>g<4 False False>|><None None None>')
self.assertAccessEvents(p, 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), [])
@with_new_config
@ -259,12 +291,12 @@ class TestConfigReload(TestCase):
config['config']['common']['interval'] = None
with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['themes/test/default']['segments']['left'][0]['contents'] = 'col3'
add_watcher_events(p, 'themes/test/default', wait=False)
self.assertEqual(p.render(), '<1 2 1> col3<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'themes/test/default')
self.assertAccessEvents(p, 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), [])
self.assertTrue(p._watcher._calls)
@ -273,7 +305,7 @@ class TestConfigReload(TestCase):
config['config']['common']['interval'] = None
with get_powerline(config, run_once=True) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['themes/test/default']['segments']['left'][0]['contents'] = 'col3'
add_watcher_events(p, 'themes/test/default', wait=False)

View File

@ -22,17 +22,6 @@ def highlighted_string(s, group, **kwargs):
config = {
'config': {
'common': {
'dividers': {
'left': {
'hard': '>>',
'soft': '>',
},
'right': {
'hard': '<<',
'soft': '<',
},
},
'spaces': 0,
'interval': 0,
'watcher': 'test',
},
@ -104,6 +93,26 @@ config = {
],
},
},
'themes/powerline': {
'dividers': {
'left': {
'hard': '>>',
'soft': '>',
},
'right': {
'hard': '<<',
'soft': '<',
},
},
'spaces': 0,
},
'themes/test/__main__': {
'dividers': {
'right': {
'soft': '|',
},
},
},
'themes/vim/default': {
'default_module': 'powerline.segments.common',
'segments': {
@ -147,9 +156,9 @@ class TestRender(TestCase):
class TestLines(TestRender):
@add_args
def test_without_above(self, p, config):
self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}<{344}f {--}')
self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}<{344}f {--}', width=10)
# self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}<{344} f {--}', width=11)
self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}|{344}f {--}')
self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}|{344}f {--}', width=10)
# self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}|{344} f {--}', width=11)
self.assertEqual(list(p.render_above_lines()), [])
@with_new_config
@ -158,21 +167,21 @@ class TestLines(TestRender):
config['themes/test/default']['segments']['above'] = [old_segments]
with get_powerline(config, run_once=True, simpler_renderer=True) as p:
self.assertRenderLinesEqual(p, [
'{121} s{24}>>{344}g{34}>{34}<{344}f {--}',
'{121} s{24}>>{344}g{34}>{34}|{344}f {--}',
])
self.assertRenderLinesEqual(p, [
'{121} s {24}>>{344}g{34}>{34}<{344}f {--}',
'{121} s {24}>>{344}g{34}>{34}|{344}f {--}',
], width=10)
config['themes/test/default']['segments']['above'] = [old_segments] * 2
with get_powerline(config, run_once=True, simpler_renderer=True) as p:
self.assertRenderLinesEqual(p, [
'{121} s{24}>>{344}g{34}>{34}<{344}f {--}',
'{121} s{24}>>{344}g{34}>{34}<{344}f {--}',
'{121} s{24}>>{344}g{34}>{34}|{344}f {--}',
'{121} s{24}>>{344}g{34}>{34}|{344}f {--}',
])
self.assertRenderLinesEqual(p, [
'{121} s {24}>>{344}g{34}>{34}<{344}f {--}',
'{121} s {24}>>{344}g{34}>{34}<{344}f {--}',
'{121} s {24}>>{344}g{34}>{34}|{344}f {--}',
'{121} s {24}>>{344}g{34}>{34}|{344}f {--}',
], width=10)
@ -299,6 +308,82 @@ class TestColorschemesHierarchy(TestRender):
self.assertEqual(p.logger._pop_msgs(), [])
class TestThemeHierarchy(TestRender):
@add_args
def test_hierarchy(self, p, config):
self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}|{344}f {--}')
@add_args
def test_no_main(self, p, config):
del config['themes/test/__main__']
self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}<{344}f {--}')
self.assertEqual(p.logger._pop_msgs(), [])
@add_args
def test_no_powerline(self, p, config):
config['themes/test/__main__']['dividers'] = config['themes/powerline']['dividers']
config['themes/test/__main__']['spaces'] = 1
del config['themes/powerline']
self.assertRenderEqual(p, '{121} s {24}>>{344}g {34}>{34}<{344} f {--}')
self.assertEqual(p.logger._pop_msgs(), [])
@add_args
def test_no_default(self, p, config):
del config['themes/test/default']
self.assertRenderEqual(p, 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/test/default',
'exception:test:powerline:Failed to create renderer: themes/test/default',
'exception:test:powerline:Failed to render: themes/test/default',
])
@add_args
def test_only_default(self, p, config):
config['themes/test/default']['dividers'] = config['themes/powerline']['dividers']
config['themes/test/default']['spaces'] = 1
del config['themes/test/__main__']
del config['themes/powerline']
self.assertRenderEqual(p, '{121} s {24}>>{344}g {34}>{34}<{344} f {--}')
@add_args
def test_only_main(self, p, config):
del config['themes/test/default']
del config['themes/powerline']
self.assertRenderEqual(p, 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/powerline',
'exception:test:powerline:Failed to load theme: themes/test/default',
'exception:test:powerline:Failed to create renderer: themes/test/default',
'exception:test:powerline:Failed to render: themes/test/default',
])
@add_args
def test_only_powerline(self, p, config):
del config['themes/test/default']
del config['themes/test/__main__']
self.assertRenderEqual(p, 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/test/__main__',
'exception:test:powerline:Failed to load theme: themes/test/default',
'exception:test:powerline:Failed to create renderer: themes/test/default',
'exception:test:powerline:Failed to render: themes/test/default',
])
@add_args
def test_nothing(self, p, config):
del config['themes/test/default']
del config['themes/powerline']
del config['themes/test/__main__']
self.assertRenderEqual(p, 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/powerline',
'exception:test:powerline:Failed to load theme: themes/test/__main__',
'exception:test:powerline:Failed to load theme: themes/test/default',
'exception:test:powerline:Failed to create renderer: themes/test/default',
'exception:test:powerline:Failed to render: themes/test/default',
])
class TestVim(TestCase):
def test_environ_update(self):
# Regression test: test that segment obtains environment from vim, not