Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Options for multiple paths #997

Merged
merged 7 commits into from Dec 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
80 changes: 69 additions & 11 deletions holoviews/ipython/parser.py
Expand Up @@ -252,6 +252,59 @@ def process_normalization(cls, parse_group):
framewise=framewise)


@classmethod
def _group_paths_without_options(cls, line_parse_result):
"""
Given a parsed options specification as a list of groups, combine
groups without options with the first subsequent group which has
options.
A line of the form
'A B C [opts] D E [opts_2]'
results in
[({A, B, C}, [opts]), ({D, E}, [opts_2])]
"""
active_pathspecs = set()
for group in line_parse_result:
active_pathspecs.add(group['pathspec'])

has_options = (
'norm_options' in group or
'plot_options' in group or
'style_options' in group
)
if has_options:
yield active_pathspecs, group
active_pathspecs = set()

if active_pathspecs:
yield active_pathspecs, {}


@classmethod
def _merge_options(cls, old_opts, new_opts):
"""
Update the old_opts option dictionary with the options defined in
new_opts. Instead of a shallow update as would be performed by calling
old_opts.update(new_opts), this updates the dictionaries of all option
types separately.

Given two dictionaries
old_opts = {'a': {'x': 'old', 'y': 'old'}}
and
new_opts = {'a': {'y': 'new', 'z': 'new'}, 'b': {'k': 'new'}}
this returns a dictionary
{'a': {'x': 'old', 'y': 'new', 'z': 'new'}, 'b': {'k': 'new'}}
"""
merged = dict(old_opts)

for option_type, options in new_opts.items():
if option_type not in merged:
merged[option_type] = {}

merged[option_type].update(options)

return merged


@classmethod
def parse(cls, line, ns={}):
Expand All @@ -268,30 +321,35 @@ def parse(cls, line, ns={}):
if (processed.strip() != line.strip()):
raise SyntaxError("Failed to parse remainder of string: %r" % line[e:])

grouped_paths = cls._group_paths_without_options(cls.opts_spec.parseString(line))
parse = {}
for group in cls.opts_spec.parseString(line):
for pathspecs, group in grouped_paths:
options = {}

normalization = cls.process_normalization(group)
if normalization is not None:
options['norm'] = Options(**normalization)
options['norm'] = normalization

if 'plot_options' in group:
plotopts = group['plot_options'][0]
opts = cls.todict(plotopts, 'brackets', ns=ns)
options['plot'] = Options(**{cls.aliases.get(k,k):v for k,v in opts.items()})
options['plot'] = {cls.aliases.get(k,k):v for k,v in opts.items()}

if 'style_options' in group:
styleopts = group['style_options'][0]
opts = cls.todict(styleopts, 'parens', ns=ns)
options['style'] = Options(**{cls.aliases.get(k,k):v for k,v in opts.items()})

if group['pathspec'] in parse:
# Update in case same pathspec accidentally repeated by the user.
parse[group['pathspec']].update(options)
else:
parse[group['pathspec']] = options
return parse
options['style'] = {cls.aliases.get(k,k):v for k,v in opts.items()}

for pathspec in pathspecs:
parse[pathspec] = cls._merge_options(parse.get(pathspec, {}), options)

return {
path: {
option_type: Options(**option_pairs)
for option_type, option_pairs in options.items()
}
for path, options in parse.items()
}



Expand Down
90 changes: 90 additions & 0 deletions tests/testparsers.py
Expand Up @@ -63,6 +63,32 @@ def test_plot_opts_nested_brackets(self):
expected = {'Curve': {'plot': Options(title_format='A, B')}}
self.assertEqual(OptsSpec.parse(line), expected)

def test_plot_opts_multiple_paths(self):
line = "Image Curve [fig_inches=(3, 3) title_format='foo bar']"
expected = {'Image':
{'plot':
Options(title_format='foo bar', fig_inches=(3, 3))},
'Curve':
{'plot':
Options(title_format='foo bar', fig_inches=(3, 3))}}
self.assertEqual(OptsSpec.parse(line), expected)

def test_plot_opts_multiple_paths_2(self):
line = "Image Curve Layout Overlay[fig_inches=(3, 3) title_format='foo bar']"
expected = {'Image':
{'plot':
Options(title_format='foo bar', fig_inches=(3, 3))},
'Curve':
{'plot':
Options(title_format='foo bar', fig_inches=(3, 3))},
'Layout':
{'plot':
Options(title_format='foo bar', fig_inches=(3, 3))},
'Overlay':
{'plot':
Options(title_format='foo bar', fig_inches=(3, 3))}}
self.assertEqual(OptsSpec.parse(line), expected)


class OptsSpecStyleOptionsTests(ComparisonTestCase):

Expand Down Expand Up @@ -136,6 +162,16 @@ def test_style_opts_cycle_list(self):
expected = {'Curve': {'style': Options(color=Cycle(values=['r', 'g', 'b']))}}
self.assertEqual(OptsSpec.parse(line, {'Cycle': Cycle}), expected)

def test_style_opts_multiple_paths(self):
line = "Image Curve (color='beautiful')"
expected = {'Image':
{'style':
Options(color='beautiful')},
'Curve':
{'style':
Options(color='beautiful')}}
self.assertEqual(OptsSpec.parse(line), expected)



class OptsNormPlotOptionsTests(ComparisonTestCase):
Expand Down Expand Up @@ -167,6 +203,16 @@ def test_norm_opts_simple_explicit_2(self):
{'norm': Options(axiswise=True, framewise=True)}}
self.assertEqual(OptsSpec.parse(line), expected)

def test_norm_opts_multiple_paths(self):
line = "Image Curve {+axiswise +framewise}"
expected = {'Image':
{'norm':
Options(axiswise=True, framewise=True)},
'Curve':
{'norm':
Options(axiswise=True, framewise=True)}}
self.assertEqual(OptsSpec.parse(line), expected)


class OptsSpecCombinedOptionsTests(ComparisonTestCase):

Expand Down Expand Up @@ -197,3 +243,47 @@ def test_combined_two_types_2(self):
'style': Options(string='foo'),
'plot': Options(foo='bar baz')}}
self.assertEqual(OptsSpec.parse(line), expected)

def test_combined_multiple_paths(self):
line = "Image Curve {+framewise} [fig_inches=(3, 3) title_format='foo bar'] (c='b') Layout [string='foo'] Overlay"
expected = {'Image':
{'norm':
Options(framewise=True, axiswise=False),
'plot':
Options(title_format='foo bar', fig_inches=(3, 3)),
'style':
Options(c='b')},
'Curve':
{'norm':
Options(framewise=True, axiswise=False),
'plot':
Options(title_format='foo bar', fig_inches=(3, 3)),
'style':
Options(c='b')},
'Layout':
{'plot':
Options(string='foo')},
'Overlay':
{}}
self.assertEqual(OptsSpec.parse(line), expected)

def test_combined_multiple_paths_merge(self):
line = "Image Curve [fig_inches=(3, 3)] (c='b') Image (s=3)"
expected = {'Image':
{'plot':
Options(fig_inches=(3, 3)),
'style':
Options(c='b', s=3)},
'Curve':
{'plot':
Options(fig_inches=(3, 3)),
'style':
Options(c='b')}}
self.assertEqual(OptsSpec.parse(line), expected)

def test_combined_multiple_paths_merge_precedence(self):
line = "Image (s=0, c='b') Image (s=3)"
expected = {'Image':
{'style':
Options(c='b', s=3)}}
self.assertEqual(OptsSpec.parse(line), expected)