Skip to content

Commit 12bd4fe

Browse files
authored
Return evaluate result in DAP response body instead of writing to stdout (#2027)
When a DAP client sends an `evaluate` request with `context: "repl"` and no `frameId`, debugpy forces the expression through the exec code path. Previously, if the expression could be compiled as an eval (e.g. `2 + 2`), `evaluate_expression` would compute the result but write it to `sys.stdout` and return `None`. The caller in `internal_evaluate_expression_json` would then send back `result=""` in the response body. Clients that read the response body would get nothing, while the actual value was emitted as a DAP output event. This changes `evaluate_expression` to return the computed result from the eval-within-exec path instead of printing it. The caller now captures that return value and includes it in the response body. Pure exec statements (e.g. `x = 42`) continue to return `None` and produce `result=""` as before. VS Code is unaffected because it always provides a `frameId`, which routes through the normal eval path where results already go into the response body.
1 parent 8bd57a7 commit 12bd4fe

4 files changed

Lines changed: 47 additions & 23 deletions

File tree

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,12 +1239,16 @@ def __create_frame():
12391239

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

12501254
# Ok, we have the result (could be an error), let's put it into the saved variables.

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,9 @@ def method():
473473
There are some changes in this function depending on whether it's an exec or an eval.
474474
475475
When it's an exec (i.e.: is_exec==True):
476-
This function returns None.
476+
If the expression can be compiled as an eval, the result of the evaluation is returned.
477+
If the expression can only be compiled as an exec (i.e.: a statement), None is returned.
477478
Any exception that happens during the evaluation is reraised.
478-
If the expression could actually be evaluated, the variable is printed to the console if not None.
479479
480480
When it's an eval (i.e.: is_exec==False):
481481
This function returns the result from the evaluation.
@@ -539,12 +539,13 @@ def method():
539539

540540
if is_exec:
541541
try:
542-
# Try to make it an eval (if it is an eval we can print it, otherwise we'll exec it and
543-
# it will have whatever the user actually did)
542+
# Try to make it an eval (if it is an eval we can return the result to the caller,
543+
# otherwise we'll exec it and it will have whatever the user actually did)
544544
compiled = compile_as_eval(expression)
545545
except Exception:
546546
compiled = None
547547

548+
result = None
548549
if compiled is None:
549550
try:
550551
compiled = _compile_as_exec(expression)
@@ -574,9 +575,7 @@ def method():
574575
result = t.evaluated_value
575576
else:
576577
result = eval(compiled, updated_globals, updated_locals)
577-
if result is not None: # Only print if it's not None (as python does)
578-
sys.stdout.write("%s\n" % (result,))
579-
return
578+
return result
580579

581580
else:
582581
ret = eval_in_context(expression, updated_globals, updated_locals, py_db)

src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3633,9 +3633,9 @@ def test_evaluate(case_setup_dap):
36333633
# Ok, 'foo_value' is now set in 'email' module.
36343634
exec_response = json_facade.evaluate("email.foo_value")
36353635

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

36403640
json_facade.write_continue()
36413641

src/debugpy/_vendored/pydevd/tests_python/test_evaluate_expression.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -238,23 +238,44 @@ async def main():
238238
assert async_call is not None # Make sure it's in the locals.
239239
frame = sys._getframe()
240240
eval_txt = "await async_call(10)"
241-
from io import StringIO
241+
result = evaluate_expression(py_db, frame, eval_txt, is_exec=True)
242242

243-
_original_stdout = sys.stdout
244-
try:
245-
stringio = sys.stdout = StringIO()
246-
evaluate_expression(py_db, frame, eval_txt, is_exec=True)
247-
finally:
248-
sys.stdout = _original_stdout
249-
250-
# I.e.: Check that we printed the value obtained in the exec.
251-
assert "10\n" in stringio.getvalue()
243+
# The eval-within-exec path should return the result directly.
244+
assert result == 10
252245

253246
import asyncio
254247

255248
asyncio.run(main())
256249

257250

251+
@pytest.mark.skipif(IS_PY313_0, reason="Crashes on Python 3.13.0")
252+
def test_evaluate_expression_sync_exec_as_eval(disable_critical_log):
253+
from _pydevd_bundle.pydevd_vars import evaluate_expression
254+
from io import StringIO
255+
256+
frame = next(iter(obtain_frame()))
257+
258+
# An expression evaluated via the exec path should return the result directly
259+
# and must not write to stdout.
260+
_original_stdout = sys.stdout
261+
try:
262+
stringio = sys.stdout = StringIO()
263+
result = evaluate_expression(None, frame, "1 + 2", is_exec=True)
264+
finally:
265+
sys.stdout = _original_stdout
266+
assert result == 3
267+
assert stringio.getvalue() == ""
268+
269+
# None results should return None.
270+
result = evaluate_expression(None, frame, "None", is_exec=True)
271+
assert result is None
272+
273+
# Statements (pure exec) should return None.
274+
result = evaluate_expression(None, frame, "x = 42", is_exec=True)
275+
assert result is None
276+
assert frame.f_locals["x"] == 42
277+
278+
258279
@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")
259280
def test_evaluate_expression_async_exec_error(disable_critical_log):
260281
py_db = _DummyPyDB()

0 commit comments

Comments
 (0)