fix(code-runtime-python): write dispatch frames through def-time bound primitives

The review's remaining functional item: send_sync's body resolves
_encode_json_plain (module global) and self.write_encoded (class attribute) at
call time, so a program rebinding either before the first binding call could
turn a legitimate call into an exception. dispatch now writes the call frame
through def-time bound write_encoded+_encode_json_plain, and the log sink goes
through the bound send; the dispatch rebind test also rebinds those two names
(verified fail-before by reverting to send_sync). The annotation test title
matches its assertion direction, and the note (en + zh) registers the
error-class constructor, dispatch primitives, and dont_inherit mechanisms.
Pairing re-recorded.
This commit is contained in:
Chinesezjc
2026-08-31 14:50:39 +08:00
committed by Tianyi Cui
parent c4c79f094d
commit 125306324f
5 changed files with 25 additions and 14 deletions
@@ -946,6 +946,13 @@ async def _run(channel: ProtocolChannel) -> None:
_lossless_json_violation_cls = _lossless_json_violation
_get_event_loop_cls = asyncio.get_event_loop
_send_sync_cls = channel.send_sync
# The frame WRITE primitives are bound too: send_sync's body resolves
# `_encode_json_plain` (module global) and `self.write_encoded` (class
# attribute) at call time, so a program rebinding either before the first
# binding call could turn a legitimate call into an exception. dispatch
# writes through these directly, and the log sink through the bound send.
_write_encoded_cls = channel.write_encoded
_encode_plain_cls = _encode_json_plain
# 1. Boot handshake.
boot = channel.read_frame()
if boot is None or boot.get("type") != "boot":
@@ -1025,7 +1032,7 @@ async def _run(channel: ProtocolChannel) -> None:
logs = LogBuffer(
int(boot["maxLogBytes"]),
sink=lambda text, truncated=False: channel.send_sync(
sink=lambda text, truncated=False: _send_sync_cls(
{"type": "log", "text": text, **({"truncated": True} if truncated else {})}
),
)
@@ -1092,14 +1099,16 @@ async def _run(channel: ProtocolChannel) -> None:
fut: asyncio.Future[Any] = loop.create_future()
pending[call_id] = (loop, fut)
try:
_send_sync_cls(
{
"type": "call",
"id": call_id,
"global": global_name,
"name": name,
"args": args,
}
_write_encoded_cls(
_encode_plain_cls(
{
"type": "call",
"id": call_id,
"global": global_name,
"name": name,
"args": args,
}
)
)
except (TypeError, ValueError) as exc:
pending.pop(call_id, None)
@@ -711,6 +711,8 @@ describe('PythonCodeRuntime — programs and bindings', () => {
'__main__._lossless_json_violation = boom',
'__main__.asyncio = boom',
'__main__.ProtocolChannel.send_sync = boom',
'__main__._encode_json_plain = boom',
'__main__.ProtocolChannel.write_encoded = boom',
'first = await tools.echo({"n": 1})',
'return first',
].join('\n'),
@@ -2745,7 +2747,7 @@ describe('PythonCodeRuntime — budgets, termination, disposal', () => {
expect(result.value).toBe("read: ''")
}, 15_000)
it('keeps runtime type annotations unevaluated-as-strings when the program reads them', async () => {
it('keeps runtime type annotations as live classes, not PEP 563 strings, when the program reads them', async () => {
// bootstrap.py imports `from __future__ import annotations`; without
// dont_inherit=True on compile(), that PEP 563 flag leaks into the program's
// compiled code and stringifies its type annotations, changing the semantics