diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 3f0cf59c6d..211ec2dbde 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -460,6 +460,7 @@ def __init__(self, callable, **params): self._is_overlay = False self.args = None self.kwargs = None + self._stream_memoization = self.memoize @property def argspec(self): @@ -486,6 +487,7 @@ def clone(self, callable=None, **overrides): def __call__(self, *args, **kwargs): # Nothing to do for callbacks that accept no arguments + kwarg_hash = kwargs.pop('memoization_hash', []) (self.args, self.kwargs) = (args, kwargs) if not args and not kwargs: return self.callable() inputs = [i for i in self.inputs if isinstance(i, DynamicMap)] @@ -493,12 +495,12 @@ def __call__(self, *args, **kwargs): for stream in [s for i in inputs for s in get_nested_streams(i)]: if stream not in streams: streams.append(stream) - memoize = self.memoize and not any(s.transient and s._triggering for s in streams) - values = tuple(tuple(sorted(s.contents.items())) for s in streams) - key = args + tuple(sorted(kwargs.items())) + values + memoize = self._stream_memoization and not any(s.transient and s._triggering for s in streams) + values = tuple(tuple(sorted(s.hashkey.items())) for s in streams) + key = args + tuple(sorted(kwarg_hash)) + values - hashed_key = util.deephash(key) - if memoize and hashed_key in self._memoized: + hashed_key = util.deephash(key) if self.memoize else None + if hashed_key is not None and memoize and hashed_key in self._memoized: return self._memoized[hashed_key] if self.argspec.varargs is not None: @@ -591,14 +593,14 @@ def dynamicmap_memoization(callable_obj, streams): DynamicMap). Memoization is disabled if any of the streams require it it and are currently in a triggered state. """ - memoization_state = bool(callable_obj.memoize) - callable_obj.memoize &= not any(s.transient and s._triggering for s in streams) + memoization_state = bool(callable_obj._stream_memoization) + callable_obj._stream_memoization &= not any(s.transient and s._triggering for s in streams) try: yield except: raise finally: - callable_obj.memoize = memoization_state + callable_obj._stream_memoization = memoization_state @@ -837,6 +839,7 @@ def _execute_callback(self, *args): # 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] + hash_items = tuple(tuple(sorted(s.hashkey.items())) for s in self.streams) flattened = [(k,v) for kws in kwarg_items for (k,v) in kws if k not in kdims] @@ -845,6 +848,7 @@ def _execute_callback(self, *args): args = () else: kwargs = dict(flattened) + kwargs['memoization_hash'] = hash_items with dynamicmap_memoization(self.callback, self.streams): retval = self.callback(*args, **kwargs) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 07de6b4e24..98e7c028f0 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -101,7 +101,7 @@ def default(self, obj): elif isinstance(obj, np.ndarray): return obj.tolist() if pd and isinstance(obj, (pd.Series, pd.DataFrame)): - return repr(sorted(list(obj.to_dict().items()))) + return obj.to_csv().encode('utf-8') elif isinstance(obj, self.string_hashable): return str(obj) elif isinstance(obj, self.repr_hashable): diff --git a/holoviews/streams.py b/holoviews/streams.py index 2370d46232..f0ab325489 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -4,6 +4,8 @@ server-side or in Javascript in the Jupyter notebook (client-side). """ +import uuid + import param import numpy as np from numbers import Number @@ -296,6 +298,15 @@ def contents(self): return {self._rename.get(k,k):v for (k,v) in filtered.items() if (self._rename.get(k,True) is not None)} + @property + def hashkey(self): + """ + The object the memoization hash is computed from. By default + returns the stream contents but can be overridden to provide + a custom hash key. + """ + return self.contents + def _set_stream_parameters(self, **kwargs): """ @@ -352,6 +363,34 @@ def transform(self): return {'counter': self.counter + 1} +class StreamData(Stream): + """ + A Stream used to pipe arbitrary data to a callback. + Unlike other streams memoization can be disabled for a + StreamData stream (and is disabled by default). + """ + + data = param.Parameter(default=None, constant=True, doc=""" + Arbitrary data being streamed to a DynamicMap callback.""") + + def __init__(self, memoize=False, **params): + super(StreamData, self).__init__(**params) + self._memoize = memoize + + def send(self, data): + """ + A convenience method to send an event with data without + supplying a keyword. + """ + self.event(data=data) + + @property + def hashkey(self): + if self._memoize: + return self.contents + return {'hash': uuid.uuid4().hex} + + class LinkedStream(Stream): """ A LinkedStream indicates is automatically linked to plot interactions @@ -404,11 +443,11 @@ class PointerXY(LinkedStream): the plot bounds, the position values are set to None. """ - x = param.ClassSelector(class_=(Number, util.basestring), default=None, + x = param.ClassSelector(class_=(Number, util.basestring, tuple), default=None, constant=True, doc=""" Pointer position along the x-axis in data coordinates""") - y = param.ClassSelector(class_=(Number, util.basestring), default=None, + y = param.ClassSelector(class_=(Number, util.basestring, tuple), default=None, constant=True, doc=""" Pointer position along the y-axis in data coordinates""") diff --git a/tests/testdynamic.py b/tests/testdynamic.py index 0fca392422..02a725a83d 100644 --- a/tests/testdynamic.py +++ b/tests/testdynamic.py @@ -1,3 +1,4 @@ +import uuid from collections import deque import time @@ -572,6 +573,35 @@ def history_callback(x, y, history=deque(maxlen=10)): self.assertEqual(xresets, 2) self.assertEqual(yresets, 2) + def test_dynamic_callable_stream_hashkey(self): + # Enable transient stream meaning memoization only happens when + # stream is inactive, should have sample for each call to + # stream.update + def history_callback(x, history=deque(maxlen=10)): + if x is not None: + history.append(x) + return Curve(list(history)) + + class NoMemoize(PointerX): + @property + def hashkey(self): return {'hash': uuid.uuid4().hex} + + x = NoMemoize() + dmap = DynamicMap(history_callback, kdims=[], streams=[x]) + + # Add stream subscriber mocking plot + x.add_subscriber(lambda **kwargs: dmap[()]) + + for i in range(2): + x.event(x=1) + self.assertEqual(dmap[()], Curve([1, 1, 1])) + + for i in range(2): + x.event(x=2) + + self.assertEqual(dmap[()], Curve([1, 1, 1, 2, 2, 2])) + + class TestPeriodicStreamUpdate(ComparisonTestCase):