Step in/step over support for IPython. Fixes #869

This commit is contained in:
Fabio Zadrozny 2022-07-29 14:27:40 -03:00
parent a294092d9c
commit 6b276e339c
16 changed files with 6358 additions and 4801 deletions

View file

@ -53,6 +53,8 @@ class PyDBAdditionalThreadInfo(object):
# of the last request for a given thread and pydev_smart_parent_offset/pydev_smart_child_offset relies on it).
'pydev_smart_step_into_variants',
'target_id_to_smart_step_into_variant',
'pydev_use_scoped_step_frame',
]
# ENDIF
@ -90,6 +92,18 @@ class PyDBAdditionalThreadInfo(object):
self.pydev_smart_step_into_variants = ()
self.target_id_to_smart_step_into_variant = {}
# Flag to indicate ipython use-case where each line will be executed as a call/line/return
# in a new new frame but in practice we want to consider each new frame as if it was all
# part of the same frame.
#
# In practice this means that a step over shouldn't revert to a step in and we need some
# special logic to know when we should stop in a step over as we need to consider 2
# different frames as being equal if they're logically the continuation of a frame
# being executed by ipython line by line.
#
# See: https://github.com/microsoft/debugpy/issues/869#issuecomment-1132141003
self.pydev_use_scoped_step_frame = False
def get_topmost_frame(self, thread):
'''
Gets the topmost frame for the given thread. Note that it may be None

View file

@ -255,11 +255,6 @@ INTERACTIVE_MODE_AVAILABLE = sys.platform in ('darwin', 'win32') or os.getenv('D
# If not specified, uses default heuristic to determine if it should be loaded.
USE_CYTHON_FLAG = os.getenv('PYDEVD_USE_CYTHON')
# Use to disable loading the lib to set tracing to all threads (default is using heuristics based on where we're running).
LOAD_NATIVE_LIB_FLAG = os.getenv('PYDEVD_LOAD_NATIVE_LIB', '').lower()
LOG_TIME = os.getenv('PYDEVD_LOG_TIME', 'true').lower() in ENV_TRUE_LOWER_VALUES
if USE_CYTHON_FLAG is not None:
USE_CYTHON_FLAG = USE_CYTHON_FLAG.lower()
if USE_CYTHON_FLAG not in ENV_TRUE_LOWER_VALUES and USE_CYTHON_FLAG not in ENV_FALSE_LOWER_VALUES:
@ -270,6 +265,26 @@ else:
if not CYTHON_SUPPORTED:
USE_CYTHON_FLAG = 'no'
# If true in env, forces frame eval to be used (raises error if not available).
# If false in env, disables it.
# If not specified, uses default heuristic to determine if it should be loaded.
PYDEVD_USE_FRAME_EVAL = os.getenv('PYDEVD_USE_FRAME_EVAL', '').lower()
PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING = is_true_in_env('PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING')
# If specified in PYDEVD_IPYTHON_CONTEXT it must be a string with the basename
# and then the name of 2 methods in which the evaluate is done.
PYDEVD_IPYTHON_CONTEXT = ('interactiveshell.py', 'run_code', 'run_ast_nodes')
_ipython_ctx = os.getenv('PYDEVD_IPYTHON_CONTEXT')
if _ipython_ctx:
PYDEVD_IPYTHON_CONTEXT = tuple(x.strip() for x in _ipython_ctx.split(','))
assert len(PYDEVD_IPYTHON_CONTEXT) == 3, 'Invalid PYDEVD_IPYTHON_CONTEXT: %s' % (_ipython_ctx,)
# Use to disable loading the lib to set tracing to all threads (default is using heuristics based on where we're running).
LOAD_NATIVE_LIB_FLAG = os.getenv('PYDEVD_LOAD_NATIVE_LIB', '').lower()
LOG_TIME = os.getenv('PYDEVD_LOG_TIME', 'true').lower() in ENV_TRUE_LOWER_VALUES
SHOW_COMPILE_CYTHON_COMMAND_LINE = is_true_in_env('PYDEVD_SHOW_COMPILE_CYTHON_COMMAND_LINE')
LOAD_VALUES_ASYNC = is_true_in_env('PYDEVD_LOAD_VALUES_ASYNC')

File diff suppressed because it is too large Load diff

View file

@ -24,3 +24,4 @@ cdef class PyDBAdditionalThreadInfo:
cdef public int pydev_smart_child_offset
cdef public tuple pydev_smart_step_into_variants
cdef public dict target_id_to_smart_step_into_variant
cdef public bint pydev_use_scoped_step_frame

View file

@ -59,6 +59,8 @@ cdef class PyDBAdditionalThreadInfo:
# # of the last request for a given thread and pydev_smart_parent_offset/pydev_smart_child_offset relies on it).
# 'pydev_smart_step_into_variants',
# 'target_id_to_smart_step_into_variant',
#
# 'pydev_use_scoped_step_frame',
# ]
# ENDIF
@ -96,6 +98,18 @@ cdef class PyDBAdditionalThreadInfo:
self.pydev_smart_step_into_variants = ()
self.target_id_to_smart_step_into_variant = {}
# Flag to indicate ipython use-case where each line will be executed as a call/line/return
# in a new new frame but in practice we want to consider each new frame as if it was all
# part of the same frame.
#
# In practice this means that a step over shouldn't revert to a step in and we need some
# special logic to know when we should stop in a step over as we need to consider 2
# different frames as being equal if they're logically the continuation of a frame
# being executed by ipython line by line.
#
# See: https://github.com/microsoft/debugpy/issues/869#issuecomment-1132141003
self.pydev_use_scoped_step_frame = False
def get_topmost_frame(self, thread):
'''
Gets the topmost frame for the given thread. Note that it may be None
@ -150,7 +164,7 @@ import re
from _pydev_bundle import pydev_log
from _pydevd_bundle import pydevd_dont_trace
from _pydevd_bundle.pydevd_constants import (RETURN_VALUES_DICT, NO_FTRACE,
EXCEPTION_TYPE_HANDLED, EXCEPTION_TYPE_USER_UNHANDLED)
EXCEPTION_TYPE_HANDLED, EXCEPTION_TYPE_USER_UNHANDLED, PYDEVD_IPYTHON_CONTEXT)
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raised, remove_exception_from_frame, ignore_exception_trace
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
@ -657,6 +671,31 @@ cdef class PyDBFrame:
return f
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
cdef _is_same_frame(self, target_frame, current_frame):
cdef PyDBAdditionalThreadInfo info;
# ELSE
# def _is_same_frame(self, target_frame, current_frame):
# ENDIF
if target_frame is current_frame:
return True
info = self._args[2]
if info.pydev_use_scoped_step_frame:
# If using scoped step we don't check the target, we just need to check
# if the current matches the same heuristic where the target was defined.
if target_frame is not None and current_frame is not None:
if target_frame.f_code.co_filename == current_frame.f_code.co_filename:
# The co_name may be different (it may include the line number), but
# the filename must still be the same.
f = current_frame.f_back
if f is not None and f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[1]:
f = f.f_back
if f is not None and f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[2]:
return True
return False
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
cpdef trace_dispatch(self, frame, str event, arg):
cdef tuple abs_path_canonical_path_and_base;
@ -772,7 +811,13 @@ cdef class PyDBFrame:
# Solving this may not be trivial as we'd need to put a scope in the step
# in, but we may have to do it anyways to have a step in which doesn't end
# up in asyncio).
if stop_frame is frame:
#
# Note2: we don't revert to a step in if we're doing scoped stepping
# (because on scoped stepping we're always receiving a call/line/return
# event for each line in ipython, so, we can't revert to step in on return
# as the return shouldn't mean that we've actually completed executing a
# frame in this case).
if stop_frame is frame and not info.pydev_use_scoped_step_frame:
if step_cmd in (108, 159, 107, 144):
f = self._get_unfiltered_back_frame(main_debugger, frame)
if f is not None:
@ -809,7 +854,7 @@ cdef class PyDBFrame:
# event == 'call' or event == 'c_XXX'
return self.trace_dispatch
else:
else: # Not coroutine nor generator
if event == 'line':
is_line = True
is_call = False
@ -828,7 +873,12 @@ cdef class PyDBFrame:
# to make a step in or step over at that location).
# Note: this is especially troublesome when we're skipping code with the
# @DontTrace comment.
if stop_frame is frame and is_return and step_cmd in (108, 109, 159, 160, 128):
if (
stop_frame is frame and
not info.pydev_use_scoped_step_frame and is_return and
step_cmd in (108, 109, 159, 160, 128)
):
if step_cmd in (108, 109, 128):
info.pydev_step_cmd = 107
else:
@ -876,7 +926,7 @@ cdef class PyDBFrame:
if step_cmd == -1:
can_skip = True
elif step_cmd in (108, 109, 159, 160) and stop_frame is not frame:
elif step_cmd in (108, 109, 159, 160) and not self._is_same_frame(stop_frame, frame):
can_skip = True
elif step_cmd == 128 and (
@ -896,7 +946,7 @@ cdef class PyDBFrame:
elif step_cmd == 206:
f = frame
while f is not None:
if f is stop_frame:
if self._is_same_frame(stop_frame, f):
break
f = f.f_back
else:
@ -907,7 +957,7 @@ cdef class PyDBFrame:
main_debugger.has_plugin_line_breaks or main_debugger.has_plugin_exception_breaks):
can_skip = plugin_manager.can_skip(main_debugger, frame)
if can_skip and main_debugger.show_return_values and info.pydev_step_cmd in (108, 159) and frame.f_back is stop_frame:
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):
# trace function for showing return values after step over
can_skip = False
@ -1006,7 +1056,7 @@ cdef class PyDBFrame:
breakpoint = breakpoints_for_file[line]
new_frame = frame
stop = True
if step_cmd in (108, 159) and (stop_frame is frame and is_line):
if step_cmd in (108, 159) and (self._is_same_frame(stop_frame, frame) and is_line):
stop = False # we don't stop on breakpoint if we have to stop by step-over (it will be processed later)
elif plugin_manager is not None and main_debugger.has_plugin_line_breaks:
result = plugin_manager.get_breakpoint(main_debugger, self, frame, event, self._args)
@ -1050,8 +1100,8 @@ cdef class PyDBFrame:
if main_debugger.show_return_values:
if is_return and (
(info.pydev_step_cmd in (108, 159, 128) and (frame.f_back is stop_frame)) or
(info.pydev_step_cmd in (109, 160) and (frame is stop_frame)) or
(info.pydev_step_cmd in (108, 159, 128) and (self._is_same_frame(stop_frame, frame.f_back))) or
(info.pydev_step_cmd in (109, 160) and (self._is_same_frame(stop_frame, frame))) or
(info.pydev_step_cmd in (107, 206)) or
(
info.pydev_step_cmd == 144
@ -1115,12 +1165,36 @@ cdef class PyDBFrame:
elif step_cmd in (107, 144, 206):
force_check_project_scope = step_cmd == 144
if is_line:
if force_check_project_scope or main_debugger.is_files_filter_enabled:
stop = not main_debugger.apply_files_filter(frame, frame.f_code.co_filename, force_check_project_scope)
if not info.pydev_use_scoped_step_frame:
if force_check_project_scope or main_debugger.is_files_filter_enabled:
stop = not main_debugger.apply_files_filter(frame, frame.f_code.co_filename, force_check_project_scope)
else:
stop = True
else:
stop = True
# We can only stop inside the ipython call.
filename = frame.f_code.co_filename
if filename.endswith('.pyc'):
filename = filename[:-1]
elif is_return and frame.f_back is not None:
if not filename.endswith(PYDEVD_IPYTHON_CONTEXT[0]):
f = frame.f_back
while f is not None:
if f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[1]:
f2 = f.f_back
if f2 is not None and f2.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[2]:
pydev_log.debug('Stop inside ipython call')
stop = True
break
f = f.f_back
del f
if not stop:
# In scoped mode if step in didn't work in this context it won't work
# afterwards anyways.
return None if is_call else NO_FTRACE
elif is_return and frame.f_back is not None and not info.pydev_use_scoped_step_frame:
if main_debugger.get_file_type(frame.f_back) == main_debugger.PYDEV_FILE:
stop = False
else:
@ -1141,7 +1215,7 @@ cdef class PyDBFrame:
# i.e.: Check if we're stepping into the proper context.
f = frame
while f is not None:
if f is stop_frame:
if self._is_same_frame(stop_frame, f):
break
f = f.f_back
else:
@ -1156,7 +1230,7 @@ cdef class PyDBFrame:
# Note: when dealing with a step over my code it's the same as a step over (the
# difference is that when we return from a frame in one we go to regular step
# into and in the other we go to a step into my code).
stop = stop_frame is frame and is_line
stop = self._is_same_frame(stop_frame, frame) and is_line
# Note: don't stop on a return for step over, only for line events
# i.e.: don't stop in: (stop_frame is frame.f_back and is_return) as we'd stop twice in that line.
@ -1168,11 +1242,11 @@ cdef class PyDBFrame:
elif step_cmd == 128:
stop = False
back = frame.f_back
if stop_frame is frame and is_return:
if self._is_same_frame(stop_frame, frame) and is_return:
# We're exiting the smart step into initial frame (so, we probably didn't find our target).
stop = True
elif stop_frame is back and is_line:
elif self._is_same_frame(stop_frame, back) and is_line:
if info.pydev_smart_child_offset != -1:
# i.e.: in this case, we're not interested in the pause in the parent, rather
# we're interested in the pause in the child (when the parent is at the proper place).
@ -1203,7 +1277,7 @@ cdef class PyDBFrame:
# not be the case next time either, so, disable tracing for this frame.
return None if is_call else NO_FTRACE
elif back is not None and stop_frame is back.f_back and is_line:
elif back is not None and self._is_same_frame(stop_frame, back.f_back) and is_line:
# Ok, we have to track 2 stops at this point, the parent and the child offset.
# This happens when handling a step into which targets a function inside a list comprehension
# or generator (in which case an intermediary frame is created due to an internal function call).
@ -1237,7 +1311,7 @@ cdef class PyDBFrame:
return None if is_call else NO_FTRACE
elif step_cmd in (109, 160):
stop = is_return and stop_frame is frame
stop = is_return and self._is_same_frame(stop_frame, frame)
else:
stop = False

View file

@ -5,7 +5,7 @@ import re
from _pydev_bundle import pydev_log
from _pydevd_bundle import pydevd_dont_trace
from _pydevd_bundle.pydevd_constants import (RETURN_VALUES_DICT, NO_FTRACE,
EXCEPTION_TYPE_HANDLED, EXCEPTION_TYPE_USER_UNHANDLED)
EXCEPTION_TYPE_HANDLED, EXCEPTION_TYPE_USER_UNHANDLED, PYDEVD_IPYTHON_CONTEXT)
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raised, remove_exception_from_frame, ignore_exception_trace
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
@ -524,6 +524,31 @@ class PyDBFrame:
return f
# IFDEF CYTHON
# cdef _is_same_frame(self, target_frame, current_frame):
# cdef PyDBAdditionalThreadInfo info;
# ELSE
def _is_same_frame(self, target_frame, current_frame):
# ENDIF
if target_frame is current_frame:
return True
info = self._args[2]
if info.pydev_use_scoped_step_frame:
# If using scoped step we don't check the target, we just need to check
# if the current matches the same heuristic where the target was defined.
if target_frame is not None and current_frame is not None:
if target_frame.f_code.co_filename == current_frame.f_code.co_filename:
# The co_name may be different (it may include the line number), but
# the filename must still be the same.
f = current_frame.f_back
if f is not None and f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[1]:
f = f.f_back
if f is not None and f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[2]:
return True
return False
# IFDEF CYTHON
# cpdef trace_dispatch(self, frame, str event, arg):
# cdef tuple abs_path_canonical_path_and_base;
@ -639,7 +664,13 @@ class PyDBFrame:
# Solving this may not be trivial as we'd need to put a scope in the step
# in, but we may have to do it anyways to have a step in which doesn't end
# up in asyncio).
if stop_frame is frame:
#
# Note2: we don't revert to a step in if we're doing scoped stepping
# (because on scoped stepping we're always receiving a call/line/return
# event for each line in ipython, so, we can't revert to step in on return
# as the return shouldn't mean that we've actually completed executing a
# frame in this case).
if stop_frame is frame and not info.pydev_use_scoped_step_frame:
if step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE, CMD_STEP_INTO, CMD_STEP_INTO_MY_CODE):
f = self._get_unfiltered_back_frame(main_debugger, frame)
if f is not None:
@ -676,7 +707,7 @@ class PyDBFrame:
# event == 'call' or event == 'c_XXX'
return self.trace_dispatch
else:
else: # Not coroutine nor generator
if event == 'line':
is_line = True
is_call = False
@ -695,7 +726,12 @@ class PyDBFrame:
# to make a step in or step over at that location).
# Note: this is especially troublesome when we're skipping code with the
# @DontTrace comment.
if stop_frame is frame and is_return and step_cmd in (CMD_STEP_OVER, CMD_STEP_RETURN, CMD_STEP_OVER_MY_CODE, CMD_STEP_RETURN_MY_CODE, CMD_SMART_STEP_INTO):
if (
stop_frame is frame and
not info.pydev_use_scoped_step_frame and is_return and
step_cmd in (CMD_STEP_OVER, CMD_STEP_RETURN, CMD_STEP_OVER_MY_CODE, CMD_STEP_RETURN_MY_CODE, CMD_SMART_STEP_INTO)
):
if step_cmd in (CMD_STEP_OVER, CMD_STEP_RETURN, CMD_SMART_STEP_INTO):
info.pydev_step_cmd = CMD_STEP_INTO
else:
@ -743,7 +779,7 @@ class PyDBFrame:
if step_cmd == -1:
can_skip = True
elif step_cmd in (CMD_STEP_OVER, CMD_STEP_RETURN, CMD_STEP_OVER_MY_CODE, CMD_STEP_RETURN_MY_CODE) and stop_frame is not frame:
elif step_cmd in (CMD_STEP_OVER, CMD_STEP_RETURN, CMD_STEP_OVER_MY_CODE, CMD_STEP_RETURN_MY_CODE) and not self._is_same_frame(stop_frame, frame):
can_skip = True
elif step_cmd == CMD_SMART_STEP_INTO and (
@ -763,7 +799,7 @@ class PyDBFrame:
elif step_cmd == CMD_STEP_INTO_COROUTINE:
f = frame
while f is not None:
if f is stop_frame:
if self._is_same_frame(stop_frame, f):
break
f = f.f_back
else:
@ -774,7 +810,7 @@ class PyDBFrame:
main_debugger.has_plugin_line_breaks or main_debugger.has_plugin_exception_breaks):
can_skip = plugin_manager.can_skip(main_debugger, frame)
if can_skip and main_debugger.show_return_values and info.pydev_step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE) and frame.f_back is stop_frame:
if can_skip and main_debugger.show_return_values and info.pydev_step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE) and self._is_same_frame(stop_frame, frame.f_back):
# trace function for showing return values after step over
can_skip = False
@ -873,7 +909,7 @@ class PyDBFrame:
breakpoint = breakpoints_for_file[line]
new_frame = frame
stop = True
if step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE) and (stop_frame is frame and is_line):
if step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE) and (self._is_same_frame(stop_frame, frame) and is_line):
stop = False # we don't stop on breakpoint if we have to stop by step-over (it will be processed later)
elif plugin_manager is not None and main_debugger.has_plugin_line_breaks:
result = plugin_manager.get_breakpoint(main_debugger, self, frame, event, self._args)
@ -917,8 +953,8 @@ class PyDBFrame:
if main_debugger.show_return_values:
if is_return and (
(info.pydev_step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE, CMD_SMART_STEP_INTO) and (frame.f_back is stop_frame)) or
(info.pydev_step_cmd in (CMD_STEP_RETURN, CMD_STEP_RETURN_MY_CODE) and (frame is stop_frame)) or
(info.pydev_step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE, CMD_SMART_STEP_INTO) and (self._is_same_frame(stop_frame, frame.f_back))) or
(info.pydev_step_cmd in (CMD_STEP_RETURN, CMD_STEP_RETURN_MY_CODE) and (self._is_same_frame(stop_frame, frame))) or
(info.pydev_step_cmd in (CMD_STEP_INTO, CMD_STEP_INTO_COROUTINE)) or
(
info.pydev_step_cmd == CMD_STEP_INTO_MY_CODE
@ -982,12 +1018,36 @@ class PyDBFrame:
elif step_cmd in (CMD_STEP_INTO, CMD_STEP_INTO_MY_CODE, CMD_STEP_INTO_COROUTINE):
force_check_project_scope = step_cmd == CMD_STEP_INTO_MY_CODE
if is_line:
if force_check_project_scope or main_debugger.is_files_filter_enabled:
stop = not main_debugger.apply_files_filter(frame, frame.f_code.co_filename, force_check_project_scope)
if not info.pydev_use_scoped_step_frame:
if force_check_project_scope or main_debugger.is_files_filter_enabled:
stop = not main_debugger.apply_files_filter(frame, frame.f_code.co_filename, force_check_project_scope)
else:
stop = True
else:
stop = True
# We can only stop inside the ipython call.
filename = frame.f_code.co_filename
if filename.endswith('.pyc'):
filename = filename[:-1]
elif is_return and frame.f_back is not None:
if not filename.endswith(PYDEVD_IPYTHON_CONTEXT[0]):
f = frame.f_back
while f is not None:
if f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[1]:
f2 = f.f_back
if f2 is not None and f2.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[2]:
pydev_log.debug('Stop inside ipython call')
stop = True
break
f = f.f_back
del f
if not stop:
# In scoped mode if step in didn't work in this context it won't work
# afterwards anyways.
return None if is_call else NO_FTRACE
elif is_return and frame.f_back is not None and not info.pydev_use_scoped_step_frame:
if main_debugger.get_file_type(frame.f_back) == main_debugger.PYDEV_FILE:
stop = False
else:
@ -1008,7 +1068,7 @@ class PyDBFrame:
# i.e.: Check if we're stepping into the proper context.
f = frame
while f is not None:
if f is stop_frame:
if self._is_same_frame(stop_frame, f):
break
f = f.f_back
else:
@ -1023,7 +1083,7 @@ class PyDBFrame:
# Note: when dealing with a step over my code it's the same as a step over (the
# difference is that when we return from a frame in one we go to regular step
# into and in the other we go to a step into my code).
stop = stop_frame is frame and is_line
stop = self._is_same_frame(stop_frame, frame) and is_line
# Note: don't stop on a return for step over, only for line events
# i.e.: don't stop in: (stop_frame is frame.f_back and is_return) as we'd stop twice in that line.
@ -1035,11 +1095,11 @@ class PyDBFrame:
elif step_cmd == CMD_SMART_STEP_INTO:
stop = False
back = frame.f_back
if stop_frame is frame and is_return:
if self._is_same_frame(stop_frame, frame) and is_return:
# We're exiting the smart step into initial frame (so, we probably didn't find our target).
stop = True
elif stop_frame is back and is_line:
elif self._is_same_frame(stop_frame, back) and is_line:
if info.pydev_smart_child_offset != -1:
# i.e.: in this case, we're not interested in the pause in the parent, rather
# we're interested in the pause in the child (when the parent is at the proper place).
@ -1070,7 +1130,7 @@ class PyDBFrame:
# not be the case next time either, so, disable tracing for this frame.
return None if is_call else NO_FTRACE
elif back is not None and stop_frame is back.f_back and is_line:
elif back is not None and self._is_same_frame(stop_frame, back.f_back) and is_line:
# Ok, we have to track 2 stops at this point, the parent and the child offset.
# This happens when handling a step into which targets a function inside a list comprehension
# or generator (in which case an intermediary frame is created due to an internal function call).
@ -1104,7 +1164,7 @@ class PyDBFrame:
return None if is_call else NO_FTRACE
elif step_cmd in (CMD_STEP_RETURN, CMD_STEP_RETURN_MY_CODE):
stop = is_return and stop_frame is frame
stop = is_return and self._is_same_frame(stop_frame, frame)
else:
stop = False

View file

@ -3,34 +3,42 @@ import os
from _pydev_bundle import pydev_log
from _pydevd_bundle.pydevd_trace_dispatch import USING_CYTHON
from _pydevd_bundle.pydevd_constants import USE_CYTHON_FLAG, ENV_FALSE_LOWER_VALUES, \
ENV_TRUE_LOWER_VALUES, IS_PY36_OR_GREATER, IS_PY38_OR_GREATER, SUPPORT_GEVENT, IS_PYTHON_STACKLESS
ENV_TRUE_LOWER_VALUES, IS_PY36_OR_GREATER, IS_PY38_OR_GREATER, SUPPORT_GEVENT, IS_PYTHON_STACKLESS, \
PYDEVD_USE_FRAME_EVAL, PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING
frame_eval_func = None
stop_frame_eval = None
dummy_trace_dispatch = None
clear_thread_local_info = None
USING_FRAME_EVAL = False
# "NO" means we should not use frame evaluation, 'YES' we should use it (and fail if not there) and unspecified uses if possible.
use_frame_eval = os.environ.get('PYDEVD_USE_FRAME_EVAL', '').lower()
if (
PYDEVD_USE_FRAME_EVAL in ENV_FALSE_LOWER_VALUES or
USE_CYTHON_FLAG in ENV_FALSE_LOWER_VALUES or
not USING_CYTHON or
if use_frame_eval in ENV_FALSE_LOWER_VALUES or USE_CYTHON_FLAG in ENV_FALSE_LOWER_VALUES or not USING_CYTHON:
pass
# Frame eval mode does not work with ipython compatible debugging (this happens because the
# way that frame eval works is run untraced and set tracing only for the frames with
# breakpoints, but ipython compatible debugging creates separate frames for what's logically
# the same frame).
PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING
):
USING_FRAME_EVAL = False
elif SUPPORT_GEVENT or (IS_PYTHON_STACKLESS and not IS_PY38_OR_GREATER):
pass
USING_FRAME_EVAL = False
# i.e gevent and frame eval mode don't get along very well.
# https://github.com/microsoft/debugpy/issues/189
# Same problem with Stackless.
# https://github.com/stackless-dev/stackless/issues/240
elif use_frame_eval in ENV_TRUE_LOWER_VALUES:
elif PYDEVD_USE_FRAME_EVAL in ENV_TRUE_LOWER_VALUES:
# Fail if unable to use
from _pydevd_frame_eval.pydevd_frame_eval_cython_wrapper import frame_eval_func, stop_frame_eval, dummy_trace_dispatch, clear_thread_local_info
USING_FRAME_EVAL = True
else:
USING_FRAME_EVAL = False
# Try to use if possible
if IS_PY36_OR_GREATER:
try:

View file

@ -1,4 +1,4 @@
/* Generated by Cython 0.29.30 */
/* Generated by Cython 0.29.32 */
/* BEGIN: Cython Metadata
{
@ -32,8 +32,8 @@ END: Cython Metadata */
#elif PY_VERSION_HEX < 0x02060000 || (0x03000000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x03030000)
#error Cython requires Python 2.6+ or Python 3.3+.
#else
#define CYTHON_ABI "0_29_30"
#define CYTHON_HEX_VERSION 0x001D1EF0
#define CYTHON_ABI "0_29_32"
#define CYTHON_HEX_VERSION 0x001D20F0
#define CYTHON_FUTURE_DIVISION 0
#include <stddef.h>
#ifndef offsetof
@ -72,6 +72,7 @@ END: Cython Metadata */
#define CYTHON_COMPILING_IN_PYPY 1
#define CYTHON_COMPILING_IN_PYSTON 0
#define CYTHON_COMPILING_IN_CPYTHON 0
#define CYTHON_COMPILING_IN_NOGIL 0
#undef CYTHON_USE_TYPE_SLOTS
#define CYTHON_USE_TYPE_SLOTS 0
#undef CYTHON_USE_PYTYPE_LOOKUP
@ -115,6 +116,7 @@ END: Cython Metadata */
#define CYTHON_COMPILING_IN_PYPY 0
#define CYTHON_COMPILING_IN_PYSTON 1
#define CYTHON_COMPILING_IN_CPYTHON 0
#define CYTHON_COMPILING_IN_NOGIL 0
#ifndef CYTHON_USE_TYPE_SLOTS
#define CYTHON_USE_TYPE_SLOTS 1
#endif
@ -155,10 +157,56 @@ END: Cython Metadata */
#ifndef CYTHON_UPDATE_DESCRIPTOR_DOC
#define CYTHON_UPDATE_DESCRIPTOR_DOC 0
#endif
#elif defined(PY_NOGIL)
#define CYTHON_COMPILING_IN_PYPY 0
#define CYTHON_COMPILING_IN_PYSTON 0
#define CYTHON_COMPILING_IN_CPYTHON 0
#define CYTHON_COMPILING_IN_NOGIL 1
#ifndef CYTHON_USE_TYPE_SLOTS
#define CYTHON_USE_TYPE_SLOTS 1
#endif
#undef CYTHON_USE_PYTYPE_LOOKUP
#define CYTHON_USE_PYTYPE_LOOKUP 0
#ifndef CYTHON_USE_ASYNC_SLOTS
#define CYTHON_USE_ASYNC_SLOTS 1
#endif
#undef CYTHON_USE_PYLIST_INTERNALS
#define CYTHON_USE_PYLIST_INTERNALS 0
#ifndef CYTHON_USE_UNICODE_INTERNALS
#define CYTHON_USE_UNICODE_INTERNALS 1
#endif
#undef CYTHON_USE_UNICODE_WRITER
#define CYTHON_USE_UNICODE_WRITER 0
#undef CYTHON_USE_PYLONG_INTERNALS
#define CYTHON_USE_PYLONG_INTERNALS 0
#ifndef CYTHON_AVOID_BORROWED_REFS
#define CYTHON_AVOID_BORROWED_REFS 0
#endif
#ifndef CYTHON_ASSUME_SAFE_MACROS
#define CYTHON_ASSUME_SAFE_MACROS 1
#endif
#ifndef CYTHON_UNPACK_METHODS
#define CYTHON_UNPACK_METHODS 1
#endif
#undef CYTHON_FAST_THREAD_STATE
#define CYTHON_FAST_THREAD_STATE 0
#undef CYTHON_FAST_PYCALL
#define CYTHON_FAST_PYCALL 0
#ifndef CYTHON_PEP489_MULTI_PHASE_INIT
#define CYTHON_PEP489_MULTI_PHASE_INIT 1
#endif
#ifndef CYTHON_USE_TP_FINALIZE
#define CYTHON_USE_TP_FINALIZE 1
#endif
#undef CYTHON_USE_DICT_VERSIONS
#define CYTHON_USE_DICT_VERSIONS 0
#undef CYTHON_USE_EXC_INFO_STACK
#define CYTHON_USE_EXC_INFO_STACK 0
#else
#define CYTHON_COMPILING_IN_PYPY 0
#define CYTHON_COMPILING_IN_PYSTON 0
#define CYTHON_COMPILING_IN_CPYTHON 1
#define CYTHON_COMPILING_IN_NOGIL 0
#ifndef CYTHON_USE_TYPE_SLOTS
#define CYTHON_USE_TYPE_SLOTS 1
#endif
@ -993,6 +1041,7 @@ struct __pyx_obj_14_pydevd_bundle_13pydevd_cython_PyDBAdditionalThreadInfo {
int pydev_smart_child_offset;
PyObject *pydev_smart_step_into_variants;
PyObject *target_id_to_smart_step_into_variant;
int pydev_use_scoped_step_frame;
};

View file

@ -133,12 +133,12 @@ conda deactivate
cd /D x:\pydev\plugins\org.python.pydev.core\pysrc
set PYTHONPATH=x:\pydev\plugins\org.python.pydev.core\pysrc
C:\bin\Python38-32\python build_tools\build.py
python build_tools\build.py
${ptvsd_folder}
cd /D X:\ptvsd_workspace\ptvsd\src\debugpy\_vendored\pydevd
set PYTHONPATH=X:\ptvsd_workspace\ptvsd\src\debugpy\_vendored\pydevd
C:\bin\Python38-32\python build_tools\build.py
python build_tools\build.py
cd ~/Desktop/Pydev/plugins/org.python.pydev.core/pysrc

View file

@ -55,7 +55,8 @@ from _pydevd_bundle.pydevd_constants import (get_thread_id, get_current_thread_i
DebugInfoHolder, PYTHON_SUSPEND, STATE_SUSPEND, STATE_RUN, get_frame,
clear_cached_thread_id, INTERACTIVE_MODE_AVAILABLE, SHOW_DEBUG_INFO_ENV, NULL,
NO_FTRACE, IS_IRONPYTHON, JSON_PROTOCOL, IS_CPYTHON, HTTP_JSON_PROTOCOL, USE_CUSTOM_SYS_CURRENT_FRAMES_MAP, call_only_once,
ForkSafeLock, IGNORE_BASENAMES_STARTING_WITH, EXCEPTION_TYPE_UNHANDLED, SUPPORT_GEVENT)
ForkSafeLock, IGNORE_BASENAMES_STARTING_WITH, EXCEPTION_TYPE_UNHANDLED, SUPPORT_GEVENT,
PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING, PYDEVD_IPYTHON_CONTEXT)
from _pydevd_bundle.pydevd_defaults import PydevdCustomization # Note: import alias used on pydev_monkey.
from _pydevd_bundle.pydevd_custom_frames import CustomFramesContainer, custom_frames_container_init
from _pydevd_bundle.pydevd_dont_trace_files import DONT_TRACE, PYDEV_FILE, LIB_FILE, DONT_TRACE_DIRS
@ -181,6 +182,9 @@ _CACHE_FILE_TYPE = {}
pydev_log.debug('Using GEVENT_SUPPORT: %s', pydevd_constants.SUPPORT_GEVENT)
pydev_log.debug('Using GEVENT_SHOW_PAUSED_GREENLETS: %s', pydevd_constants.GEVENT_SHOW_PAUSED_GREENLETS)
pydev_log.debug('pydevd __file__: %s', os.path.abspath(__file__))
pydev_log.debug('Using PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING: %s', pydevd_constants.PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING)
if pydevd_constants.PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING:
pydev_log.debug('PYDEVD_IPYTHON_CONTEXT: %s', pydevd_constants.PYDEVD_IPYTHON_CONTEXT)
#=======================================================================================================================
@ -2170,6 +2174,23 @@ class PyDB(object):
info.pydev_step_cmd = -1
info.pydev_state = STATE_RUN
if PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING:
info.pydev_use_scoped_step_frame = False
if info.pydev_step_cmd in (
CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE,
CMD_STEP_INTO, CMD_STEP_INTO_MY_CODE
):
# i.e.: We're stepping: check if the stepping should be scoped (i.e.: in ipython
# each line is executed separately in a new frame, in which case we need to consider
# the next line as if it was still in the same frame).
f = frame.f_back
if f and f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[1]:
f = f.f_back
if f and f.f_code.co_name == PYDEVD_IPYTHON_CONTEXT[2]:
info.pydev_use_scoped_step_frame = True
pydev_log.info('Using (ipython) scoped stepping.')
del f
del frame
cmd = self.cmd_factory.make_thread_run_message(get_current_thread_id(thread), info.pydev_step_cmd)
self.writer.add_command(cmd)

