Support step into target. Fixes #288

This commit is contained in:
Fabio Zadrozny 2021-03-12 14:06:11 -03:00
parent cdc975c4fb
commit 2ebc4e71b5
26 changed files with 6106 additions and 4553 deletions

View file

@ -28,7 +28,6 @@ class PyDBAdditionalThreadInfo(object):
'pydev_original_step_cmd',
'pydev_step_cmd',
'pydev_notify_kill',
'pydev_smart_step_stop',
'pydev_django_resolve_frame',
'pydev_call_from_jinja2',
'pydev_call_inside_jinja2',
@ -44,6 +43,14 @@ class PyDBAdditionalThreadInfo(object):
'top_level_thread_tracer_unhandled',
'thread_tracer',
'step_in_initial_location',
# Used for CMD_SMART_STEP_INTO (to know which smart step into variant to use)
'pydev_smart_parent_offset',
# Used for CMD_SMART_STEP_INTO (list[_pydevd_bundle.pydevd_bytecode_utils.Variant])
# Filled when the cmd_get_smart_step_into_variants is requested (so, this is a copy
# of the last request for a given thread and pydev_smart_parent_offset relies on it).
'pydev_smart_step_into_variants',
]
# ENDIF
@ -61,7 +68,6 @@ class PyDBAdditionalThreadInfo(object):
self.pydev_step_cmd = -1 # Something as CMD_STEP_INTO, CMD_STEP_OVER, etc.
self.pydev_notify_kill = False
self.pydev_smart_step_stop = None
self.pydev_django_resolve_frame = False
self.pydev_call_from_jinja2 = None
self.pydev_call_inside_jinja2 = None
@ -77,6 +83,8 @@ class PyDBAdditionalThreadInfo(object):
self.top_level_thread_tracer_unhandled = None
self.thread_tracer = None
self.step_in_initial_location = None
self.pydev_smart_parent_offset = -1
self.pydev_smart_step_into_variants = ()
def get_topmost_frame(self, thread):
'''

View file

@ -11,9 +11,9 @@ from _pydevd_bundle.pydevd_comm import (InternalGetThreadStack, internal_get_com
internal_get_description, internal_get_frame, internal_evaluate_expression, InternalConsoleExec,
internal_get_variable_json, internal_change_variable, internal_change_variable_json,
internal_evaluate_expression_json, internal_set_expression_json, internal_get_exception_details_json,
internal_step_in_thread)
internal_step_in_thread, internal_smart_step_into)
from _pydevd_bundle.pydevd_comm_constants import (CMD_THREAD_SUSPEND, file_system_encoding,
CMD_STEP_INTO_MY_CODE, CMD_STOP_ON_START)
CMD_STEP_INTO_MY_CODE, CMD_STOP_ON_START, CMD_SMART_STEP_INTO)
from _pydevd_bundle.pydevd_constants import (get_current_thread_id, set_protocol, get_protocol,
HTTP_JSON_PROTOCOL, JSON_PROTOCOL, IS_PY3K, DebugInfoHolder, dict_keys, dict_items, IS_WINDOWS)
from _pydevd_bundle.pydevd_net_command_factory_json import NetCommandFactoryJson
@ -228,8 +228,28 @@ class PyDevdAPI(object):
elif thread_id.startswith('__frame__:'):
sys.stderr.write("Can't make tasklet step command: %s\n" % (thread_id,))
def request_smart_step_into(self, py_db, seq, thread_id, offset):
t = pydevd_find_thread_by_id(thread_id)
if t:
py_db.post_method_as_internal_command(
thread_id, internal_smart_step_into, thread_id, offset, set_additional_thread_info=set_additional_thread_info)
elif thread_id.startswith('__frame__:'):
sys.stderr.write("Can't set next statement in tasklet: %s\n" % (thread_id,))
def request_smart_step_into_by_func_name(self, py_db, seq, thread_id, line, func_name):
# Same thing as set next, just with a different cmd id.
self.request_set_next(py_db, seq, thread_id, CMD_SMART_STEP_INTO, None, line, func_name)
def request_set_next(self, py_db, seq, thread_id, set_next_cmd_id, original_filename, line, func_name):
'''
set_next_cmd_id may actually be one of:
CMD_RUN_TO_LINE
CMD_SET_NEXT_STATEMENT
CMD_SMART_STEP_INTO -- note: request_smart_step_into is preferred if it's possible
to work with bytecode offset.
:param Optional[str] original_filename:
If available, the filename may be source translated, otherwise no translation will take
place (the set next just needs the line afterwards as it executes locally, but for

View file

@ -0,0 +1,300 @@
"""Bytecode analysing utils. Originally added for using in smart step into."""
import dis
import inspect
from collections import namedtuple
from _pydevd_bundle.pydevd_constants import IS_PY3K, KeyifyList
from bisect import bisect
_LOAD_OPNAMES = {
'LOAD_BUILD_CLASS',
'LOAD_CONST',
'LOAD_NAME',
'LOAD_ATTR',
'LOAD_GLOBAL',
'LOAD_FAST',
'LOAD_CLOSURE',
'LOAD_DEREF',
}
_CALL_OPNAMES = {
'CALL_FUNCTION',
'CALL_FUNCTION_KW',
}
if IS_PY3K:
for opname in ('LOAD_CLASSDEREF', 'LOAD_METHOD'):
_LOAD_OPNAMES.add(opname)
for opname in ('CALL_FUNCTION_EX', 'CALL_METHOD'):
_CALL_OPNAMES.add(opname)
else:
_LOAD_OPNAMES.add('LOAD_LOCALS')
for opname in ('CALL_FUNCTION_VAR', 'CALL_FUNCTION_VAR_KW'):
_CALL_OPNAMES.add(opname)
_BINARY_OPS = set([opname for opname in dis.opname if opname.startswith('BINARY_')])
_BINARY_OP_MAP = {
'BINARY_POWER': '__pow__',
'BINARY_MULTIPLY': '__mul__',
'BINARY_MATRIX_MULTIPLY': '__matmul__',
'BINARY_FLOOR_DIVIDE': '__floordiv__',
'BINARY_TRUE_DIVIDE': '__div__',
'BINARY_MODULO': '__mod__',
'BINARY_ADD': '__add__',
'BINARY_SUBTRACT': '__sub__',
'BINARY_LSHIFT': '__lshift__',
'BINARY_RSHIFT': '__rshift__',
'BINARY_AND': '__and__',
'BINARY_OR': '__or__',
'BINARY_XOR': '__xor__',
'BINARY_SUBSCR': '__getitem__',
}
if not IS_PY3K:
_BINARY_OP_MAP['BINARY_DIVIDE'] = '__div__'
_UNARY_OPS = set([opname for opname in dis.opname if opname.startswith('UNARY_') and opname != 'UNARY_NOT'])
_UNARY_OP_MAP = {
'UNARY_POSITIVE': '__pos__',
'UNARY_NEGATIVE': '__neg__',
'UNARY_INVERT': '__invert__',
}
_MAKE_OPS = set([opname for opname in dis.opname if opname.startswith('MAKE_')])
_COMP_OP_MAP = {
'<': '__lt__',
'<=': '__le__',
'==': '__eq__',
'!=': '__ne__',
'>': '__gt__',
'>=': '__ge__',
'in': '__contains__',
'not in': '__contains__',
}
def _is_load_opname(opname):
return opname in _LOAD_OPNAMES
def _is_call_opname(opname):
return opname in _CALL_OPNAMES
def _is_binary_opname(opname):
return opname in _BINARY_OPS
def _is_unary_opname(opname):
return opname in _UNARY_OPS
def _is_make_opname(opname):
return opname in _MAKE_OPS
# Similar to :py:class:`dis._Instruction` but without fields we don't use. Also :py:class:`dis._Instruction`
# is not available in Python 2.
Instruction = namedtuple("Instruction", ["opname", "opcode", "arg", "argval", "lineno", "offset"])
if IS_PY3K:
long = int
try:
_unpack_opargs = dis._unpack_opargs
except AttributeError:
def _unpack_opargs(code):
n = len(code)
i = 0
extended_arg = 0
while i < n:
c = code[i]
op = ord(c)
offset = i
arg = None
i += 1
if op >= dis.HAVE_ARGUMENT:
arg = ord(code[i]) + ord(code[i + 1]) * 256 + extended_arg
extended_arg = 0
i += 2
if op == dis.EXTENDED_ARG:
extended_arg = arg * long(65536)
yield (offset, op, arg)
def _code_to_name(inst):
"""If thw instruction's ``argval`` is :py:class:`types.CodeType`, replace it with the name and return the updated instruction.
:type inst: :py:class:`Instruction`
:rtype: :py:class:`Instruction`
"""
if inspect.iscode(inst.argval):
return inst._replace(argval=inst.argval.co_name)
return inst
def _get_smart_step_into_candidates(code):
"""Iterate through the bytecode and return a list of instructions which can be smart step into candidates.
:param code: A code object where we searching for calls.
:type code: :py:class:`types.CodeType`
:return: list of :py:class:`~Instruction` that represents the objects that were called
by one of the Python call instructions.
:raise: :py:class:`RuntimeError` if failed to parse the bytecode or if dis cannot be used.
"""
try:
linestarts = dict(dis.findlinestarts(code))
except Exception:
raise RuntimeError("Unable to get smart step into candidates because dis.findlinestarts is not available.")
varnames = code.co_varnames
names = code.co_names
constants = code.co_consts
freevars = code.co_freevars
lineno = None
stk = [] # only the instructions related to calls are pushed in the stack
result = []
for offset, op, arg in _unpack_opargs(code.co_code):
try:
if linestarts is not None:
lineno = linestarts.get(offset, None) or lineno
opname = dis.opname[op]
argval = None
if arg is None:
if _is_binary_opname(opname):
stk.pop()
result.append(Instruction(opname, op, arg, _BINARY_OP_MAP[opname], lineno, offset))
elif _is_unary_opname(opname):
result.append(Instruction(opname, op, arg, _UNARY_OP_MAP[opname], lineno, offset))
if opname == 'COMPARE_OP':
stk.pop()
cmp_op = dis.cmp_op[arg]
if cmp_op not in ('exception match', 'BAD'):
result.append(Instruction(opname, op, arg, _COMP_OP_MAP.get(cmp_op, cmp_op), lineno, offset))
if _is_load_opname(opname):
if opname == 'LOAD_CONST':
argval = constants[arg]
elif opname == 'LOAD_NAME' or opname == 'LOAD_GLOBAL':
argval = names[arg]
elif opname == 'LOAD_ATTR':
stk.pop()
argval = names[arg]
elif opname == 'LOAD_FAST':
argval = varnames[arg]
elif IS_PY3K and opname == 'LOAD_METHOD':
stk.pop()
argval = names[arg]
elif opname == 'LOAD_DEREF':
argval = freevars[arg]
stk.append(Instruction(opname, op, arg, argval, lineno, offset))
elif _is_make_opname(opname):
tos = stk.pop() # qualified name of the function or function code in Python 2
argc = 0
if IS_PY3K:
stk.pop() # function code
for flag in (0x01, 0x02, 0x04, 0x08):
if arg & flag:
argc += 1 # each flag means one extra element to pop
else:
argc = arg
tos = _code_to_name(tos)
while argc > 0:
stk.pop()
argc -= 1
stk.append(tos)
elif _is_call_opname(opname):
argc = arg # the number of the function or method arguments
if opname == 'CALL_FUNCTION_KW' or not IS_PY3K and opname == 'CALL_FUNCTION_VAR':
stk.pop() # pop the mapping or iterable with arguments or parameters
elif not IS_PY3K and opname == 'CALL_FUNCTION_VAR_KW':
stk.pop() # pop the mapping with arguments
stk.pop() # pop the iterable with parameters
elif not IS_PY3K and opname == 'CALL_FUNCTION':
argc = arg & 0xff # positional args
argc += ((arg >> 8) * 2) # keyword args
elif opname == 'CALL_FUNCTION_EX':
has_keyword_args = arg & 0x01
if has_keyword_args:
stk.pop()
stk.pop() # positional args
argc = 0
while argc > 0:
stk.pop() # popping args from the stack
argc -= 1
tos = _code_to_name(stk[-1])
if tos.opname == 'LOAD_BUILD_CLASS':
# an internal `CALL_FUNCTION` for building a class
continue
result.append(tos._replace(offset=offset)) # the actual offset is not when a function was loaded but when it was called
except:
err_msg = "Bytecode parsing error at: offset(%d), opname(%s), arg(%d)" % (offset, dis.opname[op], arg)
raise RuntimeError(err_msg)
return result
# Note that the offset is unique within the frame (so, we can use it as the target id).
# Also, as the offset is the instruction offset within the frame, it's possible to
# to inspect the parent frame for frame.f_lasti to know where we actually are (as the
# caller name may not always match the new frame name).
Variant = namedtuple('Variant', ['name', 'is_visited', 'line', 'offset', 'call_order'])
def calculate_smart_step_into_variants(frame, start_line, end_line, base=0):
"""
Calculate smart step into variants for the given line range.
:param frame:
:type frame: :py:class:`types.FrameType`
:param start_line:
:param end_line:
:return: A list of call names from the first to the last.
:note: it's guaranteed that the offsets appear in order.
:raise: :py:class:`RuntimeError` if failed to parse the bytecode or if dis cannot be used.
"""
variants = []
is_context_reached = False
code = frame.f_code
lasti = frame.f_lasti
call_order_cache = {}
for inst in _get_smart_step_into_candidates(code):
if not isinstance(inst.argval, str):
continue
if inst.lineno and inst.lineno > end_line:
break
if not is_context_reached and inst.lineno is not None and inst.lineno >= start_line:
is_context_reached = True
if not is_context_reached:
continue
call_order = call_order_cache.get(inst.argval, 0) + 1
call_order_cache[inst.argval] = call_order
variants.append(
Variant(
inst.argval, inst.offset <= lasti, inst.lineno - base, inst.offset, call_order))
return variants
def get_smart_step_into_variant_from_frame_offset(frame_f_lasti, variants):
"""
Given the frame.f_lasti, return the related `Variant`.
:note: if the offset is found before any variant available or no variants are
available, None is returned.
:rtype: Variant|NoneType
"""
if not variants:
return None
i = bisect(KeyifyList(variants, lambda entry:entry.offset), frame_f_lasti)
if i == 0:
return None
else:
return variants[i - 1]

View file

@ -63,7 +63,6 @@ each command has a format:
* PYDB - pydevd, the python end
'''
import itertools
import linecache
import os
@ -71,14 +70,14 @@ from _pydev_bundle.pydev_imports import _queue
from _pydev_imps._pydev_saved_modules import time
from _pydev_imps._pydev_saved_modules import threading
from _pydev_imps._pydev_saved_modules import socket as socket_module
from _pydevd_bundle.pydevd_constants import (DebugInfoHolder, get_thread_id, IS_WINDOWS, IS_JYTHON,
from _pydevd_bundle.pydevd_constants import (DebugInfoHolder, IS_WINDOWS, IS_JYTHON,
IS_PY2, IS_PY36_OR_GREATER, STATE_RUN, dict_keys, ASYNC_EVAL_TIMEOUT_SEC,
get_global_debugger, GetGlobalDebugger, set_global_debugger, silence_warnings_decorator) # Keep for backward compatibility @UnusedImport
from _pydev_bundle.pydev_override import overrides
import weakref
from _pydev_bundle._pydev_completer import extract_token_and_qualifier
from _pydevd_bundle._debug_adapter.pydevd_schema import VariablesResponseBody, \
SetVariableResponseBody
SetVariableResponseBody, StepInTarget, StepInTargetsResponseBody
from _pydevd_bundle._debug_adapter import pydevd_base_schema, pydevd_schema
from _pydevd_bundle.pydevd_net_command import NetCommand
from _pydevd_bundle.pydevd_xml import ExceptionOnEvaluate
@ -95,7 +94,7 @@ except:
from urllib.parse import quote_plus, unquote_plus # @Reimport @UnresolvedImport
import pydevconsole
from _pydevd_bundle import pydevd_vars, pydevd_utils, pydevd_io, pydevd_reload
from _pydevd_bundle import pydevd_vars, pydevd_io, pydevd_reload, pydevd_bytecode_utils
from _pydevd_bundle import pydevd_xml
from _pydevd_bundle import pydevd_vm_type
import sys
@ -614,7 +613,6 @@ def internal_reload_code(dbg, seq, module_name, filename):
else:
# Too much info...
# _send_io_message(dbg, 'code reload: This usually means you are trying to reload the __main__ module (which cannot be reloaded).\n')
from _pydevd_bundle import pydevd_reload
for module, module_name in modules_to_reload.values():
_send_io_message(dbg, 'code reload: Start reloading module: "' + module_name + '" ... \n')
found_module_to_reload = True
@ -691,9 +689,30 @@ def internal_step_in_thread(py_db, thread_id, cmd_id, set_additional_thread_info
resume_threads('*', except_thread=thread_to_step)
def internal_smart_step_into(py_db, thread_id, offset, set_additional_thread_info):
thread_to_step = pydevd_find_thread_by_id(thread_id)
if thread_to_step:
info = set_additional_thread_info(thread_to_step)
info.pydev_original_step_cmd = CMD_SMART_STEP_INTO
info.pydev_step_cmd = CMD_SMART_STEP_INTO
info.pydev_step_stop = None
info.pydev_smart_parent_offset = int(offset)
info.pydev_state = STATE_RUN
if py_db.stepping_resumes_all_threads:
resume_threads('*', except_thread=thread_to_step)
class InternalSetNextStatementThread(InternalThreadCommand):
def __init__(self, thread_id, cmd_id, line, func_name, seq=0):
'''
cmd_id may actually be one of:
CMD_RUN_TO_LINE
CMD_SET_NEXT_STATEMENT
CMD_SMART_STEP_INTO
'''
self.thread_id = thread_id
self.cmd_id = cmd_id
self.line = line
@ -709,13 +728,15 @@ class InternalSetNextStatementThread(InternalThreadCommand):
def do_it(self, dbg):
t = pydevd_find_thread_by_id(self.thread_id)
if t:
t.additional_info.pydev_original_step_cmd = self.cmd_id
t.additional_info.pydev_step_cmd = self.cmd_id
t.additional_info.pydev_step_stop = None
t.additional_info.pydev_next_line = int(self.line)
t.additional_info.pydev_func_name = self.func_name
t.additional_info.pydev_state = STATE_RUN
t.additional_info.pydev_message = str(self.seq)
info = t.additional_info
info.pydev_original_step_cmd = self.cmd_id
info.pydev_step_cmd = self.cmd_id
info.pydev_step_stop = None
info.pydev_next_line = int(self.line)
info.pydev_func_name = self.func_name
info.pydev_message = str(self.seq)
info.pydev_smart_parent_offset = -1
info.pydev_state = STATE_RUN
@silence_warnings_decorator
@ -934,6 +955,99 @@ def internal_get_frame(dbg, seq, thread_id, frame_id):
dbg.writer.add_command(cmd)
def internal_get_smart_step_into_variants(dbg, seq, thread_id, frame_id, start_line, end_line, set_additional_thread_info):
try:
thread = pydevd_find_thread_by_id(thread_id)
frame = dbg.find_frame(thread_id, frame_id)
if thread is None or frame is None:
cmd = dbg.cmd_factory.make_error_message(seq, "Frame not found: %s from thread: %s" % (frame_id, thread_id))
dbg.writer.add_command(cmd)
return
variants = pydevd_bytecode_utils.calculate_smart_step_into_variants(frame, int(start_line), int(end_line))
info = set_additional_thread_info(thread)
# Store the last request (may be used afterwards when stepping).
info.pydev_smart_step_into_variants = tuple(variants)
xml = "<xml>"
for variant in variants:
xml += '<variant name="%s" isVisited="%s" line="%s" offset="%s" callOrder="%s"/>' % (
quote(variant.name),
str(variant.is_visited).lower(),
variant.line,
variant.offset,
variant.call_order,
)
xml += "</xml>"
cmd = NetCommand(CMD_GET_SMART_STEP_INTO_VARIANTS, seq, xml)
dbg.writer.add_command(cmd)
except:
# Error is expected (if `dis` module cannot be used -- i.e.: Jython).
pydev_log.exception('Error calculating Smart Step Into Variants.')
cmd = dbg.cmd_factory.make_error_message(
seq, "Error getting smart step into variants for frame: %s from thread: %s"
% (frame_id, thread_id))
dbg.writer.add_command(cmd)
def internal_get_step_in_targets_json(dbg, seq, thread_id, frame_id, request, set_additional_thread_info):
try:
thread = pydevd_find_thread_by_id(thread_id)
frame = dbg.find_frame(thread_id, frame_id)
if thread is None or frame is None:
body = StepInTargetsResponseBody([])
variables_response = pydevd_base_schema.build_response(
request,
kwargs={
'body': body,
'success': False,
'message': 'Thread to get step in targets seems to have resumed already.'
})
cmd = NetCommand(CMD_RETURN, 0, variables_response, is_json=True)
dbg.writer.add_command(cmd)
return
start_line = 0
end_line = 99999999
variants = pydevd_bytecode_utils.calculate_smart_step_into_variants(frame, start_line, end_line)
info = set_additional_thread_info(thread)
targets = []
for variant in variants:
if not variant.is_visited:
if variant.call_order > 1:
targets.append(StepInTarget(id=variant.offset, label='%s (call %s)' % (variant.name, variant.call_order),))
else:
targets.append(StepInTarget(id=variant.offset, label=variant.name))
if len(targets) >= 15: # Show at most 15 targets.
break
# Store the last request (may be used afterwards when stepping).
info.pydev_smart_step_into_variants = tuple(variants)
body = StepInTargetsResponseBody(targets=targets)
response = pydevd_base_schema.build_response(request, kwargs={'body': body})
cmd = NetCommand(CMD_RETURN, 0, response, is_json=True)
dbg.writer.add_command(cmd)
except Exception as e:
# Error is expected (if `dis` module cannot be used -- i.e.: Jython).
pydev_log.exception('Error calculating Smart Step Into Variants.')
body = StepInTargetsResponseBody([])
variables_response = pydevd_base_schema.build_response(
request,
kwargs={
'body': body,
'success': False,
'message': str(e)
})
cmd = NetCommand(CMD_RETURN, 0, variables_response, is_json=True)
dbg.writer.add_command(cmd)
def internal_get_next_statement_targets(dbg, seq, thread_id, frame_id):
''' gets the valid line numbers for use with set next statement '''
try:

View file

@ -81,6 +81,8 @@ CMD_STEP_RETURN_MY_CODE = 160
CMD_SET_PY_EXCEPTION_JSON = 161
CMD_SET_PATH_MAPPING_JSON = 162
CMD_GET_SMART_STEP_INTO_VARIANTS = 163 # XXX: PyCharm has 160 for this (we're currently incompatible anyways).
CMD_REDIRECT_OUTPUT = 200
CMD_GET_NEXT_STATEMENT_TARGETS = 201
CMD_SET_PROJECT_ROOTS = 202
@ -176,6 +178,7 @@ ID_TO_MEANING = {
'161': 'CMD_SET_PY_EXCEPTION_JSON',
'162': 'CMD_SET_PATH_MAPPING_JSON',
'163': 'CMD_GET_SMART_STEP_INTO_VARIANTS',
'200': 'CMD_REDIRECT_OUTPUT',
'201': 'CMD_GET_NEXT_STATEMENT_TARGETS',

View file

@ -743,6 +743,19 @@ class Null:
NULL = Null()
class KeyifyList(object):
def __init__(self, inner, key):
self.inner = inner
self.key = key
def __len__(self):
return len(self.inner)
def __getitem__(self, k):
return self.key(self.inner[k])
def call_only_once(func):
'''
To be used as a decorator

File diff suppressed because it is too large Load diff

View file

@ -20,3 +20,5 @@ cdef class PyDBAdditionalThreadInfo:
cdef public object top_level_thread_tracer_unhandled;
cdef public object thread_tracer;
cdef public object step_in_initial_location;
cdef public int pydev_smart_parent_offset;
cdef public tuple pydev_smart_step_into_variants;

View file

@ -34,7 +34,6 @@ cdef class PyDBAdditionalThreadInfo:
# 'pydev_original_step_cmd',
# 'pydev_step_cmd',
# 'pydev_notify_kill',
# 'pydev_smart_step_stop',
# 'pydev_django_resolve_frame',
# 'pydev_call_from_jinja2',
# 'pydev_call_inside_jinja2',
@ -50,6 +49,14 @@ cdef class PyDBAdditionalThreadInfo:
# 'top_level_thread_tracer_unhandled',
# 'thread_tracer',
# 'step_in_initial_location',
#
# # Used for CMD_SMART_STEP_INTO (to know which smart step into variant to use)
# 'pydev_smart_parent_offset',
#
# # Used for CMD_SMART_STEP_INTO (list[_pydevd_bundle.pydevd_bytecode_utils.Variant])
# # Filled when the cmd_get_smart_step_into_variants is requested (so, this is a copy
# # of the last request for a given thread and pydev_smart_parent_offset relies on it).
# 'pydev_smart_step_into_variants',
# ]
# ENDIF
@ -67,7 +74,6 @@ cdef class PyDBAdditionalThreadInfo:
self.pydev_step_cmd = -1 # Something as CMD_STEP_INTO, CMD_STEP_OVER, etc.
self.pydev_notify_kill = False
self.pydev_smart_step_stop = None
self.pydev_django_resolve_frame = False
self.pydev_call_from_jinja2 = None
self.pydev_call_inside_jinja2 = None
@ -83,6 +89,8 @@ cdef class PyDBAdditionalThreadInfo:
self.top_level_thread_tracer_unhandled = None
self.thread_tracer = None
self.step_in_initial_location = None
self.pydev_smart_parent_offset = -1
self.pydev_smart_step_into_variants = ()
def get_topmost_frame(self, thread):
'''
@ -143,6 +151,7 @@ from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raise
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
from _pydevd_bundle.pydevd_comm_constants import constant_to_str
from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
# ELSE
@ -641,6 +650,7 @@ cdef class PyDBFrame:
cdef bint is_exception_event;
cdef bint has_exception_breakpoints;
cdef bint can_skip;
cdef bint stop;
cdef PyDBAdditionalThreadInfo info;
cdef int step_cmd;
cdef int line;
@ -649,6 +659,7 @@ cdef class PyDBFrame:
cdef bint is_return;
cdef bint should_stop;
cdef dict breakpoints_for_file;
cdef dict stop_info;
cdef str curr_func_name;
cdef bint exist_result;
cdef dict frame_skips_cache;
@ -660,6 +671,8 @@ cdef class PyDBFrame:
cdef bint is_coroutine_or_generator;
cdef int bp_line;
cdef object bp;
cdef int pydev_smart_parent_offset
cdef tuple pydev_smart_step_into_variants
# ELSE
# def trace_dispatch(self, frame, event, arg):
# ENDIF
@ -796,8 +809,8 @@ 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):
if step_cmd in (108, 109):
if stop_frame is 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:
info.pydev_step_cmd = 144
@ -845,6 +858,9 @@ cdef class PyDBFrame:
elif step_cmd in (108, 109, 159, 160) and stop_frame is not frame:
can_skip = True
elif step_cmd == 128 and stop_frame is not frame and stop_frame is not frame.f_back:
can_skip = True
elif step_cmd == 144:
if (
main_debugger.apply_files_filter(frame, frame.f_code.co_filename, True)
@ -994,7 +1010,7 @@ cdef class PyDBFrame:
if main_debugger.show_return_values:
if is_return and (
(info.pydev_step_cmd in (108, 159) and (frame.f_back is stop_frame)) or
(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 (107, 206)) or
(
@ -1111,19 +1127,32 @@ cdef class PyDBFrame:
elif step_cmd == 128:
stop = False
if info.pydev_smart_step_stop is frame:
info.pydev_func_name = '.invalid.' # Must match the type in cython
info.pydev_smart_step_stop = None
if stop_frame is 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 frame.f_back and is_line:
pydev_smart_parent_offset = info.pydev_smart_parent_offset
pydev_smart_step_into_variants = info.pydev_smart_step_into_variants
if pydev_smart_parent_offset >= 0 and pydev_smart_step_into_variants:
# Preferred mode (when the smart step into variants are available
# and the offset is set).
stop = get_smart_step_into_variant_from_frame_offset(frame.f_back.f_lasti, pydev_smart_step_into_variants) is \
get_smart_step_into_variant_from_frame_offset(pydev_smart_parent_offset, pydev_smart_step_into_variants)
if is_line or is_exception_event:
curr_func_name = frame.f_code.co_name
else:
# Only the name/line is available, so, check that.
curr_func_name = frame.f_code.co_name
# global context is set with an empty name
if curr_func_name in ('?', '<module>') or curr_func_name is None:
curr_func_name = ''
# global context is set with an empty name
if curr_func_name in ('?', '<module>') or curr_func_name is None:
curr_func_name = ''
if curr_func_name == info.pydev_func_name and stop_frame.f_lineno == info.pydev_next_line:
stop = True
if curr_func_name == info.pydev_func_name:
stop = True
if not stop:
# In smart step into, if we didn't hit it in this frame once, that'll
# not be the case next time either, so, disable tracing for this frame.
return None if is_call else NO_FTRACE
elif step_cmd in (109, 160):
stop = is_return and stop_frame is frame

View file

@ -87,6 +87,7 @@ DONT_TRACE = {
'pydevd_api.py': PYDEV_FILE,
'pydevd_base_schema.py': PYDEV_FILE,
'pydevd_breakpoints.py': PYDEV_FILE,
'pydevd_bytecode_utils.py': PYDEV_FILE,
'pydevd_code_to_source.py': PYDEV_FILE,
'pydevd_collect_bytecode_info.py': PYDEV_FILE,
'pydevd_comm.py': PYDEV_FILE,

View file

@ -10,6 +10,7 @@ from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raise
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
from _pydevd_bundle.pydevd_comm_constants import constant_to_str
from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
# IFDEF CYTHON
# cython_inline_constant: CMD_STEP_INTO = 107
@ -520,6 +521,7 @@ class PyDBFrame:
# cdef bint is_exception_event;
# cdef bint has_exception_breakpoints;
# cdef bint can_skip;
# cdef bint stop;
# cdef PyDBAdditionalThreadInfo info;
# cdef int step_cmd;
# cdef int line;
@ -528,6 +530,7 @@ class PyDBFrame:
# cdef bint is_return;
# cdef bint should_stop;
# cdef dict breakpoints_for_file;
# cdef dict stop_info;
# cdef str curr_func_name;
# cdef bint exist_result;
# cdef dict frame_skips_cache;
@ -539,6 +542,8 @@ class PyDBFrame:
# cdef bint is_coroutine_or_generator;
# cdef int bp_line;
# cdef object bp;
# cdef int pydev_smart_parent_offset
# cdef tuple pydev_smart_step_into_variants
# ELSE
def trace_dispatch(self, frame, event, arg):
# ENDIF
@ -675,8 +680,8 @@ 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):
if step_cmd in (CMD_STEP_OVER, CMD_STEP_RETURN):
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 step_cmd in (CMD_STEP_OVER, CMD_STEP_RETURN, CMD_SMART_STEP_INTO):
info.pydev_step_cmd = CMD_STEP_INTO
else:
info.pydev_step_cmd = CMD_STEP_INTO_MY_CODE
@ -724,6 +729,9 @@ class PyDBFrame:
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:
can_skip = True
elif step_cmd == CMD_SMART_STEP_INTO and stop_frame is not frame and stop_frame is not frame.f_back:
can_skip = True
elif step_cmd == CMD_STEP_INTO_MY_CODE:
if (
main_debugger.apply_files_filter(frame, frame.f_code.co_filename, True)
@ -873,7 +881,7 @@ 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) and (frame.f_back is stop_frame)) or
(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_INTO, CMD_STEP_INTO_COROUTINE)) or
(
@ -990,19 +998,32 @@ class PyDBFrame:
elif step_cmd == CMD_SMART_STEP_INTO:
stop = False
if info.pydev_smart_step_stop is frame:
info.pydev_func_name = '.invalid.' # Must match the type in cython
info.pydev_smart_step_stop = None
if stop_frame is 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 frame.f_back and is_line:
pydev_smart_parent_offset = info.pydev_smart_parent_offset
pydev_smart_step_into_variants = info.pydev_smart_step_into_variants
if pydev_smart_parent_offset >= 0 and pydev_smart_step_into_variants:
# Preferred mode (when the smart step into variants are available
# and the offset is set).
stop = get_smart_step_into_variant_from_frame_offset(frame.f_back.f_lasti, pydev_smart_step_into_variants) is \
get_smart_step_into_variant_from_frame_offset(pydev_smart_parent_offset, pydev_smart_step_into_variants)
if is_line or is_exception_event:
curr_func_name = frame.f_code.co_name
else:
# Only the name/line is available, so, check that.
curr_func_name = frame.f_code.co_name
# global context is set with an empty name
if curr_func_name in ('?', '<module>') or curr_func_name is None:
curr_func_name = ''
# global context is set with an empty name
if curr_func_name in ('?', '<module>') or curr_func_name is None:
curr_func_name = ''
if curr_func_name == info.pydev_func_name and stop_frame.f_lineno == info.pydev_next_line:
stop = True
if curr_func_name == info.pydev_func_name:
stop = True
if not stop:
# In smart step into, if we didn't hit it in this frame once, that'll
# not be the case next time either, so, disable tracing for this frame.
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

View file

@ -14,7 +14,7 @@ from _pydevd_bundle.pydevd_comm_constants import CMD_THREAD_CREATE, CMD_RETURN,
CMD_STEP_RETURN, CMD_STEP_CAUGHT_EXCEPTION, CMD_ADD_EXCEPTION_BREAK, CMD_SET_BREAK, \
CMD_SET_NEXT_STATEMENT, CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION, \
CMD_THREAD_RESUME_SINGLE_NOTIFICATION, CMD_THREAD_KILL, CMD_STOP_ON_START, CMD_INPUT_REQUESTED, \
CMD_EXIT, CMD_STEP_INTO_COROUTINE, CMD_STEP_RETURN_MY_CODE
CMD_EXIT, CMD_STEP_INTO_COROUTINE, CMD_STEP_RETURN_MY_CODE, CMD_SMART_STEP_INTO
from _pydevd_bundle.pydevd_constants import get_thread_id, dict_values, ForkSafeLock
from _pydevd_bundle.pydevd_net_command import NetCommand, NULL_NET_COMMAND
from _pydevd_bundle.pydevd_net_command_factory_xml import NetCommandFactory
@ -301,6 +301,7 @@ class NetCommandFactoryJson(NetCommandFactory):
CMD_STEP_INTO_MY_CODE,
CMD_STOP_ON_START,
CMD_STEP_INTO_COROUTINE,
CMD_SMART_STEP_INTO,
])
_EXCEPTION_REASONS = set([
CMD_STEP_CAUGHT_EXCEPTION,

View file

@ -10,7 +10,7 @@ from _pydevd_bundle.pydevd_additional_thread_info import set_additional_thread_i
from _pydevd_bundle.pydevd_breakpoints import get_exception_class
from _pydevd_bundle.pydevd_comm import (
InternalEvaluateConsoleExpression, InternalConsoleGetCompletions, InternalRunCustomOperation,
internal_get_next_statement_targets)
internal_get_next_statement_targets, internal_get_smart_step_into_variants)
from _pydevd_bundle.pydevd_constants import IS_PY3K, NEXT_VALUE_SEPARATOR, IS_WINDOWS, IS_PY2, NULL
from _pydevd_bundle.pydevd_comm_constants import ID_TO_MEANING, CMD_EXEC_EXPRESSION, CMD_AUTHENTICATE
from _pydevd_bundle.pydevd_api import PyDevdAPI
@ -167,7 +167,19 @@ class _PyDevCommandProcessor(object):
cmd_run_to_line = _cmd_set_next
cmd_set_next_statement = _cmd_set_next
cmd_smart_step_into = _cmd_set_next
def cmd_smart_step_into(self, py_db, cmd_id, seq, text):
thread_id, line_or_bytecode_offset, func_name = text.split('\t', 2)
if line_or_bytecode_offset.startswith('offset='):
# In this case we request the smart step into to stop given the parent frame
# and the location of the parent frame bytecode offset and not just the func_name
# (this implies that `CMD_GET_SMART_STEP_INTO_VARIANTS` was previously used
# to know what are the valid stop points).
offset = int(line_or_bytecode_offset[len('offset='):])
return self.api.request_smart_step_into(py_db, seq, thread_id, offset)
else:
# If the offset wasn't passed, just use the line/func_name to do the stop.
return self.api.request_smart_step_into_by_func_name(py_db, seq, thread_id, line_or_bytecode_offset, func_name)
def cmd_reload_code(self, py_db, cmd_id, seq, text):
text = text.strip()
@ -672,6 +684,12 @@ class _PyDevCommandProcessor(object):
py_db.post_method_as_internal_command(
thread_id, internal_get_next_statement_targets, seq, thread_id, frame_id)
def cmd_get_smart_step_into_variants(self, py_db, cmd_id, seq, text):
thread_id, frame_id, start_line, end_line = text.split('\t', 3)
py_db.post_method_as_internal_command(
thread_id, internal_get_smart_step_into_variants, seq, thread_id, frame_id, start_line, end_line, set_additional_thread_info=set_additional_thread_info)
def cmd_set_project_roots(self, py_db, cmd_id, seq, text):
self.api.set_project_roots(py_db, text.split(u'\t'))

View file

@ -15,7 +15,8 @@ from _pydevd_bundle._debug_adapter.pydevd_schema import (
ProcessEvent, Scope, ScopesResponseBody, SetExpressionResponseBody,
SetVariableResponseBody, SourceBreakpoint, SourceResponseBody,
VariablesResponseBody, SetBreakpointsResponseBody, Response,
Capabilities, PydevdAuthorizeRequest, Request)
Capabilities, PydevdAuthorizeRequest, Request, StepInTargetsResponse, StepInTarget,
StepInTargetsResponseBody)
from _pydevd_bundle.pydevd_api import PyDevdAPI
from _pydevd_bundle.pydevd_breakpoints import get_exception_class
from _pydevd_bundle.pydevd_comm_constants import (
@ -30,6 +31,9 @@ from _pydevd_bundle.pydevd_constants import (PY_IMPL_NAME, DebugInfoHolder, PY_V
PY_IMPL_VERSION_STR, IS_64BIT_PROCESS)
from _pydevd_bundle.pydevd_trace_dispatch import USING_CYTHON
from _pydevd_frame_eval.pydevd_frame_eval_main import USING_FRAME_EVAL
from _pydevd_bundle.pydevd_comm import internal_get_step_in_targets_json
from _pydevd_bundle.pydevd_additional_thread_info import set_additional_thread_info
from _pydevd_bundle.pydevd_thread_lifecycle import pydevd_find_thread_by_id
def _convert_rules_to_exclude_filters(rules, on_error):
@ -229,7 +233,7 @@ class PyDevJsonCommandProcessor(object):
supportsFunctionBreakpoints=False,
supportsStepBack=False,
supportsRestartFrame=False,
supportsStepInTargetsRequest=False,
supportsStepInTargetsRequest=True,
supportsRestartRequest=False,
supportsLoadedSourcesRequest=False,
supportsTerminateThreadsRequest=False,
@ -541,16 +545,67 @@ class PyDevJsonCommandProcessor(object):
arguments = request.arguments # : :type arguments: StepInArguments
thread_id = arguments.threadId
if py_db.get_use_libraries_filter():
step_cmd_id = CMD_STEP_INTO_MY_CODE
else:
step_cmd_id = CMD_STEP_INTO
target_id = arguments.targetId
if target_id is not None:
thread = pydevd_find_thread_by_id(thread_id)
info = set_additional_thread_info(thread)
pydev_smart_step_into_variants = info.pydev_smart_step_into_variants
if not pydev_smart_step_into_variants:
variables_response = pydevd_base_schema.build_response(
request,
kwargs={
'success': False,
'message': 'Unable to step into target (no targets are saved in the thread info).'
})
return NetCommand(CMD_RETURN, 0, variables_response, is_json=True)
self.api.request_step(py_db, thread_id, step_cmd_id)
for variant in pydev_smart_step_into_variants:
if variant.offset == target_id:
self.api.request_smart_step_into(py_db, request.seq, thread_id, variant.offset)
break
else:
variables_response = pydevd_base_schema.build_response(
request,
kwargs={
'success': False,
'message': 'Unable to find step into target %s. Available targets: %s' % (
target_id, [variant.offset for variant in pydev_smart_step_into_variants])
})
return NetCommand(CMD_RETURN, 0, variables_response, is_json=True)
else:
if py_db.get_use_libraries_filter():
step_cmd_id = CMD_STEP_INTO_MY_CODE
else:
step_cmd_id = CMD_STEP_INTO
self.api.request_step(py_db, thread_id, step_cmd_id)
response = pydevd_base_schema.build_response(request)
return NetCommand(CMD_RETURN, 0, response, is_json=True)
def on_stepintargets_request(self, py_db, request):
'''
:param StepInTargetsRequest request:
'''
frame_id = request.arguments.frameId
thread_id = py_db.suspended_frames_manager.get_thread_id_for_variable_reference(
frame_id)
if thread_id is None:
body = StepInTargetsResponseBody([])
variables_response = pydevd_base_schema.build_response(
request,
kwargs={
'body': body,
'success': False,
'message': 'Unable to get thread_id from frame_id (thread to get step in targets seems to have resumed already).'
})
return NetCommand(CMD_RETURN, 0, variables_response, is_json=True)
py_db.post_method_as_internal_command(
thread_id, internal_get_step_in_targets_json, request.seq, thread_id, frame_id, request, set_additional_thread_info)
def on_stepout_request(self, py_db, request):
'''
:param StepOutRequest request:

View file

@ -1,5 +1,5 @@
import bisect
from _pydevd_bundle.pydevd_constants import dict_items, NULL
from _pydevd_bundle.pydevd_constants import dict_items, NULL, KeyifyList
import pydevd_file_utils
@ -34,19 +34,6 @@ class SourceMappingEntry(object):
__repr__ = __str__
class _KeyifyList(object):
def __init__(self, inner, key):
self.inner = inner
self.key = key
def __len__(self):
return len(self.inner)
def __getitem__(self, k):
return self.key(self.inner[k])
class SourceMapping(object):
def __init__(self, on_source_mapping_changed=NULL):
@ -141,14 +128,14 @@ class SourceMapping(object):
mappings = self._mappings_to_server.get(absolute_normalized_filename)
if mappings:
i = bisect.bisect(_KeyifyList(mappings, lambda entry:entry.line), lineno)
i = bisect.bisect(KeyifyList(mappings, lambda entry:entry.line), lineno)
if i >= len(mappings):
i -= 1
if i == 0:
entry = mappings[i]
elif i > 0:
else:
entry = mappings[i - 1]
if not entry.contains_line(lineno):

View file

@ -500,6 +500,7 @@ cdef PyObject * get_bytecode_while_frame_eval(PyFrameObject * frame_obj, int exc
cdef int CMD_STEP_OVER_MY_CODE = 159
cdef int CMD_STEP_INTO_MY_CODE = 144
cdef int CMD_STEP_INTO_COROUTINE = 206
cdef int CMD_SMART_STEP_INTO = 128
cdef bint can_skip = True
try:
thread_info = _thread_local_info.thread_info
@ -540,7 +541,7 @@ cdef PyObject * get_bytecode_while_frame_eval(PyFrameObject * frame_obj, int exc
if apply_to_global:
thread_info.thread_trace_func = trace_func
if additional_info.pydev_step_cmd in (CMD_STEP_INTO, CMD_STEP_INTO_MY_CODE, CMD_STEP_INTO_COROUTINE) or \
if additional_info.pydev_step_cmd in (CMD_STEP_INTO, CMD_STEP_INTO_MY_CODE, CMD_STEP_INTO_COROUTINE, CMD_SMART_STEP_INTO) or \
main_debugger.break_on_caught_exceptions or \
main_debugger.break_on_user_uncaught_exceptions or \
main_debugger.has_plugin_exception_breaks or \

View file

@ -1974,21 +1974,13 @@ class PyDB(object):
# When in a coroutine we switch to CMD_STEP_INTO_COROUTINE.
info.pydev_step_cmd = CMD_STEP_INTO_COROUTINE
info.pydev_step_stop = frame
info.pydev_smart_step_stop = None
self.set_trace_for_frame_and_parents(frame)
else:
info.pydev_step_stop = None
info.pydev_smart_step_stop = None
self.set_trace_for_frame_and_parents(frame)
elif info.pydev_step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE):
elif info.pydev_step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE, CMD_SMART_STEP_INTO):
info.pydev_step_stop = frame
info.pydev_smart_step_stop = None
self.set_trace_for_frame_and_parents(frame)
elif info.pydev_step_cmd == CMD_SMART_STEP_INTO:
info.pydev_step_stop = None
info.pydev_smart_step_stop = frame
self.set_trace_for_frame_and_parents(frame)
elif info.pydev_step_cmd == CMD_RUN_TO_LINE or info.pydev_step_cmd == CMD_SET_NEXT_STATEMENT:

View file

@ -82,6 +82,9 @@ CMD_STEP_OVER_MY_CODE = 159
CMD_STEP_RETURN_MY_CODE = 160
CMD_SET_PY_EXCEPTION = 161
CMD_SET_PATH_MAPPING_JSON = 162
CMD_GET_SMART_STEP_INTO_VARIANTS = 163 # XXX: PyCharm has 160 for this (we're currently incompatible anyways).
CMD_REDIRECT_OUTPUT = 200
CMD_GET_NEXT_STATEMENT_TARGETS = 201
@ -1291,6 +1294,9 @@ class AbstractWriterThread(threading.Thread):
def write_set_next_statement(self, thread_id, line, func_name):
self.write("%s\t%s\t%s\t%s\t%s" % (CMD_SET_NEXT_STATEMENT, self.next_seq(), thread_id, line, func_name,))
def write_smart_step_into(self, thread_id, line, func_name):
self.write("%s\t%s\t%s\t%s\t%s" % (CMD_SMART_STEP_INTO, self.next_seq(), thread_id, line, func_name,))
def write_debug_console_expression(self, locator):
self.write("%s\t%s\t%s" % (CMD_EVALUATE_CONSOLE_EXPRESSION, self.next_seq(), locator))
@ -1381,6 +1387,14 @@ class AbstractWriterThread(threading.Thread):
return frame_names
return [msg.thread.frame['name']]
def get_step_into_variants(self, thread_id, frame_id, start_line, end_line):
self.write("%s\t%s\t%s\t%s\t%s\t%s" % (CMD_GET_SMART_STEP_INTO_VARIANTS, self.next_seq(), thread_id, frame_id, start_line, end_line))
msg = self.wait_for_message(CMD_GET_SMART_STEP_INTO_VARIANTS)
if msg.variant:
variant_info = [(variant['name'], variant['isVisited'], variant['line'], variant['callOrder'], variant['offset']) for variant in msg.variant]
return variant_info
return []
def wait_for_thread_join(self, main_thread_id):
def condition():

View file

@ -0,0 +1,20 @@
def bar():
print('on bar mark')
def call_outer(*args):
print('on outer mark')
def foo(*args):
print('on foo mark')
def main():
call_outer(foo(bar())) # break here
if __name__ == '__main__':
main()
print('TEST SUCEEDED')

View file

@ -0,0 +1,17 @@
def foo(arg):
print('on foo mark', arg)
return arg + 1
def main():
# Note that we have multiple foo calls and we have to differentiate and stop at the
# proper one.
foo(foo(foo(foo(1)))) # break here
if __name__ == '__main__':
main()
print('TEST SUCEEDED')

View file

@ -17,7 +17,8 @@ from tests_python.debugger_unittest import (CMD_SET_PROPERTY_TRACE, REASON_CAUGH
IS_APPVEYOR, wait_for_condition, CMD_GET_FRAME, CMD_GET_BREAKPOINT_EXCEPTION,
CMD_THREAD_SUSPEND, CMD_STEP_OVER, REASON_STEP_OVER, CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION,
CMD_THREAD_RESUME_SINGLE_NOTIFICATION, REASON_STEP_RETURN, REASON_STEP_RETURN_MY_CODE,
REASON_STEP_OVER_MY_CODE, REASON_STEP_INTO, CMD_THREAD_KILL, IS_PYPY, REASON_STOP_ON_START)
REASON_STEP_OVER_MY_CODE, REASON_STEP_INTO, CMD_THREAD_KILL, IS_PYPY, REASON_STOP_ON_START,
CMD_SMART_STEP_INTO)
from _pydevd_bundle.pydevd_constants import IS_WINDOWS, IS_PY38_OR_GREATER, \
IS_MAC
from _pydevd_bundle.pydevd_comm_constants import CMD_RELOAD_CODE, CMD_INPUT_REQUESTED
@ -3478,6 +3479,55 @@ def test_step_return_my_code(case_setup):
writer.finished_ok = True
def test_smart_step_into_case1(case_setup):
with case_setup.test_file('_debugger_case_smart_step_into.py') as writer:
line = writer.get_line_index_with_content('break here')
writer.write_add_breakpoint(line)
writer.write_make_initial_run()
hit = writer.wait_for_breakpoint_hit(line=line)
found = writer.get_step_into_variants(hit.thread_id, hit.frame_id, line, line)
# Remove the offset to compare (as it changes for each python version)
assert [x[:-1] for x in found] == [
('bar', 'false', '14', '1'), ('foo', 'false', '14', '1'), ('call_outer', 'false', '14', '1')]
# Note: this is just using the name, not really taking using the context.
writer.write_smart_step_into(hit.thread_id, line, 'foo')
hit = writer.wait_for_breakpoint_hit(reason=CMD_SMART_STEP_INTO)
assert hit.line == writer.get_line_index_with_content('on foo mark')
writer.write_run_thread(hit.thread_id)
writer.finished_ok = True
def test_smart_step_into_case2(case_setup):
with case_setup.test_file('_debugger_case_smart_step_into2.py') as writer:
line = writer.get_line_index_with_content('break here')
writer.write_add_breakpoint(line)
writer.write_make_initial_run()
hit = writer.wait_for_breakpoint_hit(line=line)
found = writer.get_step_into_variants(hit.thread_id, hit.frame_id, line, line)
# Note: we have multiple 'foo' calls, so, we have to differentiate to
# know in which one we want to stop.
writer.write_smart_step_into(hit.thread_id, 'offset=' + found[2][-1], 'foo')
hit = writer.wait_for_breakpoint_hit(reason=CMD_SMART_STEP_INTO)
assert hit.line == writer.get_line_index_with_content('on foo mark')
writer.write_get_frame(hit.thread_id, hit.frame_id)
writer.wait_for_var([
(
'<var name="arg" type="int" qualifier="__builtin__" value="int: 3"',
'<var name="arg" type="int" qualifier="builtins" value="int: 3"',
)
])
writer.write_run_thread(hit.thread_id)
writer.finished_ok = True
def test_step_over_my_code(case_setup):
with case_setup.test_file('my_code/my_code.py') as writer:
writer.write_set_project_roots([debugger_unittest._get_debugger_test_file('my_code')])

