From 765b64c0a4eeb64930ce73b415d013de07097810 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Thu, 7 Oct 2021 11:08:28 -0300 Subject: [PATCH] Don't evaluate twice if an exception is raised. Fixes #742 --- .../pydevd/_pydevd_bundle/pydevd_comm.py | 74 +++++++++++-------- .../pydevd/_pydevd_bundle/pydevd_vars.py | 31 ++++---- .../pydevd/_pydevd_bundle/pydevd_xml.py | 4 +- .../pydevd/tests_python/test_debugger_json.py | 54 ++++++++++++-- 4 files changed, 109 insertions(+), 54 deletions(-) diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index d4964607..917e3a5c 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -1187,6 +1187,7 @@ def internal_evaluate_expression_json(py_db, request, thread_id): eval_result = None else: frame = py_db.find_frame(thread_id, frame_id) + eval_result = pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=False) is_error = isinstance_checked(eval_result, ExceptionOnEvaluate) if is_error: @@ -1201,41 +1202,24 @@ def internal_evaluate_expression_json(py_db, request, thread_id): _evaluate_response(py_db, request, result=msg, error_message=msg) return else: - try_exec = context == 'repl' + # We only try the exec if the failure we had was due to not being able + # to evaluate the expression. + try: + pydevd_vars.compile_as_eval(expression) + except Exception: + try_exec = context == 'repl' + else: + try_exec = False + if context == 'repl': + # In the repl we should show the exception to the user. + _evaluate_response_return_exception(py_db, request, eval_result.etype, eval_result.result, eval_result.tb) + return if try_exec: try: pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=True) except (Exception, KeyboardInterrupt): - try: - exc, exc_type, initial_tb = sys.exc_info() - tb = initial_tb - - # Show the traceback without pydevd frames. - temp_tb = tb - while temp_tb: - if py_db.get_file_type(temp_tb.tb_frame) == PYDEV_FILE: - tb = temp_tb.tb_next - temp_tb = temp_tb.tb_next - - if tb is None: - tb = initial_tb - err = ''.join(traceback.format_exception(exc, exc_type, tb)) - - # Make sure we don't keep references to them. - exc = None - exc_type = None - tb = None - temp_tb = None - initial_tb = None - except: - err = '' - pydev_log.exception(err) - - # Currently there is an issue in VSC where returning success=false for an - # eval request, in repl context, VSC does not show the error response in - # the debug console. So return the error message in result as well. - _evaluate_response(py_db, request, result=err, error_message=err) + _evaluate_response_return_exception(py_db, request, *sys.exc_info()) return # No result on exec. _evaluate_response(py_db, request, result='') @@ -1272,6 +1256,36 @@ def internal_evaluate_expression_json(py_db, request, thread_id): variables_response = pydevd_base_schema.build_response(request, kwargs={'body':body}) py_db.writer.add_command(NetCommand(CMD_RETURN, 0, variables_response, is_json=True)) +def _evaluate_response_return_exception(py_db, request, exc_type, exc, initial_tb): + try: + tb = initial_tb + + # Show the traceback without pydevd frames. + temp_tb = tb + while temp_tb: + if py_db.get_file_type(temp_tb.tb_frame) == PYDEV_FILE: + tb = temp_tb.tb_next + temp_tb = temp_tb.tb_next + + if tb is None: + tb = initial_tb + err = ''.join(traceback.format_exception(exc_type, exc, tb)) + + # Make sure we don't keep references to them. + exc = None + exc_type = None + tb = None + temp_tb = None + initial_tb = None + except: + err = '' + pydev_log.exception(err) + + # Currently there is an issue in VSC where returning success=false for an + # eval request, in repl context, VSC does not show the error response in + # the debug console. So return the error message in result as well. + _evaluate_response(py_db, request, result=err, error_message=err) + @silence_warnings_decorator def internal_evaluate_expression(dbg, seq, thread_id, frame_id, expression, is_exec, trim_if_too_big, attr_to_set_result): diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py index d436386a..1af1a778 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py @@ -259,20 +259,8 @@ def eval_in_context(expression, globals, locals): try: result = eval(_expression_to_evaluate(expression), globals, locals) except (Exception, KeyboardInterrupt): - s = StringIO() - traceback.print_exc(file=s) - result = s.getvalue() - - try: - try: - etype, value, tb = sys.exc_info() - result = value - finally: - etype = value = tb = None - except: - pass - - result = ExceptionOnEvaluate(result) + etype, result, tb = sys.exc_info() + result = ExceptionOnEvaluate(result, etype, tb) # Ok, we have the initial error message, but let's see if we're dealing with a name mangling error... try: @@ -373,6 +361,19 @@ def _evaluate_with_timeouts(original_func): return new_func +def compile_as_eval(expression): + ''' + + :param expression: + The expression to be compiled. + + :return: code object + + :raises Exception if the expression cannot be evaluated. + ''' + return compile(_expression_to_evaluate(expression), '', 'eval') + + @_evaluate_with_timeouts def evaluate_expression(py_db, frame, expression, is_exec): ''' @@ -416,7 +417,7 @@ def evaluate_expression(py_db, frame, expression, is_exec): try: # try to make it an eval (if it is an eval we can print it, otherwise we'll exec it and # it will have whatever the user actually did) - compiled = compile(_expression_to_evaluate(expression), '', 'eval') + compiled = compile_as_eval(expression) except Exception: Exec(_expression_to_evaluate(expression), updated_globals, frame.f_locals) pydevd_save_locals.save_locals(frame) diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_xml.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_xml.py index 202d6b0e..bf7c4fdd 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_xml.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_xml.py @@ -25,8 +25,10 @@ def make_valid_xml_value(s): class ExceptionOnEvaluate: - def __init__(self, result): + def __init__(self, result, etype, tb): self.result = result + self.etype = etype + self.tb = tb _IS_JYTHON = sys.platform.startswith("java") 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 6beeec16..556652d4 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -2226,14 +2226,16 @@ def test_evaluate_unicode(case_setup): assert reference > 0 variables_response = json_facade.get_variables_response(reference) child_variables = variables_response.to_dict()['body']['variables'] - assert len(child_variables) == 1 - assert json_facade.pop_variables_reference(child_variables)[0] > 0 - assert child_variables == [{ - u'type': u'SyntaxError', - u'evaluateName': u'\u16a0.result', - u'name': u'result', - u'value': u"SyntaxError('invalid syntax', ('', 1, 1, '\\xe1\\x9a\\xa0'))" - }] + assert len(child_variables) == 2 + for c in child_variables: + if c[u'type'] == u'SyntaxError': + assert c.pop('variablesReference') > 0 + assert c == { + u'type': u'SyntaxError', + u'evaluateName': u'\u16a0.result', + u'name': u'result', + u'value': u"SyntaxError('invalid syntax', ('', 1, 1, '\\xe1\\x9a\\xa0'))" + } else: assert evaluate_response_body == { @@ -2329,6 +2331,42 @@ def test_evaluate_repl_redirect(case_setup): writer.finished_ok = True +def test_evaluate_no_double_exec(case_setup, pyfile): + + @pyfile + def exec_code(): + + def print_and_raise(): + print('Something') + raise RuntimeError() + + print('Break here') + print('TEST SUCEEDED!') + + with case_setup.test_file(exec_code) as writer: + json_facade = JsonFacade(writer) + json_facade.write_launch(justMyCode=False) + json_facade.write_set_breakpoints(writer.get_line_index_with_content('Break here')) + json_facade.write_make_initial_run() + + json_hit = json_facade.wait_for_thread_stopped() + json_hit = json_facade.get_stack_as_json_hit(json_hit.thread_id) + + json_facade.evaluate( + "print_and_raise()", + frameId=json_hit.frame_id, + context="repl", + success=False, + ) + + messages = json_facade.mark_messages( + OutputEvent, lambda output_event: u'Something' in output_event.body.output) + assert len(messages) == 1 + + json_facade.write_continue() + writer.finished_ok = True + + def test_evaluate_variable_references(case_setup): from _pydevd_bundle._debug_adapter.pydevd_schema import EvaluateRequest from _pydevd_bundle._debug_adapter.pydevd_schema import EvaluateArguments