View file

@ -0,0 +1,21 @@
import asyncio
import sys
async def gen():
f = sys._getframe()
for i in range(10):
await asyncio.sleep(.01)
assert f is sys._getframe()
yield i
async def run():
async for p in gen():
print(p)
if __name__ == "__main__":
loop = asyncio.get_event_loop_policy().get_event_loop()
loop.run_until_complete(run())
print('TEST SUCEEDED')

View file

@ -0,0 +1,145 @@
from ast import Module
from ast import stmt
from typing import List as ListType
import ast
import inspect
from ast import PyCF_ONLY_AST, PyCF_ALLOW_TOP_LEVEL_AWAIT
import types
import os
from contextlib import contextmanager
PyCF_DONT_IMPLY_DEDENT = 0x200 # Matches pythonrun.h
_assign_nodes = (ast.AugAssign, ast.AnnAssign, ast.Assign)
_single_targets_nodes = (ast.AugAssign, ast.AnnAssign)
user_module = types.ModuleType("__main__",
doc="Automatically created module for IPython interactive environment")
stored = []
def tracefunc(frame, event, arg):
if '_debugger_case_scoped_stepping_target' not in frame.f_code.co_filename:
return None
stored.append(frame)
print('\n---')
print(event, id(frame), os.path.basename(frame.f_code.co_filename), frame.f_lineno, arg, frame.f_code.co_name)
assert frame.f_back.f_code.co_name == 'run_code'
return None
@contextmanager
def tracing_info():
import sys
sys.settrace(tracefunc)
try:
yield
finally:
sys.settrace(None)
# Note: this is roughly what IPython itself does at:
# https://github.com/ipython/ipython/blob/master/IPython/core/interactiveshell.py
class Runner:
async def run_ast_nodes(
self,
nodelist: ListType[stmt],
cell_name: str,
interactivity="last_expr",
compiler=compile,
):
if not nodelist:
return
if interactivity == 'last_expr_or_assign':
if isinstance(nodelist[-1], _assign_nodes):
asg = nodelist[-1]
if isinstance(asg, ast.Assign) and len(asg.targets) == 1:
target = asg.targets[0]
elif isinstance(asg, _single_targets_nodes):
target = asg.target
else:
target = None
if isinstance(target, ast.Name):
nnode = ast.Expr(ast.Name(target.id, ast.Load()))
ast.fix_missing_locations(nnode)
nodelist.append(nnode)
interactivity = 'last_expr'
_async = False
if interactivity == 'last_expr':
if isinstance(nodelist[-1], ast.Expr):
interactivity = "last"
else:
interactivity = "none"
if interactivity == 'none':
to_run_exec, to_run_interactive = nodelist, []
elif interactivity == 'last':
to_run_exec, to_run_interactive = nodelist[:-1], nodelist[-1:]
elif interactivity == 'all':
to_run_exec, to_run_interactive = [], nodelist
else:
raise ValueError("Interactivity was %r" % interactivity)
def compare(code):
is_async = inspect.CO_COROUTINE & code.co_flags == inspect.CO_COROUTINE
return is_async
# Refactor that to just change the mod constructor.
to_run = []
for node in to_run_exec:
to_run.append((node, "exec"))
for node in to_run_interactive:
to_run.append((node, "single"))
for node, mode in to_run:
if mode == "exec":
mod = Module([node], [])
elif mode == "single":
mod = ast.Interactive([node])
code = compiler(mod, cell_name, mode, PyCF_DONT_IMPLY_DEDENT |
PyCF_ALLOW_TOP_LEVEL_AWAIT)
asy = compare(code)
if await self.run_code(code, async_=asy):
return True
async def run_code(self, code_obj, *, async_=False):
if async_:
await eval(code_obj, self.user_global_ns, self.user_ns)
else:
exec(code_obj, self.user_global_ns, self.user_ns)
@property
def user_global_ns(self):
return user_module.__dict__
@property
def user_ns(self):
return user_module.__dict__
async def main():
SCOPED_STEPPING_TARGET = os.getenv('SCOPED_STEPPING_TARGET', '_debugger_case_scoped_stepping_target.py')
filename = os.path.join(os.path.dirname(__file__), SCOPED_STEPPING_TARGET)
assert os.path.exists(filename), '%s does not exist.' % (filename,)
with open(filename, 'r') as stream:
source = stream.read()
code_ast = compile(
source,
filename,
'exec',
PyCF_DONT_IMPLY_DEDENT | PyCF_ONLY_AST | PyCF_ALLOW_TOP_LEVEL_AWAIT,
1)
runner = Runner()
await runner.run_ast_nodes(code_ast.body, filename)
if __name__ == '__main__':
import asyncio
asyncio.run(main())
print('TEST SUCEEDED!')

