Skip to content

Commit 274b804

Browse files
authored
Merge branch 'main' into manually-reset-event
2 parents ed1bc1a + c56a7aa commit 274b804

20 files changed

+494
-463
lines changed

.github/workflows/ci.yml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,29 @@ jobs:
2222
fail-fast: false
2323
matrix:
2424
os: [ubuntu-latest, windows-latest, macos-latest]
25+
qt:
26+
- qt5
27+
- qt6
2528
python-version:
2629
- "3.10"
2730
- "3.11"
2831
- "3.12"
2932
- "3.13"
30-
- "pypy-3.10"
33+
- "3.14"
34+
# 3.14t needs a jupyter-core release
35+
# - "3.14t"
36+
- "pypy-3.11"
37+
exclude:
38+
# qt6 not supported on 3.14 yet
39+
- python-version: "3.14"
40+
qt: qt6
41+
- python-version: "3.13"
42+
qt: qt5
43+
- python-version: "3.12"
44+
qt: qt5
45+
- python-version: "3.11"
46+
qt: qt5
47+
3148
steps:
3249
- name: Checkout
3350
uses: actions/checkout@v5
@@ -36,6 +53,11 @@ jobs:
3653
with:
3754
python-version: ${{ matrix.python-version }}
3855

56+
- name: set qt env
57+
run: |
58+
echo "QT=${{ matrix.qt }}" >> $GITHUB_ENV
59+
shell: bash
60+
3961
- name: Install hatch
4062
run: |
4163
python --version
@@ -51,7 +73,7 @@ jobs:
5173
timeout-minutes: 15
5274
if: ${{ startsWith( matrix.python-version, 'pypy' ) }}
5375
run: |
54-
hatch run test:nowarn
76+
hatch run test:nowarn --ignore=tests/test_debugger.py
5577
5678
- name: Run the tests on Windows
5779
timeout-minutes: 15

.github/workflows/downstream.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ jobs:
5656
test_command: "pytest -vv -raXxs -W default --durations 10 --color=yes -k 'not (test_input_request or signal_kernel_subprocess)'"
5757

5858
ipyparallel:
59-
if: false
6059
runs-on: ubuntu-latest
6160
timeout-minutes: 20
6261
steps:
@@ -90,7 +89,6 @@ jobs:
9089
9190
qtconsole:
9291
runs-on: ubuntu-latest
93-
if: false
9492
timeout-minutes: 20
9593
steps:
9694
- name: Checkout
@@ -119,7 +117,7 @@ jobs:
119117
shell: bash -l {0}
120118
run: |
121119
cd ${GITHUB_WORKSPACE}/../qtconsole
122-
xvfb-run --auto-servernum ${pythonLocation}/bin/python -m pytest -x -vv -s --full-trace --color=yes qtconsole
120+
xvfb-run --auto-servernum ${pythonLocation}/bin/python -m pytest -x -vv -s --full-trace --color=yes qtconsole -k "not test_scroll"
123121
124122
spyder_kernels:
125123
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 44 additions & 95 deletions
Large diffs are not rendered by default.

ipykernel/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import re
66

77
# Version string must appear intact for hatch versioning
8-
__version__ = "7.0.0a2"
8+
__version__ = "7.0.1"
99

1010
# Build up version_info tuple for backwards compatibility
1111
pattern = r"(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)"

ipykernel/displayhook.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def __init__(self, session, pub_socket):
2929

3030
self._parent_header: ContextVar[dict[str, Any]] = ContextVar("parent_header")
3131
self._parent_header.set({})
32+
self._parent_header_global = {}
3233

3334
def get_execution_count(self):
3435
"""This method is replaced in kernelapp"""
@@ -57,11 +58,16 @@ def __call__(self, obj):
5758

5859
@property
5960
def parent_header(self):
60-
return self._parent_header.get()
61+
try:
62+
return self._parent_header.get()
63+
except LookupError:
64+
return self._parent_header_global
6165

6266
def set_parent(self, parent):
6367
"""Set the parent header."""
64-
self._parent_header.set(extract_header(parent))
68+
parent_header = extract_header(parent)
69+
self._parent_header.set(parent_header)
70+
self._parent_header_global = parent_header
6571

6672

6773
class ZMQShellDisplayHook(DisplayHook):
@@ -83,11 +89,16 @@ def __init__(self, *args, **kwargs):
8389

8490
@property
8591
def parent_header(self):
86-
return self._parent_header.get()
92+
try:
93+
return self._parent_header.get()
94+
except LookupError:
95+
return self._parent_header_global
8796

8897
def set_parent(self, parent):
89-
"""Set the parent for outbound messages."""
90-
self._parent_header.set(extract_header(parent))
98+
"""Set the parent header."""
99+
parent_header = extract_header(parent)
100+
self._parent_header.set(parent_header)
101+
self._parent_header_global = parent_header
91102

