Skip to content

Commit

Permalink
Merge 47ab063 into 5e6d0d2
Browse files Browse the repository at this point in the history
  • Loading branch information
jlstevens committed Mar 29, 2017
2 parents 5e6d0d2 + 47ab063 commit 743e658
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 21 deletions.
11 changes: 5 additions & 6 deletions holoviews/core/spaces.py
Expand Up @@ -594,16 +594,15 @@ def _validate_key(self, key):

def event(self, trigger=True, **kwargs):
"""
This method allows any of the available stream parameters to be
updated in an event.
This method allows any of the available stream parameters
(renamed as appropriate) to be updated in an event.
"""
stream_params = set(util.stream_parameters(self.streams))
updated_streams = []
for stream in self.streams:
overlap = set(stream.params().keys()) & stream_params & set(kwargs.keys())
if overlap:
stream.update(**dict({k:kwargs[k] for k in overlap}, trigger=False))
updated_streams.append(stream)
rkwargs = util.rename_stream_kwargs(stream, kwargs, reverse=True)
stream.update(**dict(rkwargs, trigger=False))
updated_streams.append(stream)

if updated_streams and trigger:
updated_streams[0].trigger(updated_streams)
Expand Down
35 changes: 35 additions & 0 deletions holoviews/core/util.py
Expand Up @@ -914,6 +914,41 @@ def wrap_tuple(unwrapped):
return (unwrapped if isinstance(unwrapped, tuple) else (unwrapped,))


def stream_name_mapping(stream, exclude_params=['name'], reverse=False):
"""
Return a complete dictionary mapping between stream parameter names
to their applicable renames, excluding parameters listed in
exclude_params.
If reverse is True, the mapping is from the renamed strings to the
original stream parameter names.
"""
filtered = [k for k in stream.params().keys() if k not in exclude_params]
mapping = {k:stream._rename.get(k,k) for k in filtered}
if reverse:
return {v:k for k,v in mapping.items()}
else:
return mapping

def rename_stream_kwargs(stream, kwargs, reverse=False):
"""
Given a stream and a kwargs dictionary of parameter values, map to
the corresponding dictionary where the keys are substituted with the
appropriately renamed string.
If reverse, the output will be a dictionary using the original
parameter names given a dictionary using the renamed equivalents.
"""
mapped_kwargs = {}
mapping = stream_name_mapping(stream, reverse=reverse)
for k,v in kwargs.items():
if k not in mapping:
msg = 'Could not map key {key} {direction} renamed equivalent'
direction = 'from' if reverse else 'to'
raise KeyError(msg.format(key=repr(k), direction=direction))
mapped_kwargs[mapping[k]] = v
return mapped_kwargs


def stream_parameters(streams, no_duplicates=True, exclude=['name']):
"""
Expand Down
2 changes: 1 addition & 1 deletion holoviews/plotting/util.py
Expand Up @@ -298,7 +298,7 @@ def attach_streams(plot, obj):
"""
def append_refresh(dmap):
for stream in get_nested_streams(dmap):
stream._hidden_subscribers.append(plot.refresh)
stream.add_subscriber(plot.refresh)
return obj.traverse(append_refresh, [DynamicMap])


Expand Down
52 changes: 40 additions & 12 deletions holoviews/streams.py
Expand Up @@ -51,8 +51,7 @@ def trigger(cls, streams):

