From 14849d585d291c7fa07e597b5c8a858b8b869190 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 14 Oct 2024 17:23:58 +0200 Subject: [PATCH] Ensure proper cleanup of stream subscribers (#6389) --- holoviews/plotting/bokeh/plot.py | 11 ++-- holoviews/plotting/plot.py | 2 +- holoviews/tests/plotting/bokeh/test_plot.py | 53 ++++++++++++++++++- holoviews/tests/plotting/plotly/test_plot.py | 54 +++++++++++++++++++- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 91a72967b6..109b7067e3 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -249,10 +249,12 @@ def cleanup(self): if plot.subplots: plot.subplots.clear() - if isinstance(plot, GenericElementPlot): - for callback in plot.callbacks: - streams += callback.streams - callback.cleanup() + if not isinstance(plot, (GenericElementPlot, GenericOverlayPlot)): + continue + + for callback in plot.callbacks: + streams += callback.streams + callback.cleanup() for stream in set(streams): stream._subscribers = [ @@ -261,7 +263,6 @@ def cleanup(self): get_method_owner(subscriber) not in plots ] - def _fontsize(self, key, label='fontsize', common=True): """ Converts integer fontsizes to a string specifying diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index eb963afea2..444dd68120 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -195,7 +195,7 @@ def cleanup(self): """ plots = self.traverse(lambda x: x, [Plot]) for plot in plots: - if not isinstance(plot, (GenericCompositePlot, GenericElementPlot, GenericOverlayPlot)): + if not isinstance(plot, (GenericElementPlot, GenericOverlayPlot)): continue for stream in set(plot.streams): stream._subscribers = [ diff --git a/holoviews/tests/plotting/bokeh/test_plot.py b/holoviews/tests/plotting/bokeh/test_plot.py index 5c7a08a2e1..03c9761f27 100644 --- a/holoviews/tests/plotting/bokeh/test_plot.py +++ b/holoviews/tests/plotting/bokeh/test_plot.py @@ -14,9 +14,11 @@ from holoviews import Curve from holoviews.core.element import Element from holoviews.core.options import Store +from holoviews.core.spaces import DynamicMap from holoviews.element.comparison import ComparisonTestCase from holoviews.plotting.bokeh.callbacks import Callback from holoviews.plotting.bokeh.element import ElementPlot +from holoviews.streams import Pipe bokeh_renderer = Store.renderers['bokeh'] @@ -82,11 +84,60 @@ def _test_hover_info(self, element, tooltips, line_policy='nearest', formatters= self.assertIn(lookup[2:-1], cds.data) # Ensure all the glyph renderers have a hover tool - print(renderers, hover) for renderer in renderers: self.assertTrue(any(renderer in h.renderers for h in hover)) +def test_element_plot_stream_cleanup(): + stream = Pipe() + + dmap = DynamicMap(Curve, streams=[stream]) + + plot = bokeh_renderer.get_plot(dmap) + + assert len(stream._subscribers) == 1 + + plot.cleanup() + + assert not stream._subscribers + + +def test_overlay_plot_stream_cleanup(): + stream1 = Pipe() + stream2 = Pipe() + + dmap1 = DynamicMap(Curve, streams=[stream1]) + dmap2 = DynamicMap(Curve, streams=[stream2]) + + plot = bokeh_renderer.get_plot(dmap1 * dmap2) + + assert len(stream1._subscribers) == 4 + assert len(stream2._subscribers) == 4 + + plot.cleanup() + + assert not stream1._subscribers + assert not stream2._subscribers + + +def test_layout_plot_stream_cleanup(): + stream1 = Pipe() + stream2 = Pipe() + + dmap1 = DynamicMap(Curve, streams=[stream1]) + dmap2 = DynamicMap(Curve, streams=[stream2]) + + plot = bokeh_renderer.get_plot(dmap1 + dmap2) + + assert len(stream1._subscribers) == 2 + assert len(stream2._subscribers) == 2 + + plot.cleanup() + + assert not stream1._subscribers + assert not stream2._subscribers + + def test_sync_two_plots(): curve = lambda i: Curve(np.arange(10) * i, label="ABC"[i]) plot1 = curve(0) * curve(1) diff --git a/holoviews/tests/plotting/plotly/test_plot.py b/holoviews/tests/plotting/plotly/test_plot.py index 3a033f8848..0f71a99521 100644 --- a/holoviews/tests/plotting/plotly/test_plot.py +++ b/holoviews/tests/plotting/plotly/test_plot.py @@ -2,16 +2,68 @@ import pyviz_comms as comms from param import concrete_descendents -from holoviews.core import Store +from holoviews.core import DynamicMap, Store +from holoviews.element import Curve from holoviews.element.comparison import ComparisonTestCase from holoviews.plotting.plotly.element import ElementPlot from holoviews.plotting.plotly.util import figure_grid +from holoviews.streams import Pipe from .. import option_intersections plotly_renderer = Store.renderers['plotly'] +def test_element_plot_stream_cleanup(): + stream = Pipe() + + dmap = DynamicMap(Curve, streams=[stream]) + + plot = plotly_renderer.get_plot(dmap) + + assert len(stream._subscribers) == 1 + + plot.cleanup() + + assert not stream._subscribers + + +def test_overlay_plot_stream_cleanup(): + stream1 = Pipe() + stream2 = Pipe() + + dmap1 = DynamicMap(Curve, streams=[stream1]) + dmap2 = DynamicMap(Curve, streams=[stream2]) + + plot = plotly_renderer.get_plot(dmap1 * dmap2) + + assert len(stream1._subscribers) == 4 + assert len(stream2._subscribers) == 4 + + plot.cleanup() + + assert not stream1._subscribers + assert not stream2._subscribers + + +def test_layout_plot_stream_cleanup(): + stream1 = Pipe() + stream2 = Pipe() + + dmap1 = DynamicMap(Curve, streams=[stream1]) + dmap2 = DynamicMap(Curve, streams=[stream2]) + + plot = plotly_renderer.get_plot(dmap1 + dmap2) + + assert len(stream1._subscribers) == 2 + assert len(stream2._subscribers) == 2 + + plot.cleanup() + + assert not stream1._subscribers + assert not stream2._subscribers + + class TestPlotlyPlot(ComparisonTestCase): def setUp(self):