0
0
.dotfiles/powerline-bin/powerline/lint/__init__.py

626 lines
21 KiB
Python
Raw Normal View History

2024-09-07 11:49:53 -04:00
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
import os
import logging
from collections import defaultdict
from itertools import chain
from functools import partial
from powerline import generate_config_finder, get_config_paths, load_config
from powerline.segments.vim import vim_modes
from powerline.lib.dict import mergedicts_copy
from powerline.lib.config import ConfigLoader
from powerline.lib.unicode import unicode
from powerline.lib.path import join
from powerline.lint.markedjson import load
from powerline.lint.markedjson.error import echoerr, EchoErr, MarkedError
from powerline.lint.checks import (check_matcher_func, check_ext, check_config, check_top_theme,
check_color, check_translated_group_name, check_group,
check_segment_module, check_exinclude_function, type_keys,
check_segment_function, check_args, get_one_segment_function,
check_highlight_groups, check_highlight_group, check_full_segment_data,
get_all_possible_functions, check_segment_data_key, register_common_name,
highlight_group_spec, check_log_file_level, check_logging_handler)
from powerline.lint.spec import Spec
from powerline.lint.context import Context
def open_file(path):
return open(path, 'rb')
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
function_name_re = '^(\w+\.)*[a-zA-Z_]\w*$'
divider_spec = Spec().printable().len(
'le', 3, (lambda value: 'Divider {0!r} is too large!'.format(value))).copy
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
gen_components_spec = (lambda *components: Spec().list(Spec().type(unicode).oneof(set(components))))
log_level_spec = Spec().re('^[A-Z]+$').func(
(lambda value, *args: (True, True, not hasattr(logging, value))),
(lambda value: 'unknown debugging level {0}'.format(value))
).copy
log_format_spec = Spec().type(unicode).copy
main_spec = (Spec(
common=Spec(
default_top_theme=top_theme_spec().optional(),
term_truecolor=Spec().type(bool).optional(),
term_escape_style=Spec().type(unicode).oneof(set(('auto', 'xterm', 'fbterm'))).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(os.path.expanduser(value.value)))),
(lambda value: 'path does not exist: {0}'.format(value))
).optional(),
log_file=Spec().either(
Spec().type(unicode).func(
(
lambda value, *args: (
True,
True,
not os.path.isdir(os.path.dirname(os.path.expanduser(value)))
)
),
(lambda value: 'directory does not exist: {0}'.format(os.path.dirname(value)))
),
Spec().list(Spec().either(
Spec().type(unicode, type(None)),
Spec().tuple(
Spec().re(function_name_re).func(check_logging_handler),
Spec().tuple(
Spec().type(list).optional(),
Spec().type(dict).optional(),
),
log_level_spec().func(check_log_file_level).optional(),
log_format_spec().optional(),
),
))
).optional(),
log_level=log_level_spec().optional(),
log_format=log_format_spec().optional(),
interval=Spec().either(Spec().cmp('gt', 0.0), Spec().type(type(None))).optional(),
reload_config=Spec().type(bool).optional(),
watcher=Spec().type(unicode).oneof(set(('auto', 'inotify', 'stat'))).optional(),
).context_message('Error while loading common configuration (key {key})'),
ext=Spec(
vim=ext_spec().update(
components=gen_components_spec('statusline', 'tabline').optional(),
local_themes=Spec(
__tabline__=ext_theme_spec(),
).unknown_spec(
Spec().re(function_name_re).func(partial(check_matcher_func, 'vim')),
ext_theme_spec()
),
).optional(),
ipython=ext_spec().update(
local_themes=Spec(
in2=ext_theme_spec(),
out=ext_theme_spec(),
rewrite=ext_theme_spec(),
),
).optional(),
shell=ext_spec().update(
components=gen_components_spec('tmux', 'prompt').optional(),
local_themes=Spec(
continuation=ext_theme_spec(),
select=ext_theme_spec(),
),
).optional(),
wm=ext_spec().update(
local_themes=Spec().unknown_spec(
Spec().re('^[0-9A-Za-z-]+$'),
ext_theme_spec()
).optional(),
update_interval=Spec().cmp('gt', 0.0).optional(),
).optional(),
).unknown_spec(
check_ext,
ext_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'))
color_spec = Spec().type(unicode).func(check_color).copy
name_spec = Spec().type(unicode).len('gt', 0).optional().copy
group_name_spec = Spec().ident().copy
group_spec = Spec().either(Spec(
fg=color_spec(),
bg=color_spec(),
attrs=Spec().list(Spec().type(unicode).oneof(set(('bold', 'italic', 'underline')))),
), group_name_spec().func(check_group)).copy
groups_spec = Spec().unknown_spec(
group_name_spec(),
group_spec(),
).context_message('Error while loading groups (key {key})').copy
colorscheme_spec = (Spec(
name=name_spec(),
groups=groups_spec(),
).context_message('Error while loading coloscheme'))
mode_translations_value_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(),
).copy
top_colorscheme_spec = (Spec(
name=name_spec(),
groups=groups_spec(),
mode_translations=Spec().unknown_spec(
Spec().type(unicode),
mode_translations_value_spec(),
).optional().context_message('Error while loading mode translations (key {key})').optional(),
).context_message('Error while loading top-level coloscheme'))
vim_mode_spec = Spec().oneof(set(list(vim_modes) + ['nc', 'tab_nc', 'buf_nc'])).copy
vim_colorscheme_spec = (Spec(
name=name_spec(),
groups=groups_spec(),
mode_translations=Spec().unknown_spec(
vim_mode_spec(),
mode_translations_value_spec(),
).optional().context_message('Error while loading mode translations (key {key})'),
).context_message('Error while loading vim colorscheme'))
shell_mode_spec = Spec().re('^(?:[\w\-]+|\.safe)$').copy
shell_colorscheme_spec = (Spec(
name=name_spec(),
groups=groups_spec(),
mode_translations=Spec().unknown_spec(
shell_mode_spec(),
mode_translations_value_spec(),
).optional().context_message('Error while loading mode translations (key {key})'),
).context_message('Error while loading shell colorscheme'))
args_spec = Spec(
pl=Spec().error('pl object must be set by powerline').optional(),
segment_info=Spec().error('Segment info dictionary must be set by powerline').optional(),
).unknown_spec(Spec(), Spec()).optional().copy
segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy
exinclude_spec = Spec().re(function_name_re).func(check_exinclude_function).copy
segment_spec_base = Spec(
name=Spec().re('^[a-zA-Z_]\w*$').optional(),
function=Spec().re(function_name_re).func(check_segment_function).optional(),
exclude_modes=Spec().list(vim_mode_spec()).optional(),
include_modes=Spec().list(vim_mode_spec()).optional(),
exclude_function=exinclude_spec().optional(),
include_function=exinclude_spec().optional(),
draw_hard_divider=Spec().type(bool).optional(),
draw_soft_divider=Spec().type(bool).optional(),
draw_inner_divider=Spec().type(bool).optional(),
display=Spec().type(bool).optional(),
module=segment_module_spec(),
priority=Spec().type(int, float, type(None)).optional(),
after=Spec().printable().optional(),
before=Spec().printable().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_function, *args, **kwargs)),
contents=Spec().printable().optional(),
highlight_groups=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
subsegment_spec = segment_spec_base().update(
type=Spec().oneof(set((key for key in type_keys if key != 'segment_list'))).optional(),
)
segment_spec = segment_spec_base().update(
type=Spec().oneof(type_keys).optional(),
segments=Spec().optional().list(subsegment_spec),
)
segments_spec = Spec().optional().list(segment_spec).copy
segdict_spec = 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})').copy
divside_spec = Spec(
hard=divider_spec(),
soft=divider_spec(),
).copy
segment_data_value_spec = Spec(
after=Spec().printable().optional(),
before=Spec().printable().optional(),
display=Spec().type(bool).optional(),
args=args_spec().func(lambda *args, **kwargs: check_args(get_all_possible_functions, *args, **kwargs)),
contents=Spec().printable().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(),
cursor_space=Spec().type(int, float).cmp('le', 100).cmp('gt', 0).optional(),
cursor_columns=Spec().type(int).cmp('gt', 0).optional(),
).context_message('Error while loading theme').copy
top_theme_spec = common_theme_spec().update(
dividers=dividers_spec(),
spaces=spaces_spec(),
use_non_breaking_spaces=Spec().type(bool).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})'),
)
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()),
)
def register_common_names():
register_common_name('player', 'powerline.segments.common.players', '_player')
def load_json_file(path):
with open_file(path) as F:
try:
config, hadproblem = load(F)
except MarkedError as e:
return True, None, str(e)
else:
return hadproblem, config, None
def updated_with_config(d):
hadproblem, config, error = load_json_file(d['path'])
d.update(
hadproblem=hadproblem,
config=config,
error=error,
)
return d
def find_all_ext_config_files(search_paths, subdir):
for config_root in search_paths:
top_config_subpath = join(config_root, subdir)
if not os.path.isdir(top_config_subpath):
if os.path.exists(top_config_subpath):
yield {
'error': 'Path {0} is not a directory'.format(top_config_subpath),
'path': top_config_subpath,
}
continue
for ext_name in os.listdir(top_config_subpath):
ext_path = os.path.join(top_config_subpath, ext_name)
if not os.path.isdir(ext_path):
if ext_name.endswith('.json') and os.path.isfile(ext_path):
yield updated_with_config({
'error': False,
'path': ext_path,
'name': ext_name[:-5],
'ext': None,
'type': 'top_' + subdir,
})
else:
yield {
'error': 'Path {0} is not a directory or configuration file'.format(ext_path),
'path': ext_path,
}
continue
for config_file_name in os.listdir(ext_path):
config_file_path = os.path.join(ext_path, config_file_name)
if config_file_name.endswith('.json') and os.path.isfile(config_file_path):
yield updated_with_config({
'error': False,
'path': config_file_path,
'name': config_file_name[:-5],
'ext': ext_name,
'type': subdir,
})
else:
yield {
'error': 'Path {0} is not a configuration file'.format(config_file_path),
'path': config_file_path,
}
def dict2(d):
return defaultdict(dict, ((k, dict(v)) for k, v in d.items()))
def check(paths=None, debug=False, echoerr=echoerr, require_ext=None):
'''Check configuration sanity
:param list paths:
Paths from which configuration should be loaded.
:param bool debug:
Determines whether some information useful for debugging linter should
be output.
:param function echoerr:
Function that will be used to echo the error(s). Should accept four
optional keyword parameters: ``problem`` and ``problem_mark``, and
``context`` and ``context_mark``.
:param str require_ext:
Require configuration for some extension to be present.
:return:
``False`` if user configuration seems to be completely sane and ``True``
if some problems were found.
'''
hadproblem = False
register_common_names()
search_paths = paths or get_config_paths()
find_config_files = generate_config_finder(lambda: search_paths)
logger = logging.getLogger('powerline-lint')
logger.setLevel(logging.DEBUG if debug else logging.ERROR)
logger.addHandler(logging.StreamHandler())
ee = EchoErr(echoerr, logger)
if require_ext:
used_main_spec = main_spec.copy()
try:
used_main_spec['ext'][require_ext].required()
except KeyError:
used_main_spec['ext'][require_ext] = ext_spec()
else:
used_main_spec = main_spec
lhadproblem = [False]
load_json_config = generate_json_config_loader(lhadproblem)
config_loader = ConfigLoader(run_once=True, load=load_json_config)
lists = {
'colorschemes': set(),
'themes': set(),
'exts': set(),
}
found_dir = {
'themes': False,
'colorschemes': False,
}
config_paths = defaultdict(lambda: defaultdict(dict))
loaded_configs = defaultdict(lambda: defaultdict(dict))
for d in chain(
find_all_ext_config_files(search_paths, 'colorschemes'),
find_all_ext_config_files(search_paths, 'themes'),
):
if d['error']:
hadproblem = True
ee(problem=d['error'])
continue
if d['hadproblem']:
hadproblem = True
if d['ext']:
found_dir[d['type']] = True
lists['exts'].add(d['ext'])
if d['name'] == '__main__':
pass
elif d['name'].startswith('__') or d['name'].endswith('__'):
hadproblem = True
ee(problem='File name is not supposed to start or end with “__”: {0}'.format(
d['path']))
else:
lists[d['type']].add(d['name'])
config_paths[d['type']][d['ext']][d['name']] = d['path']
loaded_configs[d['type']][d['ext']][d['name']] = d['config']
else:
config_paths[d['type']][d['name']] = d['path']
loaded_configs[d['type']][d['name']] = d['config']
for typ in ('themes', 'colorschemes'):
if not found_dir[typ]:
hadproblem = True
ee(problem='Subdirectory {0} was not found in paths {1}'.format(typ, ', '.join(search_paths)))
diff = set(config_paths['colorschemes']) - set(config_paths['themes'])
if diff:
hadproblem = True
for ext in diff:
typ = 'colorschemes' if ext in config_paths['themes'] else 'themes'
if not config_paths['top_' + typ] or typ == 'themes':
ee(problem='{0} extension {1} not present in {2}'.format(
ext,
'configuration' if (
ext in loaded_configs['themes'] and ext in loaded_configs['colorschemes']
) else 'directory',
typ,
))
try:
main_config = load_config('config', find_config_files, config_loader)
except IOError:
main_config = {}
ee(problem='Configuration file not found: config.json')
hadproblem = True
except MarkedError as e:
main_config = {}
ee(problem=str(e))
hadproblem = True
else:
if used_main_spec.match(
main_config,
data={'configs': config_paths, 'lists': lists},
context=Context(main_config),
echoerr=ee
)[1]:
hadproblem = True
import_paths = [os.path.expanduser(path) for path in main_config.get('common', {}).get('paths', [])]
try:
colors_config = load_config('colors', find_config_files, config_loader)
except IOError:
colors_config = {}
ee(problem='Configuration file not found: colors.json')
hadproblem = True
except MarkedError as e:
colors_config = {}
ee(problem=str(e))
hadproblem = True
else:
if colors_spec.match(colors_config, context=Context(colors_config), echoerr=ee)[1]:
hadproblem = True
if lhadproblem[0]:
hadproblem = True
top_colorscheme_configs = dict(loaded_configs['top_colorschemes'])
data = {
'ext': None,
'top_colorscheme_configs': top_colorscheme_configs,
'ext_colorscheme_configs': {},
'colors_config': colors_config
}
for colorscheme, config in loaded_configs['top_colorschemes'].items():
data['colorscheme'] = colorscheme
if top_colorscheme_spec.match(config, context=Context(config), data=data, echoerr=ee)[1]:
hadproblem = True
ext_colorscheme_configs = dict2(loaded_configs['colorschemes'])
for ext, econfigs in ext_colorscheme_configs.items():
data = {
'ext': ext,
'top_colorscheme_configs': top_colorscheme_configs,
'ext_colorscheme_configs': ext_colorscheme_configs,
'colors_config': colors_config,
}
for colorscheme, config in econfigs.items():
data['colorscheme'] = colorscheme
if ext == 'vim':
spec = vim_colorscheme_spec
elif ext == 'shell':
spec = shell_colorscheme_spec
else:
spec = colorscheme_spec
if spec.match(config, context=Context(config), data=data, echoerr=ee)[1]:
hadproblem = True
colorscheme_configs = {}
for ext in lists['exts']:
colorscheme_configs[ext] = {}
for colorscheme in lists['colorschemes']:
econfigs = ext_colorscheme_configs[ext]
ecconfigs = econfigs.get(colorscheme)
mconfigs = (
top_colorscheme_configs.get(colorscheme),
econfigs.get('__main__'),
ecconfigs,
)
if not (mconfigs[0] or mconfigs[2]):
continue
config = None
for mconfig in mconfigs:
if not mconfig:
continue
if config:
config = mergedicts_copy(config, mconfig)
else:
config = mconfig
colorscheme_configs[ext][colorscheme] = config
theme_configs = dict2(loaded_configs['themes'])
top_theme_configs = dict(loaded_configs['top_themes'])
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 == '__main__':
data['theme_type'] = 'main'
spec = main_theme_spec
else:
data['theme_type'] = 'regular'
spec = theme_spec
if spec.match(config, context=Context(config), data=data, echoerr=ee)[1]:
hadproblem = True
for top_theme, config in top_theme_configs.items():
data = {
'ext': None,
'colorscheme_configs': colorscheme_configs,
'import_paths': import_paths,
'main_config': main_config,
'theme_configs': theme_configs,
'ext_theme_configs': None,
'colors_config': colors_config
}
data['theme_type'] = 'top'
data['theme'] = top_theme
if top_theme_spec.match(config, context=Context(config), data=data, echoerr=ee)[1]:
hadproblem = True
return hadproblem