From 9431feaf74b899d3096ff0fb9bba0255bb1f7629 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Mon, 27 Nov 2023 14:51:26 -0300 Subject: [PATCH] wip - django templates --- _pydevd_bundle/pydevd_frame_utils.py | 3 + .../pydevd_net_command_factory_xml.py | 10 +- _pydevd_bundle/pydevd_plugin_utils.py | 31 ++- _pydevd_bundle/pydevd_suspended_frames.py | 4 +- .../pydevd_sys_monitoring.py | 191 ++++++++++++------ pydevd.py | 32 +-- pydevd_plugins/django_debug.py | 13 +- pydevd_plugins/jinja2_debug.py | 11 + tests_python/debugger_unittest.py | 2 + tests_python/test_debugger.py | 53 +++++ 10 files changed, 263 insertions(+), 87 deletions(-) diff --git a/_pydevd_bundle/pydevd_frame_utils.py b/_pydevd_bundle/pydevd_frame_utils.py index f079757a..d1181006 100644 --- a/_pydevd_bundle/pydevd_frame_utils.py +++ b/_pydevd_bundle/pydevd_frame_utils.py @@ -34,6 +34,9 @@ def __init__(self, name, filename): self.co_firstlineno = 1 self.co_flags = 0 + def co_lines(self): + return () + def add_exception_to_frame(frame, exception_info): frame.f_locals['__exception__'] = exception_info diff --git a/_pydevd_bundle/pydevd_net_command_factory_xml.py b/_pydevd_bundle/pydevd_net_command_factory_xml.py index 7df81059..ae2f8e72 100644 --- a/_pydevd_bundle/pydevd_net_command_factory_xml.py +++ b/_pydevd_bundle/pydevd_net_command_factory_xml.py @@ -236,7 +236,7 @@ def make_thread_suspend_str( frames_list, stop_reason=None, message=None, - suspend_type="trace", + trace_suspend_type="trace", ): """ :return tuple(str,str): @@ -273,8 +273,8 @@ def make_thread_suspend_str( append(' stop_reason="%s"' % (stop_reason,)) if message is not None: append(' message="%s"' % (message,)) - if suspend_type is not None: - append(' suspend_type="%s"' % (suspend_type,)) + if trace_suspend_type is not None: + append(' trace_suspend_type="%s"' % (trace_suspend_type,)) append('>') thread_stack_str = self.make_thread_stack_str(py_db, frames_list) append(thread_stack_str) @@ -282,10 +282,10 @@ def make_thread_suspend_str( return ''.join(cmd_text_list), thread_stack_str - def make_thread_suspend_message(self, py_db, thread_id, frames_list, stop_reason, message, suspend_type): + def make_thread_suspend_message(self, py_db, thread_id, frames_list, stop_reason, message, trace_suspend_type): try: thread_suspend_str, thread_stack_str = self.make_thread_suspend_str( - py_db, thread_id, frames_list, stop_reason, message, suspend_type) + py_db, thread_id, frames_list, stop_reason, message, trace_suspend_type) cmd = NetCommand(CMD_THREAD_SUSPEND, 0, thread_suspend_str) cmd.thread_stack_str = thread_stack_str cmd.thread_suspend_str = thread_suspend_str diff --git a/_pydevd_bundle/pydevd_plugin_utils.py b/_pydevd_bundle/pydevd_plugin_utils.py index 234d308c..972b8a34 100644 --- a/_pydevd_bundle/pydevd_plugin_utils.py +++ b/_pydevd_bundle/pydevd_plugin_utils.py @@ -1,6 +1,7 @@ import types from _pydev_bundle import pydev_log +from typing import Tuple, Literal try: from pydevd_plugins import django_debug except: @@ -98,19 +99,43 @@ def can_skip(self, py_db, frame): return False return True - def has_exception_breaks(self, py_db): + def required_events_breakpoint(self) -> Tuple[Literal['line', 'call'], ...]: + ret = () + for plugin in self.active_plugins: + new = plugin.required_events_breakpoint() + if new: + ret += new + + return ret + + def required_events_stepping(self) -> Tuple[Literal['line', 'call', 'return'], ...]: + ret = () + for plugin in self.active_plugins: + new = plugin.required_events_stepping() + if new: + ret += new + + return ret + + def is_tracked_frame(self, frame) -> bool: + for plugin in self.active_plugins: + if plugin.is_tracked_frame(frame): + return True + return False + + def has_exception_breaks(self, py_db) -> bool: for plugin in self.active_plugins: if plugin.has_exception_breaks(py_db): return True return False - def has_line_breaks(self, py_db): + def has_line_breaks(self, py_db) -> bool: for plugin in self.active_plugins: if plugin.has_line_breaks(py_db): return True return False - def cmd_step_into(self, py_db, frame, event, info, thread, stop_info, stop): + def cmd_step_into(self, py_db, frame, event, info, thread, stop_info, stop: bool): ''' :param stop_info: in/out information. If it should stop then it'll be filled by the plugin. diff --git a/_pydevd_bundle/pydevd_suspended_frames.py b/_pydevd_bundle/pydevd_suspended_frames.py index 34c404d0..b4016d53 100644 --- a/_pydevd_bundle/pydevd_suspended_frames.py +++ b/_pydevd_bundle/pydevd_suspended_frames.py @@ -433,13 +433,13 @@ def find_frame(self, thread_id, frame_id): with self._lock: return self._frame_id_to_frame.get(frame_id) - def create_thread_suspend_command(self, thread_id, stop_reason, message, suspend_type): + def create_thread_suspend_command(self, thread_id, stop_reason, message, trace_suspend_type): with self._lock: # First one is topmost frame suspended. frames_list = self._thread_id_to_frames_list[thread_id] cmd = self.py_db.cmd_factory.make_thread_suspend_message( - self.py_db, thread_id, frames_list, stop_reason, message, suspend_type) + self.py_db, thread_id, frames_list, stop_reason, message, trace_suspend_type) frames_list = None return cmd diff --git a/_pydevd_sys_monitoring/pydevd_sys_monitoring.py b/_pydevd_sys_monitoring/pydevd_sys_monitoring.py index e9a7f2aa..c3234aec 100644 --- a/_pydevd_sys_monitoring/pydevd_sys_monitoring.py +++ b/_pydevd_sys_monitoring/pydevd_sys_monitoring.py @@ -12,7 +12,8 @@ from _pydevd_bundle import pydevd_dont_trace from _pydevd_bundle.pydevd_additional_thread_info import _set_additional_thread_info_lock, PyDBAdditionalThreadInfo from _pydevd_bundle.pydevd_constants import GlobalDebuggerHolder, ForkSafeLock, \ - PYDEVD_IPYTHON_CONTEXT, EXCEPTION_TYPE_USER_UNHANDLED, RETURN_VALUES_DICT + PYDEVD_IPYTHON_CONTEXT, EXCEPTION_TYPE_USER_UNHANDLED, RETURN_VALUES_DICT, \ + PYTHON_SUSPEND from pydevd_file_utils import NORM_PATHS_AND_BASE_CONTAINER, \ get_abs_path_real_path_and_base_from_file, \ get_abs_path_real_path_and_base_from_frame @@ -226,10 +227,18 @@ def __init__(self): self.breakpoint_found: bool = False self.function_breakpoint_found: bool = False - # When breakpoints_mtime != PyDb.mtime the validity of breakpoints have + # 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 + + self.plugin_line_stepping: bool = False + self.plugin_call_stepping: bool = False + self.plugin_return_stepping: bool = False + + # When pydb_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). - self.breakpoints_mtime: int = -1 + self.pydb_mtime: int = -1 self.bp_line_to_breakpoint: Dict[int, Any] = {} self.function_breakpoint = None @@ -293,7 +302,7 @@ def _get_code_line_info(code_obj, _cache={}): _code_to_func_code_info_cache: Dict[CodeType, 'FuncCodeInfo'] = {} -def get_func_code_info(code_obj, frame_or_depth) -> FuncCodeInfo: +def _get_func_code_info(code_obj, frame_or_depth) -> FuncCodeInfo: ''' Provides code-object related info. @@ -308,12 +317,12 @@ def get_func_code_info(code_obj, frame_or_depth) -> FuncCodeInfo: func_code_info = _code_to_func_code_info_cache.get(code_obj) if func_code_info is not None: - if func_code_info.breakpoints_mtime == py_db.mtime: + if func_code_info.pydb_mtime == py_db.mtime: # if DEBUG: - # print('get_func_code_info: matched mtime', key, code_obj) + # print('_get_func_code_info: matched mtime', key, code_obj) return func_code_info - # print('get_func_code_info: new (mtime did not match)', key, code_obj) + # print('_get_func_code_info: new (mtime did not match)', key, code_obj) co_filename: str = code_obj.co_filename co_name: str = code_obj.co_name @@ -324,7 +333,7 @@ def get_func_code_info(code_obj, frame_or_depth) -> FuncCodeInfo: code_line_info = _get_code_line_info(code_obj) line_to_offset = code_line_info.line_to_offset func_code_info.first_code_line = code_line_info.first_line - func_code_info.breakpoints_mtime = py_db.mtime + func_code_info.pydb_mtime = py_db.mtime func_code_info.co_filename = co_filename func_code_info.co_name = co_name @@ -421,6 +430,21 @@ 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.plugin: + plugin_manager = py_db.plugin + is_tracked_frame = plugin_manager.is_tracked_frame(frame) + + if is_tracked_frame: + if py_db.has_plugin_line_breaks: + required_events_breakpoint = plugin_manager.required_events_breakpoint() + func_code_info.plugin_line_breakpoint_found = 'line' in required_events_breakpoint + func_code_info.plugin_call_breakpoint_found = 'call' in required_events_breakpoint + + required_events_stepping = plugin_manager.required_events_stepping() + func_code_info.plugin_line_stepping: bool = 'line' in required_events_stepping + func_code_info.plugin_call_stepping: bool = 'call' in required_events_stepping + func_code_info.plugin_return_stepping: bool = 'return' in required_events_stepping + _code_to_func_code_info_cache[code_obj] = func_code_info return func_code_info @@ -454,7 +478,7 @@ def enable_code_tracing(thread: Optional[threading.Thread], code, frame): if py_db is None or py_db.pydb_disposed: return monitor.DISABLE - func_code_info: FuncCodeInfo = get_func_code_info(code, frame) + func_code_info: FuncCodeInfo = _get_func_code_info(code, frame) if func_code_info.always_skip_code: # if DEBUG: # print('disable (always skip)') @@ -484,15 +508,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) @@ -543,7 +561,7 @@ def _unwind_event(code, instruction, exc): # threads may still want it... return - func_code_info: FuncCodeInfo = get_func_code_info(code, 1) + func_code_info: FuncCodeInfo = _get_func_code_info(code, 1) if func_code_info.always_skip_code: return @@ -605,7 +623,7 @@ def _raise_event(code, instruction, exc): # threads may still want it... return - func_code_info: FuncCodeInfo = get_func_code_info(code, 1) + func_code_info: FuncCodeInfo = _get_func_code_info(code, 1) if func_code_info.always_skip_code: return @@ -697,7 +715,7 @@ def _return_event(code, instruction, retval): # threads may still want it... return - func_code_info: FuncCodeInfo = get_func_code_info(code, 1) + func_code_info: FuncCodeInfo = _get_func_code_info(code, 1) if func_code_info.always_skip_code: return monitor.DISABLE @@ -707,11 +725,21 @@ def _return_event(code, instruction, retval): frame = _getframe(1) step_cmd = info.pydev_step_cmd + if step_cmd == -1: + return + + if info.suspend_type != PYTHON_SUSPEND: + if func_code_info.plugin_return_stepping: + # TODO: Plugin return stepping + pass + return + + # Python line stepping stop_frame = info.pydev_step_stop if 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 frame.f_back is not None and not info.pydev_use_scoped_step_frame: - back_func_code_info = get_func_code_info(frame.f_back.f_code, frame.f_back) + back_func_code_info = _get_func_code_info(frame.f_back.f_code, frame.f_back) if ( # Not filtered out. not back_func_code_info.always_skip_code and not back_func_code_info.always_filtered_out @@ -740,7 +768,7 @@ def _return_event(code, instruction, retval): # go to step into mode). f_back = frame.f_back if f_back is not None: - back_func_code_info = get_func_code_info(f_back.f_code, 2) + back_func_code_info = _get_func_code_info(f_back.f_code, 2) force_check_project_scope = step_cmd == CMD_STEP_OVER_MY_CODE if back_func_code_info is not None and not back_func_code_info.always_skip_code \ @@ -828,7 +856,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: @@ -877,13 +905,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: + 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 @@ -910,7 +936,7 @@ def _line_event(code, line): # threads may still want it... return - func_code_info: FuncCodeInfo = get_func_code_info(code, 1) + func_code_info: FuncCodeInfo = _get_func_code_info(code, 1) if func_code_info.always_skip_code or func_code_info.always_filtered_out: return monitor.DISABLE @@ -951,7 +977,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: @@ -965,7 +991,12 @@ def _line_event(code, line): if step_cmd == -1: return - elif step_cmd in (CMD_STEP_INTO, CMD_STEP_INTO_MY_CODE, CMD_STEP_INTO_COROUTINE): + if info.suspend_type != PYTHON_SUSPEND: + # Plugin stepping + return + + # Python stepping now + if 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 not info.pydev_use_scoped_step_frame: if func_code_info.always_filtered_out or (force_check_project_scope and func_code_info.filtered_out_force_checked): @@ -1012,33 +1043,28 @@ def _line_event(code, line): py_db.do_wait_suspend(thread_info.thread, frame, 'line', None) return - # if plugin_manager is not None: - # result = plugin_manager.cmd_step_over(py_db, frame, event, self._args, stop_info, stop) - # if result: - # stop, plugin_stop = result - - elif step_cmd == CMD_SMART_STEP_INTO and ( - stop_frame is not None and - stop_frame is not frame and - stop_frame is not frame.f_back and - (frame.f_back is None or stop_frame is not frame.f_back.f_back)): - can_skip = True - - elif step_cmd == CMD_STEP_INTO_MY_CODE: - if ( - py_db.apply_files_filter(frame, frame.f_code.co_filename, True) - and (frame.f_back is None or py_db.apply_files_filter(frame.f_back, frame.f_back.f_code.co_filename, True)) - ): - can_skip = True - - elif step_cmd == CMD_STEP_INTO_COROUTINE: - f = frame - while f is not None: - if _is_same_frame(info, stop_frame, f): - break - f = f.f_back - else: - can_skip = True + # elif step_cmd == CMD_SMART_STEP_INTO and ( + # stop_frame is not None and + # stop_frame is not frame and + # stop_frame is not frame.f_back and + # (frame.f_back is None or stop_frame is not frame.f_back.f_back)): + # can_skip = True + # + # elif step_cmd == CMD_STEP_INTO_MY_CODE: + # if ( + # py_db.apply_files_filter(frame, frame.f_code.co_filename, True) + # and (frame.f_back is None or py_db.apply_files_filter(frame.f_back, frame.f_back.f_code.co_filename, True)) + # ): + # can_skip = True + # + # elif step_cmd == CMD_STEP_INTO_COROUTINE: + # f = frame + # while f is not None: + # if _is_same_frame(info, stop_frame, f): + # break + # f = f.f_back + # else: + # can_skip = True # if can_skip: # if plugin_manager is not None and ( @@ -1069,7 +1095,7 @@ def _start_method_event(code, instruction_offset): frame = _getframe(1) - func_code_info: FuncCodeInfo = get_func_code_info(code, frame) + func_code_info: FuncCodeInfo = _get_func_code_info(code, frame) if func_code_info.always_skip_code: # if DEBUG: # print('disable (always skip)') @@ -1084,9 +1110,48 @@ 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 py_db.plugin: + plugin_manager = py_db.plugin + + info = thread_info.additional_info + if func_code_info.plugin_call_breakpoint_found: + result = plugin_manager.get_breakpoint(py_db, frame, 'call', 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 + + step_cmd = info.pydev_step_cmd + if step_cmd != -1 and func_code_info.plugin_call_stepping and info.suspend_type != PYTHON_SUSPEND: + + # Step return makes no sense for plugins (I guess?!?), so, just handle as step into. + if step_cmd in (CMD_STEP_INTO, CMD_STEP_INTO_MY_CODE, CMD_STEP_INTO_COROUTINE, CMD_SMART_STEP_INTO) or step_cmd in (CMD_STEP_RETURN, CMD_STEP_RETURN_MY_CODE): + stop_info = {} + stop = False + result = plugin_manager.cmd_step_into(py_db, frame, 'call', info, thread_info.thread, stop_info, stop) + if result: + stop, plugin_stop = result + if plugin_stop: + plugin_manager.stop(py_db, frame, 'call', thread_info.thread, stop_info, None, step_cmd) + return + + elif step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE): + if plugin_manager is not None: + stop_info = {} + stop = False + result = plugin_manager.cmd_step_over(py_db, frame, 'call', info, thread_info.thread, stop_info, stop) + if result: + stop, plugin_stop = result + if plugin_stop: + plugin_manager.stop(py_db, frame, 'call', thread_info.thread, stop_info, None, step_cmd) + return + def _resume_method_event(code, instruction_offset): # Same thing as _start_method_event but without checking function breakpoint @@ -1108,7 +1173,7 @@ def _resume_method_event(code, instruction_offset): frame = _getframe(1) - func_code_info: FuncCodeInfo = get_func_code_info(code, frame) + func_code_info: FuncCodeInfo = _get_func_code_info(code, frame) if func_code_info.always_skip_code: # if DEBUG: # print('disable (always skip)') @@ -1248,7 +1313,7 @@ def restart_events() -> None: # Note: if breakpoints change, update_monitor_events usually needs to be # called first, then the line event tracing must be set for existing frames # and then this function must be called at the end. - sys.monitoring.restart_events() + monitor.restart_events() def _is_same_frame(info, target_frame, current_frame): diff --git a/pydevd.py b/pydevd.py index 549792c7..3ffa75aa 100644 --- a/pydevd.py +++ b/pydevd.py @@ -21,6 +21,7 @@ # things as needed for gevent. from _pydevd_bundle import pydevd_constants from typing import Optional +from types import FrameType import atexit import dis @@ -1337,7 +1338,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 @@ -1348,13 +1349,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 @@ -1364,19 +1365,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 @@ -2060,7 +2061,7 @@ def do_wait_suspend(self, thread, frame, event, arg, exception_type=None): # @U # Send the suspend message message = thread.additional_info.pydev_message - suspend_type = thread.additional_info.trace_suspend_type + trace_suspend_type = thread.additional_info.trace_suspend_type thread.additional_info.trace_suspend_type = 'trace' # Reset to trace mode for next call. stop_reason = thread.stop_reason @@ -2094,7 +2095,7 @@ def do_wait_suspend(self, thread, frame, event, arg, exception_type=None): # @U with self.suspended_frames_manager.track_frames(self) as frames_tracker: frames_tracker.track(thread_id, frames_list) - cmd = frames_tracker.create_thread_suspend_command(thread_id, stop_reason, message, suspend_type) + cmd = frames_tracker.create_thread_suspend_command(thread_id, stop_reason, message, trace_suspend_type) self.writer.add_command(cmd) with CustomFramesContainer.custom_frames_lock: # @UndefinedVariable @@ -2109,12 +2110,12 @@ def do_wait_suspend(self, thread, frame, event, arg, exception_type=None): # @U frame_custom_thread_id, custom_frame.name)) self.writer.add_command( - frames_tracker.create_thread_suspend_command(frame_custom_thread_id, CMD_THREAD_SUSPEND, "", suspend_type)) + frames_tracker.create_thread_suspend_command(frame_custom_thread_id, CMD_THREAD_SUSPEND, "", trace_suspend_type)) from_this_thread.append(frame_custom_thread_id) with self._threads_suspended_single_notification.notify_thread_suspended(thread_id, thread, stop_reason): - keep_suspended = self._do_wait_suspend(thread, frame, event, arg, suspend_type, from_this_thread, frames_tracker) + keep_suspended = self._do_wait_suspend(thread, frame, event, arg, trace_suspend_type, from_this_thread, frames_tracker) frames_list = None @@ -2125,7 +2126,7 @@ def do_wait_suspend(self, thread, frame, event, arg, exception_type=None): # @U if DebugInfoHolder.DEBUG_TRACE_LEVEL > 2: pydev_log.debug('Leaving PyDB.do_wait_suspend: %s (%s) %s', thread, thread_id, id(thread)) - def _do_wait_suspend(self, thread, frame, event, arg, suspend_type, from_this_thread, frames_tracker): + def _do_wait_suspend(self, thread, frame, event, arg, trace_suspend_type, from_this_thread, frames_tracker): info = thread.additional_info info.step_in_initial_location = None keep_suspended = False @@ -2202,7 +2203,7 @@ def _do_wait_suspend(self, thread, frame, event, arg, suspend_type, from_this_th thread.stop_reason = CMD_THREAD_SUSPEND # return to the suspend state and wait for other command (without sending any # additional notification to the client). - return self._do_wait_suspend(thread, frame, event, arg, suspend_type, from_this_thread, frames_tracker) + return self._do_wait_suspend(thread, frame, event, arg, trace_suspend_type, from_this_thread, frames_tracker) elif info.pydev_step_cmd in (CMD_STEP_RETURN, CMD_STEP_RETURN_MY_CODE): back_frame = frame.f_back @@ -2285,6 +2286,11 @@ def set_trace_for_frame_and_parents(self, thread: Optional[threading.Thread], fr DEBUG = True # 'defaulttags' in frame.f_code.co_filename while frame is not None: + if not isinstance(frame, FrameType): + # This is the case for django/jinja frames. + frame = frame.f_back + continue + # Don't change the tracing on debugger-related files file_type = self.get_file_type(frame) if PYDEVD_USE_SYS_MONITORING: diff --git a/pydevd_plugins/django_debug.py b/pydevd_plugins/django_debug.py index 9b3d57c6..e621891a 100644 --- a/pydevd_plugins/django_debug.py +++ b/pydevd_plugins/django_debug.py @@ -153,7 +153,7 @@ def _inherits(cls, *names): _IGNORE_RENDER_OF_CLASSES = ('TextNode', 'NodeList') -def _is_django_render_call(frame, debug=False): +def _is_django_render_call(frame): try: name = frame.f_code.co_name if name != 'render': @@ -448,6 +448,17 @@ def can_skip(py_db, frame): return True +is_tracked_frame = _is_django_render_call + + +def required_events_breakpoint(): + return ('call',) + + +def required_events_stepping(): + return ('call', 'return') + + def has_exception_breaks(py_db): if len(py_db.django_exception_break) > 0: return True diff --git a/pydevd_plugins/jinja2_debug.py b/pydevd_plugins/jinja2_debug.py index 6fdc4371..4903effe 100644 --- a/pydevd_plugins/jinja2_debug.py +++ b/pydevd_plugins/jinja2_debug.py @@ -344,6 +344,17 @@ def can_skip(pydb, frame): return True +is_tracked_frame = _is_jinja2_render_call + + +def required_events_breakpoint(): + return ('line',) + + +def required_events_stepping(): + return ('call', 'line', 'return') + + def cmd_step_into(pydb, frame, event, info, thread, stop_info, stop): plugin_stop = False if _is_jinja2_suspended(thread): diff --git a/tests_python/debugger_unittest.py b/tests_python/debugger_unittest.py index 9fd7d9e2..f5a4aa0d 100644 --- a/tests_python/debugger_unittest.py +++ b/tests_python/debugger_unittest.py @@ -688,6 +688,8 @@ def _ignore_stderr_line(self, line): 'pydev debugger: New process is launching', 'pydev debugger: To debug that process', '*** Multiprocess', + 'WARNING: This is a development server. Do not use it in a production deployment', + 'Press CTRL+C to quit', )): return True diff --git a/tests_python/test_debugger.py b/tests_python/test_debugger.py index cb16bf73..2b8a8bc4 100644 --- a/tests_python/test_debugger.py +++ b/tests_python/test_debugger.py @@ -1020,6 +1020,59 @@ 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 == '' + 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_step_in(hit.thread_id) + + hit = writer.wait_for_breakpoint_hit(REASON_STEP_INTO, line=7) + + writer.write_run_thread(hit.thread_id) + + contents = t.wait_for_contents() + + contents = contents.replace(' ', '').replace('\r', '').replace('\n', '') + if contents != '': + raise AssertionError('%s != ' % (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: