1# vim:fileencoding=utf-8:noet 2from __future__ import (unicode_literals, division, absolute_import, print_function) 3 4import os 5import logging 6 7from collections import defaultdict 8from itertools import chain 9from functools import partial 10 11from powerline import generate_config_finder, get_config_paths, load_config 12from powerline.segments.vim import vim_modes 13from powerline.lib.dict import mergedicts_copy 14from powerline.lib.config import ConfigLoader 15from powerline.lib.unicode import unicode 16from powerline.lib.path import join 17from powerline.lint.markedjson import load 18from powerline.lint.markedjson.error import echoerr, EchoErr, MarkedError 19from powerline.lint.checks import (check_matcher_func, check_ext, check_config, check_top_theme, 20 check_color, check_translated_group_name, check_group, 21 check_segment_module, check_exinclude_function, type_keys, 22 check_segment_function, check_args, get_one_segment_function, 23 check_highlight_groups, check_highlight_group, check_full_segment_data, 24 get_all_possible_functions, check_segment_data_key, register_common_name, 25 highlight_group_spec, check_log_file_level, check_logging_handler) 26from powerline.lint.spec import Spec 27from powerline.lint.context import Context 28 29 30def open_file(path): 31 return open(path, 'rb') 32 33 34def generate_json_config_loader(lhadproblem): 35 def load_json_config(config_file_path, load=load, open_file=open_file): 36 with open_file(config_file_path) as config_file_fp: 37 r, hadproblem = load(config_file_fp) 38 if hadproblem: 39 lhadproblem[0] = True 40 return r 41 return load_json_config 42 43 44function_name_re = '^(\w+\.)*[a-zA-Z_]\w*$' 45 46 47divider_spec = Spec().printable().len( 48 'le', 3, (lambda value: 'Divider {0!r} is too large!'.format(value))).copy 49ext_theme_spec = Spec().type(unicode).func(lambda *args: check_config('themes', *args)).copy 50top_theme_spec = Spec().type(unicode).func(check_top_theme).copy 51ext_spec = Spec( 52 colorscheme=Spec().type(unicode).func( 53 (lambda *args: check_config('colorschemes', *args)) 54 ), 55 theme=ext_theme_spec(), 56 top_theme=top_theme_spec().optional(), 57).copy 58gen_components_spec = (lambda *components: Spec().list(Spec().type(unicode).oneof(set(components)))) 59log_level_spec = Spec().re('^[A-Z]+$').func( 60 (lambda value, *args: (True, True, not hasattr(logging, value))), 61 (lambda value: 'unknown debugging level {0}'.format(value)) 62).copy 63log_format_spec = Spec().type(unicode).copy 64main_spec = (Spec( 65 common=Spec( 66 default_top_theme=top_theme_spec().optional(), 67 term_truecolor=Spec().type(bool).optional(), 68 term_escape_style=Spec().type(unicode).oneof(set(('auto', 'xterm', 'fbterm'))).optional(), 69 # Python is capable of loading from zip archives. Thus checking path 70 # only for existence of the path, not for it being a directory 71 paths=Spec().list( 72 (lambda value, *args: (True, True, not os.path.exists(os.path.expanduser(value.value)))), 73 (lambda value: 'path does not exist: {0}'.format(value)) 74 ).optional(), 75 log_file=Spec().either( 76 Spec().type(unicode).func( 77 ( 78 lambda value, *args: ( 79 True, 80 True, 81 not os.path.isdir(os.path.dirname(os.path.expanduser(value))) 82 ) 83 ), 84 (lambda value: 'directory does not exist: {0}'.format(os.path.dirname(value))) 85 ), 86 Spec().list(Spec().either( 87 Spec().type(unicode, type(None)), 88 Spec().tuple( 89 Spec().re(function_name_re).func(check_logging_handler), 90 Spec().tuple( 91 Spec().type(list).optional(), 92 Spec().type(dict).optional(), 93 ), 94 log_level_spec().func(check_log_file_level).optional(), 95 log_format_spec().optional(), 96 ), 97 )) 98 ).optional(), 99 log_level=log_level_spec().optional(), 100 log_format=log_format_spec().optional(), 101 interval=Spec().either(Spec().cmp('gt', 0.0), Spec().type(type(None))).optional(), 102 reload_config=Spec().type(bool).optional(), 103 watcher=Spec().type(unicode).oneof(set(('auto', 'inotify', 'stat'))).optional(), 104 ).context_message('Error while loading common configuration (key {key})'), 105 ext=Spec( 106 vim=ext_spec().update( 107 components=gen_components_spec('statusline', 'tabline').optional(), 108 local_themes=Spec( 109 __tabline__=ext_theme_spec(), 110 ).unknown_spec( 111 Spec().re(function_name_re).func(partial(check_matcher_func, 'vim')), 112 ext_theme_spec() 113 ), 114 ).optional(), 115 ipython=ext_spec().update( 116 local_themes=Spec( 117 in2=ext_theme_spec(), 118 out=ext_theme_spec(), 119 rewrite=ext_theme_spec(), 120 ), 121 ).optional(), 122 shell=ext_spec().update( 123 components=gen_components_spec('tmux', 'prompt').optional(), 124 local_themes=Spec( 125 continuation=ext_theme_spec(), 126 select=ext_theme_spec(), 127 ), 128 ).optional(), 129 wm=ext_spec().update( 130 local_themes=Spec().unknown_spec( 131 Spec().re('^[0-9A-Za-z-]+$'), 132 ext_theme_spec() 133 ).optional(), 134 update_interval=Spec().cmp('gt', 0.0).optional(), 135 ).optional(), 136 ).unknown_spec( 137 check_ext, 138 ext_spec(), 139 ).context_message('Error while loading extensions configuration (key {key})'), 140).context_message('Error while loading main configuration')) 141 142term_color_spec = Spec().unsigned().cmp('le', 255).copy 143true_color_spec = Spec().re( 144 '^[0-9a-fA-F]{6}$', 145 (lambda value: '"{0}" is not a six-digit hexadecimal unsigned integer written as a string'.format(value)) 146).copy 147colors_spec = (Spec( 148 colors=Spec().unknown_spec( 149 Spec().ident(), 150 Spec().either( 151 Spec().tuple(term_color_spec(), true_color_spec()), 152 term_color_spec() 153 ) 154 ).context_message('Error while checking colors (key {key})'), 155 gradients=Spec().unknown_spec( 156 Spec().ident(), 157 Spec().tuple( 158 Spec().len('gt', 1).list(term_color_spec()), 159 Spec().len('gt', 1).list(true_color_spec()).optional(), 160 ) 161 ).context_message('Error while checking gradients (key {key})'), 162).context_message('Error while loading colors configuration')) 163 164 165color_spec = Spec().type(unicode).func(check_color).copy 166name_spec = Spec().type(unicode).len('gt', 0).optional().copy 167group_name_spec = Spec().ident().copy 168group_spec = Spec().either(Spec( 169 fg=color_spec(), 170 bg=color_spec(), 171 attrs=Spec().list(Spec().type(unicode).oneof(set(('bold', 'italic', 'underline')))), 172), group_name_spec().func(check_group)).copy 173groups_spec = Spec().unknown_spec( 174 group_name_spec(), 175 group_spec(), 176).context_message('Error while loading groups (key {key})').copy 177colorscheme_spec = (Spec( 178 name=name_spec(), 179 groups=groups_spec(), 180).context_message('Error while loading coloscheme')) 181mode_translations_value_spec = Spec( 182 colors=Spec().unknown_spec( 183 color_spec(), 184 color_spec(), 185 ).optional(), 186 groups=Spec().unknown_spec( 187 group_name_spec().func(check_translated_group_name), 188 group_spec(), 189 ).optional(), 190).copy 191top_colorscheme_spec = (Spec( 192 name=name_spec(), 193 groups=groups_spec(), 194 mode_translations=Spec().unknown_spec( 195 Spec().type(unicode), 196 mode_translations_value_spec(), 197 ).optional().context_message('Error while loading mode translations (key {key})').optional(), 198).context_message('Error while loading top-level coloscheme')) 199vim_mode_spec = Spec().oneof(set(list(vim_modes) + ['nc', 'tab_nc', 'buf_nc'])).copy 200vim_colorscheme_spec = (Spec( 201 name=name_spec(), 202 groups=groups_spec(), 203 mode_translations=Spec().unknown_spec( 204 vim_mode_spec(), 205 mode_translations_value_spec(), 206 ).optional().context_message('Error while loading mode translations (key {key})'), 207).context_message('Error while loading vim colorscheme')) 208shell_mode_spec = Spec().re('^(?:[\w\-]+|\.safe)$').copy 209shell_colorscheme_spec = (Spec( 210 name=name_spec(), 211 groups=groups_spec(), 212 mode_translations=Spec().unknown_spec( 213 shell_mode_spec(), 214 mode_translations_value_spec(), 215 ).optional().context_message('Error while loading mode translations (key {key})'), 216).context_message('Error while loading shell colorscheme')) 217 218 219args_spec = Spec( 220 pl=Spec().error('pl object must be set by powerline').optional(), 221 segment_info=Spec().error('Segment info dictionary must be set by powerline').optional(), 222).unknown_spec(Spec(), Spec()).optional().copy 223segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy 224exinclude_spec = Spec().re(function_name_re).func(check_exinclude_function).copy 225segment_spec_base = Spec( 226 name=Spec().re('^[a-zA-Z_]\w*$').optional(), 227 function=Spec().re(function_name_re).func(check_segment_function).optional(), 228 exclude_modes=Spec().list(vim_mode_spec()).optional(), 229 include_modes=Spec().list(vim_mode_spec()).optional(), 230 exclude_function=exinclude_spec().optional(), 231 include_function=exinclude_spec().optional(), 232 draw_hard_divider=Spec().type(bool).optional(), 233 draw_soft_divider=Spec().type(bool).optional(), 234 draw_inner_divider=Spec().type(bool).optional(), 235 display=Spec().type(bool).optional(), 236 module=segment_module_spec(), 237 priority=Spec().type(int, float, type(None)).optional(), 238 after=Spec().printable().optional(), 239 before=Spec().printable().optional(), 240 width=Spec().either(Spec().unsigned(), Spec().cmp('eq', 'auto')).optional(), 241 align=Spec().oneof(set('lr')).optional(), 242 args=args_spec().func(lambda *args, **kwargs: check_args(get_one_segment_function, *args, **kwargs)), 243 contents=Spec().printable().optional(), 244 highlight_groups=Spec().list( 245 highlight_group_spec().re( 246 '^(?:(?!:divider$).)+$', 247 (lambda value: 'it is recommended that only divider highlight group names end with ":divider"') 248 ) 249 ).func(check_highlight_groups).optional(), 250 divider_highlight_group=highlight_group_spec().func(check_highlight_group).re( 251 ':divider$', 252 (lambda value: 'it is recommended that divider highlight group names end with ":divider"') 253 ).optional(), 254).func(check_full_segment_data).copy 255subsegment_spec = segment_spec_base().update( 256 type=Spec().oneof(set((key for key in type_keys if key != 'segment_list'))).optional(), 257) 258segment_spec = segment_spec_base().update( 259 type=Spec().oneof(type_keys).optional(), 260 segments=Spec().optional().list(subsegment_spec), 261) 262segments_spec = Spec().optional().list(segment_spec).copy 263segdict_spec = Spec( 264 left=segments_spec().context_message('Error while loading segments from left side (key {key})'), 265 right=segments_spec().context_message('Error while loading segments from right side (key {key})'), 266).func( 267 (lambda value, *args: (True, True, not (('left' in value) or ('right' in value)))), 268 (lambda value: 'segments dictionary must contain either left, right or both keys') 269).context_message('Error while loading segments (key {key})').copy 270divside_spec = Spec( 271 hard=divider_spec(), 272 soft=divider_spec(), 273).copy 274segment_data_value_spec = Spec( 275 after=Spec().printable().optional(), 276 before=Spec().printable().optional(), 277 display=Spec().type(bool).optional(), 278 args=args_spec().func(lambda *args, **kwargs: check_args(get_all_possible_functions, *args, **kwargs)), 279 contents=Spec().printable().optional(), 280).copy 281dividers_spec = Spec( 282 left=divside_spec(), 283 right=divside_spec(), 284).copy 285spaces_spec = Spec().unsigned().cmp( 286 'le', 2, (lambda value: 'Are you sure you need such a big ({0}) number of spaces?'.format(value)) 287).copy 288common_theme_spec = Spec( 289 default_module=segment_module_spec().optional(), 290 cursor_space=Spec().type(int, float).cmp('le', 100).cmp('gt', 0).optional(), 291 cursor_columns=Spec().type(int).cmp('gt', 0).optional(), 292).context_message('Error while loading theme').copy 293top_theme_spec = common_theme_spec().update( 294 dividers=dividers_spec(), 295 spaces=spaces_spec(), 296 use_non_breaking_spaces=Spec().type(bool).optional(), 297 segment_data=Spec().unknown_spec( 298 Spec().func(check_segment_data_key), 299 segment_data_value_spec(), 300 ).optional().context_message('Error while loading segment data (key {key})'), 301) 302main_theme_spec = common_theme_spec().update( 303 dividers=dividers_spec().optional(), 304 spaces=spaces_spec().optional(), 305 segment_data=Spec().unknown_spec( 306 Spec().func(check_segment_data_key), 307 segment_data_value_spec(), 308 ).optional().context_message('Error while loading segment data (key {key})'), 309) 310theme_spec = common_theme_spec().update( 311 dividers=dividers_spec().optional(), 312 spaces=spaces_spec().optional(), 313 segment_data=Spec().unknown_spec( 314 Spec().func(check_segment_data_key), 315 segment_data_value_spec(), 316 ).optional().context_message('Error while loading segment data (key {key})'), 317 segments=segdict_spec().update(above=Spec().list(segdict_spec()).optional()), 318) 319 320 321def register_common_names(): 322 register_common_name('player', 'powerline.segments.common.players', '_player') 323 324 325def load_json_file(path): 326 with open_file(path) as F: 327 try: 328 config, hadproblem = load(F) 329 except MarkedError as e: 330 return True, None, str(e) 331 else: 332 return hadproblem, config, None 333 334 335def updated_with_config(d): 336 hadproblem, config, error = load_json_file(d['path']) 337 d.update( 338 hadproblem=hadproblem, 339 config=config, 340 error=error, 341 ) 342 return d 343 344 345def find_all_ext_config_files(search_paths, subdir): 346 for config_root in search_paths: 347 top_config_subpath = join(config_root, subdir) 348 if not os.path.isdir(top_config_subpath): 349 if os.path.exists(top_config_subpath): 350 yield { 351 'error': 'Path {0} is not a directory'.format(top_config_subpath), 352 'path': top_config_subpath, 353 } 354 continue 355 for ext_name in os.listdir(top_config_subpath): 356 ext_path = os.path.join(top_config_subpath, ext_name) 357 if not os.path.isdir(ext_path): 358 if ext_name.endswith('.json') and os.path.isfile(ext_path): 359 yield updated_with_config({ 360 'error': False, 361 'path': ext_path, 362 'name': ext_name[:-5], 363 'ext': None, 364 'type': 'top_' + subdir, 365 }) 366 else: 367 yield { 368 'error': 'Path {0} is not a directory or configuration file'.format(ext_path), 369 'path': ext_path, 370 } 371 continue 372 for config_file_name in os.listdir(ext_path): 373 config_file_path = os.path.join(ext_path, config_file_name) 374 if config_file_name.endswith('.json') and os.path.isfile(config_file_path): 375 yield updated_with_config({ 376 'error': False, 377 'path': config_file_path, 378 'name': config_file_name[:-5], 379 'ext': ext_name, 380 'type': subdir, 381 }) 382 else: 383 yield { 384 'error': 'Path {0} is not a configuration file'.format(config_file_path), 385 'path': config_file_path, 386 } 387 388 389def dict2(d): 390 return defaultdict(dict, ((k, dict(v)) for k, v in d.items())) 391 392 393def check(paths=None, debug=False, echoerr=echoerr, require_ext=None): 394 '''Check configuration sanity 395 396 :param list paths: 397 Paths from which configuration should be loaded. 398 :param bool debug: 399 Determines whether some information useful for debugging linter should 400 be output. 401 :param function echoerr: 402 Function that will be used to echo the error(s). Should accept four 403 optional keyword parameters: ``problem`` and ``problem_mark``, and 404 ``context`` and ``context_mark``. 405 :param str require_ext: 406 Require configuration for some extension to be present. 407 408 :return: 409 ``False`` if user configuration seems to be completely sane and ``True`` 410 if some problems were found. 411 ''' 412 hadproblem = False 413 414 register_common_names() 415 search_paths = paths or get_config_paths() 416 find_config_files = generate_config_finder(lambda: search_paths) 417 418 logger = logging.getLogger('powerline-lint') 419 logger.setLevel(logging.DEBUG if debug else logging.ERROR) 420 logger.addHandler(logging.StreamHandler()) 421 422 ee = EchoErr(echoerr, logger) 423 424 if require_ext: 425 used_main_spec = main_spec.copy() 426 try: 427 used_main_spec['ext'][require_ext].required() 428 except KeyError: 429 used_main_spec['ext'][require_ext] = ext_spec() 430 else: 431 used_main_spec = main_spec 432 433 lhadproblem = [False] 434 load_json_config = generate_json_config_loader(lhadproblem) 435 436 config_loader = ConfigLoader(run_once=True, load=load_json_config) 437 438 lists = { 439 'colorschemes': set(), 440 'themes': set(), 441 'exts': set(), 442 } 443 found_dir = { 444 'themes': False, 445 'colorschemes': False, 446 } 447 config_paths = defaultdict(lambda: defaultdict(dict)) 448 loaded_configs = defaultdict(lambda: defaultdict(dict)) 449 for d in chain( 450 find_all_ext_config_files(search_paths, 'colorschemes'), 451 find_all_ext_config_files(search_paths, 'themes'), 452 ): 453 if d['error']: 454 hadproblem = True 455 ee(problem=d['error']) 456 continue 457 if d['hadproblem']: 458 hadproblem = True 459 if d['ext']: 460 found_dir[d['type']] = True 461 lists['exts'].add(d['ext']) 462 if d['name'] == '__main__': 463 pass 464 elif d['name'].startswith('__') or d['name'].endswith('__'): 465 hadproblem = True 466 ee(problem='File name is not supposed to start or end with “__”: {0}'.format( 467 d['path'])) 468 else: 469 lists[d['type']].add(d['name']) 470 config_paths[d['type']][d['ext']][d['name']] = d['path'] 471 loaded_configs[d['type']][d['ext']][d['name']] = d['config'] 472 else: 473 config_paths[d['type']][d['name']] = d['path'] 474 loaded_configs[d['type']][d['name']] = d['config'] 475 476 for typ in ('themes', 'colorschemes'): 477 if not found_dir[typ]: 478 hadproblem = True 479 ee(problem='Subdirectory {0} was not found in paths {1}'.format(typ, ', '.join(search_paths))) 480 481 diff = set(config_paths['colorschemes']) - set(config_paths['themes']) 482 if diff: 483 hadproblem = True 484 for ext in diff: 485 typ = 'colorschemes' if ext in config_paths['themes'] else 'themes' 486 if not config_paths['top_' + typ] or typ == 'themes': 487 ee(problem='{0} extension {1} not present in {2}'.format( 488 ext, 489 'configuration' if ( 490 ext in loaded_configs['themes'] and ext in loaded_configs['colorschemes'] 491 ) else 'directory', 492 typ, 493 )) 494 495 try: 496 main_config = load_config('config', find_config_files, config_loader) 497 except IOError: 498 main_config = {} 499 ee(problem='Configuration file not found: config.json') 500 hadproblem = True 501 except MarkedError as e: 502 main_config = {} 503 ee(problem=str(e)) 504 hadproblem = True 505 else: 506 if used_main_spec.match( 507 main_config, 508 data={'configs': config_paths, 'lists': lists}, 509 context=Context(main_config), 510 echoerr=ee 511 )[1]: 512 hadproblem = True 513 514 import_paths = [os.path.expanduser(path) for path in main_config.get('common', {}).get('paths', [])] 515 516 try: 517 colors_config = load_config('colors', find_config_files, config_loader) 518 except IOError: 519 colors_config = {} 520 ee(problem='Configuration file not found: colors.json') 521 hadproblem = True 522 except MarkedError as e: 523 colors_config = {} 524 ee(problem=str(e)) 525 hadproblem = True 526 else: 527 if colors_spec.match(colors_config, context=Context(colors_config), echoerr=ee)[1]: 528 hadproblem = True 529 530 if lhadproblem[0]: 531 hadproblem = True 532 533 top_colorscheme_configs = dict(loaded_configs['top_colorschemes']) 534 data = { 535 'ext': None, 536 'top_colorscheme_configs': top_colorscheme_configs, 537 'ext_colorscheme_configs': {}, 538 'colors_config': colors_config 539 } 540 for colorscheme, config in loaded_configs['top_colorschemes'].items(): 541 data['colorscheme'] = colorscheme 542 if top_colorscheme_spec.match(config, context=Context(config), data=data, echoerr=ee)[1]: 543 hadproblem = True 544 545 ext_colorscheme_configs = dict2(loaded_configs['colorschemes']) 546 for ext, econfigs in ext_colorscheme_configs.items(): 547 data = { 548 'ext': ext, 549 'top_colorscheme_configs': top_colorscheme_configs, 550 'ext_colorscheme_configs': ext_colorscheme_configs, 551 'colors_config': colors_config, 552 } 553 for colorscheme, config in econfigs.items(): 554 data['colorscheme'] = colorscheme 555 if ext == 'vim': 556 spec = vim_colorscheme_spec 557 elif ext == 'shell': 558 spec = shell_colorscheme_spec 559 else: 560 spec = colorscheme_spec 561 if spec.match(config, context=Context(config), data=data, echoerr=ee)[1]: 562 hadproblem = True 563 564 colorscheme_configs = {} 565 for ext in lists['exts']: 566 colorscheme_configs[ext] = {} 567 for colorscheme in lists['colorschemes']: 568 econfigs = ext_colorscheme_configs[ext] 569 ecconfigs = econfigs.get(colorscheme) 570 mconfigs = ( 571 top_colorscheme_configs.get(colorscheme), 572 econfigs.get('__main__'), 573 ecconfigs, 574 ) 575 if not (mconfigs[0] or mconfigs[2]): 576 continue 577 config = None 578 for mconfig in mconfigs: 579 if not mconfig: 580 continue 581 if config: 582 config = mergedicts_copy(config, mconfig) 583 else: 584 config = mconfig 585 colorscheme_configs[ext][colorscheme] = config 586 587 theme_configs = dict2(loaded_configs['themes']) 588 top_theme_configs = dict(loaded_configs['top_themes']) 589 for ext, configs in theme_configs.items(): 590 data = { 591 'ext': ext, 592 'colorscheme_configs': colorscheme_configs, 593 'import_paths': import_paths, 594 'main_config': main_config, 595 'top_themes': top_theme_configs, 596 'ext_theme_configs': configs, 597 'colors_config': colors_config 598 } 599 for theme, config in configs.items(): 600 data['theme'] = theme 601 if theme == '__main__': 602 data['theme_type'] = 'main' 603 spec = main_theme_spec 604 else: 605 data['theme_type'] = 'regular' 606 spec = theme_spec 607 if spec.match(config, context=Context(config), data=data, echoerr=ee)[1]: 608 hadproblem = True 609 610 for top_theme, config in top_theme_configs.items(): 611 data = { 612 'ext': None, 613 'colorscheme_configs': colorscheme_configs, 614 'import_paths': import_paths, 615 'main_config': main_config, 616 'theme_configs': theme_configs, 617 'ext_theme_configs': None, 618 'colors_config': colors_config 619 } 620 data['theme_type'] = 'top' 621 data['theme'] = top_theme 622 if top_theme_spec.match(config, context=Context(config), data=data, echoerr=ee)[1]: 623 hadproblem = True 624 625 return hadproblem 626