92103
def start_displayhook(self):
93104
"""Start the display hook."""

ipykernel/eventloops.py

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ def register_integration(*toolkitnames):
4040
You can provide alternative names for the same toolkit.
4141
4242
The decorated function should take a single argument, the IPython kernel
43-
instance, arrange for the event loop to call ``kernel.do_one_iteration()``
44-
at least every ``kernel._poll_interval`` seconds, and start the event loop.
43+
instance, arrange for the event loop to yield the asyncio loop when a
44+
message is received by the main shell zmq stream or at least every
45+
``kernel._poll_interval`` seconds, and start the event loop.
4546
4647
:mod:`ipykernel.eventloops` provides and registers such functions
4748
for a few common event loops.
@@ -68,6 +69,15 @@ def exit_decorator(exit_func):
6869
return decorator
6970

7071

72+
def get_shell_stream(kernel):
73+
# Return the zmq stream that receives messages for the main shell.
74+
if kernel._supports_kernel_subshells:
75+
manager = kernel.shell_channel_thread.manager
76+
socket_pair = manager.get_shell_channel_to_subshell_pair(None)
77+
return socket_pair.to_stream
78+
return kernel.shell_stream
79+
80+
7181
def _notify_stream_qt(kernel):
7282
import operator
7383
from functools import lru_cache
@@ -87,17 +97,20 @@ def exit_loop():
8797
kernel._qt_notifier.setEnabled(False)
8898
kernel.app.qt_event_loop.quit()
8999

90-
def process_stream_events():
100+
def process_stream_events_wrap(shell_stream, *args, **kwargs):
91101
"""fall back to main loop when there's a socket event"""
92102
# call flush to ensure that the stream doesn't lose events
93103
# due to our consuming of the edge-triggered FD
94104
# flush returns the number of events consumed.
95105
# if there were any, wake it up
96-
if kernel.shell_stream.flush(limit=1):
106+
if shell_stream.flush(limit=1):
97107
exit_loop()
98108

109+
shell_stream = get_shell_stream(kernel)
110+
process_stream_events = partial(process_stream_events_wrap, shell_stream)
111+
99112
if not hasattr(kernel, "_qt_notifier"):
100-
fd = kernel.shell_stream.getsockopt(zmq.FD)
113+
fd = shell_stream.getsockopt(zmq.FD)
101114
kernel._qt_notifier = QtCore.QSocketNotifier(
102115
fd, enum_helper("QtCore.QSocketNotifier.Type").Read, kernel.app.qt_event_loop
103116
)
@@ -177,9 +190,11 @@ def loop_wx(kernel):
177190
# Wx uses milliseconds
178191
poll_interval = int(1000 * kernel._poll_interval)
179192

180-
def wake():
193+
shell_stream = get_shell_stream(kernel)
194+
195+
def wake(shell_stream):
181196
"""wake from wx"""
182-
if kernel.shell_stream.flush(limit=1):
197+
if shell_stream.flush(limit=1):
183198
kernel.app.ExitMainLoop()
184199
return
185200

@@ -201,7 +216,7 @@ def on_timer(self, event):
201216
# wx.Timer to defer back to the tornado event loop.
202217
class IPWxApp(wx.App): # type:ignore[misc]
203218
def OnInit(self):
204-
self.frame = TimerFrame(wake)
219+
self.frame = TimerFrame(partial(wake, shell_stream))
205220
self.frame.Show(False)
206221
return True
207222

@@ -248,14 +263,14 @@ def __init__(self, app):
248263

249264
def exit_loop():
250265
"""fall back to main loop"""
251-
app.tk.deletefilehandler(kernel.shell_stream.getsockopt(zmq.FD))
266+
app.tk.deletefilehandler(shell_stream.getsockopt(zmq.FD))
252267
app.quit()
253268
app.destroy()
254269
del kernel.app_wrapper
255270

256-
def process_stream_events(*a, **kw):
271+
def process_stream_events_wrap(shell_stream, *a, **kw):
257272
"""fall back to main loop when there's a socket event"""
258-
if kernel.shell_stream.flush(limit=1):
273+
if shell_stream.flush(limit=1):
259274
exit_loop()
260275

261276
# allow for scheduling exits from the loop in case a timeout needs to
@@ -268,9 +283,10 @@ def _schedule_exit(delay):
268283