View file

@ -342,8 +342,8 @@ class JsonFacade(object):
pause_response = self.wait_for_response(pause_request)
assert pause_response.success
def write_step_in(self, thread_id):
arguments = pydevd_schema.StepInArguments(threadId=thread_id)
def write_step_in(self, thread_id, target_id=None):
arguments = pydevd_schema.StepInArguments(threadId=thread_id, targetId=target_id)
self.wait_for_response(self.write_request(pydevd_schema.StepInRequest(arguments)))
def write_step_next(self, thread_id, wait_for_response=True):
@ -386,6 +386,19 @@ class JsonFacade(object):
return name_to_scopes
def get_step_in_targets(self, frame_id):
request = self.write_request(pydevd_schema.StepInTargetsRequest(
pydevd_schema.StepInTargetsArguments(frame_id)))
# : :type response: StepInTargetsResponse
response = self.wait_for_response(request)
# : :type body: StepInTargetsResponseBody
body = response.body
targets = body.targets
# : :type targets: List[StepInTarget]
return targets
def get_name_to_var(self, variables_reference):
variables_response = self.get_variables_response(variables_reference)
return dict((variable['name'], pydevd_schema.Variable(**variable)) for variable in variables_response.body.variables)
@ -5433,6 +5446,54 @@ def do_something():
writer.finished_ok = True
def test_step_into_target_basic(case_setup):
with case_setup.test_file('_debugger_case_smart_step_into.py') as writer:
json_facade = JsonFacade(writer)
bp = writer.get_line_index_with_content('break here')
json_facade.write_set_breakpoints([bp])
json_facade.write_make_initial_run()
# At this point we know that 'do_something' was called at least once.
hit = json_facade.wait_for_thread_stopped(line=bp)
# : :type step_in_targets: List[StepInTarget]
step_in_targets = json_facade.get_step_in_targets(hit.frame_id)
label_to_id = dict((target['label'], target['id']) for target in step_in_targets)
assert set(label_to_id.keys()) == {'bar', 'foo', 'call_outer'}
json_facade.write_step_in(hit.thread_id, target_id=label_to_id['foo'])
on_foo_mark_line = writer.get_line_index_with_content('on foo mark')
hit = json_facade.wait_for_thread_stopped(reason='step', line=on_foo_mark_line)
json_facade.write_continue()
writer.finished_ok = True
def test_step_into_target_multiple(case_setup):
with case_setup.test_file('_debugger_case_smart_step_into2.py') as writer:
json_facade = JsonFacade(writer)
bp = writer.get_line_index_with_content('break here')
json_facade.write_set_breakpoints([bp])
json_facade.write_make_initial_run()
# At this point we know that 'do_something' was called at least once.
hit = json_facade.wait_for_thread_stopped(line=bp)
# : :type step_in_targets: List[StepInTarget]
step_in_targets = json_facade.get_step_in_targets(hit.frame_id)
label_to_id = dict((target['label'], target['id']) for target in step_in_targets)
assert set(label_to_id.keys()) == {'foo', 'foo (call 2)', 'foo (call 3)', 'foo (call 4)'}
json_facade.write_step_in(hit.thread_id, target_id=label_to_id['foo (call 2)'])
on_foo_mark_line = writer.get_line_index_with_content('on foo mark')
hit = json_facade.wait_for_thread_stopped(reason='step', line=on_foo_mark_line)
json_facade.write_continue()
writer.finished_ok = True
if __name__ == '__main__':
pytest.main(['-k', 'test_case_skipping_filters', '-s'])

