1010import sys
1111import threading
1212import typing as t
13+ import warnings
1314from contextlib import contextmanager
1415from functools import partial
1516
1617import comm
1718from IPython .core import release
1819from IPython .utils .tokenutil import line_at_cursor , token_at_cursor
1920from traitlets import Any , Bool , HasTraits , Instance , List , Type , default , observe , observe_compat
21+ from traitlets .utils .importstring import import_item
2022from zmq .eventloop .zmqstream import ZMQStream
2123
2224from .comm .comm import BaseComm
@@ -74,9 +76,15 @@ class IPythonKernel(KernelBase):
7476 shell = Instance ("IPython.core.interactiveshell.InteractiveShellABC" , allow_none = True )
7577 shell_class = Type (ZMQInteractiveShell )
7678
77- # use fully-qualified name to ensure lazy import and prevent the issue from
78- # https://github.com/ipython/ipykernel/issues/1198
79- debugger_class = Type ("ipykernel.debugger.Debugger" )
79+ # Do not use a Type() trait: traitlets resolves (and thus imports) a Type
80+ # trait's string default as soon as the owning HasTraits instance is
81+ # created, which would force the expensive debugpy import on every
82+ # kernel startup. Resolved lazily instead, see the `debugger` property.
83+ debugger_class_name = "ipykernel.debugger.Debugger"
84+
85+ # Set by the deprecated `debugger_class` setter, takes precedence over
86+ # `debugger_class_name` when not None.
87+ _debugger_class : type | None = None
8088
8189 compiler_class = Type (XCachingCompiler )
8290
@@ -117,24 +125,14 @@ def __init__(self, **kwargs):
117125 """Initialize the kernel."""
118126 super ().__init__ (** kwargs )
119127
120- from .debugger import _is_debugpy_available
121-
122128 self ._kernel_modules = [
123129 m .__file__ for m in sys .modules .copy ().values () if hasattr (m , "__file__" ) and m .__file__
124130 ]
125131
126- # Initialize the Debugger
127- if _is_debugpy_available :
128- self .debugger = self .debugger_class (
129- self .log ,
130- self .debugpy_stream ,
131- self ._publish_debug_event ,
132- self .debug_shell_socket ,
133- self .session ,
134- self ._kernel_modules ,
135- self .debug_just_my_code ,
136- self .filter_internal_frames ,
137- )
132+ # The debugger itself (and the debugpy import it requires) is
133+ # created lazily on first use, see the `debugger` property below.
134+ self ._debugger = None
135+ self ._debugger_init_attempted = False
138136
139137 # Initialize the InteractiveShell subclass
140138 self .shell = self .shell_class .instance (
@@ -216,10 +214,101 @@ def __init__(self, **kwargs):
216214 "file_extension" : ".py" ,
217215 }
218216
219- def dispatch_debugpy (self , msg ):
220- from .debugger import _is_debugpy_available
217+ @property
218+ def debugger_class (self ):
219+ """Deprecated, use :attr:`debugger_class_name` instead.
220+
221+ .. deprecated:: 7.4
222+ Accessing this attribute imports the debugger module (and thus
223+ debugpy), which is exactly what ``debugger_class_name`` exists to
224+ avoid.
225+ """
226+ warnings .warn (
227+ "IPythonKernel.debugger_class is deprecated in ipykernel 7.4,"
228+ " use IPythonKernel.debugger_class_name instead." ,
229+ DeprecationWarning ,
230+ stacklevel = 2 ,
231+ )
232+ return self ._resolve_debugger_class ()
233+
234+ @debugger_class .setter
235+ def debugger_class (self , value ):
236+ warnings .warn (
237+ "IPythonKernel.debugger_class is deprecated in ipykernel 7.4,"
238+ " set IPythonKernel.debugger_class_name to the fully qualified"
239+ " name of the class instead." ,
240+ DeprecationWarning ,
241+ stacklevel = 2 ,
242+ )
243+ self ._debugger_class = value
244+
245+ def _resolve_debugger_class (self ):
246+ """Return the class to instantiate the debugger from.
247+
248+ Honors the deprecated ``debugger_class`` attribute, whether it was set
249+ on an instance or overridden by a subclass, before falling back to
250+ ``debugger_class_name``.
251+ """
252+ if self ._debugger_class is not None :
253+ return self ._debugger_class
254+ for klass in type (self ).__mro__ :
255+ if klass is IPythonKernel :
256+ break
257+ if "debugger_class" in klass .__dict__ :
258+ warnings .warn (
259+ f"{ klass .__module__ } .{ klass .__qualname__ } overrides"
260+ " `debugger_class`, which is deprecated in ipykernel 7.4;"
261+ " override `debugger_class_name` with the fully qualified"
262+ " name of the class instead." ,
263+ DeprecationWarning ,
264+ stacklevel = 3 ,
265+ )
266+ # The subclass attribute shadows the property defined here, so
267+ # this resolves the override (a plain class or a Type trait).
268+ return self .debugger_class
269+ return import_item (self .debugger_class_name )
270+
271+ @property
272+ def debugger (self ):
273+ """The debugger instance, created lazily on first use.
221274
222- if _is_debugpy_available :
275+ Importing debugpy is expensive, so we avoid it until a debug
276+ request actually comes in.
277+ """
278+ if self ._debugger is None and not self ._debugger_init_attempted :
279+ self ._debugger_init_attempted = True
280+ from .debugger import _is_debugpy_available
281+
282+ if _is_debugpy_available :
283+ debugger_class = self ._resolve_debugger_class ()
284+ self ._debugger = debugger_class (
285+ self .log ,
286+ self .debugpy_stream ,
287+ self ._publish_debug_event ,
288+ self .debug_shell_socket ,
289+ self .session ,
290+ self ._kernel_modules ,
291+ self .debug_just_my_code ,
292+ self .filter_internal_frames ,
293+ )
294+ # Mirrors the guard that used to live in `start()`: without a
295+ # debugpy stream or a control thread there is nothing to poll
296+ # on, and no loop to poll from.
297+ if self .debugpy_stream is not None and self .control_thread is not None :
298+ asyncio .run_coroutine_threadsafe (
299+ self .poll_stopped_queue (), self .control_thread .io_loop .asyncio_loop
300+ )
301+ return self ._debugger
302+
303+ @debugger .setter
304+ def debugger (self , value ):
305+ # `debugger` used to be a plain instance attribute assigned in
306+ # __init__; keep it writable for subclasses that replace it.
307+ self ._debugger = value
308+ self ._debugger_init_attempted = True
309+
310+ def dispatch_debugpy (self , msg ):
311+ if self .debugger is not None :
223312 # The first frame is the socket id, we can drop it
224313 frame = msg [1 ].bytes .decode ("utf-8" )
225314 self .log .debug ("Debugpy received: %s" , frame )
@@ -245,10 +334,6 @@ def start(self):
245334 else :
246335 self .debugpy_stream .on_recv (self .dispatch_debugpy , copy = False )
247336 super ().start ()
248- if self .debugpy_stream :
249- asyncio .run_coroutine_threadsafe (
250- self .poll_stopped_queue (), self .control_thread .io_loop .asyncio_loop
251- )
252337
253338 def set_parent (self , ident , parent , channel = "shell" ):
254339 """Overridden from parent to tell the display hook and output streams
@@ -535,9 +620,7 @@ def do_complete(self, code, cursor_pos):
535620
536621 async def do_debug_request (self , msg ):
537622 """Handle a debug request."""
538- from .debugger import _is_debugpy_available
539-
540- if _is_debugpy_available :
623+ if self .debugger is not None :
541624 return await self .debugger .process_request (msg )
542625 return None
543626
0 commit comments