@@ -71,7 +71,8 @@ def _get_comm_manager(*args, **kwargs):
7171
7272import threading
7373
74- threading_start = threading .Thread .start
74+ _threading_Thread_run = threading .Thread .run
75+ _threading_Thread__init__ = threading .Thread .__init__
7576
7677
7778class 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