diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index d63899f7..8fd52229 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -1420,7 +1420,6 @@ def build_exception_info_response(dbg, thread_id, request_seq, set_additional_th try: try: frames_list = dbg.suspended_frames_manager.get_frames_list(thread_id) - memo = set() while frames_list is not None and len(frames_list): frames = [] @@ -1483,8 +1482,7 @@ def build_exception_info_response(dbg, thread_id, request_seq, set_additional_th stack_str += frames_list.exc_context_msg stack_str_lst.append(stack_str) - frames_list = create_frames_list_from_exception_cause( - frames_list.trace_obj, None, frames_list.exc_type, frames_list.exc_desc, memo) + frames_list = frames_list.chained_frames_list if frames_list is None or not frames_list: break diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py index c34ed304..c6786c0f 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py @@ -97,6 +97,8 @@ class FramesList(object): # This is to know whether an exception was extracted from a __cause__ or __context__. self.exc_context_msg = '' + self.chained_frames_list = None + def append(self, frame): self._frames.append(frame) @@ -128,7 +130,13 @@ class FramesList(object): lst.append('\n ') lst.append(repr(frame)) lst.append(',') + + if self.chained_frames_list is not None: + lst.append('\n--- Chained ---\n') + lst.append(str(self.chained_frames_list)) + lst.append('\n)') + return ''.join(lst) __str__ = __repr__ @@ -142,7 +150,8 @@ class _DummyFrameWrapper(object): self.f_back = f_back self.f_trace = None original_code = frame.f_code - self.f_code = FCode(original_code.co_name , original_code.co_filename) + name = original_code.co_name + self.f_code = FCode(name, original_code.co_filename) @property def f_locals(self): @@ -152,6 +161,11 @@ class _DummyFrameWrapper(object): def f_globals(self): return self._base_frame.f_globals + def __str__(self): + return "<_DummyFrameWrapper, file '%s', line %s, %s" % (self.f_code.co_filename, self.f_lineno, self.f_code.co_name) + + __repr__ = __str__ + _cause_message = ( "\nThe above exception was the direct cause " @@ -231,36 +245,6 @@ def create_frames_list_from_traceback(trace_obj, frame, exc_type, exc_desc, exce lst.append((tb.tb_frame, tb.tb_lineno)) tb = tb.tb_next - curr = exc_desc - memo = set() - while True: - initial = curr - try: - curr = getattr(initial, '__cause__', None) - except Exception: - curr = None - - if curr is None: - try: - curr = getattr(initial, '__context__', None) - except Exception: - curr = None - - if curr is None or id(curr) in memo: - break - - # The traceback module does this, so, let's play safe here too... - memo.add(id(curr)) - - tb = getattr(curr, '__traceback__', None) - - while tb is not None: - # Note: we don't use the actual tb.tb_frame because if the cause of the exception - # uses the same frame object, the id(frame) would be the same and the frame_id_to_lineno - # would be wrong as the same frame needs to appear with 2 different lines. - lst.append((_DummyFrameWrapper(tb.tb_frame, tb.tb_lineno, None), tb.tb_lineno)) - tb = tb.tb_next - frames_list = None for tb_frame, tb_lineno in reversed(lst): @@ -290,6 +274,18 @@ def create_frames_list_from_traceback(trace_obj, frame, exc_type, exc_desc, exce if len(frames_list) > 0: frames_list.current_frame = frames_list.last_frame() + curr = frames_list + memo = set() + memo.add(id(exc_desc)) + + while True: + chained = create_frames_list_from_exception_cause(None, None, None, curr.exc_desc, memo) + if chained is None: + break + else: + curr.chained_frames_list = chained + curr = chained + return frames_list diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py index 23746150..2d7a8046 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py @@ -230,7 +230,7 @@ class NetCommandFactoryJson(NetCommandFactory): frames_list = pydevd_frame_utils.create_frames_list_from_frame(topmost_frame) for frame_id, frame, method_name, original_filename, filename_in_utf8, lineno, applied_mapping, show_as_current_frame in self._iter_visible_frames_info( - py_db, frames_list + py_db, frames_list, flatten_chained=True ): try: diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py index 5ed644be..da2a680b 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py @@ -164,33 +164,46 @@ class NetCommandFactory(object): except: return self.make_error_message(0, get_exception_traceback_str()) - def _iter_visible_frames_info(self, py_db, frames_list): + def _iter_visible_frames_info(self, py_db, frames_list, flatten_chained=False): assert frames_list.__class__ == FramesList - for frame in frames_list: - show_as_current_frame = frame is frames_list.current_frame - if frame.f_code is None: - pydev_log.info('Frame without f_code: %s', frame) - continue # IronPython sometimes does not have it! + is_chained = False + while True: + for frame in frames_list: + show_as_current_frame = frame is frames_list.current_frame + if frame.f_code is None: + pydev_log.info('Frame without f_code: %s', frame) + continue # IronPython sometimes does not have it! - method_name = frame.f_code.co_name # method name (if in method) or ? if global - if method_name is None: - pydev_log.info('Frame without co_name: %s', frame) - continue # IronPython sometimes does not have it! + method_name = frame.f_code.co_name # method name (if in method) or ? if global + if method_name is None: + pydev_log.info('Frame without co_name: %s', frame) + continue # IronPython sometimes does not have it! - abs_path_real_path_and_base = get_abs_path_real_path_and_base_from_frame(frame) - if py_db.get_file_type(frame, abs_path_real_path_and_base) == py_db.PYDEV_FILE: - # Skip pydevd files. - frame = frame.f_back - continue + if is_chained: + method_name = '[Chained Exc: %s] %s' % (frames_list.exc_desc, method_name) - frame_id = id(frame) - lineno = frames_list.frame_id_to_lineno.get(frame_id, frame.f_lineno) + abs_path_real_path_and_base = get_abs_path_real_path_and_base_from_frame(frame) + if py_db.get_file_type(frame, abs_path_real_path_and_base) == py_db.PYDEV_FILE: + # Skip pydevd files. + frame = frame.f_back + continue - filename_in_utf8, lineno, changed = py_db.source_mapping.map_to_client(abs_path_real_path_and_base[0], lineno) - new_filename_in_utf8, applied_mapping = pydevd_file_utils.map_file_to_client(filename_in_utf8) - applied_mapping = applied_mapping or changed + frame_id = id(frame) + lineno = frames_list.frame_id_to_lineno.get(frame_id, frame.f_lineno) - yield frame_id, frame, method_name, abs_path_real_path_and_base[0], new_filename_in_utf8, lineno, applied_mapping, show_as_current_frame + filename_in_utf8, lineno, changed = py_db.source_mapping.map_to_client(abs_path_real_path_and_base[0], lineno) + new_filename_in_utf8, applied_mapping = pydevd_file_utils.map_file_to_client(filename_in_utf8) + applied_mapping = applied_mapping or changed + + yield frame_id, frame, method_name, abs_path_real_path_and_base[0], new_filename_in_utf8, lineno, applied_mapping, show_as_current_frame + + if not flatten_chained: + break + + frames_list = frames_list.chained_frames_list + if frames_list is None or len(frames_list) == 0: + break + is_chained = True def make_thread_stack_str(self, py_db, frames_list): assert frames_list.__class__ == FramesList @@ -200,7 +213,7 @@ class NetCommandFactory(object): try: for frame_id, frame, method_name, _original_filename, filename_in_utf8, lineno, _applied_mapping, _show_as_current_frame in self._iter_visible_frames_info( - py_db, frames_list + py_db, frames_list, flatten_chained=True ): # print("file is ", filename_in_utf8) diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py index 9262185d..7057252a 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py @@ -1657,13 +1657,13 @@ def test_case_throw_exc_reason_xml(case_setup): name_and_lines.append((frame['name'], frame['line'])) assert name_and_lines == [ - ('method2', '2'), - ('method', '6'), - ('foobar', '16'), - ('handle', '10'), - ('foobar', '18'), ('foobar', '20'), ('', '23'), + ('[Chained Exc: another while handling] foobar', '18'), + ('[Chained Exc: another while handling] handle', '10'), + ('[Chained Exc: TEST SUCEEDED] foobar', '16'), + ('[Chained Exc: TEST SUCEEDED] method', '6'), + ('[Chained Exc: TEST SUCEEDED] method2', '2'), ] hit = writer.wait_for_breakpoint_hit(REASON_UNCAUGHT_EXCEPTION) diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py index 60c9846b..a357cfd8 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -751,7 +751,15 @@ def test_case_throw_exc_reason(case_setup): stack_frames = json_hit.stack_trace_response.body.stackFrames # Note that the additional context doesn't really appear in the stack # frames, only in the details. - assert [x['name'] for x in stack_frames] == ['foobar', ''] + assert [x['name'] for x in stack_frames] == [ + 'foobar', + '', + '[Chained Exc: another while handling] foobar', + '[Chained Exc: another while handling] handle', + '[Chained Exc: TEST SUCEEDED] foobar', + '[Chained Exc: TEST SUCEEDED] method', + '[Chained Exc: TEST SUCEEDED] method2', + ] body = exc_info_response.body assert body.exceptionId.endswith('RuntimeError') @@ -760,10 +768,16 @@ def test_case_throw_exc_reason(case_setup): # Check that we have all the lines (including the cause/context) in the stack trace. import re - lines_and_names = re.findall(r',\sline\s(\d+),\sin\s([\w|<|>]+)', body.details.stackTrace) + lines_and_names = re.findall(r',\sline\s(\d+),\sin\s(\[Chained Exception\]\s)?([\w|<|>]+)', body.details.stackTrace) assert lines_and_names == [ - ('16', 'foobar'), ('6', 'method'), ('2', 'method2'), ('18', 'foobar'), ('10', 'handle'), ('20', 'foobar'), ('23', '') - ] + ('16', '', 'foobar'), + ('6', '', 'method'), + ('2', '', 'method2'), + ('18', '', 'foobar'), + ('10', '', 'handle'), + ('20', '', 'foobar'), + ('23', '', ''), + ], 'Did not find the expected names in:\n%s' % (body.details.stackTrace,) json_facade.write_continue() @@ -802,7 +816,12 @@ def test_case_throw_exc_reason_shown(case_setup): stack_frames = json_hit.stack_trace_response.body.stackFrames # Note that the additional context doesn't really appear in the stack # frames, only in the details. - assert [x['name'] for x in stack_frames] == ['method', ''] + assert [x['name'] for x in stack_frames] == [ + 'method', + '', + "[Chained Exc: 'foo'] method", + "[Chained Exc: 'foo'] method2", + ] body = exc_info_response.body assert body.exceptionId == 'Exception' @@ -3323,7 +3342,7 @@ def test_exception_details(case_setup, max_frames): else: json_facade.write_launch(maxExceptionStackFrames=max_frames) min_expected_lines = 10 - max_expected_lines = 21 + max_expected_lines = 22 json_facade.write_set_exception_breakpoints(['raised']) diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_frame_utils.py b/src/debugpy/_vendored/pydevd/tests_python/test_frame_utils.py new file mode 100644 index 00000000..2e595d4f --- /dev/null +++ b/src/debugpy/_vendored/pydevd/tests_python/test_frame_utils.py @@ -0,0 +1,34 @@ +import sys +from _pydevd_bundle.pydevd_constants import EXCEPTION_TYPE_USER_UNHANDLED + + +def test_create_frames_list_from_traceback(): + + def method(): + raise RuntimeError('first') + + def method1(): + try: + method() + except Exception as e: + raise RuntimeError('second') from e + + def method2(): + try: + method1() + except Exception as e: + raise RuntimeError('third') from e + + try: + method2() + except Exception as e: + exc_type, exc_desc, trace_obj = sys.exc_info() + frame = sys._getframe() + + from _pydevd_bundle.pydevd_frame_utils import create_frames_list_from_traceback + frames_list = create_frames_list_from_traceback(trace_obj, frame, exc_type, exc_desc, exception_type=EXCEPTION_TYPE_USER_UNHANDLED) + assert str(frames_list.exc_desc) == 'third' + assert str(frames_list.chained_frames_list.exc_desc) == 'second' + assert str(frames_list.chained_frames_list.chained_frames_list.exc_desc) == 'first' + assert frames_list.chained_frames_list.chained_frames_list.chained_frames_list is None + diff --git a/tests/debugpy/test_exception.py b/tests/debugpy/test_exception.py index 385f32b6..31f315e7 100644 --- a/tests/debugpy/test_exception.py +++ b/tests/debugpy/test_exception.py @@ -354,7 +354,7 @@ def test_exception_stack(pyfile, target, run, max_frames): max_frames, (min_expected_lines, max_expected_lines) = { "all": (0, (100, 221)), "default": (None, (100, 221)), - 10: (10, (10, 21)), + 10: (10, (10, 22)), }[max_frames] if max_frames is not None: session.config["maxExceptionStackFrames"] = max_frames