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

FIX: close_connect #10388

Merged
merged 5 commits into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion doc/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ html-noplot:
@echo "Build finished. The HTML pages are in _build/html_stable."

html_dev-front:
@PATTERN="\(plot_mne_dspm_source_localization.py\|plot_receptive_field.py\|plot_mne_inverse_label_connectivity.py\|plot_sensors_decoding.py\|plot_stats_cluster_spatio_temporal.py\|plot_20_visualize_evoked.py\)" make html_dev-pattern;
@PATTERN="\(30_mne_dspm_loreta.py\|50_decoding.py\|30_strf.py\|20_cluster_1samp_spatiotemporal.py\|20_visualize_evoked.py\)" make html_dev-pattern;

dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml
Expand Down
5 changes: 3 additions & 2 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

from datetime import datetime, timezone
import faulthandler
import gc
import os
import subprocess
import sys
import time
import warnings
from datetime import datetime, timezone
import faulthandler

import numpy as np
import matplotlib
Expand Down Expand Up @@ -732,6 +732,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines):
size=xxl),
],
# \u00AD is an optional hyphen (not rendered unless needed)
# If these are changed, the Makefile should be updated, too
'carousel': [
dict(title='Source Estimation',
text='Distributed, sparse, mixed-norm, beam\u00ADformers, dipole fitting, and more.', # noqa E501
Expand Down
13 changes: 4 additions & 9 deletions mne/gui/_coreg.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ def _get_default(var, val):
self._mri_fids_modified = False
self._mri_scale_modified = False
self._accept_close_event = True
self._auto_cleanup = True
self._fid_colors = tuple(
DEFAULTS['coreg'][f'{key}_color'] for key in
('lpa', 'nasion', 'rpa'))
Expand Down Expand Up @@ -231,7 +230,8 @@ def _get_default(var, val):
# setup the window
self._renderer = _get_renderer(
size=self._defaults["size"], bgcolor=self._defaults["bgcolor"])
self._renderer._window_close_connect(self._close_callback)
self._renderer._window_close_connect(self._clean)
self._renderer._window_close_connect(self._close_callback, after=False)
self._renderer.set_interaction(interaction)

# coregistration model setup
Expand Down Expand Up @@ -1717,11 +1717,9 @@ def _configure_status_bar(self):
'status_message', 'hide', value=None, input_value=False
)

def _set_automatic_cleanup(self, state):
"""Enable/Disable automatic cleanup (for testing purposes only)."""
self._auto_cleanup = state

def _clean(self):
if not self._accept_close_event:
return
self._renderer = None
self._widgets.clear()
self._actors.clear()
Expand Down Expand Up @@ -1792,7 +1790,4 @@ def _close_callback(self):
modal=not MNE_3D_BACKEND_TESTING,
)
self._widgets["close_dialog"].show()

if self._accept_close_event and self._auto_cleanup:
self._clean()
return self._accept_close_event
3 changes: 2 additions & 1 deletion mne/gui/tests/test_coreg_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,9 @@ def test_coreg_gui_pyvista(tmp_path, renderer_interactive_pyvistaqt):
assert not coreg._trans_modified
assert op.isfile(tmp_trans)

