Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1239,12 +1239,16 @@ def __create_frame():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot generated:
-1249

[unverified] The "%s" % (exec_result,) formatting at line 1249 now happens outside the try/except that catches exceptions from evaluate_expression. Previously, the equivalent "%s\n" % (result,) was inside evaluate_expression (at pydevd_vars.py:577), meaning any __str__ exception was caught by the caller's except (Exception, KeyboardInterrupt). Now an object with a broken __str__ will raise an unhandled exception at the response-formatting stage.

Suggested fix — move the formatting inside the try/except:

try:
    exec_result = pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=True)
    result = "%s" % (exec_result,) if exec_result is not None else ""
except (Exception, KeyboardInterrupt):
    _evaluate_response_return_exception(py_db, request, *sys.exc_info())
    return
_evaluate_response(py_db, request, result=result)

The Advocate notes this uses the same %s as before, but the error-handling boundary has shifted — this is a new failure mode, not pre-existing.

[verified]

if try_exec:
try:
pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=True)
exec_result = pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=True)
except (Exception, KeyboardInterrupt):
_evaluate_response_return_exception(py_db, request, *sys.exc_info())
return
# No result on exec.
_evaluate_response(py_db, request, result="")
# Use simple string formatting rather than the richer obtain_as_variable path
# (which provides type info, variablesReference, etc.) because the exec path is
# typically reached when there is no frameId, meaning thread_id="*" and no
# frame_tracker is available to build a structured variable response.
result = "%s" % (exec_result,) if exec_result is not None else ""
_evaluate_response(py_db, request, result=result)
return

# Ok, we have the result (could be an error), let's put it into the saved variables.
Expand Down
13 changes: 6 additions & 7 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,9 @@ def method():
There are some changes in this function depending on whether it's an exec or an eval.

When it's an exec (i.e.: is_exec==True):
This function returns None.
If the expression can be compiled as an eval, the result of the evaluation is returned.
If the expression can only be compiled as an exec (i.e.: a statement), None is returned.
Any exception that happens during the evaluation is reraised.
If the expression could actually be evaluated, the variable is printed to the console if not None.

When it's an eval (i.e.: is_exec==False):
This function returns the result from the evaluation.
Expand Down Expand Up @@ -539,12 +539,13 @@ def method():

if 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)
# Try to make it an eval (if it is an eval we can return the result to the caller,
# otherwise we'll exec it and it will have whatever the user actually did)
compiled = compile_as_eval(expression)
except Exception:
compiled = None

result = None
if compiled is None:
try:
compiled = _compile_as_exec(expression)
Expand Down Expand Up @@ -574,9 +575,7 @@ def method():
result = t.evaluated_value
else:
result = eval(compiled, updated_globals, updated_locals)
if result is not None: # Only print if it's not None (as python does)
sys.stdout.write("%s\n" % (result,))
return
return result

else:
ret = eval_in_context(expression, updated_globals, updated_locals, py_db)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3633,9 +3633,9 @@ def test_evaluate(case_setup_dap):
# Ok, 'foo_value' is now set in 'email' module.
exec_response = json_facade.evaluate("email.foo_value")

# We don't actually get variables without a frameId, we can just evaluate and observe side effects
# (so, the result is always empty -- or an error).
assert exec_response.body.result == ""
# Without a frameId the exec path is used, which now returns the result
# for eval-able expressions.
assert exec_response.body.result == "True"

json_facade.write_continue()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,23 +238,44 @@ async def main():
assert async_call is not None # Make sure it's in the locals.
frame = sys._getframe()
eval_txt = "await async_call(10)"
from io import StringIO
result = evaluate_expression(py_db, frame, eval_txt, is_exec=True)

_original_stdout = sys.stdout
try:
stringio = sys.stdout = StringIO()
evaluate_expression(py_db, frame, eval_txt, is_exec=True)
finally:
sys.stdout = _original_stdout

# I.e.: Check that we printed the value obtained in the exec.
assert "10\n" in stringio.getvalue()
# The eval-within-exec path should return the result directly.
assert result == 10

import asyncio

asyncio.run(main())


@pytest.mark.skipif(IS_PY313_0, reason="Crashes on Python 3.13.0")
def test_evaluate_expression_sync_exec_as_eval(disable_critical_log):
from _pydevd_bundle.pydevd_vars import evaluate_expression
from io import StringIO

frame = next(iter(obtain_frame()))

# An expression evaluated via the exec path should return the result directly
# and must not write to stdout.
_original_stdout = sys.stdout
try:
stringio = sys.stdout = StringIO()
result = evaluate_expression(None, frame, "1 + 2", is_exec=True)
finally:
sys.stdout = _original_stdout
assert result == 3
assert stringio.getvalue() == ""

# None results should return None.
result = evaluate_expression(None, frame, "None", is_exec=True)
assert result is None

# Statements (pure exec) should return None.
result = evaluate_expression(None, frame, "x = 42", is_exec=True)
assert result is None
assert frame.f_locals["x"] == 42


@pytest.mark.skipif(not CAN_EVALUATE_TOP_LEVEL_ASYNC or IS_PY313_0, reason="Requires top-level async evaluation. Crashes on Python 3.13.0")
def test_evaluate_expression_async_exec_error(disable_critical_log):
py_db = _DummyPyDB()
Expand Down
Loading