Skip to content

Commit

Permalink
wip - plugins stop (missing step over)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Nov 26, 2023
1 parent 435bbe3 commit d6b5736
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 24 deletions.
18 changes: 18 additions & 0 deletions _pydevd_bundle/pydevd_plugin_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ def can_skip(self, py_db, frame):
return False
return True

def must_track_lines(self, py_db, frame):
for plugin in self.active_plugins:
if plugin.must_track_lines(py_db, frame):
return True
return False

def must_track_calls(self, py_db, frame):
for plugin in self.active_plugins:
if plugin.must_track_calls(py_db, frame):
return True
return False

def must_track_exceptions(self, py_db, frame):
for plugin in self.active_plugins:
if plugin.must_track_exceptions(py_db, frame):
return True
return False

def has_exception_breaks(self, py_db):
for plugin in self.active_plugins:
if plugin.has_exception_breaks(py_db):
Expand Down
47 changes: 30 additions & 17 deletions _pydevd_sys_monitoring/pydevd_sys_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ def __init__(self):
self.breakpoint_found: bool = False
self.function_breakpoint_found: bool = False

# A plugin can choose whether to stop on function calls or line events.
self.plugin_line_breakpoint_found: bool = False
self.plugin_call_breakpoint_found: bool = False

# When breakpoints_mtime != PyDb.mtime the validity of breakpoints have
# to be re-evaluated (if invalid a new FuncCodeInfo must be created and
# tracing can't be disabled for the related frames).
Expand Down Expand Up @@ -421,6 +425,12 @@ def get_func_code_info(code_obj, frame_or_depth) -> FuncCodeInfo:
func_code_info.breakpoint_found = bool(bp_line_to_breakpoint)
func_code_info.bp_line_to_breakpoint = bp_line_to_breakpoint

if py_db.has_plugin_line_breaks:
if py_db.plugin.must_track_lines(py_db, frame):
func_code_info.plugin_line_breakpoint_found = True
if py_db.plugin.must_track_calls(py_db, frame):
func_code_info.plugin_call_breakpoint_found = True

_code_to_func_code_info_cache[code_obj] = func_code_info
return func_code_info