269284
# For Tkinter, we create a Tk object and call its withdraw method.
270285
kernel.app_wrapper = BasicAppWrapper(app)
271-
app.tk.createfilehandler(
272-
kernel.shell_stream.getsockopt(zmq.FD), READABLE, process_stream_events
273-
)
286+
shell_stream = get_shell_stream(kernel)
287+
process_stream_events = partial(process_stream_events_wrap, shell_stream)
288+
289+
app.tk.createfilehandler(shell_stream.getsockopt(zmq.FD), READABLE, process_stream_events)
274290
# schedule initial call after start
275291
app.after(0, process_stream_events)
276292

@@ -283,15 +299,19 @@ def _schedule_exit(delay):
283299

284300
nest_asyncio.apply()
285301

286-
doi = kernel.do_one_iteration
287302
# Tk uses milliseconds
288303
poll_interval = int(1000 * kernel._poll_interval)
289304

305+
shell_stream = get_shell_stream(kernel)
306+
290307
class TimedAppWrapper:
291-
def __init__(self, app, func):
308+
def __init__(self, app, shell_stream):
292309
self.app = app
310+
self.shell_stream = shell_stream
293311
self.app.withdraw()
294-
self.func = func
312+
313+
async def func(self):
314+
self.shell_stream.flush(limit=1)
295315

296316
def on_timer(self):
297317
loop = asyncio.get_event_loop()
@@ -305,16 +325,18 @@ def start(self):
305325
self.on_timer() # Call it once to get things going.
306326
self.app.mainloop()
307327

308-
kernel.app_wrapper = TimedAppWrapper(app, doi)
328+
kernel.app_wrapper = TimedAppWrapper(app, shell_stream)
309329
kernel.app_wrapper.start()
310330

311331

312332
@loop_tk.exit
313333
def loop_tk_exit(kernel):
314334
"""Exit the tk loop."""
315335
try:
336+
kernel.app_wrapper.app.quit()
316337
kernel.app_wrapper.app.destroy()
317338
del kernel.app_wrapper
339+
kernel.eventloop = None
318340
except (RuntimeError, AttributeError):
319341
pass
320342

@@ -359,6 +381,7 @@ def loop_cocoa(kernel):
359381
from ._eventloop_macos import mainloop, stop
360382

361383
real_excepthook = sys.excepthook
384+
shell_stream = get_shell_stream(kernel)
362385

363386
def handle_int(etype, value, tb):
364387
"""don't let KeyboardInterrupts look like crashes"""
@@ -377,7 +400,7 @@ def handle_int(etype, value, tb):
377400
# don't let interrupts during mainloop invoke crash_handler:
378401
sys.excepthook = handle_int
379402
mainloop(kernel._poll_interval)
380-
if kernel.shell_stream.flush(limit=1):
403+
if shell_stream.flush(limit=1):
381404
# events to process, return control to kernel
382405
return
383406
except BaseException:
@@ -415,13 +438,14 @@ def loop_asyncio(kernel):
415438
loop._should_close = False # type:ignore[attr-defined]
416439

417440
# pause eventloop when there's an event on a zmq socket
418-
def process_stream_events(stream):
441+
def process_stream_events(shell_stream):
419442
"""fall back to main loop when there's a socket event"""
420-
if stream.flush(limit=1):
443+
if shell_stream.flush(limit=1):
421444
loop.stop()
422445

423-
notifier = partial(process_stream_events, kernel.shell_stream)
424-
loop.add_reader(kernel.shell_stream.getsockopt(zmq.FD), notifier)
446+
shell_stream = get_shell_stream(kernel)
447+
notifier = partial(process_stream_events, shell_stream)
448+
loop.add_reader(shell_stream.getsockopt(zmq.FD), notifier)
425449
loop.call_soon(notifier)
426450

427451
while True:

ipykernel/iostream.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -456,8 +456,6 @@ def __init__(
456456
"parent_header"
457457
)
458458
self._parent_header.set({})
459-
self._thread_to_parent = {}
460-
self._thread_to_parent_header = {}
461459
self._parent_header_global = {}
462460
self._master_pid = os.getpid()
463461
self._flush_pending = False
@@ -512,21 +510,11 @@ def __init__(
512510
@property
513511
def parent_header(self):
514512
try:
515-
# asyncio-specific
513+
# asyncio or thread-specific
516514
return self._parent_header.get()
517515
except LookupError:
518-
try:
519-
# thread-specific
520-
identity = threading.current_thread().ident
521-
# retrieve the outermost (oldest ancestor,
522-
# discounting the kernel thread) thread identity
523-
while identity in self._thread_to_parent:
524-
identity = self._thread_to_parent[identity]
525-
# use the header of the oldest ancestor
526-
return self._thread_to_parent_header[identity]
527-
except KeyError:
528-
# global (fallback)
529-
return self._parent_header_global
516+
# global (fallback)
517+
return self._parent_header_global
530518

531519
@parent_header.setter
532520
def parent_header(self, value):

0 commit comments

Comments
 (0)