View file

@ -0,0 +1,9 @@
a = 1
def method():
b = 2
method() # break here
c = 3

View file

@ -0,0 +1,9 @@
# Note that await in the top-level isn't valid in general, but we compile
# it specifically accepting it, so, that's ok.
import asyncio
await asyncio.sleep(.01)
a = 1 # Break here
await asyncio.sleep(.01)
b = 2
await asyncio.sleep(.01)

View file

@ -6134,6 +6134,89 @@ print('TEST SUCEEDED')
writer.finished_ok = True
_TOP_LEVEL_AWAIT_AVAILABLE = False
try:
from ast import PyCF_ONLY_AST, PyCF_ALLOW_TOP_LEVEL_AWAIT
_TOP_LEVEL_AWAIT_AVAILABLE = True
except ImportError:
pass
@pytest.mark.skipif(not _TOP_LEVEL_AWAIT_AVAILABLE, reason="Top-level await required.")
def test_ipython_stepping_basic(case_setup):
def get_environ(self):
env = os.environ.copy()
# Test setup
env["SCOPED_STEPPING_TARGET"] = '_debugger_case_scoped_stepping_target.py'
# Actually setup the debugging
env["PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING"] = "1"
env["PYDEVD_IPYTHON_CONTEXT"] = '_debugger_case_scoped_stepping.py, run_code, run_ast_nodes'
return env
with case_setup.test_file('_debugger_case_scoped_stepping.py', get_environ=get_environ) as writer:
json_facade = JsonFacade(writer)
json_facade.write_launch(justMyCode=False)
target_file = debugger_unittest._get_debugger_test_file('_debugger_case_scoped_stepping_target.py')
break_line = writer.get_line_index_with_content('a = 1', filename=target_file)
assert break_line == 1
json_facade.write_set_breakpoints(break_line, filename=target_file)
json_facade.write_make_initial_run()
json_hit = json_facade.wait_for_thread_stopped(line=break_line, file='_debugger_case_scoped_stepping_target.py')
json_facade.write_step_next(json_hit.thread_id)
json_hit = json_facade.wait_for_thread_stopped('step', line=break_line + 1, file='_debugger_case_scoped_stepping_target.py')
json_facade.write_step_next(json_hit.thread_id)
json_hit = json_facade.wait_for_thread_stopped('step', line=break_line + 2, file='_debugger_case_scoped_stepping_target.py')
json_facade.write_continue()
writer.finished_ok = True
@pytest.mark.skipif(not _TOP_LEVEL_AWAIT_AVAILABLE, reason="Top-level await required.")
def test_ipython_stepping_step_in(case_setup):
def get_environ(self):
env = os.environ.copy()
# Test setup
env["SCOPED_STEPPING_TARGET"] = '_debugger_case_scoped_stepping_target2.py'
# Actually setup the debugging
env["PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING"] = "1"
env["PYDEVD_IPYTHON_CONTEXT"] = '_debugger_case_scoped_stepping.py, run_code, run_ast_nodes'
return env
with case_setup.test_file('_debugger_case_scoped_stepping.py', get_environ=get_environ) as writer:
json_facade = JsonFacade(writer)
json_facade.write_launch(justMyCode=False)
target_file = debugger_unittest._get_debugger_test_file('_debugger_case_scoped_stepping_target2.py')
break_line = writer.get_line_index_with_content('break here', filename=target_file)
json_facade.write_set_breakpoints(break_line, filename=target_file)
json_facade.write_make_initial_run()
json_hit = json_facade.wait_for_thread_stopped(line=break_line, file='_debugger_case_scoped_stepping_target2.py')
json_facade.write_step_in(json_hit.thread_id)
stop_at = writer.get_line_index_with_content('b = 2', filename=target_file)
json_hit = json_facade.wait_for_thread_stopped('step', line=stop_at, file='_debugger_case_scoped_stepping_target2.py')
json_facade.write_step_in(json_hit.thread_id)
stop_at = writer.get_line_index_with_content('method() # break here', filename=target_file)
json_hit = json_facade.wait_for_thread_stopped('step', line=stop_at, file='_debugger_case_scoped_stepping_target2.py')
json_facade.write_step_in(json_hit.thread_id)
stop_at = writer.get_line_index_with_content('c = 3', filename=target_file)
json_hit = json_facade.wait_for_thread_stopped('step', line=stop_at, file='_debugger_case_scoped_stepping_target2.py')
json_facade.write_continue()
writer.finished_ok = True
if __name__ == '__main__':
pytest.main(['-k', 'test_replace_process', '-s'])