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

Dynamic inheritance in OptionTrees #796

Merged
merged 9 commits into from
Jul 26, 2016
17 changes: 12 additions & 5 deletions holoviews/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,9 +430,13 @@ def _merge_options(self, identifier, group_name, options):
if group_name not in self.groups:
raise KeyError("Group %s not defined on SettingTree" % group_name)

current_node = self[identifier] if identifier in self.children else self
group_options = current_node.groups[group_name]

if identifier in self.children:
current_node = self[identifier]
group_options = current_node.groups[group_name]
else:
#When creating a node (nothing to merge with) ensure it is empty
group_options = Options(group_name,
allowed_keywords=self.groups[group_name].allowed_keywords)
try:
return (group_options(**override_kwargs)
if options.merge_keywords else Options(group_name, **override_kwargs))
Expand Down Expand Up @@ -465,7 +469,9 @@ def __getattr__(self, identifier):
if valid_id in self.children:
return self.__dict__[valid_id]

self.__setattr__(identifier, self.groups)
# When creating a intermediate child node, leave kwargs empty
self.__setattr__(identifier, {k:Options(k, allowed_keywords=v.allowed_keywords)
for k,v in self.groups.items()})
return self[identifier]


Expand Down Expand Up @@ -536,7 +542,8 @@ def closest(self, obj, group):
components = (obj.__class__.__name__,
group_sanitizer(obj.group),
label_sanitizer(obj.label))
return self.find(components).options(group)
target = '.'.join([c for c in components if c])
return self.find(components).options(group, target=target)



Expand Down
205 changes: 205 additions & 0 deletions tests/testoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@

Options.skip_invalid = False

try:
# Needed a backend to register backend and options
from holoviews.plotting import mpl
except:
pass

try:
# Needed to register backend and options
from holoviews.plotting import bokeh
except:
pass

class TestOptions(ComparisonTestCase):

def test_options_init(self):
Expand Down Expand Up @@ -180,6 +192,199 @@ def test_optiontree_inheritance_flipped(self):
{'kw2':'value2', 'kw4':'value4'})


class TestStoreInheritanceDynamic(ComparisonTestCase):
"""
Tests to prevent regression after fix in PR #646
"""

def setUp(self):
self.store_copy = OptionTree(sorted(Store.options().items()),
groups=['style', 'plot', 'norm'])
self.backend = 'matplotlib'
Store.current_backend = self.backend
super(TestStoreInheritanceDynamic, self).setUp()

def tearDown(self):
Store.options(val=self.store_copy)
super(TestStoreInheritanceDynamic, self).tearDown()

def initialize_option_tree(self):
Store.options(val=OptionTree(groups=['plot', 'style']))
options = Store.options()
options.Image = Options('style', cmap='hot', interpolation='nearest')
return options

def test_merge_keywords(self):
options = self.initialize_option_tree()
options.Image = Options('style', clims=(0, 0.5))

expected = {'clims': (0, 0.5), 'cmap': 'hot', 'interpolation': 'nearest'}
direct_kws = options.Image.groups['style'].kwargs
inherited_kws = options.Image.options('style').kwargs
self.assertEqual(direct_kws, expected)
self.assertEqual(inherited_kws, expected)

def test_merge_keywords_disabled(self):
options = self.initialize_option_tree()
options.Image = Options('style', clims=(0, 0.5), merge_keywords=False)

expected = {'clims': (0, 0.5)}
direct_kws = options.Image.groups['style'].kwargs
inherited_kws = options.Image.options('style').kwargs
self.assertEqual(direct_kws, expected)
self.assertEqual(inherited_kws, expected)

def test_specification_general_to_specific_group(self):
"""
Test order of specification starting with general and moving
to specific
"""
if 'matplotlib' not in Store.renderers:
raise SkipTest("General to specific option test requires matplotlib")

options = self.initialize_option_tree()

obj = Image(np.random.rand(10,10), group='SomeGroup')

options.Image = Options('style', cmap='viridis')
options.Image.SomeGroup = Options('style', alpha=0.2)

expected = {'alpha': 0.2, 'cmap': 'viridis', 'interpolation': 'nearest'}
lookup = Store.lookup_options('matplotlib', obj, 'style')

self.assertEqual(lookup.kwargs, expected)
# Check the tree is structured as expected
node1 = options.Image.groups['style']
node2 = options.Image.SomeGroup.groups['style']

self.assertEqual(node1.kwargs, {'cmap': 'viridis', 'interpolation': 'nearest'})
self.assertEqual(node2.kwargs, {'alpha': 0.2})


def test_specification_general_to_specific_group_and_label(self):
"""
Test order of specification starting with general and moving
to specific
"""
if 'matplotlib' not in Store.renderers:
raise SkipTest("General to specific option test requires matplotlib")

options = self.initialize_option_tree()

obj = Image(np.random.rand(10,10), group='SomeGroup', label='SomeLabel')

