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
``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:
``display``

View File

@ -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 <config-themes-seg-exclude_modes>`. 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
<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``
: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 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(),

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_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,

View File

@ -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,

View File

@ -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