View file

@ -0,0 +1,129 @@
import sys
def check(found, expected):
assert len(found) == len(expected), '%s != %s' % (found, expected)
last_offset = -1
for f, e in zip(found, expected):
if isinstance(e.name, (list, tuple, set)):
assert f.name in e.name
else:
assert f.name == e.name
assert f.is_visited == e.is_visited
assert f.line == e.line
assert f.call_order == e.call_order
# We can't check the offset because it may be different among different python versions
# so, just check that it's always in order.
assert f.offset > last_offset
last_offset = f.offset
def test_smart_step_into_bytecode_info():
from _pydevd_bundle import pydevd_bytecode_utils
from _pydevd_bundle.pydevd_bytecode_utils import Variant
def function():
def some(arg):
pass
def call(arg):
pass
yield sys._getframe()
call(some(call(some())))
generator = iter(function())
frame = next(generator)
found = pydevd_bytecode_utils.calculate_smart_step_into_variants(
frame, 0, 99999, base=function.__code__.co_firstlineno)
check(found, [
Variant(name=('_getframe', 'sys'), is_visited=True, line=8, offset=20, call_order=1),
Variant(name='some', is_visited=False, line=9, offset=34, call_order=1),
Variant(name='call', is_visited=False, line=9, offset=36, call_order=1),
Variant(name='some', is_visited=False, line=9, offset=38, call_order=2),
Variant(name='call', is_visited=False, line=9, offset=40, call_order=2),
])
def test_get_smart_step_into_variant_from_frame_offset():
from _pydevd_bundle import pydevd_bytecode_utils
from _pydevd_bundle.pydevd_bytecode_utils import Variant
found = [
Variant(name='_getframe', is_visited=True, line=8, offset=20, call_order=1),
Variant(name='some', is_visited=False, line=9, offset=34, call_order=1),
Variant(name='call', is_visited=False, line=9, offset=36, call_order=1),
Variant(name='some', is_visited=False, line=9, offset=38, call_order=2),
Variant(name='call', is_visited=False, line=9, offset=40, call_order=2),
]
assert pydevd_bytecode_utils.get_smart_step_into_variant_from_frame_offset(19, found) is None
assert pydevd_bytecode_utils.get_smart_step_into_variant_from_frame_offset(20, found).offset == 20
assert pydevd_bytecode_utils.get_smart_step_into_variant_from_frame_offset(33, found).offset == 20
assert pydevd_bytecode_utils.get_smart_step_into_variant_from_frame_offset(34, found).offset == 34
assert pydevd_bytecode_utils.get_smart_step_into_variant_from_frame_offset(35, found).offset == 34
assert pydevd_bytecode_utils.get_smart_step_into_variant_from_frame_offset(36, found).offset == 36
assert pydevd_bytecode_utils.get_smart_step_into_variant_from_frame_offset(44, found).offset == 40
def test_smart_step_into_bytecode_info_eq():
from _pydevd_bundle import pydevd_bytecode_utils
from _pydevd_bundle.pydevd_bytecode_utils import Variant
def function():
a = 1
b = 1
if a == b:
pass
if a != b:
pass
if a > b:
pass
if a >= b:
pass
if a < b:
pass
if a <= b:
pass
if a is b:
pass
yield sys._getframe()
generator = iter(function())
frame = next(generator)
found = pydevd_bytecode_utils.calculate_smart_step_into_variants(
frame, 0, 99999, base=function.__code__.co_firstlineno)
if sys.version_info[:2] < (3, 9):
check(found, [
Variant(name='__eq__', is_visited=True, line=3, offset=18, call_order=1),
Variant(name='__ne__', is_visited=True, line=5, offset=33, call_order=1),
Variant(name='__gt__', is_visited=True, line=7, offset=48, call_order=1),
Variant(name='__ge__', is_visited=True, line=9, offset=63, call_order=1),
Variant(name='__lt__', is_visited=True, line=11, offset=78, call_order=1),
Variant(name='__le__', is_visited=True, line=13, offset=93, call_order=1),
Variant(name='is', is_visited=True, line=15, offset=108, call_order=1),
Variant(name=('_getframe', 'sys'), is_visited=True, line=18, offset=123, call_order=1),
])
else:
check(found, [
Variant(name='__eq__', is_visited=True, line=3, offset=18, call_order=1),
Variant(name='__ne__', is_visited=True, line=5, offset=33, call_order=1),
Variant(name='__gt__', is_visited=True, line=7, offset=48, call_order=1),
Variant(name='__ge__', is_visited=True, line=9, offset=63, call_order=1),
Variant(name='__lt__', is_visited=True, line=11, offset=78, call_order=1),
Variant(name='__le__', is_visited=True, line=13, offset=93, call_order=1),
Variant(name=('_getframe', 'sys'), is_visited=True, line=18, offset=123, call_order=1),
])

