Skip to content

Commit e1258de

Browse files
committed
Use thread ancestry hierarchy to support nested threads
Lint
1 parent 5956899 commit e1258de

File tree

2 files changed

+53
-19
lines changed

2 files changed

+53
-19
lines changed

ipykernel/iostream.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,8 @@ def __init__(
451451
"parent_header"
452452
)
453453
self._parent_header.set({})
454-
self._thread_parents = {}
454+
self._thread_to_parent = {}
455+
self._thread_to_parent_header = {}
455456
self._parent_header_global = {}
456457
self._master_pid = os.getpid()
457458
self._flush_pending = False
@@ -509,7 +510,13 @@ def parent_header(self):
509510
except LookupError:
510511
try:
511512
# thread-specific
512-
return self._thread_parents[threading.current_thread().ident]
513+
identity = threading.current_thread().ident
514+
# retrieve the outermost (oldest ancestor,
515+
# discounting the kernel thread) thread identity
516+
while identity in self._thread_to_parent:
517+
identity = self._thread_to_parent[identity]
518+
# use the header of the oldest ancestor
519+
return self._thread_to_parent_header[identity]
513520
except KeyError:
514521
# global (fallback)
515522
return self._parent_header_global

ipykernel/ipkernel.py

+44-17
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def _get_comm_manager(*args, **kwargs):
7171

7272
import threading
7373

74-
threading_start = threading.Thread.start
74+
_threading_Thread_run = threading.Thread.run
75+
_threading_Thread__init__ = threading.Thread.__init__
7576

7677

7778
class IPythonKernel(KernelBase):
@@ -158,6 +159,9 @@ def __init__(self, **kwargs):
158159

159160
appnope.nope()
160161

162+
self._new_threads_parent_header = {}
163+
self._initialize_thread_hooks()
164+
161165
if hasattr(gc, "callbacks"):
162166
# while `gc.callbacks` exists since Python 3.3, pypy does not
163167
# implement it even as of 3.9.
@@ -356,7 +360,7 @@ def set_sigint_result():
356360
async def execute_request(self, stream, ident, parent):
357361
"""Override for cell output - cell reconciliation."""
358362
parent_header = extract_header(parent)
359-
self._associate_identity_of_new_threads_with(parent_header)
363+
self._associate_new_top_level_threads_with(parent_header)
360364
await super().execute_request(stream, ident, parent)
361365

362366
async def do_execute(
@@ -724,31 +728,47 @@ def do_clear(self):
724728
self.shell.reset(False)
725729
return dict(status="ok")
726730

727-
def _associate_identity_of_new_threads_with(self, parent_header):
728-
"""Intercept the identity of any thread started after this method finished,
729-
730-
and associate the thread's output with the parent header frame, which allows
731-
to direct the outputs to the cell which started the thread.
731+
def _associate_new_top_level_threads_with(self, parent_header):
732+
"""Store the parent header to associate it with new top-level threads"""
733+
self._new_threads_parent_header = parent_header
732734

733-
This is a no-op if the `self._stdout` and `self._stderr` are not
734-
sub-classes of `OutStream`.
735-
"""
735+
def _initialize_thread_hooks(self):
736+
"""Store thread hierarchy and thread-parent_header associations."""
736737
stdout = self._stdout
737738
stderr = self._stderr
739+
kernel_thread_ident = threading.get_ident()
740+
kernel = self
738741

739-
def start_closure(self: threading.Thread):
742+
def run_closure(self: threading.Thread):
740743
"""Wrap the `threading.Thread.start` to intercept thread identity.
741744
742745
This is needed because there is no "start" hook yet, but there
743746
might be one in the future: https://bugs.python.org/issue14073
747+
748+
This is a no-op if the `self._stdout` and `self._stderr` are not
749+
sub-classes of `OutStream`.
744750
"""
745751

746-
threading_start(self)
752+
try:
753+
parent = self._ipykernel_parent_thread_ident # type:ignore[attr-defined]
754+
except AttributeError:
755+
return
747756
for stream in [stdout, stderr]:
748757
if isinstance(stream, OutStream):
749-
stream._thread_parents[self.ident] = parent_header
758+
if parent == kernel_thread_ident:
759+
stream._thread_to_parent_header[
760+
self.ident
761+
] = kernel._new_threads_parent_header
762+
else:
763+
stream._thread_to_parent[self.ident] = parent
764+
_threading_Thread_run(self)
765+
766+
def init_closure(self: threading.Thread, *args, **kwargs):
767+
_threading_Thread__init__(self, *args, **kwargs)
768+
self._ipykernel_parent_thread_ident = threading.get_ident() # type:ignore[attr-defined]
750769

751-
threading.Thread.start = start_closure # type:ignore[method-assign]
770+
threading.Thread.__init__ = init_closure # type:ignore[method-assign]
771+
threading.Thread.run = run_closure # type:ignore[method-assign]
752772

753773
def _clean_thread_parent_frames(
754774
self, phase: t.Literal["start", "stop"], info: t.Dict[str, t.Any]
@@ -768,11 +788,18 @@ def _clean_thread_parent_frames(
768788
active_threads = {thread.ident for thread in threading.enumerate()}
769789
for stream in [self._stdout, self._stderr]:
770790
if isinstance(stream, OutStream):
771-
thread_parents = stream._thread_parents
772-
for identity in list(thread_parents.keys()):
791+
thread_to_parent_header = stream._thread_to_parent_header
792+
for identity in list(thread_to_parent_header.keys()):
793+
if identity not in active_threads:
794+
try:
795+
del thread_to_parent_header[identity]
796+
except KeyError:
797+
pass
798+
thread_to_parent = stream._thread_to_parent
799+
for identity in list(thread_to_parent.keys()):
773800
if identity not in active_threads:
774801
try:
775-
del thread_parents[identity]
802+
del thread_to_parent[identity]
776803
except KeyError:
777804
pass
778805

0 commit comments

Comments
 (0)