# first, disable auto cleanup
coreg._renderer._window_close_disconnect(after=True)
# test _close_callback()
coreg._set_automatic_cleanup(False)
coreg.close()
coreg._widgets['close_dialog'].trigger('Discard') # do not save
coreg._clean() # finally, cleanup internal structures
Expand Down
8 changes: 8 additions & 0 deletions mne/gui/tests/test_gui_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,15 @@ def _check_widget_trigger(widget, mock, before, after, call_count=True,
# --- END: dialog ---

renderer.show()

renderer._window_close_connect(lambda: mock('first'), after=False)
renderer._window_close_connect(lambda: mock('last'))
old_call_count = mock.call_count
renderer.close()
if renderer._kind == 'qt':
assert mock.call_count == old_call_count + 2
assert mock.call_args_list[-1].args == ('last',)
assert mock.call_args_list[-2].args == ('first',)


def test_gui_api_qt(renderer_interactive_pyvistaqt):
Expand Down
13 changes: 11 additions & 2 deletions mne/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@ def _file_like(obj):
return all(callable(getattr(obj, name, None)) for name in ('read', 'seek'))


def _fullname(obj):
klass = obj.__class__
module = klass.__module__
if module == 'builtins':
return klass.__qualname__
return module + '.' + klass.__qualname__


def _assert_no_instances(cls, when=''):
__tracebackhide__ = True
n = 0
Expand All @@ -350,14 +358,15 @@ def _assert_no_instances(cls, when=''):
if isinstance(r, (list, dict)):
rep = f'len={len(r)}'
r_ = gc.get_referrers(r)
types = (x.__class__.__name__ for x in r_)
types = (_fullname(x) for x in r_)
types = "/".join(sorted(set(
x for x in types if x is not None)))
rep += f', {len(r_)} referrers: {types}'
del r_
else:
rep = repr(r)[:100].replace('\n', ' ')
ref.append(f'{r.__class__.__name__}: {rep}')
name = _fullname(r)
ref.append(f'{name}: {rep}')
count += 1
del r
del rr
Expand Down
2 changes: 1 addition & 1 deletion mne/viz/_brain/_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ def _clean(self):
for renderer in self._renderer._all_renderers:
renderer.RemoveAllLights()
# app_window cannot be set to None because it is used in __del__
for key in ('lighting', 'interactor'):
for key in ('lighting', 'interactor', '_RenderWindow'):
setattr(self.plotter, key, None)
# Qt LeaveEvent requires _Iren so we use _FakeIren instead of None
# to resolve the ref to vtkGenericRenderWindowInteractor
Expand Down
6 changes: 5 additions & 1 deletion mne/viz/backends/_abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,11 @@ def _window_initialize(self):
self._interactor_fraction = None

@abstractmethod
def _window_close_connect(self, func):
def _window_close_connect(self, func, *, after=True):
pass

@abstractmethod
def _window_close_disconnect(self, after=True):
pass

@abstractmethod
Expand Down
5 changes: 4 additions & 1 deletion mne/viz/backends/_notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,10 @@ def __init__(self, brain, width, height, dpi):


class _IpyWindow(_AbstractWindow):
def _window_close_connect(self, func):
def _window_close_connect(self, func, *, after=True):
pass

def _window_close_disconnect(self, after=True):
pass

def _window_get_dpi(self):
Expand Down
24 changes: 20 additions & 4 deletions mne/viz/backends/_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,29 +543,45 @@ def _window_initialize(self):
self._window = self.figure.plotter.app_window
self._window.setLocale(QLocale(QLocale.Language.English))
self._window.signal_close.connect(self._window_clean)
self._window_close_callbacks = list()
self._window_before_close_callbacks = list()
self._window_after_close_callbacks = list()

# patch closeEvent
def closeEvent(event):
# functions to call before closing
accept_close_event = True
for callback in self._window_close_callbacks:
for callback in self._window_before_close_callbacks:
ret = callback()
# check if one of the callbacks ignores the close event
if isinstance(ret, bool) and not ret:
accept_close_event = False

if accept_close_event:
self._window.signal_close.emit()
event.accept()
else:
event.ignore()

# functions to call after closing
for callback in self._window_after_close_callbacks:
callback()
self._window.closeEvent = closeEvent

def _window_clean(self):
self.figure._plotter = None
self._interactor = None

def _window_close_connect(self, func):
self._window_close_callbacks.append(func)
def _window_close_connect(self, func, *, after=True):
if after:
self._window_after_close_callbacks.append(func)
else:
self._window_before_close_callbacks.append(func)

def _window_close_disconnect(self, after=True):
if after:
self._window_after_close_callbacks.clear()
else:
self._window_before_close_callbacks.clear()

def _window_get_dpi(self):
return self._window.windowHandle().screen().logicalDotsPerInch()
Expand Down
2 changes: 1 addition & 1 deletion tutorials/clinical/10_ieeg_localize.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def plot_overlay(image, compare, title, thresh=None):
# reg_affine, _ = mne.transforms.compute_volume_registration(
# CT_orig, T1, pipeline='rigids', zooms=dict(translation=5)))
#
# And instead we just hard-code the resulting 4x4 matrix:
# Instead we just hard-code the resulting 4x4 matrix:

reg_affine = np.array([
[0.99270756, -0.03243313, 0.11610254, -133.094156],
Expand Down
2 changes: 1 addition & 1 deletion tutorials/preprocessing/70_fnirs_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@
mne.viz.plot_evoked_topo(epochs['Right'].average(picks='hbo'), color='r',
axes=axes, legend=False)

# Tidy the legend.
# Tidy the legend:
leg_lines = [line for line in axes.lines if line.get_c() == 'b'][:1]
leg_lines.append([line for line in axes.lines if line.get_c() == 'r'][0])
fig.legend(leg_lines, ['Left', 'Right'], loc='lower right')