Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplified how Event subscribers are handled #1235

Merged
merged 17 commits into from
Mar 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,16 +594,20 @@ 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))
for k in stream_params - set(kwargs.keys()):
raise KeyError('Key %r does not correspond to any stream parameter')

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)
applicable_kws = {k:v for k,v in kwargs.items()
if k in set(stream.contents.keys())}
rkwargs = util.rename_stream_kwargs(stream, applicable_kws, 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
21 changes: 21 additions & 0 deletions tests/testdynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,24 @@ 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])

regexp = '(.+?)does not correspond to any stream parameter'
with self.assertRaisesRegexp(KeyError, regexp):
dmap.event(x=1, y=2)

36 changes: 26 additions & 10 deletions tests/teststreams.py
Original file line number Diff line number Diff line change
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 @@ -152,14 +153,15 @@ def test_simple_rename_constructor(self):
self.assertEqual(xy.contents, {'xtest':0, 'ytest':4})

def test_invalid_rename_constructor(self):
with self.assertRaises(KeyError) as cm:
regexp = '(.+?)is not a stream parameter'
with self.assertRaisesRegexp(KeyError, regexp):
PositionXY(rename={'x':'xtest', 'z':'ytest'}, x=0, y=4)
self.assertEqual(str(cm).endswith('is not a stream parameter'), True)
self.assertEqual(str(cm).endswith(), True)

def test_clashing_rename_constructor(self):
with self.assertRaises(KeyError) as cm:
regexp = '(.+?)parameter of the same name'
with self.assertRaisesRegexp(KeyError, regexp):
PositionXY(rename={'x':'xtest', 'y':'x'}, x=0, y=4)
self.assertEqual(str(cm).endswith('parameter of the same name'), True)

def test_simple_rename_method(self):
xy = PositionXY(x=0, y=4)
Expand All @@ -168,15 +170,29 @@ def test_simple_rename_method(self):

def test_invalid_rename_method(self):
xy = PositionXY(x=0, y=4)
with self.assertRaises(KeyError) as cm:
regexp = '(.+?)is not a stream parameter'
with self.assertRaisesRegexp(KeyError, regexp):
renamed = xy.rename(x='xtest', z='ytest')
self.assertEqual(str(cm).endswith('is not a stream parameter'), True)


def test_clashing_rename_method(self):
xy = PositionXY(x=0, y=4)
with self.assertRaises(KeyError) as cm:
regexp = '(.+?)parameter of the same name'
with self.assertRaisesRegexp(KeyError, regexp):
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(y='ytest')
regexp = "ytest' is not a parameter of(.+?)"
with self.assertRaisesRegexp(ValueError, regexp):
renamed.update(ytest=8)


class TestPlotSizeTransform(ComparisonTestCase):
Expand Down