# Currently building a simple set of subscribers
groups = [stream.subscribers for stream in streams]
hidden = [stream._hidden_subscribers for stream in streams]
subscribers = util.unique_iterator([s for subscribers in groups+hidden
subscribers = util.unique_iterator([s for subscribers in groups
for s in subscribers])
for subscriber in subscribers:
subscriber(**dict(union))
Expand All @@ -61,8 +60,7 @@ def trigger(cls, streams):
stream.deactivate()


def __init__(self, rename={}, source=None, subscribers=[],
linked=True, **params):
def __init__(self, rename={}, source=None, subscribers=[], linked=True, **params):
"""
The rename argument allows multiple streams with similar event
state to be used by remapping parameter names.
Expand All @@ -75,8 +73,10 @@ def __init__(self, rename={}, source=None, subscribers=[],
plot, to disable this set linked=False
"""
self._source = source
self.subscribers = subscribers
self._hidden_subscribers = []
self._subscribers = []
for subscriber in subscribers:
self.add_subscriber(subscriber)

self.linked = linked
self._rename = self._validate_rename(rename)

Expand All @@ -89,6 +89,28 @@ def __init__(self, rename={}, source=None, subscribers=[],
if source:
self.registry[id(source)].append(self)


@property
def subscribers(self):
" Property returning the subscriber list"
return self._subscribers

def clear(self):
"""
Clear all subscribers registered to this stream.
"""
self._subscribers = []

def add_subscriber(self, subscriber):
"""
Register a callable subscriber to this stream which will be
invoked either when update is called with trigger=True or when
this stream is passed to the trigger classmethod.
"""
if not callable(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():
Expand All @@ -109,7 +131,6 @@ def rename(self, **mapping):
params = {k:v for k,v in self.get_param_values() if k != 'name'}
return self.__class__(rename=mapping,
source=self._source,
subscribers=self.subscribers,
linked=self.linked, **params)


Expand Down Expand Up @@ -156,15 +177,23 @@ def _set_stream_parameters(self, **kwargs):
constants = [p.constant for p in params]
for param in params:
param.constant = False
self.set_param(**kwargs)
try:
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):
"""
The update method updates the stream parameters in response to
some event. If the stream has a custom transform method, this
is applied to transform the parameter values accordingly.
The update method updates the stream parameters (without any
renaming applied) in response to some event. If the stream has a
custom transform method, this is applied to transform the
parameter values accordingly.
If trigger is enabled, the trigger classmethod is invoked on
this particular Stream instance.
Expand All @@ -176,7 +205,6 @@ 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)
Expand Down
20 changes: 20 additions & 0 deletions tests/testdynamic.py
Expand Up @@ -226,3 +226,23 @@ def fn2(x, y):
self.assertEqual(overlay.Scatter.I, fn(1, 2))
# Ensure dmap2 callback was called only once
self.assertEqual(counter[0], 1)

def test_dynamic_event_renaming_valid(self):

def fn(x, y):
return Scatter([(x, y)])

xy = PositionXY(rename={'x':'x1','y':'y1'})
dmap = DynamicMap(fn, kdims=[], streams=[xy])
dmap.event(x1=1, y1=2)

def test_dynamic_event_renaming_invalid(self):
def fn(x, y):
return Scatter([(x, y)])

xy = PositionXY(rename={'x':'x1','y':'y1'})
dmap = DynamicMap(fn, kdims=[], streams=[xy])
with self.assertRaises(KeyError) as cm:
dmap.event(x=1, y=2)
self.assertEqual(str(cm).endswith('from renamed equivalent'), True)

18 changes: 16 additions & 2 deletions tests/teststreams.py
Expand Up @@ -9,8 +9,9 @@ def test_all_stream_parameters_constant():
all_stream_cls = [v for v in globals().values() if
isinstance(v, type) and issubclass(v, Stream)]
for stream_cls in all_stream_cls:
for name, param in stream_cls.params().items():
if param.constant != True:
for name, p in stream_cls.params().items():
if name == 'name': continue
if p.constant != True:
raise TypeError('Parameter %s of stream %s not declared constant'
% (name, stream_cls.__name__))

Expand Down Expand Up @@ -178,6 +179,19 @@ def test_clashing_rename_method(self):
renamed = xy.rename(x='xtest', y='x')
self.assertEqual(str(cm).endswith('parameter of the same name'), True)

def test_update_rename_valid(self):
xy = PositionXY(x=0, y=4)
renamed = xy.rename(x='xtest', y='ytest')
renamed.update(x=4, y=8)
self.assertEqual(renamed.contents, {'xtest':4, 'ytest':8})

def test_update_rename_invalid(self):
xy = PositionXY(x=0, y=4)
renamed = xy.rename(x='xtest', y='ytest')
with self.assertRaises(ValueError) as cm:
renamed.update(xtest=4, ytest=8)
self.assertEqual(str(cm).startsswith("'ytest' is not a parameter of"), True)


class TestPlotSizeTransform(ComparisonTestCase):

Expand Down

0 comments on commit 743e658

Please sign in to comment.