@@ -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
150164from _pydev_bundle import pydev_log
151165from _pydevd_bundle import pydevd_dont_trace
152166from _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 )
154168from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raised, remove_exception_from_frame, ignore_exception_trace
155169from _pydevd_bundle.pydevd_utils import get_clsname_for_code
156170from 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