View file

@ -473,3 +473,26 @@ def test_interrupt_main_thread():
actual_timeout, timeout)
else:
raise AssertionError('KeyboardInterrupt not generated in main thread.')
def test_get_smart_step_into_variant_from_frame_offset():
from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
variants = []
assert get_smart_step_into_variant_from_frame_offset(0, variants) is None
class DummyVariant(object):
def __init__(self, offset):
self.offset = offset
variants.append(DummyVariant(1))
assert get_smart_step_into_variant_from_frame_offset(0, variants) is None
assert get_smart_step_into_variant_from_frame_offset(1, variants) is variants[0]
assert get_smart_step_into_variant_from_frame_offset(2, variants) is variants[0]
variants.append(DummyVariant(3))
assert get_smart_step_into_variant_from_frame_offset(0, variants) is None
assert get_smart_step_into_variant_from_frame_offset(1, variants) is variants[0]
assert get_smart_step_into_variant_from_frame_offset(2, variants) is variants[0]
assert get_smart_step_into_variant_from_frame_offset(3, variants) is variants[1]
assert get_smart_step_into_variant_from_frame_offset(4, variants) is variants[1]

View file

@ -174,6 +174,7 @@ class Client(components.Component):
"supportsGotoTargetsRequest": True,
"supportsClipboardContext": True,
"exceptionBreakpointFilters": exception_breakpoint_filters,
"supportsStepInTargetsRequest": True,
}
# Common code for "launch" and "attach" request handlers.