@@ -59,6 +59,8 @@ cdef class PyDBAdditionalThreadInfo:
59
59
# # of the last request for a given thread and pydev_smart_parent_offset/pydev_smart_child_offset relies on it).
60
60
# 'pydev_smart_step_into_variants',
61
61
# 'target_id_to_smart_step_into_variant',
62
+ #
63
+ # 'pydev_use_scoped_step_frame',
62
64
# ]
63
65
# ENDIF
64
66
@@ -96,6 +98,18 @@ cdef class PyDBAdditionalThreadInfo:
96
98
self .pydev_smart_step_into_variants = ()
97
99
self .target_id_to_smart_step_into_variant = {}
98
100
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
+
99
113
def get_topmost_frame (self , thread ):
100
114
'''
101
115
Gets the topmost frame for the given thread. Note that it may be None
@@ -150,7 +164,7 @@ import re
150
164
from _pydev_bundle import pydev_log
151
165
from _pydevd_bundle import pydevd_dont_trace
152
166
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 )
154
168
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raised, remove_exception_from_frame, ignore_exception_trace
155
169
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
156
170
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
@@ -657,6 +671,31 @@ cdef class PyDBFrame:
657
671
658
672
return f
659
673
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
+
660
699
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
661
700
cpdef trace_dispatch(self , frame, str event, arg):
662
701
cdef tuple abs_path_canonical_path_and_base;
@@ -772,7 +811,13 @@ cdef class PyDBFrame:
772
811
# Solving this may not be trivial as we'd need to put a scope in the step
773
812
# in, but we may have to do it anyways to have a step in which doesn't end
774
813
# 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:
776
821
if step_cmd in (108 , 159 , 107 , 144 ):
777
822
f = self ._get_unfiltered_back_frame(main_debugger, frame)
778
823
if f is not None :
@@ -809,7 +854,7 @@ cdef class PyDBFrame:
809
854
# event == 'call' or event == 'c_XXX'
810
855
return self .trace_dispatch
811
856
812
- else :
857
+ else : # Not coroutine nor generator
813
858
if event == ' line' :
814
859
is_line = True
815
860
is_call = False
@@ -828,7 +873,12 @@ cdef class PyDBFrame:
828
873
# to make a step in or step over at that location).
829
874
# Note: this is especially troublesome when we're skipping code with the
830
875
# @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
+
832
882
if step_cmd in (108 , 109 , 128 ):
833
883
info.pydev_step_cmd = 107
834
884
else :
@@ -876,7 +926,7 @@ cdef class PyDBFrame:
876
926
if step_cmd == - 1 :
877
927
can_skip = True
878
928
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) :
880
930
can_skip = True
881
931
882
932
elif step_cmd == 128 and (
@@ -896,7 +946,7 @@ cdef class PyDBFrame:
896
946
elif step_cmd == 206 :
897
947
f = frame
898
948
while f is not None :
899
- if f is stop_frame:
949
+ if self ._is_same_frame( stop_frame, f) :
900
950
break
901
951
f = f.f_back
902
952
else :
@@ -907,7 +957,7 @@ cdef class PyDBFrame:
907
957
main_debugger.has_plugin_line_breaks or main_debugger.has_plugin_exception_breaks):
908
958
can_skip = plugin_manager.can_skip(main_debugger, frame)
909
959
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) :
911
961
# trace function for showing return values after step over
912
962
can_skip = False
913
963
@@ -1006,7 +1056,7 @@ cdef class PyDBFrame:
1006
1056
breakpoint = breakpoints_for_file[line]
1007
1057
new_frame = frame
1008
1058
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):
1010
1060
stop = False # we don't stop on breakpoint if we have to stop by step-over (it will be processed later)
1011
1061
elif plugin_manager is not None and main_debugger.has_plugin_line_breaks:
1012
1062
result = plugin_manager.get_breakpoint(main_debugger, self , frame, event, self ._args)
@@ -1050,8 +1100,8 @@ cdef class PyDBFrame:
1050
1100
1051
1101
if main_debugger.show_return_values:
1052
1102
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
1055
1105
(info.pydev_step_cmd in (107 , 206 )) or
1056
1106
(
1057
1107
info.pydev_step_cmd == 144
@@ -1115,12 +1165,36 @@ cdef class PyDBFrame:
1115
1165
elif step_cmd in (107 , 144 , 206 ):
1116
1166
force_check_project_scope = step_cmd == 144
1117
1167
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
1120
1173
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
1122
1196
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 :
1124
1198
if main_debugger.get_file_type(frame.f_back) == main_debugger.PYDEV_FILE:
1125
1199
stop = False
1126
1200
else :
@@ -1141,7 +1215,7 @@ cdef class PyDBFrame:
1141
1215
# i.e.: Check if we're stepping into the proper context.
1142
1216
f = frame
1143
1217
while f is not None :
1144
- if f is stop_frame:
1218
+ if self ._is_same_frame( stop_frame, f) :
1145
1219
break
1146
1220
f = f.f_back
1147
1221
else :
@@ -1156,7 +1230,7 @@ cdef class PyDBFrame:
1156
1230
# Note: when dealing with a step over my code it's the same as a step over (the
1157
1231
# difference is that when we return from a frame in one we go to regular step
1158
1232
# 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
1160
1234
# Note: don't stop on a return for step over, only for line events
1161
1235
# i.e.: don't stop in: (stop_frame is frame.f_back and is_return) as we'd stop twice in that line.
1162
1236
@@ -1168,11 +1242,11 @@ cdef class PyDBFrame:
1168
1242
elif step_cmd == 128 :
1169
1243
stop = False
1170
1244
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:
1172
1246
# We're exiting the smart step into initial frame (so, we probably didn't find our target).
1173
1247
stop = True
1174
1248
1175
- elif stop_frame is back and is_line:
1249
+ elif self ._is_same_frame( stop_frame, back) and is_line:
1176
1250
if info.pydev_smart_child_offset != - 1 :
1177
1251
# i.e.: in this case, we're not interested in the pause in the parent, rather
1178
1252
# we're interested in the pause in the child (when the parent is at the proper place).
@@ -1203,7 +1277,7 @@ cdef class PyDBFrame:
1203
1277
# not be the case next time either, so, disable tracing for this frame.
1204
1278
return None if is_call else NO_FTRACE
1205
1279
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:
1207
1281
# Ok, we have to track 2 stops at this point, the parent and the child offset.
1208
1282
# This happens when handling a step into which targets a function inside a list comprehension
1209
1283
# or generator (in which case an intermediary frame is created due to an internal function call).
@@ -1237,7 +1311,7 @@ cdef class PyDBFrame:
1237
1311
return None if is_call else NO_FTRACE
1238
1312
1239
1313
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)
1241
1315
1242
1316
else :
1243
1317
stop = False
0 commit comments