options.Image = Options('style', cmap='viridis')
options.Image.SomeGroup.SomeLabel = Options('style', alpha=0.2)

expected = {'alpha': 0.2, 'cmap': 'viridis', 'interpolation': 'nearest'}
lookup = Store.lookup_options('matplotlib', obj, 'style')

self.assertEqual(lookup.kwargs, expected)
# Check the tree is structured as expected
node1 = options.Image.groups['style']
node2 = options.Image.SomeGroup.SomeLabel.groups['style']

self.assertEqual(node1.kwargs, {'cmap': 'viridis', 'interpolation': 'nearest'})
self.assertEqual(node2.kwargs, {'alpha': 0.2})

def test_specification_specific_to_general_group(self):
"""
Test order of specification starting with a specific option and
then specifying a general one
"""
if 'matplotlib' not in Store.renderers:
raise SkipTest("General to specific option test requires matplotlib")

options = self.initialize_option_tree()
options.Image.SomeGroup = Options('style', alpha=0.2)

obj = Image(np.random.rand(10,10), group='SomeGroup')
options.Image = Options('style', cmap='viridis')

expected = {'alpha': 0.2, 'cmap': 'viridis', 'interpolation': 'nearest'}
lookup = Store.lookup_options('matplotlib', obj, 'style')

self.assertEqual(lookup.kwargs, expected)
# Check the tree is structured as expected
node1 = options.Image.groups['style']
node2 = options.Image.SomeGroup.groups['style']

self.assertEqual(node1.kwargs, {'cmap': 'viridis', 'interpolation': 'nearest'})
self.assertEqual(node2.kwargs, {'alpha': 0.2})


def test_specification_specific_to_general_group_and_label(self):
"""
Test order of specification starting with general and moving
to specific
"""
if 'matplotlib' not in Store.renderers:
raise SkipTest("General to specific option test requires matplotlib")

options = self.initialize_option_tree()
options.Image.SomeGroup.SomeLabel = Options('style', alpha=0.2)
obj = Image(np.random.rand(10,10), group='SomeGroup', label='SomeLabel')

options.Image = Options('style', cmap='viridis')
expected = {'alpha': 0.2, 'cmap': 'viridis', 'interpolation': 'nearest'}
lookup = Store.lookup_options('matplotlib', obj, 'style')

self.assertEqual(lookup.kwargs, expected)
# Check the tree is structured as expected
node1 = options.Image.groups['style']
node2 = options.Image.SomeGroup.SomeLabel.groups['style']

self.assertEqual(node1.kwargs, {'cmap': 'viridis', 'interpolation': 'nearest'})
self.assertEqual(node2.kwargs, {'alpha': 0.2})

def test_custom_call_to_default_inheritance(self):
"""
Checks customs inheritance backs off to default tree correctly
using __call__.
"""
options = self.initialize_option_tree()
options.Image.A.B = Options('style', alpha=0.2)

obj = Image(np.random.rand(10, 10), group='A', label='B')
expected_obj = {'alpha': 0.2, 'cmap': 'hot', 'interpolation': 'nearest'}
obj_lookup = Store.lookup_options('matplotlib', obj, 'style')
self.assertEqual(obj_lookup.kwargs, expected_obj)

# Customize this particular object
custom_obj = obj(style=dict(clims=(0, 0.5)))
expected_custom_obj = dict(clims=(0,0.5), **expected_obj)
custom_obj_lookup = Store.lookup_options('matplotlib', custom_obj, 'style')
self.assertEqual(custom_obj_lookup.kwargs, expected_custom_obj)

def test_custom_magic_to_default_inheritance(self):
"""
Checks customs inheritance backs off to default tree correctly
simulating the %%opts cell magic.
"""
if 'matplotlib' not in Store.renderers:
raise SkipTest("Custom magic inheritance test requires matplotlib")
options = self.initialize_option_tree()
options.Image.A.B = Options('style', alpha=0.2)

obj = Image(np.random.rand(10, 10), group='A', label='B')

# Before customizing...
expected_obj = {'alpha': 0.2, 'cmap': 'hot', 'interpolation': 'nearest'}
obj_lookup = Store.lookup_options('matplotlib', obj, 'style')
self.assertEqual(obj_lookup.kwargs, expected_obj)

custom_tree = {0: OptionTree(groups=['plot', 'style', 'norm'],
style={'Image' : dict(clims=(0, 0.5))})}
Store._custom_options['matplotlib'] = custom_tree
obj.id = 0 # Manually set the id to point to the tree above

# Customize this particular object
expected_custom_obj = dict(clims=(0,0.5), **expected_obj)
custom_obj_lookup = Store.lookup_options('matplotlib', obj, 'style')
self.assertEqual(custom_obj_lookup.kwargs, expected_custom_obj)



class TestStoreInheritance(ComparisonTestCase):
"""
Tests to prevent regression after fix in 71c1f3a that resolves
Expand Down