From 12f098ba4c909d43d728856e64c38400d7be1869 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 9 Apr 2017 21:19:11 +0100 Subject: [PATCH] Replaced Stream.cleanup with resetting machinery --- holoviews/streams.py | 127 ++++++++++++++++++++++++++----------------- tests/testdynamic.py | 40 +++++++++++++- 2 files changed, 116 insertions(+), 51 deletions(-) diff --git a/holoviews/streams.py b/holoviews/streams.py index 510a9659a1..d4321ba735 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -32,20 +32,53 @@ def triggering_streams(streams): stream._triggering = False +@contextmanager +def disable_constant(parameterized): + """ + Temporarily set parameters on Parameterized object to + constant=False. + """ + params = parameterized.params().values() + constants = [p.constant for p in params] + for param in params: + param.constant = False + try: + yield + except: + raise + finally: + for (param, const) in zip(params, constants): + param.constant = const + class Stream(param.Parameterized): """ A Stream is simply a parameterized object with parameters that - change over time in response to update events. Parameters are - updated via the update method. - - Streams may have one or more subscribers which are callables passed - the parameter dictionary when the trigger classmethod is called. - - Depending on the plotting backend certain streams may interactively - subscribe to events and changes by the plotting backend. For this - purpose use the LinkedStream baseclass, which enables the linked - option by default. + change over time in response to update events and may trigger + downstream events on its subscribers. The subscribers may be + supplied as a list of callables or added later add_subscriber + method. The subscriber will be called whenever the Stream is + triggered and passed a dictionary mapping of the parameters of the + stream, which are available on the instance as the ``contents``. + + Depending on the plotting backend certain streams may + interactively subscribe to events and changes by the plotting + backend. For this purpose use the LinkedStream baseclass, which + enables the linked option by default. A source for the linking may + be supplied to the constructor in the form of another viewable + object specifying which part of a plot the data should come from. + + Since a Stream may represent a transient event you may disable + stream parameters from being memoized, indicating that each stream + event should trigger an update in a downstream DynamicMap callback. + By enabling resets on the constructor the reset method will get + called each time the stream is triggered, resetting all Stream + parameters to their defaults. + + The Stream class is meant for subclassing and should at minimum + declare a number of parameters but may also override the transform + and reset method to preprocess parameters before they are passed + to subscribers and reset them using custom logic respectively. """ # Mapping from a source id to a list of streams @@ -83,19 +116,13 @@ def trigger(cls, streams): subscriber(**dict(union)) for stream in streams: - params = stream.params().values() - constants = [p.constant for p in params] - for param in params: - param.constant = False - - stream.deactivate() - - for (param, const) in zip(params, constants): - param.constant = const + with disable_constant(stream): + if stream._reset: + stream.reset() def __init__(self, rename={}, source=None, subscribers=[], linked=False, - memoize=True, **params): + memoize=True, reset=False, **params): """ The rename argument allows multiple streams with similar event state to be used by remapping parameter names. @@ -115,6 +142,7 @@ def __init__(self, rename={}, source=None, subscribers=[], linked=False, self.memoize = memoize self.linked = linked self._rename = self._validate_rename(rename) + self._reset = reset # Whether this stream is currently triggering its subscribers self._triggering = False @@ -134,12 +162,24 @@ def subscribers(self): " Property returning the subscriber list" return self._subscribers + def clear(self): """ Clear all subscribers registered to this stream. """ self._subscribers = [] + + def reset(self): + """ + Resets stream parameters to their defaults. + """ + with disable_constant(self): + for k, p in self.params().items(): + if k != 'name': + setattr(self, k, p.default) + + def add_subscriber(self, subscriber): """ Register a callable subscriber to this stream which will be @@ -150,6 +190,7 @@ def add_subscriber(self, subscriber): raise TypeError('Subscriber must be a callable.') self._subscribers.append(subscriber) + def _validate_rename(self, mapping): param_names = [k for k in self.params().keys() if k != 'name'] for k,v in mapping.items(): @@ -160,6 +201,7 @@ def _validate_rename(self, mapping): 'stream parameter of the same name' % v) return mapping + def rename(self, **mapping): """ The rename method allows stream parameters to be allocated to @@ -172,19 +214,11 @@ def rename(self, **mapping): source=self._source, linked=self.linked, **params) - - def deactivate(self): - """ - Allows defining an action after the stream has been triggered, - e.g. resetting parameters on streams with transient events. - """ - pass - - @property def source(self): return self._source + @source.setter def source(self, source): if self._source: @@ -203,6 +237,7 @@ def transform(self): """ return {} + @property def contents(self): filtered = {k:v for k,v in self.get_param_values() if k!= 'name' } @@ -214,19 +249,8 @@ def _set_stream_parameters(self, **kwargs): Sets the stream parameters which are expected to be declared constant. """ - params = self.params().values() - constants = [p.constant for p in params] - for param in params: - param.constant = False - try: + with disable_constant(self) as constant: self.set_param(**kwargs) - except Exception as e: - for (param, const) in zip(params, constants): - param.constant = const - raise - - for (param, const) in zip(params, constants): - param.constant = const def update(self, trigger=True, **kwargs): @@ -246,6 +270,7 @@ def update(self, trigger=True, **kwargs): if trigger: self.trigger([self]) + def __repr__(self): cls_name = self.__class__.__name__ kwargs = ','.join('%s=%r' % (k,v) @@ -280,8 +305,9 @@ class PositionX(LinkedStream): position of the mouse/trackpad cursor. """ - x = param.ClassSelector(class_=(Number, util.basestring), default=0, doc=""" - Position along the x-axis in data coordinates""", constant=True) + x = param.ClassSelector(class_=(Number, util.basestring), default=None, + constant=True, doc=""" + Position along the x-axis in data coordinates""") class PositionY(LinkedStream): @@ -292,8 +318,9 @@ class PositionY(LinkedStream): position of the mouse/trackpad cursor. """ - y = param.ClassSelector(class_=(Number, util.basestring), default=0, doc=""" - Position along the y-axis in data coordinates""", constant=True) + y = param.ClassSelector(class_=(Number, util.basestring), default=None, + constant=True, doc=""" + Position along the y-axis in data coordinates""") class PositionXY(LinkedStream): @@ -304,11 +331,13 @@ class PositionXY(LinkedStream): position of the mouse/trackpad cursor. """ - x = param.ClassSelector(class_=(Number, util.basestring), default=0, doc=""" - Position along the x-axis in data coordinates""", constant=True) + x = param.ClassSelector(class_=(Number, util.basestring), default=None, + constant=True, doc=""" + Position along the x-axis in data coordinates""") - y = param.ClassSelector(class_=(Number, util.basestring), default=0, doc=""" - Position along the y-axis in data coordinates""", constant=True) + y = param.ClassSelector(class_=(Number, util.basestring), default=None, + constant=True, doc=""" + Position along the y-axis in data coordinates""") class Tap(PositionXY): diff --git a/tests/testdynamic.py b/tests/testdynamic.py index d19e37f870..cf44382f0b 100644 --- a/tests/testdynamic.py +++ b/tests/testdynamic.py @@ -4,7 +4,7 @@ from holoviews import Dimension, NdLayout, GridSpace from holoviews.core.spaces import DynamicMap, HoloMap, Callable from holoviews.element import Image, Scatter, Curve, Text -from holoviews.streams import PositionXY, PositionX +from holoviews.streams import PositionXY, PositionX, PositionY from holoviews.util import Dynamic from holoviews.element.comparison import ComparisonTestCase @@ -206,7 +206,7 @@ def fn(x, y): dmap.event(x=1, y=2) -class DynamicCallable(ComparisonTestCase): +class DynamicCallableMemoize(ComparisonTestCase): def test_dynamic_callable_memoize(self): # Always memoized only one of each held @@ -278,6 +278,42 @@ def history_callback(x, history=deque(maxlen=10)): self.assertEqual(dmap[()], Curve([1, 1, 1, 2, 2, 2])) + +class DynamicStreamReset(ComparisonTestCase): + + def test_dynamic_stream_reset(self): + # Ensure Stream reset option resets streams to default value + # when not triggering + global xresets, yresets + xresets, yresets = 0, 0 + def history_callback(x, y, history=deque(maxlen=10)): + global xresets, yresets + if x is None: + xresets += 1 + else: + history.append(x) + if y is None: + yresets += 1 + + return Curve(list(history)) + + x = PositionX(reset=True, memoize=False, x=None) + y = PositionY(reset=True, memoize=False, y=None) + dmap = DynamicMap(history_callback, kdims=[], streams=[x, y]) + + # Add stream subscriber mocking plot + x.add_subscriber(lambda **kwargs: dmap[()]) + y.add_subscriber(lambda **kwargs: dmap[()]) + + # Update each stream and count when None default appears + for i in range(2): + x.update(x=i) + y.update(y=i) + + self.assertEqual(xresets, 2) + self.assertEqual(yresets, 2) + + class DynamicCollate(ComparisonTestCase): def test_dynamic_collate_layout(self):