From 8abd6ff63327f41d9acf211e5841d52275b276d0 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 20 Oct 2020 12:17:45 -0400 Subject: [PATCH] FIX: Fix problematic bound methods [circle full] --- mne/conftest.py | 21 +++++++++++++++++++++ mne/viz/_brain/_brain.py | 23 ++++++++++++----------- mne/viz/_brain/tests/test_brain.py | 20 ++++++++++++++------ mne/viz/_brain/tests/test_notebook.py | 2 +- mne/viz/backends/_pyvista.py | 7 +++++++ mne/viz/tests/test_3d.py | 12 ++++++------ 6 files changed, 61 insertions(+), 24 deletions(-) diff --git a/mne/conftest.py b/mne/conftest.py index aff52d4bfff..12e0e863fc3 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -471,6 +471,27 @@ def download_is_error(monkeypatch): monkeypatch.setattr(mne.utils.fetching, '_get_http', _fail) +@pytest.fixture() +def brain_gc(): + """Ensure that brain can be properly garbage collected.""" + from mne.viz import Brain + gc.collect() + n = sum(isinstance(obj, Brain) for obj in gc.get_objects()) + assert n == 0, f'{n} before' + yield + gc.collect() + n = 0 + ref = list() + new = '\n' + for obj in gc.get_objects(): + if isinstance(obj, Brain): + n += 1 + ref.extend([ + f'{r.__class__.__name__}: {repr(r)[:100].replace(new, " ")}' + for r in gc.get_referrers(obj)]) + assert n == 0, f'{n} after:\n{new.join(ref)}' + + def pytest_sessionfinish(session, exitstatus): """Handle the end of the session.""" n = session.config.option.durations diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 1127e2f5973..d9514832800 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -376,6 +376,7 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): # Direct access parameters: self.plotter = self._renderer.plotter + self._iren = self._renderer.plotter.iren self.main_menu = self.plotter.main_menu self.window = self.plotter.app_window self.tool_bar = self.window.addToolBar("toolbar") @@ -428,20 +429,14 @@ def _clean(self): self._clear_callbacks() self.actions.clear() self.sliders.clear() - self.reps = None - self.plotter = None - self.main_menu = None - self.window = None - self.tool_bar = None - self.status_bar = None - self.interactor = None if self.mpl_canvas is not None: self.mpl_canvas.clear() - self.mpl_canvas = None - self.time_actor = None - self.picked_renderer = None for key in list(self.act_data_smooth.keys()): self.act_data_smooth[key] = None + for key in ('reps', 'plotter', 'main_menu', 'window', 'tool_bar', + 'status_bar', 'interactor', 'mpl_canvas', 'time_actor', + 'picked_renderer', 'act_data_smooth', '_iren'): + setattr(self, key, None) @contextlib.contextmanager def ensure_minimum_sizes(self): @@ -922,6 +917,9 @@ def _load_icons(self): self.icons["visibility_on"] = QIcon(":/visibility_on.svg") self.icons["visibility_off"] = QIcon(":/visibility_off.svg") + def _save_movie_noname(self): + return self.save_movie(None) + def _configure_tool_bar(self): self.actions["screenshot"] = self.tool_bar.addAction( self.icons["screenshot"], @@ -931,7 +929,7 @@ def _configure_tool_bar(self): self.actions["movie"] = self.tool_bar.addAction( self.icons["movie"], "Save movie...", - partial(self.save_movie, filename=None) + self._save_movie_noname, ) self.actions["visibility"] = self.tool_bar.addAction( self.icons["visibility_on"], @@ -1308,6 +1306,7 @@ def help(self): ) def _clear_callbacks(self): + from ..backends._pyvista import _remove_picking_callback for callback in self.callbacks.values(): if callback is not None: if hasattr(callback, "plotter"): @@ -1317,6 +1316,8 @@ def _clear_callbacks(self): if hasattr(callback, "slider_rep"): callback.slider_rep = None self.callbacks.clear() + if self.show_traces: + _remove_picking_callback(self._iren, self.plotter.picker) @property def interaction(self): diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 373fcc4cb08..ae8457ea1d3 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -93,7 +93,7 @@ def GetPosition(self): @testing.requires_testing_data -def test_brain_init(renderer, tmpdir, pixel_ratio): +def test_brain_init(renderer, tmpdir, pixel_ratio, brain_gc): """Test initialization of the Brain instance.""" from mne.label import read_label hemi = 'lh' @@ -229,7 +229,7 @@ def test_brain_init(renderer, tmpdir, pixel_ratio): @testing.requires_testing_data @pytest.mark.slowtest -def test_brain_save_movie(tmpdir, renderer): +def test_brain_save_movie(tmpdir, renderer, brain_gc): """Test saving a movie of a Brain instance.""" if renderer._get_3d_backend() == "mayavi": pytest.skip('Save movie only supported on PyVista') @@ -243,7 +243,7 @@ def test_brain_save_movie(tmpdir, renderer): @testing.requires_testing_data @pytest.mark.slowtest -def test_brain_time_viewer(renderer_interactive, pixel_ratio): +def test_brain_time_viewer(renderer_interactive, pixel_ratio, brain_gc): """Test time viewer primitives.""" if renderer_interactive._get_3d_backend() != 'pyvista': pytest.skip('TimeViewer tests only supported on PyVista') @@ -285,6 +285,7 @@ def test_brain_time_viewer(renderer_interactive, pixel_ratio): img = brain.screenshot(mode='rgb') want_shape = np.array([300 * pixel_ratio, 300 * pixel_ratio, 3]) assert_allclose(img.shape, want_shape) + brain.close() @testing.requires_testing_data @@ -300,7 +301,8 @@ def test_brain_time_viewer(renderer_interactive, pixel_ratio): pytest.param('mixed', marks=pytest.mark.slowtest), ]) @pytest.mark.slowtest -def test_brain_traces(renderer_interactive, hemi, src, tmpdir): +def test_brain_traces(renderer_interactive, hemi, src, tmpdir, + brain_gc): """Test brain traces.""" if renderer_interactive._get_3d_backend() != 'pyvista': pytest.skip('Only PyVista supports traces') @@ -393,6 +395,7 @@ def test_brain_traces(renderer_interactive, hemi, src, tmpdir): # only test one condition to save time if not (hemi == 'rh' and src == 'surface' and check_version('sphinx_gallery')): + brain.close() return fnames = [str(tmpdir.join(f'temp_{ii}.png')) for ii in range(2)] block_vars = dict(image_path_iterator=iter(fnames), @@ -406,6 +409,7 @@ def test_brain_traces(renderer_interactive, hemi, src, tmpdir): gallery_conf = dict(src_dir=str(tmpdir), compress_images=[]) scraper = _BrainScraper() rst = scraper(block, block_vars, gallery_conf) + assert brain.plotter is None # closed gif_0 = fnames[0][:-3] + 'gif' for fname in (gif_0, fnames[1]): assert path.basename(fname) in rst @@ -418,7 +422,7 @@ def test_brain_traces(renderer_interactive, hemi, src, tmpdir): @testing.requires_testing_data @pytest.mark.slowtest -def test_brain_linkviewer(renderer_interactive): +def test_brain_linkviewer(renderer_interactive, brain_gc): """Test _LinkViewer primitives.""" if renderer_interactive._get_3d_backend() != 'pyvista': pytest.skip('Linkviewer only supported on PyVista') @@ -449,9 +453,13 @@ def test_brain_linkviewer(renderer_interactive): link_viewer.set_fmax(1) link_viewer.set_playback_speed(value=0.1) link_viewer.toggle_playback() + del link_viewer + brain1.close() + brain2.close() + brain_data.close() -def test_brain_colormap(): +def test_brain_colormap(brain_gc): """Test brain's colormap functions.""" colormap = "coolwarm" alpha = 1.0 diff --git a/mne/viz/_brain/tests/test_notebook.py b/mne/viz/_brain/tests/test_notebook.py index 99d3c345105..48c65c2d066 100644 --- a/mne/viz/_brain/tests/test_notebook.py +++ b/mne/viz/_brain/tests/test_notebook.py @@ -12,7 +12,7 @@ @requires_version('nbformat') @requires_version('nbclient') @requires_version('ipympl') -def test_notebook_3d_backend(renderer_notebook): +def test_notebook_3d_backend(renderer_notebook, brain_gc): """Test executing a notebook that should not fail.""" import nbformat from nbclient import NotebookClient diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index fb69c421e06..a0e49ca73ee 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -931,6 +931,13 @@ def _update_picking_callback(plotter, plotter.picker = picker +def _remove_picking_callback(interactor, picker): + interactor.RemoveObservers(vtk.vtkCommand.RenderEvent) + interactor.RemoveObservers(vtk.vtkCommand.LeftButtonPressEvent) + interactor.RemoveObservers(vtk.vtkCommand.EndInteractionEvent) + picker.RemoveObservers(vtk.vtkCommand.EndPickEvent) + + def _arrow_glyph(grid, factor): glyph = vtk.vtkGlyphSource2D() glyph.SetGlyphTypeToArrow() diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 35224d518ed..1806e8558f6 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -105,7 +105,7 @@ def test_plot_head_positions(): @requires_pysurfer @traits_test @pytest.mark.slowtest -def test_plot_sparse_source_estimates(renderer_interactive): +def test_plot_sparse_source_estimates(renderer_interactive, brain_gc): """Test plotting of (sparse) source estimates.""" sample_src = read_source_spaces(src_fname) @@ -121,10 +121,10 @@ def test_plot_sparse_source_estimates(renderer_interactive): stc = SourceEstimate(stc_data, vertices, 1, 1) colormap = 'mne_analyze' - plot_source_estimates(stc, 'sample', colormap=colormap, - background=(1, 1, 0), - subjects_dir=subjects_dir, colorbar=True, - clim='auto') + brain = plot_source_estimates( + stc, 'sample', colormap=colormap, background=(1, 1, 0), + subjects_dir=subjects_dir, colorbar=True, clim='auto') + brain.close() pytest.raises(TypeError, plot_source_estimates, stc, 'sample', figure='foo', hemi='both', clim='auto', subjects_dir=subjects_dir) @@ -574,7 +574,7 @@ def test_snapshot_brain_montage(renderer): @pytest.mark.parametrize('pick_ori', ('vector', None)) @pytest.mark.parametrize('kind', ('surface', 'volume', 'mixed')) def test_plot_source_estimates(renderer_interactive, all_src_types_inv_evoked, - pick_ori, kind): + pick_ori, kind, brain_gc): """Test plotting of scalar and vector source estimates.""" invs, evoked = all_src_types_inv_evoked inv = invs[kind]