diff --git a/holoviews/streams.py b/holoviews/streams.py index 0b439cc1d1..a3444d01e5 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -166,30 +166,42 @@ def trigger(cls, streams): def _on_trigger(self): """Called when a stream has been triggered""" + @classmethod def _process_streams(cls, streams): """ Processes a list of streams promoting Parameterized objects and methods to Param based streams. """ - param_watch_support = util.param_version >= '1.8.0' - parameterizeds = [s.parameterized for s in streams if isinstance(s, Params)] + parameterizeds = defaultdict(set) valid, invalid = [], [] for s in streams: - if not isinstance(s, Stream): - if isinstance(s, param.Parameterized) and param_watch_support: - if s not in parameterizeds: - s = Params(s) - else: - continue - elif util.is_param_method(s) and param_watch_support: - if not hasattr(s, "_dinfo") or util.get_method_owner(s) in parameterizeds: - continue - else: - s = ParamMethod(s) - else: - invalid.append(s) + if isinstance(s, Stream): + pass + elif isinstance(s, param.Parameter): + s = Params(s.owner, [s.name]) + elif isinstance(s, param.Parameterized): + s = Params(s) + elif util.is_param_method(s): + if not hasattr(s, "_dinfo"): continue + s = ParamMethod(s) + else: + invalid.append(s) + continue + if isinstance(s, Params): + pid = id(s.parameterized) + overlap = (set(s.parameters) & parameterizeds[pid]) + if overlap: + pname = type(s.parameterized).__name__ + param.main.param.warning( + 'The %s parameter(s) on the %s object have ' + 'already been supplied in another stream. ' + 'Ensure that the supplied streams only specify ' + 'each parameter once, otherwise multiple ' + 'events will be triggered when the parameter ' + 'changes.' % (sorted(overlap), pname)) + parameterizeds[pid] |= set(s.parameters) valid.append(s) return valid, invalid diff --git a/holoviews/tests/teststreams.py b/holoviews/tests/teststreams.py index f0c2dd1869..0476b615d6 100644 --- a/holoviews/tests/teststreams.py +++ b/holoviews/tests/teststreams.py @@ -12,6 +12,9 @@ from holoviews.streams import * # noqa (Test all available streams) from holoviews.util import Dynamic +from .utils import LoggingComparisonTestCase + + def test_all_stream_parameters_constant(): all_stream_cls = [v for v in globals().values() if isinstance(v, type) and issubclass(v, Stream)] @@ -178,12 +181,9 @@ def test_class_value_update(self): -class TestParamsStream(ComparisonTestCase): +class TestParamsStream(LoggingComparisonTestCase): def setUp(self): - if LooseVersion(param.__version__) < '1.8.0': - raise SkipTest('Params stream requires param >= 1.8.0') - class Inner(param.Parameterized): x = param.Number(default = 0) @@ -223,6 +223,41 @@ def subscriber(**kwargs): inner.y = 2 self.assertEqual(values, [{'x': 2, 'y': 2}]) + def test_param_stream_instance_separate_parameters(self): + inner = self.inner() + + xparam = Params(inner, ['x']) + yparam = Params(inner, ['y']) + + valid, invalid = Stream._process_streams([xparam, yparam]) + self.assertEqual(len(valid), 2) + self.assertEqual(len(invalid), 0) + + def test_param_stream_instance_overlapping_parameters(self): + inner = self.inner() + + params1 = Params(inner) + params2 = Params(inner) + + Stream._process_streams([params1, params2]) + self.log_handler.assertContains('WARNING', "['x', 'y']") + + def test_param_parameter_instance_separate_parameters(self): + inner = self.inner() + + valid, invalid = Stream._process_streams([inner.param.x, inner.param.y]) + xparam, yparam = valid + + self.assertIs(xparam.parameterized, inner) + self.assertEqual(xparam.parameters, ['x']) + self.assertIs(yparam.parameterized, inner) + self.assertEqual(yparam.parameters, ['y']) + + def test_param_parameter_instance_overlapping_parameters(self): + inner = self.inner() + Stream._process_streams([inner.param.x, inner.param.x]) + self.log_handler.assertContains('WARNING', "['x']") + def test_param_stream_parameter_override(self): inner = self.inner(x=2) stream = Params(inner, parameters=['x'])