diff --git a/docs/source/configuration/reference.rst b/docs/source/configuration/reference.rst index d45ed832..0e8b384d 100644 --- a/docs/source/configuration/reference.rst +++ b/docs/source/configuration/reference.rst @@ -508,6 +508,23 @@ ascii Theme without any unicode characters at all (segment is included in all modes). When there are both ``exclude_modes`` overrides ``include_modes``. + .. _config-themes-seg-exclude_function: + + ``exclude_function``, ``include_function`` + Function name in a form ``{name}`` or ``{module}.{name}`` (in the first + form ``{module}`` defaults to ``powerline.selectors.{ext}``). Determines + under which condition specific segment will be included or excluded. By + default segment is always included and never excluded. + ``exclude_function`` overrides ``include_function``. + + .. note:: + Options :ref:`exclude_/include_modes + ` complement + ``exclude_/include_functions``: segment will be included if it is + included by either ``include_mode`` or ``include_function`` and will + be excluded if it is excluded by either ``exclude_mode`` or + ``exclude_function``. + .. _config-themes-seg-display: ``display`` diff --git a/docs/source/develop/segments.rst b/docs/source/develop/segments.rst index e1dd1b64..55838ae7 100644 --- a/docs/source/develop/segments.rst +++ b/docs/source/develop/segments.rst @@ -254,9 +254,15 @@ Segment dictionary contains the following keys: ``side`` Segment side: ``right`` or ``left``. - ``exclude_modes``, ``include_modes`` - :ref:`Mode display control lists `. May be - empty, but may not be ``None``. + ``display_condition``` + Contains function that takes three position parameters: + :py:class:`powerline.PowerlineLogger` instance, :ref:`segment_info + ` dictionary and current mode and returns either ``True`` + or ``False`` to indicate whether particular segment should be processed. + + This key is constructed based on :ref:`exclude_/include_modes keys + ` and :ref:`exclude_/include_function keys + `. ``width``, ``align`` :ref:`Width and align options `. May be ``None``. diff --git a/powerline/lint/__init__.py b/powerline/lint/__init__.py index bf88d2c6..6a5d86d5 100644 --- a/powerline/lint/__init__.py +++ b/powerline/lint/__init__.py @@ -9,6 +9,7 @@ import logging from collections import defaultdict from copy import copy +from functools import partial from powerline.lint.markedjson import load from powerline import generate_config_finder, get_config_paths, load_config @@ -478,18 +479,18 @@ def check_matcher_func(ext, match_name, data, context, echoerr): echoerr(context='Error while loading matcher functions', problem='failed to load module {0}'.format(match_module), problem_mark=match_name.mark) - return True, True + return True, False, 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 + return True, False, 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 + return True, False, True if hasattr(func, 'func_code') and hasattr(func.func_code, 'co_argcount'): if func.func_code.co_argcount != 1: @@ -502,7 +503,7 @@ def check_matcher_func(ext, match_name, data, context, echoerr): problem_mark=match_name.mark ) - return True, False + return True, False, False def check_ext(ext, data, context, echoerr): @@ -561,6 +562,9 @@ def check_top_theme(theme, data, context, echoerr): return True, False, False +function_name_re = '^(\w+\.)*[a-zA-Z_]\w*$' + + divider_spec = Spec().type(unicode).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 @@ -608,7 +612,8 @@ main_spec = (Spec( local_themes=Spec( __tabline__=ext_theme_spec(), ).unknown_spec( - lambda *args: check_matcher_func('vim', *args), ext_theme_spec() + Spec().re(function_name_re).func(partial(check_matcher_func, 'vim')), + ext_theme_spec() ), ).optional(), ipython=ext_spec().update( @@ -801,6 +806,7 @@ shell_colorscheme_spec = (Spec( generic_keys = set(( 'exclude_modes', 'include_modes', + 'exclude_function', 'include_function', 'width', 'align', 'name', 'draw_soft_divider', 'draw_hard_divider', @@ -935,7 +941,7 @@ def check_full_segment_data(segment, data, context, echoerr): return check_key_compatibility(segment_copy, data, context, echoerr) -def import_segment(name, data, context, echoerr, module): +def import_function(function_type, name, data, context, echoerr, module): context_has_marks(context) havemarks(name, module) @@ -949,7 +955,7 @@ def import_segment(name, data, context, echoerr, module): problem_mark=module.mark) return None except AttributeError: - echoerr(context='Error while loading segment function (key {key})'.format(key=context_key(context)), + echoerr(context='Error while loading {0} function (key {key})'.format(function_type, key=context_key(context)), problem='failed to load function {0} from module {1}'.format(name, module), problem_mark=name.mark) return None @@ -964,6 +970,10 @@ def import_segment(name, data, context, echoerr, module): return func +def import_segment(*args, **kwargs): + return import_function('segment', *args, **kwargs) + + def check_segment_function(function_name, data, context, echoerr): havemarks(function_name) ext = data['ext'] @@ -1334,6 +1344,17 @@ def get_all_possible_functions(data, context, echoerr): yield func +def check_exinclude_function(name, data, context, echoerr): + ext = data['ext'] + module, name = name.rpartition('.')[::2] + if not module: + module = MarkedUnicode('powerline.selectors.' + ext, None) + func = import_function('selector', name, data, context, echoerr, module=module) + if not func: + return True, False, True + return True, False, False + + 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(), @@ -1341,12 +1362,15 @@ args_spec = Spec( highlight_group_spec = Spec().type(unicode).copy segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy sub_segments_spec = Spec() +exinclude_spec = Spec().re(function_name_re).func(check_exinclude_function).copy segment_spec = Spec( type=Spec().oneof(type_keys).optional(), name=Spec().re('^[a-zA-Z_]\w*$').optional(), - function=Spec().re('^(\w+\.)*[a-zA-Z_]\w*$').func(check_segment_function).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(), diff --git a/powerline/segment.py b/powerline/segment.py index cb233380..f7a8f1a1 100644 --- a/powerline/segment.py +++ b/powerline/segment.py @@ -113,13 +113,7 @@ def process_segment_lister(pl, segment_info, parsed_segments, side, mode, colors subsegment['priority'] *= subsegment_update['priority_multiplier'] subsegment_mode = subsegment_update.get('mode') - if subsegment_mode and ( - subsegment_mode in subsegment['exclude_modes'] - or ( - subsegment['include_modes'] - and subsegment_mode not in subsegment['include_modes'] - ) - ): + if subsegment_mode and not subsegment['display_condition'](pl, segment_info, subsegment_mode): continue process_segment( @@ -218,6 +212,9 @@ def process_segment(pl, side, segment_info, parsed_segments, segment, mode, colo parsed_segments.append(segment) +always_true = lambda pl, segment_info, mode: True + + def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, get_module_attr, top_theme): data = { 'default_module': default_module or 'powerline.segments.' + ext, @@ -229,6 +226,60 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge return get_segment_key(merge, segment, theme_configs, data['segment_data'], key, function_name, name, module, default) data['get_key'] = get_key + def get_selector(function_name): + if '.' in function_name: + module, function_name = function_name.rpartition('.')[::2] + else: + module = 'powerline.selectors.' + ext + function = get_module_attr(module, function_name, prefix='segment_generator/selector_function') + if not function: + pl.error('Failed to get segment selector, ignoring it') + return function + + def get_segment_selector(segment, selector_type): + try: + function_name = segment[selector_type + '_function'] + except KeyError: + function = None + else: + function = get_selector(function_name) + try: + modes = segment[selector_type + '_modes'] + except KeyError: + modes = None + + if modes: + if function: + return lambda pl, segment_info, mode: ( + mode in modes + or function(pl=pl, segment_info=segment_info, mode=mode) + ) + else: + return lambda pl, segment_info, mode: mode in modes + else: + if function: + return lambda pl, segment_info, mode: ( + function(pl=pl, segment_info=segment_info, mode=mode) + ) + else: + return None + + def gen_display_condition(segment): + include_function = get_segment_selector(segment, 'include') + exclude_function = get_segment_selector(segment, 'exclude') + if include_function: + if exclude_function: + return lambda *args: ( + include_function(*args) + and not exclude_function(*args)) + else: + return include_function + else: + if exclude_function: + return lambda *args: not exclude_function(*args) + else: + return always_true + def get(segment, side): segment_type = segment.get('type', 'function') try: @@ -265,6 +316,8 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge get_key(True, segment, module, function_name, name, 'args', {}).items() )) + display_condition = gen_display_condition(segment) + if segment_type == 'segment_list': # Handle startup and shutdown of _contents_func? subsegments = [ @@ -292,8 +345,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge 'draw_hard_divider': None, 'draw_inner_divider': None, 'side': side, - 'exclude_modes': segment.get('exclude_modes', []), - 'include_modes': segment.get('include_modes', []), + 'display_condition': display_condition, 'width': None, 'align': None, 'expand': None, @@ -342,8 +394,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge 'draw_soft_divider': segment.get('draw_soft_divider', True), 'draw_inner_divider': segment.get('draw_inner_divider', False), 'side': side, - 'exclude_modes': segment.get('exclude_modes', []), - 'include_modes': segment.get('include_modes', []), + 'display_condition': display_condition, 'width': segment.get('width'), 'align': segment.get('align', 'l'), 'expand': expand_func, diff --git a/powerline/theme.py b/powerline/theme.py index 9fa90e54..32a8aeee 100644 --- a/powerline/theme.py +++ b/powerline/theme.py @@ -136,9 +136,7 @@ class Theme(object): parsed_segments = [] for segment in self.segments[line][side]: # No segment-local modes at this point - if mode not in segment['exclude_modes'] and ( - not segment['include_modes'] or mode in segment['include_modes'] - ): + if segment['display_condition'](self.pl, segment_info, mode): process_segment( self.pl, side, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 29bb963f..aff0341e 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -389,7 +389,7 @@ class TestThemeHierarchy(TestRender): ]) -class TestModes(TestRender): +class TestDisplayCondition(TestRender): @add_args def test_include_modes(self, p, config): config['themes/test/default']['segments'] = { @@ -432,6 +432,87 @@ class TestModes(TestRender): self.assertRenderEqual(p, '{56} s1{6-}>>{--}', mode='m2') self.assertRenderEqual(p, '{56} s2{6-}>>{--}', mode='m3') + @add_args + def test_exinclude_function_nonexistent_module(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s1', 'g1', exclude_function='xxx_nonexistent_module.foo'), + highlighted_string('s2', 'g1', exclude_function='xxx_nonexistent_module.foo', include_function='xxx_nonexistent_module.bar'), + highlighted_string('s3', 'g1', include_function='xxx_nonexistent_module.bar'), + ] + } + self.assertRenderEqual(p, '{56} s1{56}>{56}s2{56}>{56}s3{6-}>>{--}') + + @add_args + def test_exinclude_function(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s1', 'g1', exclude_function='mod.foo'), + highlighted_string('s2', 'g1', exclude_function='mod.foo', include_function='mod.bar'), + highlighted_string('s3', 'g1', include_function='mod.bar'), + ] + } + launched = set() + fool = [None] + barl = [None] + + def foo(*args, **kwargs): + launched.add('foo') + self.assertEqual(set(kwargs.keys()), set(('pl', 'segment_info', 'mode'))) + self.assertEqual(args, ()) + return fool[0] + + def bar(*args, **kwargs): + launched.add('bar') + self.assertEqual(set(kwargs.keys()), set(('pl', 'segment_info', 'mode'))) + self.assertEqual(args, ()) + return barl[0] + + with replace_item(sys.modules, 'mod', Args(foo=foo, bar=bar)): + fool[0] = True + barl[0] = True + self.assertRenderEqual(p, '{56} s3{6-}>>{--}') + self.assertEqual(launched, set(('foo', 'bar'))) + + fool[0] = False + barl[0] = True + self.assertRenderEqual(p, '{56} s1{56}>{56}s2{56}>{56}s3{6-}>>{--}') + self.assertEqual(launched, set(('foo', 'bar'))) + + fool[0] = False + barl[0] = False + self.assertRenderEqual(p, '{56} s1{6-}>>{--}') + self.assertEqual(launched, set(('foo', 'bar'))) + + fool[0] = True + barl[0] = False + self.assertRenderEqual(p, '{--}') + self.assertEqual(launched, set(('foo', 'bar'))) + + @add_args + def test_exinclude_modes_override_functions(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s1', 'g1', exclude_function='mod.foo', exclude_modes=['m2']), + highlighted_string('s2', 'g1', exclude_function='mod.foo', include_modes=['m2']), + highlighted_string('s3', 'g1', include_function='mod.foo', exclude_modes=['m2']), + highlighted_string('s4', 'g1', include_function='mod.foo', include_modes=['m2']), + ] + } + fool = [None] + + def foo(*args, **kwargs): + return fool[0] + + with replace_item(sys.modules, 'mod', Args(foo=foo)): + fool[0] = True + self.assertRenderEqual(p, '{56} s4{6-}>>{--}', mode='m2') + self.assertRenderEqual(p, '{56} s3{56}>{56}s4{6-}>>{--}', mode='m1') + + fool[0] = False + self.assertRenderEqual(p, '{56} s2{56}>{56}s4{6-}>>{--}', mode='m2') + self.assertRenderEqual(p, '{56} s1{6-}>>{--}', mode='m1') + class TestSegmentAttributes(TestRender): @add_args