Add `exclude_/include_function` support

This is first step towards fixing #1046.
This commit is contained in:
ZyX 2014-09-04 20:34:06 +04:00
parent bc557bd656
commit 7bf025ca2b
6 changed files with 203 additions and 26 deletions

View File

@ -508,6 +508,23 @@ ascii Theme without any unicode characters at all
(segment is included in all modes). When there are both (segment is included in all modes). When there are both
``exclude_modes`` overrides ``include_modes``. ``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
<config-themes-seg-exclude_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: .. _config-themes-seg-display:
``display`` ``display``

View File

@ -254,9 +254,15 @@ Segment dictionary contains the following keys:
``side`` ``side``
Segment side: ``right`` or ``left``. Segment side: ``right`` or ``left``.
``exclude_modes``, ``include_modes`` ``display_condition```
:ref:`Mode display control lists <config-themes-seg-exclude_modes>`. May be Contains function that takes three position parameters:
empty, but may not be ``None``. :py:class:`powerline.PowerlineLogger` instance, :ref:`segment_info
<dev-segments-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
<config-themes-seg-exclude_modes>` and :ref:`exclude_/include_function keys
<config-themes-seg-exclude_function>`.
``width``, ``align`` ``width``, ``align``
:ref:`Width and align options <config-themes-seg-align>`. May be ``None``. :ref:`Width and align options <config-themes-seg-align>`. May be ``None``.

View File

@ -9,6 +9,7 @@ import logging
from collections import defaultdict from collections import defaultdict
from copy import copy from copy import copy
from functools import partial
from powerline.lint.markedjson import load from powerline.lint.markedjson import load
from powerline import generate_config_finder, get_config_paths, load_config 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', echoerr(context='Error while loading matcher functions',
problem='failed to load module {0}'.format(match_module), problem='failed to load module {0}'.format(match_module),
problem_mark=match_name.mark) problem_mark=match_name.mark)
return True, True return True, False, True
except AttributeError: except AttributeError:
echoerr(context='Error while loading matcher functions', echoerr(context='Error while loading matcher functions',
problem='failed to load matcher function {0}'.format(match_function), problem='failed to load matcher function {0}'.format(match_function),
problem_mark=match_name.mark) problem_mark=match_name.mark)
return True, True return True, False, True
if not callable(func): if not callable(func):
echoerr(context='Error while loading matcher functions', echoerr(context='Error while loading matcher functions',
problem='loaded "function" {0} is not callable'.format(match_function), problem='loaded "function" {0} is not callable'.format(match_function),
problem_mark=match_name.mark) 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 hasattr(func, 'func_code') and hasattr(func.func_code, 'co_argcount'):
if func.func_code.co_argcount != 1: 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 problem_mark=match_name.mark
) )
return True, False return True, False, False
def check_ext(ext, data, context, echoerr): def check_ext(ext, data, context, echoerr):
@ -561,6 +562,9 @@ def check_top_theme(theme, data, context, echoerr):
return True, False, False return True, False, False
function_name_re = '^(\w+\.)*[a-zA-Z_]\w*$'
divider_spec = Spec().type(unicode).len( divider_spec = Spec().type(unicode).len(
'le', 3, (lambda value: 'Divider {0!r} is too large!'.format(value))).copy '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 ext_theme_spec = Spec().type(unicode).func(lambda *args: check_config('themes', *args)).copy
@ -608,7 +612,8 @@ main_spec = (Spec(
local_themes=Spec( local_themes=Spec(
__tabline__=ext_theme_spec(), __tabline__=ext_theme_spec(),
).unknown_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(), ).optional(),
ipython=ext_spec().update( ipython=ext_spec().update(
@ -801,6 +806,7 @@ shell_colorscheme_spec = (Spec(
generic_keys = set(( generic_keys = set((
'exclude_modes', 'include_modes', 'exclude_modes', 'include_modes',
'exclude_function', 'include_function',
'width', 'align', 'width', 'align',
'name', 'name',
'draw_soft_divider', 'draw_hard_divider', '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) 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) context_has_marks(context)
havemarks(name, module) havemarks(name, module)
@ -949,7 +955,7 @@ def import_segment(name, data, context, echoerr, module):
problem_mark=module.mark) problem_mark=module.mark)
return None return None
except AttributeError: 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='failed to load function {0} from module {1}'.format(name, module),
problem_mark=name.mark) problem_mark=name.mark)
return None return None
@ -964,6 +970,10 @@ def import_segment(name, data, context, echoerr, module):
return func return func
def import_segment(*args, **kwargs):
return import_function('segment', *args, **kwargs)
def check_segment_function(function_name, data, context, echoerr): def check_segment_function(function_name, data, context, echoerr):
havemarks(function_name) havemarks(function_name)
ext = data['ext'] ext = data['ext']
@ -1334,6 +1344,17 @@ def get_all_possible_functions(data, context, echoerr):
yield func 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( args_spec = Spec(
pl=Spec().error('pl object must be set by powerline').optional(), pl=Spec().error('pl object must be set by powerline').optional(),
segment_info=Spec().error('Segment info dictionary 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 highlight_group_spec = Spec().type(unicode).copy
segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy
sub_segments_spec = Spec() sub_segments_spec = Spec()
exinclude_spec = Spec().re(function_name_re).func(check_exinclude_function).copy
segment_spec = Spec( segment_spec = Spec(
type=Spec().oneof(type_keys).optional(), type=Spec().oneof(type_keys).optional(),
name=Spec().re('^[a-zA-Z_]\w*$').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(), exclude_modes=Spec().list(vim_mode_spec()).optional(),
include_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_hard_divider=Spec().type(bool).optional(),
draw_soft_divider=Spec().type(bool).optional(), draw_soft_divider=Spec().type(bool).optional(),
draw_inner_divider=Spec().type(bool).optional(), draw_inner_divider=Spec().type(bool).optional(),

View File

@ -113,13 +113,7 @@ def process_segment_lister(pl, segment_info, parsed_segments, side, mode, colors
subsegment['priority'] *= subsegment_update['priority_multiplier'] subsegment['priority'] *= subsegment_update['priority_multiplier']
subsegment_mode = subsegment_update.get('mode') subsegment_mode = subsegment_update.get('mode')
if subsegment_mode and ( if subsegment_mode and not subsegment['display_condition'](pl, segment_info, subsegment_mode):
subsegment_mode in subsegment['exclude_modes']
or (
subsegment['include_modes']
and subsegment_mode not in subsegment['include_modes']
)
):
continue continue
process_segment( process_segment(
@ -218,6 +212,9 @@ def process_segment(pl, side, segment_info, parsed_segments, segment, mode, colo
parsed_segments.append(segment) 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): def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, get_module_attr, top_theme):
data = { data = {
'default_module': default_module or 'powerline.segments.' + ext, '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) return get_segment_key(merge, segment, theme_configs, data['segment_data'], key, function_name, name, module, default)
data['get_key'] = get_key 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): def get(segment, side):
segment_type = segment.get('type', 'function') segment_type = segment.get('type', 'function')
try: 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() get_key(True, segment, module, function_name, name, 'args', {}).items()
)) ))
display_condition = gen_display_condition(segment)
if segment_type == 'segment_list': if segment_type == 'segment_list':
# Handle startup and shutdown of _contents_func? # Handle startup and shutdown of _contents_func?
subsegments = [ subsegments = [
@ -292,8 +345,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge
'draw_hard_divider': None, 'draw_hard_divider': None,
'draw_inner_divider': None, 'draw_inner_divider': None,
'side': side, 'side': side,
'exclude_modes': segment.get('exclude_modes', []), 'display_condition': display_condition,
'include_modes': segment.get('include_modes', []),
'width': None, 'width': None,
'align': None, 'align': None,
'expand': 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_soft_divider': segment.get('draw_soft_divider', True),
'draw_inner_divider': segment.get('draw_inner_divider', False), 'draw_inner_divider': segment.get('draw_inner_divider', False),
'side': side, 'side': side,
'exclude_modes': segment.get('exclude_modes', []), 'display_condition': display_condition,
'include_modes': segment.get('include_modes', []),
'width': segment.get('width'), 'width': segment.get('width'),
'align': segment.get('align', 'l'), 'align': segment.get('align', 'l'),
'expand': expand_func, 'expand': expand_func,

View File

@ -136,9 +136,7 @@ class Theme(object):
parsed_segments = [] parsed_segments = []
for segment in self.segments[line][side]: for segment in self.segments[line][side]:
# No segment-local modes at this point # No segment-local modes at this point
if mode not in segment['exclude_modes'] and ( if segment['display_condition'](self.pl, segment_info, mode):
not segment['include_modes'] or mode in segment['include_modes']
):
process_segment( process_segment(
self.pl, self.pl,
side, side,

View File

@ -389,7 +389,7 @@ class TestThemeHierarchy(TestRender):
]) ])
class TestModes(TestRender): class TestDisplayCondition(TestRender):
@add_args @add_args
def test_include_modes(self, p, config): def test_include_modes(self, p, config):
config['themes/test/default']['segments'] = { config['themes/test/default']['segments'] = {
@ -432,6 +432,87 @@ class TestModes(TestRender):
self.assertRenderEqual(p, '{56} s1{6-}>>{--}', mode='m2') self.assertRenderEqual(p, '{56} s1{6-}>>{--}', mode='m2')
self.assertRenderEqual(p, '{56} s2{6-}>>{--}', mode='m3') 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): class TestSegmentAttributes(TestRender):
@add_args @add_args