Skip to content
Open
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
1 change: 1 addition & 0 deletions doc/changes/dev/14077.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix Qt browser close messages from :meth:`mne.io.Raw.plot` appearing in the wrong Jupyter notebook cell, by `Mingjian He`_.
9 changes: 9 additions & 0 deletions mne/viz/_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from abc import ABC, abstractmethod
from collections import OrderedDict
from contextlib import contextmanager
from contextvars import copy_context
from copy import deepcopy
from itertools import cycle

Expand Down Expand Up @@ -56,6 +57,7 @@ def __init__(self, **kwargs):
from ..preprocessing import ICA

self.backend_name = None
self._close_context = copy_context()

self._data = None
self._times = None
Expand Down Expand Up @@ -490,6 +492,13 @@ def _redraw(self, update_data=True, annotations=False):

def _close(self, event=None):
"""Handle close events (via keypress or window [x])."""
# As specified by PEP 567: https://peps.python.org/pep-0567/#asyncio
# we explicitly retain the python Context used to create the figure
# in order to route stdout on close within IPykernel. See gh #14077
self._close_context.run(self._close_impl, event)

def _close_impl(self, event=None):
"""Handle close events in the context that created the browser."""
from matplotlib.pyplot import close

logger.debug(f"Closing {self.mne.instance_type} browser...")
Expand Down
30 changes: 29 additions & 1 deletion mne/viz/tests/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

from contextvars import ContextVar

import matplotlib.pyplot as plt
import numpy as np
import pytest

from mne import create_info
from mne.io import RawArray
from mne.viz._figure import _get_browser
from mne.viz._figure import _get_browser, use_browser_backend


def test_browse_figure_constructor():
Expand All @@ -23,3 +26,28 @@ def test_browse_figure_requires_two_timepoints():
assert len(raw.times) == 1
with pytest.raises(ValueError, match="at least two time points"):
_get_browser(show=False, block=False, inst=raw)


def test_browse_figure_close_context():
"""Test that deferred browser close uses its creation context."""
marker = ContextVar("browser_context", default=None)
created_token = marker.set("created")
try:
info = create_info(ch_names=["CH1"], sfreq=100.0, ch_types="eeg")
raw = RawArray(np.zeros((1, 100)), info)
with use_browser_backend("matplotlib"):
fig = raw.plot(show=False)

current_token = marker.set("current")
observed = []
close_impl = fig._close_impl
fig._close_impl = lambda event=None: observed.append(marker.get())
try:
fig._close()
finally:
fig._close_impl = close_impl
plt.close(fig)
marker.reset(current_token)
assert observed == ["created"]
finally:
marker.reset(created_token)
Loading