From e41c7b199af671066fcab5d81b6b23ea1dfe0174 Mon Sep 17 00:00:00 2001 From: payam Date: Wed, 14 Oct 2026 13:38:09 +0200 Subject: [PATCH 1/2] tear down the time viewer --- mne/viz/_brain/_brain.py | 28 ---------------------------- mne/viz/_brain/_time_viewer.py | 29 +++++++++++++++++++++++++++++ mne/viz/_brain/tests/test_brain.py | 1 + 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 32fd2122120..760066ab1bf 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -532,7 +532,6 @@ def _setup_canonical_rotation(self): def _clean(self): # resolve the reference cycle self._renderer._window_close_disconnect() - self.clear_glyphs() self.remove_annotations() # clear init actors for hemi in self.layered_meshes: @@ -541,12 +540,6 @@ def _clean(self): self.plotter.remove_actor(actor) self._silhouette_actors = None self._clear_callbacks() - self._clear_widgets() - if getattr(self, "mpl_canvas", None) is not None: - self.mpl_canvas.clear() - if getattr(self, "act_data_smooth", None) is not None: - for key in list(self.act_data_smooth.keys()): - self.act_data_smooth[key] = None # XXX this should be done in PyVista for renderer in getattr(self._renderer, "_all_renderers", ()): # VTK only renderer.RemoveAllLights() @@ -564,20 +557,8 @@ def _clean(self): # XXX end PyVista for key in ( "plotter", - "window", - "dock", - "tool_bar", - "menu_bar", - "interactor", - "mpl_canvas", - "time_actor", - "picked_renderer", - "act_data_smooth", "_scalar_bar", "_scalar_bar_ticks", - "_hover_caption", - "actions", - "widgets", "geo", "_all_data", ): @@ -636,15 +617,6 @@ def _clear_callbacks(self): if getattr(self, "iren", None) is not None: self.plotter.iren.clear_key_event_callbacks() - def _clear_widgets(self): - if not hasattr(self, "widgets"): - return - for widget in self.widgets.values(): - if widget is not None: - for key in ("triggered", "floatValueChanged"): - setattr(widget, key, None) - self.widgets.clear() - @property def interaction(self): """The interaction style.""" diff --git a/mne/viz/_brain/_time_viewer.py b/mne/viz/_brain/_time_viewer.py index 9d7e80069ea..962868c2ad1 100644 --- a/mne/viz/_brain/_time_viewer.py +++ b/mne/viz/_brain/_time_viewer.py @@ -951,6 +951,35 @@ def _configure_ui_events(self): subscribe(self, "__colormap_range_updated", self._sync_colormap_widgets) subscribe(self, "__surface_updated", self._on_surface_updated) subscribe(self, "__overlay_added", self._on_overlay_added) + # published before the window closes, while everything is still alive + subscribe(self, "figure_closing", self._on_figure_closing) + + def _on_figure_closing(self, event): + self.clear_glyphs() + self._clear_widgets() + if self.mpl_canvas is not None: + self.mpl_canvas.clear() + if self.act_data_smooth is not None: + for key in list(self.act_data_smooth.keys()): + self.act_data_smooth[key] = None + # resolve the reference cycles + for key in ( + "mpl_canvas", + "time_actor", + "act_data_smooth", + "_hover_caption", + "widgets", + ): + setattr(self, key, None) + + def _clear_widgets(self): + if not hasattr(self, "widgets"): + return + for widget in self.widgets.values(): + if widget is not None: + for key in ("triggered", "floatValueChanged"): + setattr(widget, key, None) + self.widgets.clear() def _on_time_updated(self, event): self.plot_time_line(update=True) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 3c797c0a0e8..9e550ab1587 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -1431,6 +1431,7 @@ def test_brain_traces_basic(renderer_interactive_pyvistaqt, hemi, src, brain_gc) assert "annotation" not in brain.widgets assert "extract_mode" not in brain.widgets brain.close() + assert brain.widgets is None and brain.mpl_canvas is None @testing.requires_testing_data From c6f39d64d7fb1f2360d73a0bf199bca9b6e2de4c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 15 Oct 2026 13:14:07 -0400 Subject: [PATCH 2/2] Fixes --- doc/changes/names.inc | 2 +- mne/tests/test_docstring_parameters.py | 5 ++++- mne/viz/_brain/_time_viewer.py | 2 ++ mne/viz/_brain/tests/test_brain.py | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 4be0327e6f8..22e77da39d0 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -240,7 +240,7 @@ .. _Kaisu Lankinen: http://bishoplab.berkeley.edu/Kaisu.html .. _Kalle Makela: https://github.com/Kallemakela .. _Kalle Mäkelä: https://github.com/kalomak -.. _Kambiz Tabavi: https://github.com/ktavabi +.. _Kambiz Tavabi: https://github.com/ktavabi .. _Katarina Slama: https://github.com/katarinaslama .. _Katia Al-Amir: https://github.com/katia-sentry .. _Katrin Leinweber: https://github.com/katrinleinweber diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py index 4405aff43e0..ee17d466251 100644 --- a/mne/tests/test_docstring_parameters.py +++ b/mne/tests/test_docstring_parameters.py @@ -372,9 +372,12 @@ def test_no_global_rng(): """Test that we use local generators and the modern numpy RNG API.""" root = pyproject_path.parent # only available in a dev/editable checkout bad = [] + # CI tests an installed wheel from site-packages, where examples/ (if any) + # belongs to other packages (e.g. trame-vuetify), not to us + installed = root.name in ("site-packages", "dist-packages") for sub in ("mne", "examples", "tutorials"): base = root / sub - if not base.is_dir(): # e.g. examples/tutorials absent from a wheel + if not base.is_dir() or (installed and sub != "mne"): continue for path in sorted(base.rglob("*.py")): rel = path.relative_to(root).as_posix() diff --git a/mne/viz/_brain/_time_viewer.py b/mne/viz/_brain/_time_viewer.py index 962868c2ad1..05f6969c2f0 100644 --- a/mne/viz/_brain/_time_viewer.py +++ b/mne/viz/_brain/_time_viewer.py @@ -955,6 +955,8 @@ def _configure_ui_events(self): subscribe(self, "figure_closing", self._on_figure_closing) def _on_figure_closing(self, event): + if event.source is not self: # forwarded from a linked figure + return self.clear_glyphs() self._clear_widgets() if self.mpl_canvas is not None: diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 9e550ab1587..6b426138393 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -12,6 +12,7 @@ import numpy as np import pytest from matplotlib import image +from matplotlib.figure import Figure from matplotlib.lines import Line2D from numpy.testing import assert_allclose, assert_array_equal @@ -1430,6 +1431,11 @@ def test_brain_traces_basic(renderer_interactive_pyvistaqt, hemi, src, brain_gc) else: # volume assert "annotation" not in brain.widgets assert "extract_mode" not in brain.widgets + # closing a linked figure must not tear down this one's time viewer + fig = Figure() + ui_events.link(fig, brain) + ui_events.publish(fig, ui_events.FigureClosing()) + assert brain.widgets and brain.mpl_canvas is not None brain.close() assert brain.widgets is None and brain.mpl_canvas is None