Skip to content

Return evaluate result in DAP response body instead of writing to stdout#2027

Merged
rchiodo merged 1 commit into
microsoft:mainfrom
pdepetro:main
May 27, 2026
Merged

Return evaluate result in DAP response body instead of writing to stdout#2027
rchiodo merged 1 commit into
microsoft:mainfrom
pdepetro:main

Conversation

@pdepetro
Copy link
Copy Markdown
Contributor

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.

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.
@pdepetro pdepetro requested a review from a team as a code owner May 27, 2026 13:04
Copy link
Copy Markdown
Contributor

@rchiodo rchiodo left a comment

Choose a reason for hiding this comment

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

It seems correct but I'm wondering why you wanted to change this? Is there a reason this is needed elsewhere?

Also what does VS code send if you're not stopped on a breakpoint and you try to eval something in the debug console?

@rchiodo
Copy link
Copy Markdown
Contributor

rchiodo commented May 27, 2026

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@@ -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]

@pdepetro
Copy link
Copy Markdown
Contributor Author

It seems correct but I'm wondering why you wanted to change this? Is there a reason this is needed elsewhere?

We have non-VS Code clients that are affected by this.

Also what does VS code send if you're not stopped on a breakpoint and you try to eval something in the debug console?

Evaluation without being stopped shows the behavior in VS Code as well:

Before the change:

11:17:17.422  --> evaluate
{"command":"evaluate","arguments":{"expression":"5+5","context":"repl"},"type":"request","seq":18}

11:17:17.466  <== output
{"body":{"category":"stdout","output":"10\n","source":{}},"event":"output","seq":44,"type":"event"}

after the change:

11:09:23.675  --> evaluate
{"command":"evaluate","arguments":{"expression":"5+5","context":"repl"},"type":"request","seq":18}

11:09:23.731  <-- evaluate
{"body":{"presentationHint":{},"result":"10","variablesReference":0},"command":"evaluate","request_seq":18,"seq":31,"success":true,"type":"response"}

My understanding is that the latter is the correct per the DAP spec, and matches how other debuggers respond.

Copy link
Copy Markdown
Contributor

@rchiodo rchiodo left a comment

Choose a reason for hiding this comment

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

Approved via Review Center.

@rchiodo
Copy link
Copy Markdown
Contributor

rchiodo commented May 27, 2026

Thanks Philip.

@rchiodo rchiodo merged commit 12bd4fe into microsoft:main May 27, 2026
24 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants