@@ -71,7 +71,8 @@ def _get_comm_manager(*args, **kwargs):
71
71
72
72
import threading
73
73
74
- threading_start = threading .Thread .start
74
+ _threading_Thread_run = threading .Thread .run
75
+ _threading_Thread__init__ = threading .Thread .__init__
75
76
76
77
77
78
class IPythonKernel (KernelBase ):
@@ -158,6 +159,9 @@ def __init__(self, **kwargs):
158
159
159
160
appnope .nope ()
160
161
162
+ self ._new_threads_parent_header = {}
163
+ self ._initialize_thread_hooks ()
164
+
161
165
if hasattr (gc , "callbacks" ):
162
166
# while `gc.callbacks` exists since Python 3.3, pypy does not
163
167
# implement it even as of 3.9.
@@ -356,7 +360,7 @@ def set_sigint_result():
356
360
async def execute_request (self , stream , ident , parent ):
357
361
"""Override for cell output - cell reconciliation."""
358
362
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 )
360
364
await super ().execute_request (stream , ident , parent )
361
365
362
366
async def do_execute (
@@ -724,31 +728,47 @@ def do_clear(self):
724
728
self .shell .reset (False )
725
729
return dict (status = "ok" )
726
730
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
732
734
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."""
736
737
stdout = self ._stdout
737
738
stderr = self ._stderr
739
+ kernel_thread_ident = threading .get_ident ()
740
+ kernel = self
738
741
739
- def start_closure (self : threading .Thread ):
742
+ def run_closure (self : threading .Thread ):
740
743
"""Wrap the `threading.Thread.start` to intercept thread identity.
741
744
742
745
This is needed because there is no "start" hook yet, but there
743
746
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`.
744
750
"""
745
751
746
- threading_start (self )
752
+ try :
753
+ parent = self ._ipykernel_parent_thread_ident # type:ignore[attr-defined]
754
+ except AttributeError :
755
+ return
747
756
for stream in [stdout , stderr ]:
748
757
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]
750
769
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]
752
772
753
773
def _clean_thread_parent_frames (
754
774
self , phase : t .Literal ["start" , "stop" ], info : t .Dict [str , t .Any ]
@@ -768,11 +788,18 @@ def _clean_thread_parent_frames(
768
788
active_threads = {thread .ident for thread in threading .enumerate ()}
769
789
for stream in [self ._stdout , self ._stderr ]:
770
790
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 ()):
773
800
if identity not in active_threads :
774
801
try :
775
- del thread_parents [identity ]
802
+ del thread_to_parent [identity ]
776
803
except KeyError :
777
804
pass
778
805
0 commit comments