Skip to content

Commit 6b276e3

Browse files
committed
Step in/step over support for IPython. Fixes #869
1 parent a294092 commit 6b276e3

16 files changed

+6258
-4701
lines changed

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_additional_thread_info_regular.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class PyDBAdditionalThreadInfo(object):
5353
# of the last request for a given thread and pydev_smart_parent_offset/pydev_smart_child_offset relies on it).
5454
'pydev_smart_step_into_variants',
5555
'target_id_to_smart_step_into_variant',
56+
57+
'pydev_use_scoped_step_frame',
5658
]
5759
# ENDIF
5860

@@ -90,6 +92,18 @@ def __init__(self):
9092
self.pydev_smart_step_into_variants = ()
9193
self.target_id_to_smart_step_into_variant = {}
9294

95+
# Flag to indicate ipython use-case where each line will be executed as a call/line/return
96+
# in a new new frame but in practice we want to consider each new frame as if it was all
97+
# part of the same frame.
98+
#
99+
# In practice this means that a step over shouldn't revert to a step in and we need some
100+
# special logic to know when we should stop in a step over as we need to consider 2
101+
# different frames as being equal if they're logically the continuation of a frame
102+
# being executed by ipython line by line.
103+
#
104+
# See: https://github.com/microsoft/debugpy/issues/869#issuecomment-1132141003
105+
self.pydev_use_scoped_step_frame = False
106+
93107
def get_topmost_frame(self, thread):
94108
'''
95109
Gets the topmost frame for the given thread. Note that it may be None

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,6 @@ def as_int_in_env(env_key, default):
255255
# If not specified, uses default heuristic to determine if it should be loaded.
256256
USE_CYTHON_FLAG = os.getenv('PYDEVD_USE_CYTHON')
257257

258-
# Use to disable loading the lib to set tracing to all threads (default is using heuristics based on where we're running).
259-
LOAD_NATIVE_LIB_FLAG = os.getenv('PYDEVD_LOAD_NATIVE_LIB', '').lower()
260-
261-
LOG_TIME = os.getenv('PYDEVD_LOG_TIME', 'true').lower() in ENV_TRUE_LOWER_VALUES
262-
263258
if USE_CYTHON_FLAG is not None:
264259
USE_CYTHON_FLAG = USE_CYTHON_FLAG.lower()
265260
if USE_CYTHON_FLAG not in ENV_TRUE_LOWER_VALUES and USE_CYTHON_FLAG not in ENV_FALSE_LOWER_VALUES:
@@ -270,6 +265,26 @@ def as_int_in_env(env_key, default):
270265
if not CYTHON_SUPPORTED:
271266
USE_CYTHON_FLAG = 'no'
272267

268+
# If true in env, forces frame eval to be used (raises error if not available).
269+
# If false in env, disables it.
270+
# If not specified, uses default heuristic to determine if it should be loaded.
271+
PYDEVD_USE_FRAME_EVAL = os.getenv('PYDEVD_USE_FRAME_EVAL', '').lower()
272+
273+
PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING = is_true_in_env('PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING')
274+
275+
# If specified in PYDEVD_IPYTHON_CONTEXT it must be a string with the basename
276+
# and then the name of 2 methods in which the evaluate is done.
277+
PYDEVD_IPYTHON_CONTEXT = ('interactiveshell.py', 'run_code', 'run_ast_nodes')
278+
_ipython_ctx = os.getenv('PYDEVD_IPYTHON_CONTEXT')
279+
if _ipython_ctx:
280+
PYDEVD_IPYTHON_CONTEXT = tuple(x.strip() for x in _ipython_ctx.split(','))
281+
assert len(PYDEVD_IPYTHON_CONTEXT) == 3, 'Invalid PYDEVD_IPYTHON_CONTEXT: %s' % (_ipython_ctx,)
282+
283+
# Use to disable loading the lib to set tracing to all threads (default is using heuristics based on where we're running).
284+
LOAD_NATIVE_LIB_FLAG = os.getenv('PYDEVD_LOAD_NATIVE_LIB', '').lower()
285+
286+
LOG_TIME = os.getenv('PYDEVD_LOG_TIME', 'true').lower() in ENV_TRUE_LOWER_VALUES
287+
273288
SHOW_COMPILE_CYTHON_COMMAND_LINE = is_true_in_env('PYDEVD_SHOW_COMPILE_CYTHON_COMMAND_LINE')
274289

275290
LOAD_VALUES_ASYNC = is_true_in_env('PYDEVD_LOAD_VALUES_ASYNC')

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.c

Lines changed: 5686 additions & 4641 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ cdef class PyDBAdditionalThreadInfo:
2424
cdef public int pydev_smart_child_offset
2525
cdef public tuple pydev_smart_step_into_variants
2626
cdef public dict target_id_to_smart_step_into_variant
27+
cdef public bint pydev_use_scoped_step_frame

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.pyx

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ cdef class PyDBAdditionalThreadInfo:
5959
# # of the last request for a given thread and pydev_smart_parent_offset/pydev_smart_child_offset relies on it).
6060
# 'pydev_smart_step_into_variants',
6161
# 'target_id_to_smart_step_into_variant',
62+
#
63+
# 'pydev_use_scoped_step_frame',
6264
# ]
6365
# ENDIF
6466

@@ -96,6 +98,18 @@ cdef class PyDBAdditionalThreadInfo:
9698
self.pydev_smart_step_into_variants = ()
9799
self.target_id_to_smart_step_into_variant = {}
98100

101+
# Flag to indicate ipython use-case where each line will be executed as a call/line/return
102+
# in a new new frame but in practice we want to consider each new frame as if it was all
103+
# part of the same frame.
104+
#
105+
# In practice this means that a step over shouldn't revert to a step in and we need some
106+
# special logic to know when we should stop in a step over as we need to consider 2
107+
# different frames as being equal if they're logically the continuation of a frame
108+
# being executed by ipython line by line.
109+
#
110+
# See: https://github.com/microsoft/debugpy/issues/869#issuecomment-1132141003
111+
self.pydev_use_scoped_step_frame = False
112+
99113
def get_topmost_frame(self, thread):
100114
'''
101115
Gets the topmost frame for the given thread. Note that it may be None
@@ -150,7 +164,7 @@ import re
150164
from _pydev_bundle import pydev_log
151165
from _pydevd_bundle import pydevd_dont_trace
152166
from _pydevd_bundle.pydevd_constants import (RETURN_VALUES_DICT, NO_FTRACE,
153-
EXCEPTION_TYPE_HANDLED, EXCEPTION_TYPE_USER_UNHANDLED)
167+
EXCEPTION_TYPE_HANDLED, EXCEPTION_TYPE_USER_UNHANDLED, PYDEVD_IPYTHON_CONTEXT)
154168
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raised, remove_exception_from_frame, ignore_exception_trace
155169
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
156170
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
@@ -657,6 +671,31 @@ cdef class PyDBFrame:
657671

