Skip to content

Commit

Permalink
Merge pull request #796 from ioam/disable_merge_keywords
Browse files Browse the repository at this point in the history
Dynamic inheritance in OptionTrees
  • Loading branch information
philippjfr committed Jul 26, 2016
2 parents 8d2d29c + 20b0538 commit 25c6c7f
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 5 deletions.
17 changes: 12 additions & 5 deletions holoviews/core/options.py
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
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

0 comments on commit 25c6c7f

Please sign in to comment.