Expand Down Expand Up @@ -484,15 +494,9 @@ def _enable_code_tracing(py_db, additional_info, func_code_info: FuncCodeInfo, c
_enable_step_tracing(py_db, code, step_cmd, additional_info, frame)
return # We can't always disable as we may need to track it when stepping.

if func_code_info.breakpoint_found:
if func_code_info.breakpoint_found or func_code_info.plugin_line_breakpoint_found:
_enable_line_tracing(code)

elif py_db.has_plugin_line_breaks:
can_skip = py_db.plugin.can_skip(py_db, frame)

if not can_skip:
_enable_line_tracing(code)

if is_stepping:
_enable_step_tracing(py_db, code, step_cmd, additional_info, frame)

Expand Down Expand Up @@ -828,7 +832,7 @@ def _stop_on_return(py_db, thread_info, info, step_cmd, frame, retval):
info.pydev_state = STATE_RUN


def _stop_on_breakpoint(py_db, thread_info: ThreadInfo, stop_reason, bp, frame, new_frame, stop: bool, stop_on_plugin_breakpoint: bool):
def _stop_on_breakpoint(py_db, thread_info: ThreadInfo, stop_reason, bp, frame, new_frame, stop: bool, stop_on_plugin_breakpoint: bool, bp_type:str):
'''
:param py_db:
:param thread_info:
Expand Down Expand Up @@ -877,13 +881,11 @@ def _stop_on_breakpoint(py_db, thread_info: ThreadInfo, stop_reason, bp, frame,
py_db.do_wait_suspend(thread_info.thread, frame, 'line', None)
return True

# elif stop_on_plugin_breakpoint and plugin_manager is not None:
# stop_at_frame = plugin_manager.suspend(py_db, thread, frame, bp_type)
# if stop_at_frame and info.pydev_state == STATE_SUSPEND:
# # Note: it's possible that it was suspended with a pause (and we'd stop here too).
# # print('suspend (pause)...')
# py_db.do_wait_suspend(thread_info.thread, stop_at_frame, 'line', None)
# return
elif stop_on_plugin_breakpoint and py_db.plugin is not None:
stop_at_frame = py_db.plugin.suspend(py_db, thread_info.thread, frame, bp_type)
if stop_at_frame and thread_info.additional_info.pydev_state == STATE_SUSPEND:
py_db.do_wait_suspend(thread_info.thread, stop_at_frame, 'line', None)
return

return False

Expand Down Expand Up @@ -951,7 +953,7 @@ def _line_event(code, line):
# stop_on_plugin_breakpoint, bp, new_frame, bp_type = result

if bp:
if _stop_on_breakpoint(py_db, thread_info, stop_reason, bp, frame, new_frame, stop, stop_on_plugin_breakpoint):
if _stop_on_breakpoint(py_db, thread_info, stop_reason, bp, frame, new_frame, stop, stop_on_plugin_breakpoint, 'python-line'):
return

if info.pydev_state == STATE_SUSPEND:
Expand Down Expand Up @@ -1084,9 +1086,20 @@ def _start_method_event(code, instruction_offset):
stop_reason = CMD_SET_FUNCTION_BREAK
stop_on_plugin_breakpoint = False

_stop_on_breakpoint(py_db, thread_info, stop_reason, bp, frame, new_frame, stop, stop_on_plugin_breakpoint)
_stop_on_breakpoint(py_db, thread_info, stop_reason, bp, frame, new_frame, stop, stop_on_plugin_breakpoint, 'python-function')
return

if func_code_info.plugin_call_breakpoint_found:
plugin_manager = py_db.plugin
result = plugin_manager.get_breakpoint(py_db, frame, 'call', thread_info.additional_info)
if result:
stop_reason = CMD_SET_BREAK
stop = False
stop_on_plugin_breakpoint = True
bp, new_frame, bp_type = result
_stop_on_breakpoint(py_db, thread_info, stop_reason, bp, frame, new_frame, stop, stop_on_plugin_breakpoint, bp_type)
return


def _resume_method_event(code, instruction_offset):
# Same thing as _start_method_event but without checking function breakpoint
Expand Down
14 changes: 7 additions & 7 deletions pydevd.py
Original file line number Diff line number Diff line change
Expand Up @@ -1337,7 +1337,7 @@ def apply_files_filter(self, frame, original_filename, force_check_project_scope
# If it's explicitly needed by some plugin, we can't skip it.
if not self.plugin.can_skip(self, frame):
if DEBUG:
pydev_log.debug_once('File traced (included by plugins): %s (%s)', original_filename, frame.f_code.co_name)
pydev_log.debug_once('File traced (included by plugins): %s', original_filename)
self._apply_filter_cache[cache_key] = False
return False

Expand All @@ -1348,13 +1348,13 @@ def apply_files_filter(self, frame, original_filename, force_check_project_scope
if exclude_by_filter:
# ignore files matching stepping filters
if DEBUG:
pydev_log.debug_once('File not traced (excluded by filters): %s (%s)', original_filename, frame.f_code.co_name)
pydev_log.debug_once('File not traced (excluded by filters): %s', original_filename)

self._apply_filter_cache[cache_key] = True
return True
else:
if DEBUG:
pydev_log.debug_once('File traced (explicitly included by filters): %s (%s)', original_filename, frame.f_code.co_name)
pydev_log.debug_once('File traced (explicitly included by filters): %s', original_filename)

self._apply_filter_cache[cache_key] = False
return False
Expand All @@ -1364,19 +1364,19 @@ def apply_files_filter(self, frame, original_filename, force_check_project_scope
self._apply_filter_cache[cache_key] = True
if force_check_project_scope:
if DEBUG:
pydev_log.debug_once('File not traced (not in project): %s (%s)', original_filename, frame.f_code.co_name)
pydev_log.debug_once('File not traced (not in project): %s', original_filename)
else:
if DEBUG:
pydev_log.debug_once('File not traced (not in project - force_check_project_scope): %s (%s)', original_filename, frame.f_code.co_name)
pydev_log.debug_once('File not traced (not in project - force_check_project_scope): %s', original_filename)

return True

if force_check_project_scope:
if DEBUG:
pydev_log.debug_once('File traced: %s (%s) (force_check_project_scope)', original_filename, frame.f_code.co_name)
pydev_log.debug_once('File traced: %s (force_check_project_scope)', original_filename)
else:
if DEBUG:
pydev_log.debug_once('File traced: %s (%s)', original_filename, frame.f_code.co_name)
pydev_log.debug_once('File traced: %s', original_filename)
self._apply_filter_cache[cache_key] = False
return False

Expand Down
15 changes: 15 additions & 0 deletions pydevd_plugins/django_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,21 @@ def can_skip(py_db, frame):
return True


def must_track_calls(py_db, frame):
return bool(py_db.django_breakpoints and _is_django_render_call(frame))


def must_track_lines(py_db, frame):
# We don't stop on line events here, just on calls.
return False


def must_track_exceptions(py_db, frame):
# Exceptions raised at django.template.base must be checked.
return bool(py_db.django_exception_break and
frame.f_globals.get('__name__', '') == 'django.template.base')


def has_exception_breaks(py_db):
if len(py_db.django_exception_break) > 0:
return True
Expand Down
28 changes: 28 additions & 0 deletions pydevd_plugins/jinja2_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,34 @@ def can_skip(pydb, frame):
return True


def must_track_calls(pydb, frame):
return False


def must_track_lines(pydb, frame):
if pydb.jinja2_breakpoints and _is_jinja2_render_call(frame):
filename = _get_jinja2_template_original_filename(frame)
if filename is not None:
canonical_normalized_filename = canonical_normalized_path(filename)
jinja2_breakpoints_for_file = pydb.jinja2_breakpoints.get(canonical_normalized_filename)
return bool(jinja2_breakpoints_for_file)


def must_track_exceptions(pydb, frame):
if pydb.jinja2_exception_break:
name = frame.f_code.co_name

# errors in compile time
if name in ('template', 'top-level template code', '<module>') or name.startswith('block '):
f_back = frame.f_back
module_name = ''
if f_back is not None:
module_name = f_back.f_globals.get('__name__', '')
return module_name.startswith('jinja2.')

return True


def cmd_step_into(pydb, frame, event, info, thread, stop_info, stop):
plugin_stop = False
if _is_jinja2_suspended(thread):
Expand Down
49 changes: 49 additions & 0 deletions tests_python/test_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,55 @@ def get_environ(writer):
writer.finished_ok = True


@pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
def test_case_django_step_next(case_setup_django):

def get_environ(writer):
env = os.environ.copy()
env.update({
'PYDEVD_FILTER_LIBRARIES': '1', # Global setting for in project or not
"IDE_PROJECT_ROOTS": debugger_unittest._get_debugger_test_file(writer.DJANGO_FOLDER)
})
return env

with case_setup_django.test_file(EXPECTED_RETURNCODE='any', get_environ=get_environ) as writer:
writer.write_make_initial_run()

# Wait for the first request that works...
for i in range(4):
try:
t = writer.create_request_thread('my_app')
t.start()
contents = t.wait_for_contents()
contents = contents.replace(' ', '').replace('\r', '').replace('\n', '')
assert contents == '<ul><li>v1:v1</li><li>v2:v2</li></ul>'
break
except:
if i == 3:
raise
continue

writer.write_add_breakpoint_django(3, None, 'index.html')
t = writer.create_request_thread('my_app')
t.start()

hit = writer.wait_for_breakpoint_hit(REASON_STOP_ON_BREAKPOINT, line=3)

writer.write_step_over(hit.thread_id)

hit = writer.wait_for_breakpoint_hit(REASON_STEP_OVER, line=5)

writer.write_run_thread(hit.thread_id)

contents = t.wait_for_contents()

contents = contents.replace(' ', '').replace('\r', '').replace('\n', '')
if contents != '<ul><li>v1:v1</li><li>v2:v2</li></ul>':
raise AssertionError('%s != <ul><li>v1:v1</li><li>v2:v2</li></ul>' % (contents,))

writer.finished_ok = True


@pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
def test_case_django_b(case_setup_django):
with case_setup_django.test_file(EXPECTED_RETURNCODE='any') as writer:
Expand Down

0 comments on commit d6b5736

Please sign in to comment.