658672
return f
659673

674+
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
675+
cdef _is_same_frame(self, target_frame, current_frame):
676+
cdef PyDBAdditionalThreadInfo info;
677+
# ELSE
678+
# def _is_same_frame(self, target_frame, current_frame):
679+
# ENDIF
680+
if target_frame is current_frame:
681+
return True
682+
683+
info = self._args[2]
684+
if info.pydev_use_scoped_step_frame:
685+
# If using scoped step we don't check the target, we just need to check
686+
# if the current matches the same heuristic where the target was defined.
687+
if target_frame is not None and current_frame is not None:
688+
if target_frame.f_code.co_filename == current_frame.f_code.co_filename:
689+
# The co_name may be different (it may include the line number), but
690+
# the filename must still be the same.
691+
f = current_frame.f_back
692+
if f is not None and f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[1]:
693+
f = f.f_back
694+
if f is not None and f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[2]:
695+
return True
696+
697+
return False
698+
660699
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
661700
cpdef trace_dispatch(self, frame, str event, arg):
662701
cdef tuple abs_path_canonical_path_and_base;
@@ -772,7 +811,13 @@ cdef class PyDBFrame:
772811
# Solving this may not be trivial as we'd need to put a scope in the step
773812
# in, but we may have to do it anyways to have a step in which doesn't end
774813
# up in asyncio).
775-
if stop_frame is frame:
814+
#
815+
# Note2: we don't revert to a step in if we're doing scoped stepping
816+
# (because on scoped stepping we're always receiving a call/line/return
817+
# event for each line in ipython, so, we can't revert to step in on return
818+
# as the return shouldn't mean that we've actually completed executing a
819+
# frame in this case).
820+
if stop_frame is frame and not info.pydev_use_scoped_step_frame:
776821
if step_cmd in (108, 159, 107, 144):
777822
f = self._get_unfiltered_back_frame(main_debugger, frame)
778823
if f is not None:
@@ -809,7 +854,7 @@ cdef class PyDBFrame:
809854
# event == 'call' or event == 'c_XXX'
810855
return self.trace_dispatch
811856

