diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 0fe404a943..0ea1603961 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -507,6 +507,8 @@ def _validate_mode(self): return 'key' # Any unbounded kdim (any direction) implies open mode for kdim in self.kdims: + if kdim.name in util.stream_parameters(self.streams): + return 'key' if kdim.values: continue if None in kdim.range: @@ -557,13 +559,10 @@ def _execute_callback(self, *args): retval = next(self.callback) else: # Additional validation needed to ensure kwargs don't clash + kdims = [kdim.name for kdim in self.kdims] kwarg_items = [s.contents.items() for s in self.streams] - flattened = [el for kws in kwarg_items for el in kws] - klist = [k for k,_ in flattened] - clashes = set([k for k in klist if klist.count(k) > 1]) - if clashes: - self.warning('Parameter name clashes for keys: %r' % clashes) - + flattened = [(k,v) for kws in kwarg_items for (k,v) in kws + if k not in kdims] retval = self.callback(*args, **dict(flattened)) if self.call_mode=='key': return self._style(retval) @@ -654,7 +653,7 @@ def __getitem__(self, key): for a previously generated key that is still in the cache (for one of the 'open' modes) """ - tuple_key = util.wrap_tuple(key) + tuple_key = util.wrap_tuple_streams(key, self.kdims, self.streams) # Validation for bounded mode if self.mode == 'bounded': @@ -673,8 +672,8 @@ def __getitem__(self, key): # Cache lookup try: - if self.streams: - raise KeyError('Using streams disables DynamicMap cache') + if util.dimensionless_contents(self.streams, self.kdims): + raise KeyError('Using dimensionless streams disables DynamicMap cache') cache = super(DynamicMap,self).__getitem__(key) # Return selected cache items in a new DynamicMap if isinstance(cache, DynamicMap) and self.mode=='open': @@ -704,9 +703,11 @@ def _cache(self, key, val): """ Request that a key/value pair be considered for caching. """ + cache_size = (1 if util.dimensionless_contents(self.streams, self.kdims) + else self.cache_size) if self.mode == 'open' and (self.counter % self.cache_interval)!=0: return - if len(self) >= self.cache_size: + if len(self) >= cache_size: first_key = next(self.data.iterkeys()) self.data.pop(first_key) self.data[key] = val @@ -728,7 +729,7 @@ def next(self): (key, val) = (retval if isinstance(retval, tuple) else (self.counter, retval)) - key = util.wrap_tuple(key) + key = util.wrap_tuple_streams(key, self.kdims, self.streams) if len(key) != len(self.key_dimensions): raise Exception("Generated key does not match the number of key dimensions") diff --git a/holoviews/core/util.py b/holoviews/core/util.py index ce99c8832e..3266de497b 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -778,6 +778,52 @@ def wrap_tuple(unwrapped): return (unwrapped if isinstance(unwrapped, tuple) else (unwrapped,)) + +def stream_parameters(streams, no_duplicates=True, exclude=['name']): + """ + Given a list of streams, return a flat list of parameter name, + excluding those listed in the exclude list. + + If no_duplicates is enabled, a KeyError will be raised if there are + parameter name clashes across the streams. + """ + param_groups = [s.params().keys() for s in streams] + names = [name for group in param_groups for name in group] + + if no_duplicates: + clashes = set([n for n in names if names.count(n) > 1]) + if clashes: + raise KeyError('Parameter name clashes for keys: %r' % clashes) + return [name for name in names if name not in exclude] + + +def dimensionless_contents(streams, kdims): + """ + Return a list of stream parameters that have not been associated + with any of the key dimensions. + """ + names = stream_parameters(streams) + kdim_names = [kdim.name for kdim in kdims] + return [name for name in names if name not in kdim_names] + + +def wrap_tuple_streams(unwrapped, kdims, streams): + """ + Fills in tuple keys with dimensioned stream values as appropriate. + """ + param_groups = [(s.params().keys(), s) for s in streams] + pairs = [(name,s) for (group, s) in param_groups for name in group] + substituted = [] + for pos,el in enumerate(wrap_tuple(unwrapped)): + if el is None and pos < len(kdims): + matches = [(name,s) for (name,s) in pairs if name==kdims[pos].name] + if len(matches) == 1: + (name, stream) = matches[0] + el = stream.contents[name] + substituted.append(el) + return tuple(substituted) + + def itervalues(obj): "Get value iterator from dictionary for Python 2 and 3" return iter(obj.values()) if sys.version_info.major == 3 else obj.itervalues() diff --git a/tests/testutils.py b/tests/testutils.py index 79fcc13e00..fdd4506370 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -8,7 +8,9 @@ import numpy as np -from holoviews.core.util import sanitize_identifier_fn, find_range, max_range +from holoviews.core.util import sanitize_identifier_fn, find_range, max_range, wrap_tuple_streams +from holoviews import Dimension +from holoviews.streams import PositionXY from holoviews.element.comparison import ComparisonTestCase py_version = sys.version_info.major @@ -298,3 +300,36 @@ def test_max_range2(self): lower, upper = max_range(self.ranges2) self.assertTrue(math.isnan(lower)) self.assertTrue(math.isnan(upper)) + + + +class TestWrapTupleStreams(unittest.TestCase): + + + def test_no_streams(self): + result = wrap_tuple_streams((1,2), [],[]) + self.assertEqual(result, (1,2)) + + def test_no_streams_two_kdims(self): + result = wrap_tuple_streams((1,2), + [Dimension('x'), Dimension('y')], + []) + self.assertEqual(result, (1,2)) + + def test_no_streams_none_value(self): + result = wrap_tuple_streams((1,None), + [Dimension('x'), Dimension('y')], + []) + self.assertEqual(result, (1,None)) + + def test_no_streams_one_stream_substitution(self): + result = wrap_tuple_streams((None,3), + [Dimension('x'), Dimension('y')], + [PositionXY(x=-5,y=10)]) + self.assertEqual(result, (-5,3)) + + def test_no_streams_two_stream_substitution(self): + result = wrap_tuple_streams((None,None), + [Dimension('x'), Dimension('y')], + [PositionXY(x=0,y=5)]) + self.assertEqual(result, (0,5))