Don't evaluate twice if an exception is raised. Fixes #742

This commit is contained in:
Fabio Zadrozny 2021-10-07 11:08:28 -03:00
parent f14ba78c5c
commit 765b64c0a4
4 changed files with 109 additions and 54 deletions

View file

@ -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 = '<Internal error - unable to get traceback when evaluating expression>'
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 = '<Internal error - unable to get traceback when evaluating expression>'
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):

View file

@ -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), '<string>', '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), '<string>', 'eval')
compiled = compile_as_eval(expression)
except Exception:
Exec(_expression_to_evaluate(expression), updated_globals, frame.f_locals)
pydevd_save_locals.save_locals(frame)

View file

@ -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")

View file

@ -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', ('<string>', 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', ('<string>', 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