812-
else:
857+
else: # Not coroutine nor generator
813858
if event == 'line':
814859
is_line = True
815860
is_call = False
@@ -828,7 +873,12 @@ cdef class PyDBFrame:
828873
# to make a step in or step over at that location).
829874
# Note: this is especially troublesome when we're skipping code with the
830875
# @DontTrace comment.
831-
if stop_frame is frame and is_return and step_cmd in (108, 109, 159, 160, 128):
876+
if (
877+
stop_frame is frame and
878+
not info.pydev_use_scoped_step_frame and is_return and
879+
step_cmd in (108, 109, 159, 160, 128)
880+
):
881+
832882
if step_cmd in (108, 109, 128):
833883
info.pydev_step_cmd = 107
834884
else:
@@ -876,7 +926,7 @@ cdef class PyDBFrame:
876926
if step_cmd == -1:
877927
can_skip = True
878928

879-
elif step_cmd in (108, 109, 159, 160) and stop_frame is not frame:
929+
elif step_cmd in (108, 109, 159, 160) and not self._is_same_frame(stop_frame, frame):
880930
can_skip = True
881931

882932
elif step_cmd == 128 and (
@@ -896,7 +946,7 @@ cdef class PyDBFrame:
896946
elif step_cmd == 206:
897947
f = frame
898948
while f is not None:
899-
if f is stop_frame:
949+
if self._is_same_frame(stop_frame, f):
900950
break
901951
f = f.f_back
902952
else:
@@ -907,7 +957,7 @@ cdef class PyDBFrame:
907957
main_debugger.has_plugin_line_breaks or main_debugger.has_plugin_exception_breaks):
908958
can_skip = plugin_manager.can_skip(main_debugger, frame)
909959

910-
if can_skip and main_debugger.show_return_values and info.pydev_step_cmd in (108, 159) and frame.f_back is stop_frame:
960+
if can_skip and main_debugger.show_return_values and info.pydev_step_cmd in (108, 159) and self._is_same_frame(stop_frame, frame.f_back):
911961
# trace function for showing return values after step over
912962
can_skip = False
913963

@@ -1006,7 +1056,7 @@ cdef class PyDBFrame:
10061056
breakpoint = breakpoints_for_file[line]
10071057
new_frame = frame
10081058
stop = True
1009-
if step_cmd in (108, 159) and (stop_frame is frame and is_line):
1059+
if step_cmd in (108, 159) and (self._is_same_frame(stop_frame, frame) and is_line):
10101060
stop = False # we don't stop on breakpoint if we have to stop by step-over (it will be processed later)
10111061
elif plugin_manager is not None and main_debugger.has_plugin_line_breaks:
10121062
result = plugin_manager.get_breakpoint(main_debugger, self, frame, event, self._args)
@@ -1050,8 +1100,8 @@ cdef class PyDBFrame:
10501100

