From 0acdf3bb56c4e6cf9aeb89c82220d8edbcb4beb8 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 30 Apr 2026 20:21:54 -0700 Subject: [PATCH] fix(wheel): import inbox without alias to dodge rewriter collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2433 (notifications/claude/channel) shipped 'import inbox as _inbox_module' inside a2a_mcp_server.py:main(). The build script's import rewriter expands plain 'import inbox' to 'import molecule_runtime.inbox as inbox', so the original source became 'import molecule_runtime.inbox as inbox as _inbox_module', which is invalid Python. Caught at the publish-runtime + PR-built-wheel-smoke gate (the SyntaxError trace is in run 25200422679). The wheel didn't ship to PyPI because publish-runtime's smoke-import step refused to install it, but staging is currently sitting on a broken-build commit until this fix-forward lands. Changes: - a2a_mcp_server.py: lift `import inbox` to top of file (rewriter produces clean `import molecule_runtime.inbox as inbox`), call inbox.set_notification_callback directly in main() - build_runtime_package.py: rewrite_imports() now raises ValueError when it sees 'import X as Y' for any X in the workspace allowlist, instead of silently producing a syntax-error wheel. Operator gets a clear actionable error at build time pointing at the offending line + suggested rewrites ('from X import …' or plain 'import X'). The build-time gate (this PR's rewriter check) catches the regression class earlier than the smoke-time gate (PR #2433's failure). Adding 'PR-built wheel + import smoke' to staging branch protection's required checks is filed separately so this class doesn't merge again. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/build_runtime_package.py | 27 +++++++++++++++++++++++++++ workspace/a2a_mcp_server.py | 7 +++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/scripts/build_runtime_package.py b/scripts/build_runtime_package.py index 366b86c2..910ea691 100755 --- a/scripts/build_runtime_package.py +++ b/scripts/build_runtime_package.py @@ -150,6 +150,13 @@ def rewrite_imports(text: str, regex: re.Pattern) -> str: `import X` → `import molecule_runtime.X as X` (preserve binding) `from X import Y` → `from molecule_runtime.X import Y` `from X.sub import Y` → `from molecule_runtime.X.sub import Y` + + Rejects `import X as Y` because the rewrite would produce + `import molecule_runtime.X as X as Y`, a syntax error. The PR #2433 + incident shipped this exact pattern past `Python Lint & Test` (which + runs against pre-rewrite source) but blew up the wheel-smoke gate. + Detecting it here turns the silent build failure into a build-time + error with a clear path: use `from X import …` or plain `import X`. """ def repl(m: re.Match) -> str: indent, kw, mod, rest = m.group("indent"), m.group("kw"), m.group("mod"), m.group("rest") @@ -163,6 +170,26 @@ def rewrite_imports(text: str, regex: re.Pattern) -> str: # `import X.sub` — rewrite as `import molecule_runtime.X.sub` and # leave the trailing dot pattern intact for the rest of the line. return f"{indent}import molecule_runtime.{mod}{rest}" + # Detect `import X as Y` — the regex's `rest` group captures only + # the immediate following char (whitespace, comma, or EOL), so we + # have to peek at the surrounding line context. The match start is + # at the line's `import` keyword; everything after the matched + # name on the same line is what the source author wrote. + line_start = text.rfind("\n", 0, m.start()) + 1 + line_end = text.find("\n", m.end()) + if line_end == -1: + line_end = len(text) + line_after = text[m.end() - len(rest):line_end] + # Strip comments from consideration so `import X # noqa` doesn't trip. + line_after_no_comment = line_after.split("#", 1)[0] + if re.search(r"^\s*as\s+\w+", line_after_no_comment): + raise ValueError( + f"rewrite_imports: cannot rewrite 'import {mod} as ' on a " + f"workspace module — the regex would produce " + f"'import molecule_runtime.{mod} as {mod} as ', invalid syntax. " + f"Use 'from {mod} import …' or plain 'import {mod}' instead. " + f"Offending line: {text[line_start:line_end]!r}" + ) # Plain `import X` — alias preserves the local name. return f"{indent}import molecule_runtime.{mod} as {mod}{rest}" return regex.sub(repl, text) diff --git a/workspace/a2a_mcp_server.py b/workspace/a2a_mcp_server.py index afffb956..09512f26 100644 --- a/workspace/a2a_mcp_server.py +++ b/workspace/a2a_mcp_server.py @@ -17,6 +17,10 @@ import json import logging import sys +import inbox # noqa: F401 — bridge wiring lives in main(); the rewriter +# produces `import molecule_runtime.inbox as inbox` +# which preserves this binding for set_notification_callback. + from a2a_tools import ( tool_check_task_status, tool_commit_memory, @@ -212,8 +216,7 @@ async def main(): # pragma: no cover # Loop closed during shutdown — best-effort, swallow. pass - import inbox as _inbox_module - _inbox_module.set_notification_callback(_on_inbox_message) + inbox.set_notification_callback(_on_inbox_message) buffer = "" while True: