Add most configuration checks

Changes:

- Add main configuration, colors.json, almost full themes and colorschemes
  checks
- Make powerline.lint.check return whether it had problems
- Make powerline-lint fail if .check reported problems
- Make tests run powerline-lint
- Add the script to the list of the installed scripts

Fixes #278
This commit is contained in:
ZyX 2013-03-09 16:16:00 +04:00
parent 287a88f473
commit d27f7a0411
6 changed files with 924 additions and 2 deletions

View File

@ -0,0 +1,899 @@
from powerline.lint.markedjson import load
from powerline import load_json_config, Powerline
from powerline.lint.markedjson.error import echoerr
from powerline.segments.vim import vim_modes
import itertools
import sys
import os
import re
from collections import defaultdict
from copy import copy
try:
from __builtin__ import unicode
except ImportError:
unicode = str
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()
def context_key(context):
return '/'.join((unicode(c[0]) for c in context))
class Spec(object):
def __init__(self, **keys):
new_keys = {}
self.specs = list(keys.values())
for k, v in keys.items():
new_keys[k] = len(self.specs)
self.specs.append(v)
self.keys = new_keys
self.checks = []
self.cmsg = ''
self.isoptional = False
self.uspecs = []
self.ufailmsg = lambda key: 'found unknown key: {0}'.format(key)
if keys:
self.type(dict)
def copy(self):
return self.__class__().update(self.__dict__)
def update(self, d):
self.__dict__.update(d)
self.checks = copy(self.checks)
self.uspecs = copy(self.uspecs)
self.specs = [spec.copy() for spec in self.specs]
return self
def unknown_spec(self, keyfunc, spec):
if isinstance(keyfunc, Spec):
self.specs.append(keyfunc)
keyfunc = len(self.specs) - 1
self.specs.append(spec)
self.uspecs.append((keyfunc, len(self.specs) - 1))
return self
def unknown_msg(self, msgfunc):
self.ufailmsg = msgfunc
return self
def context_message(self, msg):
self.cmsg = msg
for spec in self.specs:
if not spec.cmsg:
spec.context_message(msg)
return self
def check_type(self, value, context_mark, data, context, echoerr, t):
if type(value.value) is not t:
echoerr(context=self.cmsg.format(key=context_key(context)),
context_mark=context_mark,
problem='must be a {0} instance'.format(t.__name__),
problem_mark=value.mark)
return False, True
return True, False
def check_func(self, value, context_mark, data, context, echoerr, func, msg_func):
proceed, echo, hadproblem = func(value, data, context, echoerr)
if echo and hadproblem:
echoerr(context=self.cmsg.format(key=context_key(context)),
context_mark=context_mark,
problem=msg_func(value),
problem_mark=value.mark)
return proceed, hadproblem
def check_list(self, value, context_mark, data, context, echoerr, item_func, msg_func):
i = 0
hadproblem = False
for item in value:
if isinstance(item_func, int):
spec = self.specs[item_func]
proceed, fhadproblem = spec.match(item, value.mark, data, context + (('list item ' + unicode(i), item),), echoerr)
else:
proceed, echo, fhadproblem = item_func(item, data, context, echoerr)
if echo and fhadproblem:
echoerr(context=self.cmsg.format(key=context_key(context)+'/list item '+unicode(i)),
context_mark=value.mark,
problem=msg_func(item),
problem_mark=item.mark)
if fhadproblem:
hadproblem = True
if not proceed:
return proceed, hadproblem
i += 1
return True, hadproblem
def check_either(self, value, context_mark, data, context, echoerr, start, end):
errs = []
def new_echoerr(*args, **kwargs):
errs.append((args, kwargs))
hadproblem = False
for spec in self.specs[start:end]:
proceed, hadproblem = spec.match(value, value.mark, data, context, new_echoerr)
if not proceed:
break
if not hadproblem:
return True, False
for args, kwargs in errs:
echoerr(*args, **kwargs)
return False, hadproblem
def check_tuple(self, value, context_mark, data, context, echoerr, start, end):
hadproblem = False
for (i, item, spec) in zip(itertools.count(), value, self.specs[start:end]):
proceed, ihadproblem = spec.match(item, value.mark, data, context + (('tuple item '+unicode(i), item),), echoerr)
if ihadproblem:
hadproblem = True
if not proceed:
return False, hadproblem
return True, hadproblem
def type(self, t):
self.checks.append(('check_type', t))
return self
cmp_funcs = {
'le': lambda x, y: x <= y,
'lt': lambda x, y: x < y,
'ge': lambda x, y: x >= y,
'gt': lambda x, y: x > y,
'eq': lambda x, y: x == y,
}
cmp_msgs = {
'le': 'lesser or equal to',
'lt': 'lesser then',
'ge': 'greater or equal to',
'gt': 'greater then',
'eq': 'equal to',
}
def len(self, comparison, cint, msg_func=None):
cmp_func = self.cmp_funcs[comparison]
msg_func = msg_func or (lambda value: 'length of {0!r} is not {1} {2}'.format(value, self.cmp_msgs[comparison], cint))
self.checks.append(('check_func',
(lambda value, *args: (True, True, not cmp_func(len(value), cint))),
msg_func))
return self
def cmp(self, comparison, cint, msg_func=None):
if type(cint) is str:
self.type(unicode)
else:
self.type(type(cint))
cmp_func = self.cmp_funcs[comparison]
msg_func = msg_func or (lambda value: '{0} is not {1} {2}'.format(value, self.cmp_msgs[comparison], cint))
self.checks.append(('check_func',
(lambda value, *args: (True, True, not cmp_func(value, cint))),
msg_func))
return self
def unsigned(self, msg_func=None):
self.type(int)
self.checks.append(('check_func',
(lambda value, *args: (True, True, value < 0)),
lambda value: '{0} must be greater then zero'.format(value)))
return self
def list(self, item_func, msg_func=None):
self.type(list)
if isinstance(item_func, Spec):
self.specs.append(item_func)
item_func = len(self.specs) - 1
self.checks.append(('check_list', item_func, msg_func or (lambda item: 'failed check')))
return self
def tuple(self, *specs):
self.type(list)
max_len = len(specs)
min_len = max_len
for spec in reversed(specs):
if spec.isoptional:
min_len -= 1
else:
break
if max_len == min_len:
self.len('eq', len(specs))
else:
self.len('ge', min_len)
self.len('le', max_len)
start = len(self.specs)
for i, spec in zip(itertools.count(), specs):
self.specs.append(spec)
self.checks.append(('check_tuple', start, len(self.specs)))
return self
def func(self, func, msg_func=None):
self.checks.append(('check_func', func, msg_func or (lambda value: 'failed check')))
return self
def re(self, regex, msg_func=None):
self.type(unicode)
compiled = re.compile(regex)
msg_func = msg_func or (lambda value: 'String "{0}" does not match "{1}"'.format(value, regex))
self.checks.append(('check_func',
(lambda value, *args: (True, True, not compiled.match(value.value))),
msg_func))
return self
def ident(self, msg_func=None):
msg_func = msg_func or (lambda value: 'String "{0}" is not an alphanumeric/underscore identifier'.format(value))
return self.re('^\w+$', msg_func)
def oneof(self, collection, msg_func=None):
msg_func = msg_func or (lambda value: '"{0}" must be one of {1!r}'.format(value, list(collection)))
self.checks.append(('check_func',
lambda value, *args: (True, True, value not in collection),
msg_func))
return self
def either(self, *specs):
start = len(self.specs)
self.specs.extend(specs)
self.checks.append(('check_either', start, len(self.specs)))
return self
def optional(self):
self.isoptional = True
return self
def match_checks(self, *args):
hadproblem = False
for check in self.checks:
proceed, chadproblem = getattr(self, check[0])(*(args + check[1:]))
if chadproblem:
hadproblem = True
if not proceed:
return False, hadproblem
return True, hadproblem
def match(self, value, context_mark=None, data=None, context=EMPTYTUPLE, echoerr=echoerr):
proceed, hadproblem = self.match_checks(value, context_mark, data, context, echoerr)
if proceed:
if self.keys or self.uspecs:
for key, vali in self.keys.items():
valspec = self.specs[vali]
if key in value:
proceed, mhadproblem = valspec.match(value[key], value.mark, data, context + ((key, value[key]),), echoerr)
if mhadproblem:
hadproblem = True
if not proceed:
return False, hadproblem
else:
if not valspec.isoptional:
hadproblem = True
echoerr(context=self.cmsg.format(key=context_key(context)),
context_mark=None,
problem='required key is missing: {0}'.format(key),
problem_mark=value.mark)
for key in value.keys():
if key not in self.keys:
for keyfunc, vali in self.uspecs:
valspec = self.specs[vali]
if isinstance(keyfunc, int):
spec = self.specs[keyfunc]
proceed, khadproblem = spec.match(key, context_mark, data, context, echoerr)
else:
proceed, khadproblem = keyfunc(key, data, context, echoerr)
if khadproblem:
hadproblem = True
if proceed:
valspec.match(value[key], value.mark, data, context + ((key, value[key]),), echoerr)
break
else:
hadproblem = True
if self.ufailmsg:
echoerr(context=self.cmsg.format(key=context_key(context)),
context_mark=None,
problem=self.ufailmsg(key),
problem_mark=key.mark)
return True, hadproblem
class WithPath(object):
def __init__(self, import_paths):
self.import_paths = import_paths
def __enter__(self):
self.oldpath = sys.path
sys.path = self.import_paths + sys.path
def __exit__(self, *args):
sys.path = self.oldpath
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', [])]
match_module, separator, match_function = match_name.rpartition('.')
if not separator:
match_module = 'powerline.matchers.{0}'.format(ext)
match_function = match_name
with WithPath(import_paths):
try:
func = getattr(__import__(match_module, fromlist=[match_function]), unicode(match_function))
except ImportError:
echoerr(context='Error while loading matcher functions',
problem='failed to load module {0}'.format(match_module),
problem_mark=match_name.mark)
return True, True
except AttributeError:
echoerr(context='Error while loading matcher functions',
problem='failed to load matcher function {0}'.format(match_function),
problem_mark=match_name.mark)
return True, True
if not callable(func):
echoerr(context='Error while loading matcher functions',
problem='loaded "function" {0} is not callable'.format(match_function),
problem_mark=match_name.mark)
return True, True
if hasattr(func, 'func_code') and hasattr(func.func_code, 'co_argcount'):
if func.func_code.co_argcount != 1:
echoerr(context='Error while loading matcher functions',
problem='function {0} accepts {1} arguments instead of 1. Are you sure it is the proper function?'.format(match_function, func.func_code.co_argcount),
problem_mark=match_name.mark)
return True, False
def check_ext(ext, data, context, echoerr):
hadsomedirs = False
hadproblem = False
for subdir in ('themes', 'colorschemes'):
if ext not in data['configs'][subdir]:
hadproblem = True
echoerr(context='Error while loading {0} extension configuration'.format(ext),
context_mark=ext.mark,
problem='{0} configuration does not exist'.format(subdir))
else:
hadsomedirs = True
return hadsomedirs, hadproblem
def check_config(d, theme, data, context, echoerr):
if len(context) == 4:
ext = context[-2][0]
else:
# local_themes
ext = context[-3][0]
if ext not in data['configs']['themes'] or theme not in data['configs']['themes'][ext]:
echoerr(context='Error while loading {0} from {1} extension configuration'.format(d[:-1], ext),
problem='failed to find configuration file themes/{0}/{1}.json'.format(ext, 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(),
).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)),
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
paths=Spec().list((lambda value, *args: (True, True, not os.path.exists(value.value))),
lambda value: 'path does not exist: {0}'.format(value)).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())
),
).unknown_spec(check_ext,
Spec(
colorscheme=colorscheme_spec(),
theme=theme_spec(),
))
.context_message('Error while loading extensions configuration (key {key})'),
).context_message('Error while loading main configuration'))
term_color_spec=Spec().unsigned().cmp('le', 255).copy
true_color_spec=Spec().re('^[0-9a-fA-F]{6}$',
lambda value: '"{0}" is not a six-digit hexadecimal unsigned integer written as a string'.format(value)).copy
colors_spec = (Spec(
colors=Spec().unknown_spec(
Spec().ident(),
Spec().either(
Spec().tuple(term_color_spec(), true_color_spec()),
term_color_spec()))
.context_message('Error while checking colors (key {key})'),
gradients=Spec().unknown_spec(
Spec().ident(),
Spec().tuple(
Spec().len('gt', 1).list(term_color_spec()),
Spec().len('gt', 1).list(true_color_spec()).optional(),
)
).context_message('Error while checking gradients (key {key})'),
).context_message('Error while loading colors configuration'))
def check_color(color, data, context, echoerr):
if color not in data['colors_config'].get('colors', {}) and color not in data['colors_config'].get('gradients', {}):
echoerr(context='Error while checking highlight group in colorscheme (key {key})'.format(key=context_key(context)),
problem='found unexistent color or gradient {0}'.format(color),
problem_mark=color.mark)
return True, False, True
return True, False, False
def check_translated_group_name(group, data, context, echoerr):
if group not in context[0][1].get('groups', {}):
echoerr(context='Error while checking translated group in colorscheme (key {key})'.format(key=context_key(context)),
problem='translated group {0} is not in main groups dictionary'.format(group),
problem_mark=group.mark)
return True, False, True
return True, False, False
color_spec = Spec().type(unicode).func(check_color).copy
name_spec = Spec().type(unicode).len('gt', 0).optional().copy
group_spec = Spec(
fg=color_spec(),
bg=color_spec(),
attr=Spec().list(Spec().type(unicode).oneof(set(('bold', 'italic', 'underline')))).optional(),
).copy
group_name_spec = Spec().re('^\w+(?::\w+)?$').copy
groups_spec = Spec().unknown_spec(
group_name_spec(),
group_spec(),
).copy
colorscheme_spec = (Spec(
name=name_spec(),
groups=groups_spec().context_message('Error while loading groups (key {key})'),
).context_message('Error while loading coloscheme'))
vim_mode_spec = Spec().oneof(set(list(vim_modes) + ['nc'])).copy
vim_colorscheme_spec = (Spec(
name=name_spec(),
groups=groups_spec().context_message('Error while loading groups (key {key})'),
mode_translations=Spec().unknown_spec(
vim_mode_spec(),
Spec(
colors=Spec().unknown_spec(
color_spec(),
color_spec(),
).optional(),
groups=Spec().unknown_spec(
group_name_spec().func(check_translated_group_name),
group_spec(),
).optional(),
),
).context_message('Error while loading mode translations (key {key})'),
).context_message('Error while loading vim colorscheme'))
generic_keys = set(('exclude_modes', 'include_modes', 'width', 'align', 'name', 'draw_divider', 'priority', 'after', 'before'))
type_keys = {
'function': set(('args', 'module')),
'string': set(('contents', 'type', 'highlight_group', 'divider_highlight_group')),
'filler': set(('type', 'highlight_group', 'divider_highlight_group')),
}
required_keys = {
'function': set(),
'string': set(('contents', 'highlight_group')),
'filler': set(('highlight_group',)),
}
function_keys = set(('args', 'module'))
def check_key_compatibility(segment, data, context, echoerr):
segment_type = segment.get('type', 'function')
if segment_type not in type_keys:
echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
problem='found segment with unknown type {0}'.format(segment_type),
problem_mark=segment_type.mark)
return False, False, True
keys = set(segment)
if not ((keys - generic_keys) < type_keys[segment_type]):
unknown_keys = keys - generic_keys - type_keys[segment_type]
echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
context_mark=context[-1][1].mark,
problem='found keys not used with the current segment type: {0}'.format(
', '.join((unicode(key) for key in unknown_keys))),
problem_mark=list(unknown_keys)[0].mark)
return True, False, True
if not (keys > required_keys[segment_type]):
missing_keys = required_keys[segment_type] - keys
echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
context_mark=context[-1][1].mark,
problem='found missing required keys: {0}'.format(
', '.join((unicode(key) for key in missing_keys))))
return True, False, True
return True, False, False
def check_segment_module(module, data, context, echoerr):
with WithPath(data['import_paths']):
try:
__import__(unicode(module))
except ImportError:
echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
problem='failed to import module {0}'.format(module),
problem_mark=module.mark)
return True, False, True
return True, False, False
def check_full_segment_data(segment, data, context, echoerr):
if 'name' not in segment:
return True, False, False
ext = data['ext']
theme_segment_data = context[0][1].get('segment_data', {})
top_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', {})
if data['theme'] == top_theme_name:
top_segment_data = {}
else:
top_segment_data = data['ext_theme_configs'].get(top_theme_name, {}).get('segment_data', {})
names = [segment['name']]
if segment.get('type', 'function') == 'function':
module = segment.get('module', context[0][1].get('default_module', 'powerline.segments.'+ext))
names.insert(0, unicode(module) + '.' + unicode(names[0]))
segment_copy = segment.copy()
for key in ('before', 'after', 'args', 'contents'):
if key not in segment_copy:
for segment_data in [theme_segment_data, top_segment_data]:
for name in names:
try:
val = segment_data[name][key]
# HACK to keep marks
l = list(segment_data[name])
k = l[l.index(key)]
segment_copy[k] = val
except KeyError:
pass
return check_key_compatibility(segment_copy, data, context, echoerr)
def check_segment_name(name, data, context, echoerr):
ext = data['ext']
if context[-2][1].get('type', 'function') == 'function':
module = context[-2][1].get('module', context[0][1].get('default_module', 'powerline.segments.'+ext))
with WithPath(data['import_paths']):
try:
func = getattr(__import__(unicode(module), fromlist=[unicode(name)]), unicode(name))
except ImportError:
echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
problem='failed to import function {0} from module {1}'.format(name, module),
problem_mark=module.mark)
return True, False, True
if not callable(func):
echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
problem='imported "function" {0} from module {1} is not callable'.format(name, module),
problem_mark=module.mark)
return True, False, True
hl_groups = []
divider_hl_group = None
if func.__doc__:
H_G_USED_STR = 'Highlight groups used: '
D_H_G_USED_STR = 'Divider highlight group used: '
for line in func.__doc__.split('\n'):
if H_G_USED_STR in line:
hl_groups.append(line[line.index(H_G_USED_STR)+len(H_G_USED_STR):])
elif D_H_G_USED_STR in line:
divider_hl_group = line[line.index(D_H_G_USED_STR)+len(D_H_G_USED_STR)+2:-3]
hadproblem = False
if divider_hl_group:
r = hl_exists(divider_hl_group, data, context, echoerr, allow_gradients=True)
if r:
echoerr(context='Error while checking theme (key {key})'.format(key=context_key(context)),
problem='found highlight group {0} not defined in the following colorschemes: {1}\n(Group name was obtained from function documentation.)'.format(
divider_hl_group, ', '.join(r)),
problem_mark=name.mark)
hadproblem = True
if hl_groups:
greg = re.compile(r'``([^`]+)``( \(gradient\))?')
hl_groups = [[greg.match(subs).groups() for subs in s.split(' or ')] for s in (', '.join(hl_groups)).split(', ')]
for required_pack in hl_groups:
rs = [hl_exists(hl_group, data, context, echoerr, allow_gradients=('force' if gradient else False))
for hl_group, gradient in required_pack]
if all(rs):
echoerr(context='Error while checking theme (key {key})'.format(key=context_key(context)),
problem='found highlight groups list ({0}) with all groups not defined in some colorschemes\n(Group names were taken from function documentation.)'.format(
', '.join((unicode(h[0]) for h in required_pack))),
problem_mark=name.mark)
for r, h in zip(rs, required_pack):
echoerr(context='Error while checking theme (key {key})'.format(key=context_key(context)),
problem='found highlight group {0} not defined in the following colorschemes: {1}'.format(
h[0], ', '.join(r)))
hadproblem = True
else:
r = hl_exists(name, data, context, echoerr, allow_gradients=True)
if r:
echoerr(context='Error while checking theme (key {key})'.format(key=context_key(context)),
problem='found highlight group {0} not defined in the following colorschemes: {1}\n(If not specified otherwise in documentation, highlight group for function segments\nis the same as the function name.)'.format(
name, ', '.join(r)),
problem_mark=name.mark)
hadproblem = True
return True, False, hadproblem
else:
if name not in context[0][1].get('segment_data', {}):
top_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', {})
if data['theme'] == top_theme_name:
top_theme = {}
else:
top_theme = data['ext_theme_configs'].get(top_theme_name, {})
if name not in top_theme.get('segment_data', {}):
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)
return True, False, False
def hl_exists(hl_group, data, context, echoerr, allow_gradients=False):
ext = data['ext']
if ext not in data['colorscheme_configs']:
# No colorschemes. Error was already reported, no need to report it
# twice
return []
r = []
for colorscheme, cconfig in data['colorscheme_configs'][ext].items():
if hl_group not in cconfig.get('groups', {}):
r.append(colorscheme)
elif not allow_gradients:
group_config = cconfig['groups'][hl_group]
hadgradient = False
for ckey in ('fg', 'bg'):
color = group_config.get(ckey)
if not color:
# No color. Error was already reported.
continue
# Gradients are only allowed for function segments. Note that
# whether *either* color or gradient exists should have been
# already checked
hascolor = color in data['colors_config'].get('colors', {})
hasgradient = color in data['colors_config'].get('gradients', {})
if hasgradient:
hadgradient = True
if allow_gradients is False and not hascolor and hasgradient:
echoerr(context='Error while checking highlight group in theme (key {key})'.format(key=context_key(context)),
context_mark=hl_group.mark,
problem='group {0} is using gradient {1} instead of a color'.format(hl_group, color),
problem_mark=color.mark)
r.append(colorscheme)
continue
if allow_gradients == 'force' and not hadgradient:
echoerr(context='Error while checking highlight group in theme (key {key})'.format(key=context_key(context)),
context_mark=hl_group.mark,
problem='group {0} should have at least one gradient color, but it has no'.format(hl_group),
problem_mark=group_config.mark)
r.append(colorscheme)
return r
def check_highlight_group(hl_group, data, context, echoerr):
r = hl_exists(hl_group, data, context, echoerr)
if r:
echoerr(context='Error while checking theme (key {key})'.format(key=context_key(context)),
problem='found highlight group {0} not defined in the following colorschemes: {1}'.format(
hl_group, ', '.join(r)),
problem_mark=hl_group.mark)
return True, False, True
return True, False, False
def check_highlight_groups(hl_groups, data, context, echoerr):
rs = [hl_exists(hl_group, data, context, echoerr) for hl_group in hl_groups]
if all(rs):
echoerr(context='Error while checking theme (key {key})'.format(key=context_key(context)),
problem='found highlight groups list ({0}) with all groups not defined in some colorschemes'.format(
', '.join((unicode(h) for h in hl_groups))),
problem_mark=hl_groups.mark)
for r, hl_group in zip(rs, hl_groups):
echoerr(context='Error while checking theme (key {key})'.format(key=context_key(context)),
problem='found highlight group {0} not defined in the following colorschemes: {1}'.format(
hl_group, ', '.join(r)),
problem_mark=hl_group.mark)
return True, False, True
return True, False, False
def check_segment_data_key(key, data, context, echoerr):
ext = data['ext']
top_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', {})
is_top_theme = (data['theme'] == top_theme_name)
if is_top_theme:
themes = data['ext_theme_configs'].values()
else:
themes = [context[0][1]]
for theme in themes:
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 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
return True, False, False
highlight_group_spec = Spec().type(unicode).copy
segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy
segments_spec = Spec().optional().list(
Spec(
type=Spec().oneof(type_keys).optional(),
name=Spec().re('^[a-zA-Z_]\w+$').func(check_segment_name).optional(),
exclude_modes=Spec().list(vim_mode_spec()).optional(),
include_modes=Spec().list(vim_mode_spec()).optional(),
draw_divider=Spec().type(bool).optional(),
module=segment_module_spec(),
priority=Spec().cmp('ge', -1).optional(),
after=Spec().type(unicode).optional(),
before=Spec().type(unicode).optional(),
width=Spec().either(Spec().unsigned(), Spec().cmp('eq', 'auto')).optional(),
align=Spec().oneof(set('lr')).optional(),
# FIXME Check args
args=Spec().type(dict).optional(),
contents=Spec().type(unicode).optional(),
highlight_group=Spec().list(
highlight_group_spec().re('^(?:(?!:divider$).)+$',
lambda value: 'it is recommended that only divider highlight group names end with ":divider"')
).func(check_highlight_groups).optional(),
divider_highlight_group=highlight_group_spec().func(check_highlight_group).re(':divider$',
lambda value: 'it is recommended that divider highlight group names end with ":divider"').optional(),
).func(check_full_segment_data),
).copy
theme_spec = (Spec(
default_module=segment_module_spec(),
segment_data=Spec().unknown_spec(
Spec().func(check_segment_data_key),
Spec(
after=Spec().type(unicode).optional(),
before=Spec().type(unicode).optional(),
# FIXME Check args
args=Spec().type(dict).optional(),
contents=Spec().type(unicode).optional(),
),
).optional().context_message('Error while loading segment data (key {key})'),
segments=Spec(
left=segments_spec().context_message('Error while loading segments from left side (key {key})'),
right=segments_spec().context_message('Error while loading segments from right side (key {key})'),
).func(
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})'),
).context_message('Error while loading theme'))
def check(path=None):
search_paths = [path] if path else Powerline.get_config_paths()
dirs = {
'themes': defaultdict(lambda: []),
'colorschemes': defaultdict(lambda: [])
}
for path in reversed(search_paths):
for subdir in ('themes', 'colorschemes'):
d = os.path.join(path, subdir)
if os.path.isdir(d):
for ext in os.listdir(d):
extd = os.path.join(d, ext)
if os.path.isdir(extd):
dirs[subdir][ext].append(extd)
elif os.path.exists(d):
hadproblem = True
sys.stderr.write('Path {0} is supposed to be a directory, but it is not\n'.format(d))
configs = {
'themes': defaultdict(lambda: {}),
'colorschemes': defaultdict(lambda: {})
}
for subdir in ('themes', 'colorschemes'):
for ext in dirs[subdir]:
for d in dirs[subdir][ext]:
for config in os.listdir(d):
if config.endswith('.json'):
configs[subdir][ext][config[:-5]] = os.path.join(d, config)
diff = set(configs['themes']) ^ set(configs['colorschemes'])
if diff:
hadproblem = True
for ext in diff:
sys.stderr.write('{0} extension {1} present only in {2}\n'.format(
ext,
'configuration' if (ext in dirs['themes'] and ext in dirs['colorschemes']) else 'directory',
'themes' if ext in configs['themes'] else 'colorschemes',
))
main_config = load_json_config(search_paths, 'config', load=load, open=open_file)
hadproblem = main_spec.match(main_config, data={'configs': configs}, context=(('', main_config),))[1]
import_paths = [os.path.expanduser(path) for path in main_config.get('common', {}).get('paths', [])]
colors_config = load_json_config(search_paths, 'colors', load=load, open=open_file)
if colors_spec.match(colors_config, context=(('', colors_config),))[1]:
hadproblem = True
colorscheme_configs = defaultdict(lambda: {})
for ext in configs['colorschemes']:
data = {'ext': ext, 'colors_config': colors_config}
for colorscheme, cfile in configs['colorschemes'][ext].items():
with open_file(cfile) as config_file_fp:
config = load(config_file_fp)
colorscheme_configs[ext][colorscheme] = config
if ext == 'vim':
spec = vim_colorscheme_spec
else:
spec = colorscheme_spec
if spec.match(config, context=(('', config),), data=data)[1]:
hadproblem = True
theme_configs = defaultdict(lambda: {})
for ext in configs['themes']:
for theme, sfile in configs['themes'][ext].items():
with open_file(sfile) as config_file_fp:
config = load(config_file_fp)
theme_configs[ext][theme] = config
for ext, configs in theme_configs.items():
data = {'ext': ext, 'colorscheme_configs': colorscheme_configs, 'import_paths': import_paths,
'main_config': main_config, '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)[1]:
hadproblem = True
return hadproblem

View File

@ -58,6 +58,7 @@ class YAMLError(Exception):
def echoerr(*args, **kwargs): def echoerr(*args, **kwargs):
sys.stderr.write('\n')
sys.stderr.write(format_error(*args, **kwargs) + '\n') sys.stderr.write(format_error(*args, **kwargs) + '\n')
def format_error(context=None, context_mark=None, problem=None, problem_mark=None, note=None): def format_error(context=None, context_mark=None, problem=None, problem_mark=None, note=None):

14
scripts/powerline-lint Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''Powerline configuration checker.'''
import argparse
from powerline.lint import check
import sys
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-p', '--config_path', metavar='PATH')
if __name__ == '__main__':
args = parser.parse_args()
sys.exit(check(args.config_path))

View File

@ -25,6 +25,7 @@ setup(
url='https://github.com/Lokaltog/powerline', url='https://github.com/Lokaltog/powerline',
scripts=[ scripts=[
'scripts/powerline', 'scripts/powerline',
'scripts/powerline-lint',
], ],
keywords='', keywords='',
packages=find_packages(exclude=('tests', 'tests.*')), packages=find_packages(exclude=('tests', 'tests.*')),

View File

@ -5,6 +5,6 @@ if python -c 'import sys; sys.exit(1 * (sys.version_info[0] != 2))' ; then
pip install mercurial bzr pip install mercurial bzr
if python -c 'import sys; sys.exit(1 * (sys.version_info[1] >= 7))' ; then if python -c 'import sys; sys.exit(1 * (sys.version_info[1] >= 7))' ; then
# Python 2.6 # Python 2.6
pip install unittest2 pip install unittest2 argparse
fi fi
fi fi

View File

@ -1,5 +1,6 @@
#!/bin/sh #!/bin/sh
: ${PYTHON:=python} : ${PYTHON:=python}
FAILED=0
if ${PYTHON} -c 'import sys; sys.exit(1 * (sys.version_info >= (2, 7)))' ; then if ${PYTHON} -c 'import sys; sys.exit(1 * (sys.version_info >= (2, 7)))' ; then
# Python 2.6 # Python 2.6
export PYTHONPATH="${PYTHONPATH}:`realpath .`" export PYTHONPATH="${PYTHONPATH}:`realpath .`"
@ -9,5 +10,11 @@ if ${PYTHON} -c 'import sys; sys.exit(1 * (sys.version_info >= (2, 7)))' ; then
fi fi
done done
else else
${PYTHON} setup.py test if ! ${PYTHON} setup.py test ; then
FAILED=1
fi
fi fi
if ! ${PYTHON} scripts/powerline-lint ; then
FAILED=1
fi
exit $FAILED