From dccf9c3be08f061b64cbd32886e35f349a59b897 Mon Sep 17 00:00:00 2001 From: Daniel Falbel Date: Thu, 16 Oct 2025 14:49:52 -0300 Subject: [PATCH 1/6] add regression test --- tests/test_start_kernel.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_start_kernel.py b/tests/test_start_kernel.py index b9276e33b..16ffa101a 100644 --- a/tests/test_start_kernel.py +++ b/tests/test_start_kernel.py @@ -50,6 +50,36 @@ def test_ipython_start_kernel_userns(): assert EXPECTED in text +def test_start_kernel_background_thread(): + + cmd = dedent( + """ + import threading + from ipykernel.kernelapp import launch_new_instance + + def launch(): + launch_new_instance() + + thread = threading.Thread(target=launch) + thread.start() + thread.join() + """ + ) + + with setup_kernel(cmd) as client: + client.execute("a = 1") + msg = client.get_shell_msg(timeout=TIMEOUT) + content = msg["content"] + assert content["status"] == "ok" + + client.inspect("a") + msg = client.get_shell_msg(timeout=TIMEOUT) + content = msg["content"] + assert content["found"] + text = content["data"]["text/plain"] + assert "1" in text + + @pytest.mark.flaky(max_runs=3) def test_ipython_start_kernel_no_userns(): # Issue #4188 - user_ns should be passed to shell as None, not {} From fd9591d1f9ff79d38bb13723c206c0d531eb8434 Mon Sep 17 00:00:00 2001 From: Daniel Falbel Date: Thu, 16 Oct 2025 14:50:43 -0300 Subject: [PATCH 2/6] store and use a reference to the kernel launching thread instead of assuming it's always the main thread --- ipykernel/ipkernel.py | 2 +- ipykernel/kernelbase.py | 6 +++--- ipykernel/shellchannel.py | 4 ++++ ipykernel/subshell_manager.py | 8 ++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/ipykernel/ipkernel.py b/ipykernel/ipkernel.py index 3e3927cc5..3160c6536 100644 --- a/ipykernel/ipkernel.py +++ b/ipykernel/ipkernel.py @@ -452,7 +452,7 @@ async def run_cell(*args, **kwargs): cm = ( self._cancel_on_sigint - if threading.current_thread() == threading.main_thread() + if threading.current_thread() == self.shell_channel_thread.parent_thread else self._dummy_context_manager ) with cm(coro_future): diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index b815b934b..bde03e686 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -677,19 +677,19 @@ async def shell_main(self, subshell_id: str | None, msg): """Handler of shell messages for a single subshell""" if self._supports_kernel_subshells: if subshell_id is None: - assert threading.current_thread() == threading.main_thread() + assert threading.current_thread() == self.shell_channel_thread.parent_thread asyncio_lock = self._main_asyncio_lock else: assert threading.current_thread() not in ( self.shell_channel_thread, - threading.main_thread(), + self.shell_channel_thread.parent_thread, ) asyncio_lock = self.shell_channel_thread.manager.get_subshell_asyncio_lock( subshell_id ) else: assert subshell_id is None - assert threading.current_thread() == threading.main_thread() + assert threading.current_thread() == self.shell_channel_thread.parent_thread asyncio_lock = self._main_asyncio_lock # Whilst executing a shell message, do not accept any other shell messages on the diff --git a/ipykernel/shellchannel.py b/ipykernel/shellchannel.py index 8335e8887..c40b5a0c4 100644 --- a/ipykernel/shellchannel.py +++ b/ipykernel/shellchannel.py @@ -4,6 +4,7 @@ import asyncio from typing import Any +from threading import current_thread import zmq @@ -28,6 +29,8 @@ def __init__( self._manager: SubshellManager | None = None self._zmq_context = context # Avoid use of self._context self._shell_socket = shell_socket + # Record the parent thread - the thread that started the app (usually the main thread) + self.parent_thread = current_thread() self.asyncio_lock = asyncio.Lock() @@ -35,6 +38,7 @@ def __init__( def manager(self) -> SubshellManager: # Lazy initialisation. if self._manager is None: + assert current_thread() == self.parent_thread self._manager = SubshellManager( self._zmq_context, self.io_loop, diff --git a/ipykernel/subshell_manager.py b/ipykernel/subshell_manager.py index 7f65183dd..3e4e0b6bc 100644 --- a/ipykernel/subshell_manager.py +++ b/ipykernel/subshell_manager.py @@ -7,7 +7,7 @@ import typing as t import uuid from functools import partial -from threading import Lock, current_thread, main_thread +from threading import Lock, current_thread import zmq from tornado.ioloop import IOLoop @@ -41,7 +41,7 @@ def __init__( shell_socket: zmq.Socket[t.Any], ): """Initialize the subshell manager.""" - assert current_thread() == main_thread() + self._parent_thread = current_thread() self._context: zmq.Context[t.Any] = context self._shell_channel_io_loop = shell_channel_io_loop @@ -127,7 +127,7 @@ def set_on_recv_callback(self, on_recv_callback): """Set the callback used by the main shell and all subshells to receive messages sent from the shell channel thread. """ - assert current_thread() == main_thread() + assert current_thread() == self._parent_thread self._on_recv_callback = on_recv_callback self._shell_channel_to_main.on_recv(IOLoop.current(), partial(self._on_recv_callback, None)) @@ -144,7 +144,7 @@ def subshell_id_from_thread_id(self, thread_id: int) -> str | None: Only used by %subshell magic so does not have to be fast/cached. """ with self._lock_cache: - if thread_id == main_thread().ident: + if thread_id == self._parent_thread.ident: return None for id, subshell in self._cache.items(): if subshell.ident == thread_id: From d90eb28e6972b32d73d4468390d37686c07f9a36 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:55:23 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ipykernel/shellchannel.py | 2 +- tests/test_start_kernel.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ipykernel/shellchannel.py b/ipykernel/shellchannel.py index c40b5a0c4..8205840d1 100644 --- a/ipykernel/shellchannel.py +++ b/ipykernel/shellchannel.py @@ -3,8 +3,8 @@ from __future__ import annotations import asyncio -from typing import Any from threading import current_thread +from typing import Any import zmq diff --git a/tests/test_start_kernel.py b/tests/test_start_kernel.py index 16ffa101a..c0eeede8c 100644 --- a/tests/test_start_kernel.py +++ b/tests/test_start_kernel.py @@ -51,7 +51,6 @@ def test_ipython_start_kernel_userns(): def test_start_kernel_background_thread(): - cmd = dedent( """ import threading @@ -59,7 +58,7 @@ def test_start_kernel_background_thread(): def launch(): launch_new_instance() - + thread = threading.Thread(target=launch) thread.start() thread.join() @@ -71,14 +70,14 @@ def launch(): msg = client.get_shell_msg(timeout=TIMEOUT) content = msg["content"] assert content["status"] == "ok" - + client.inspect("a") msg = client.get_shell_msg(timeout=TIMEOUT) content = msg["content"] assert content["found"] text = content["data"]["text/plain"] assert "1" in text - + @pytest.mark.flaky(max_runs=3) def test_ipython_start_kernel_no_userns(): From d61527ed9ff4410d8649357b771bb75deed072f9 Mon Sep 17 00:00:00 2001 From: Daniel Falbel Date: Thu, 16 Oct 2025 15:01:31 -0300 Subject: [PATCH 4/6] mark flake as the others --- tests/test_start_kernel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_start_kernel.py b/tests/test_start_kernel.py index c0eeede8c..c77b79db4 100644 --- a/tests/test_start_kernel.py +++ b/tests/test_start_kernel.py @@ -50,6 +50,7 @@ def test_ipython_start_kernel_userns(): assert EXPECTED in text +@pytest.mark.flaky(max_runs=3) def test_start_kernel_background_thread(): cmd = dedent( """ From e59fce2a8efe98d1774b01efa8598f7bf87b9287 Mon Sep 17 00:00:00 2001 From: Daniel Falbel Date: Thu, 16 Oct 2025 17:18:32 -0300 Subject: [PATCH 5/6] fix CI issue --- tests/test_start_kernel.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_start_kernel.py b/tests/test_start_kernel.py index c77b79db4..756474d8c 100644 --- a/tests/test_start_kernel.py +++ b/tests/test_start_kernel.py @@ -50,14 +50,18 @@ def test_ipython_start_kernel_userns(): assert EXPECTED in text -@pytest.mark.flaky(max_runs=3) def test_start_kernel_background_thread(): cmd = dedent( """ import threading + import asyncio from ipykernel.kernelapp import launch_new_instance def launch(): + # Threads don't always have a default event loop so we need to + # create and set a default + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) launch_new_instance() thread = threading.Thread(target=launch) From 76ca4588e0a7d6346d691a4454215b32fbead0f6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:18:58 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_start_kernel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_start_kernel.py b/tests/test_start_kernel.py index 756474d8c..6e373ccff 100644 --- a/tests/test_start_kernel.py +++ b/tests/test_start_kernel.py @@ -58,7 +58,7 @@ def test_start_kernel_background_thread(): from ipykernel.kernelapp import launch_new_instance def launch(): - # Threads don't always have a default event loop so we need to + # Threads don't always have a default event loop so we need to # create and set a default loop = asyncio.new_event_loop() asyncio.set_event_loop(loop)