Skip to content

Commit 4c6d56c

Browse files
committed
test that matplotlib event loop integration is responsive
1 parent c56a7aa commit 4c6d56c

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

ipykernel/eventloops.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def loop_gtk3_exit(kernel):
373373
kernel._gtk.stop()
374374

375375

376-
@register_integration("osx")
376+
@register_integration("osx", "macosx")
377377
def loop_cocoa(kernel):
378378
"""Start the kernel, coordinating with the Cocoa CFRunLoop event loop
379379
via the matplotlib MacOSX backend.

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import no_type_check
55
from unittest.mock import MagicMock
66

7+
import pytest
78
import pytest_asyncio
89
import zmq
910
from jupyter_client.session import Session
@@ -20,6 +21,7 @@
2021
# Windows
2122
resource = None # type:ignore
2223

24+
from .utils import new_kernel
2325

2426
# Handle resource limit
2527
# Ensure a minimal soft limit of DEFAULT_SOFT if the current hard limit is at least that much.
@@ -158,3 +160,9 @@ def ipkernel():
158160
yield kernel
159161
kernel.destroy()
160162
ZMQInteractiveShell.clear_instance()
163+
164+
165+
@pytest.fixture
166+
def kc():
167+
with new_kernel() as kc:
168+
yield kc
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import sys
2+
import time
3+
4+
import pytest
5+
from jupyter_client.blocking.client import BlockingKernelClient
6+
7+
from .test_eventloop import qt_guis_avail
8+
from .utils import assemble_output
9+
10+
guis = []
11+
if not sys.platform.startswith("tk"):
12+
guis.append("tk")
13+
if qt_guis_avail:
14+
guis.append("qt")
15+
if sys.platform == "darwin":
16+
guis.append("osx")
17+
18+
backends = {
19+
"tk": "tkagg",
20+
"qt": "qtagg",
21+
"osx": "macosx",
22+
}
23+
24+
25+
def execute(
26+
kc: BlockingKernelClient,
27+
code: str,
28+
timeout=10,
29+
):
30+
msg_id = kc.execute(code)
31+
stdout, stderr = assemble_output(kc.get_iopub_msg, timeout=timeout, parent_msg_id=msg_id)
32+
assert not stderr.strip()
33+
return stdout.strip(), stderr.strip()
34+
35+
36+
@pytest.mark.parametrize("gui", guis)
37+
def test_matplotlib_gui(kc, gui):
38+
"""Make sure matplotlib activates and its eventloop runs while the kernel is also responsive"""
39+
pytest.importorskip("matplotlib", reason="this test requires matplotlib")
40+
stdout, stderr = execute(kc, f"%matplotlib {gui}")
41+
assert not stderr
42+
execute(
43+
kc,
44+
"""
45+
from concurrent.futures import Future
46+
import matplotlib as mpl
47+
import matplotlib.pyplot as plt
48+
""",
49+
)
50+
stdout, _ = execute(kc, "print(mpl.get_backend())")
51+
assert stdout == backends[gui]
52+
execute(
53+
kc,
54+
"""
55+
fig, ax = plt.subplots()
56+
timer = fig.canvas.new_timer(interval=10)
57+
f = Future()
58+
59+
call_count = 0
60+
def add_call():
61+
global call_count
62+
call_count += 1
63+
if not f.done():
64+
f.set_result(None)
65+
66+
timer.add_callback(add_call)
67+
timer.start()
68+
""",
69+
)
70+
# wait for the first call (up to 10 seconds)
71+
for _ in range(100):
72+
stdout, _ = execute(kc, "print(f.done())")
73+
if stdout.strip() == "True":
74+
break
75+
if stdout == "False":
76+
time.sleep(0.1)
77+
else:
78+
pytest.fail(f"Unexpected output {stdout}")
79+
time.sleep(0.25)
80+
stdout, _ = execute(kc, "print(call_count)")
81+
call_count = int(stdout)
82+
assert call_count > 0
83+
time.sleep(0.25)
84+
stdout, _ = execute(kc, "timer.stop()\nprint(call_count)")
85+
call_count_2 = int(stdout)
86+
assert call_count_2 > call_count
87+
stdout, _ = execute(kc, "print(call_count)")
88+
call_count_3 = int(stdout)
89+
assert call_count_3 <= call_count_2 + 5

0 commit comments

Comments
 (0)