10511101
if main_debugger.show_return_values:
10521102
if is_return and (
1053-
(info.pydev_step_cmd in (108, 159, 128) and (frame.f_back is stop_frame)) or
1054-
(info.pydev_step_cmd in (109, 160) and (frame is stop_frame)) or
1103+
(info.pydev_step_cmd in (108, 159, 128) and (self._is_same_frame(stop_frame, frame.f_back))) or
1104+
(info.pydev_step_cmd in (109, 160) and (self._is_same_frame(stop_frame, frame))) or
10551105
(info.pydev_step_cmd in (107, 206)) or
10561106
(
10571107
info.pydev_step_cmd == 144
@@ -1115,12 +1165,36 @@ cdef class PyDBFrame:
11151165
elif step_cmd in (107, 144, 206):
11161166
force_check_project_scope = step_cmd == 144
11171167
if is_line:
1118-
if force_check_project_scope or main_debugger.is_files_filter_enabled:
1119-
stop = not main_debugger.apply_files_filter(frame, frame.f_code.co_filename, force_check_project_scope)
1168+
if not info.pydev_use_scoped_step_frame:
1169+
if force_check_project_scope or main_debugger.is_files_filter_enabled:
1170+
stop = not main_debugger.apply_files_filter(frame, frame.f_code.co_filename, force_check_project_scope)
1171+
else:
1172+
stop = True
11201173
else:
1121-
stop = True
1174+
# We can only stop inside the ipython call.
1175+
filename = frame.f_code.co_filename
1176+
if filename.endswith('.pyc'):
1177+
filename = filename[:-1]
1178+
1179+
if not filename.endswith(PYDEVD_IPYTHON_CONTEXT[0]):
1180+
f = frame.f_back
1181+
while f is not None:
1182+
if f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[1]:
1183+
f2 = f.f_back
1184+
if f2 is not None and f2.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[2]:
1185+
pydev_log.debug('Stop inside ipython call')
1186+
stop = True
1187+
break
1188+
f = f.f_back
1189+
1190+
del f
1191+
1192+
if not stop:
1193+
# In scoped mode if step in didn't work in this context it won't work
1194+
# afterwards anyways.
1195+
return None if is_call else NO_FTRACE
11221196

1123-
elif is_return and frame.f_back is not None:
1197+
elif is_return and frame.f_back is not None and not info.pydev_use_scoped_step_frame:
11241198
if main_debugger.get_file_type(frame.f_back) == main_debugger.PYDEV_FILE:
11251199
stop = False
11261200
else:
@@ -1141,7 +1215,7 @@ cdef class PyDBFrame:
11411215
# i.e.: Check if we're stepping into the proper context.
11421216
f = frame
11431217
while f is not None:
1144-
if f is stop_frame:
1218+
if self._is_same_frame(stop_frame, f):
11451219
break
11461220
f = f.f_back
11471221
else:
@@ -1156,7 +1230,7 @@ cdef class PyDBFrame:
11561230
# Note: when dealing with a step over my code it's the same as a step over (the
11571231
# difference is that when we return from a frame in one we go to regular step
11581232
# into and in the other we go to a step into my code).
1159-
stop = stop_frame is frame and is_line
1233+
stop = self._is_same_frame(stop_frame, frame) and is_line
11601234
# Note: don't stop on a return for step over, only for line events
11611235
# i.e.: don't stop in: (stop_frame is frame.f_back and is_return) as we'd stop twice in that line.
11621236

@@ -1168,11 +1242,11 @@ cdef class PyDBFrame:
11681242
elif step_cmd == 128:
11691243
stop = False
11701244
back = frame.f_back
1171-
if stop_frame is frame and is_return:
1245+
if self._is_same_frame(stop_frame, frame) and is_return:
11721246
# We're exiting the smart step into initial frame (so, we probably didn't find our target).
11731247
stop = True
11741248

1175-
elif stop_frame is back and is_line:
1249+
elif self._is_same_frame(stop_frame, back) and is_line:
11761250
if info.pydev_smart_child_offset != -1:
11771251
# i.e.: in this case, we're not interested in the pause in the parent, rather
11781252
# we're interested in the pause in the child (when the parent is at the proper place).
@@ -1203,7 +1277,7 @@ cdef class PyDBFrame:
12031277
# not be the case next time either, so, disable tracing for this frame.
12041278
return None if is_call else NO_FTRACE
12051279

1206-
elif back is not None and stop_frame is back.f_back and is_line:
1280+
elif back is not None and self._is_same_frame(stop_frame, back.f_back) and is_line:
12071281
# Ok, we have to track 2 stops at this point, the parent and the child offset.
12081282
# This happens when handling a step into which targets a function inside a list comprehension
12091283
# or generator (in which case an intermediary frame is created due to an internal function call).
@@ -1237,7 +1311,7 @@ cdef class PyDBFrame:
12371311
return None if is_call else NO_FTRACE
12381312

12391313
elif step_cmd in (109, 160):
1240-
stop = is_return and stop_frame is frame
1314+
stop = is_return and self._is_same_frame(stop_frame, frame)
12411315

12421316
else:
12431317
stop = False

0 commit comments

Comments
 (0)