diff --git a/patches/apply_patch.py b/patches/apply_patch.py index b33cf5d..cb646b2 100644 --- a/patches/apply_patch.py +++ b/patches/apply_patch.py @@ -3,6 +3,21 @@ Injeta o registro das rotas aiohttp do skills_api dentro do método start() do APIServerAdapter no api_server.py do Hermes. Idempotente: se o marcador já existir, não faz nada. + +Strategy +-------- +The `web.Application(...)` call may span multiple lines, e.g.: + + self._app = web.Application( + middlewares=[...], + client_max_size=..., + ) + +We therefore use a two-phase approach: + 1. Locate the LHS assignment (`` = web.Application(``) with a + simple MULTILINE regex — this is always a single line. + 2. From that position, scan forward to find the matching closing parenthesis, + correctly handling nested parentheses. """ from __future__ import annotations @@ -12,15 +27,32 @@ from pathlib import Path MARKER = "# >>> hermes-custom: skills_api <<<" -# Regex: captura uma linha que cria a aiohttp Application, ex: -# self._app = web.Application(...) -# app = web.Application(...) -APP_ASSIGN_RE = re.compile( - r"^(?P[ \t]+)(?P(?:self\._?app|app))\s*=\s*web\.Application\([^)]*\)\s*$", +# Phase-1 regex: matches the *start* of the assignment up to and including the +# opening parenthesis of web.Application. Does NOT require the call to end on +# the same line, so it works for both single-line and multi-line forms. +APP_ASSIGN_START_RE = re.compile( + r"^(?P[ \t]+)(?P(?:self\._?app|app))\s*=\s*web\.Application\(", re.MULTILINE, ) +def _find_closing_paren(src: str, open_pos: int) -> int: + """Return the index of the ')' that closes the '(' at *open_pos*. + + Handles nested parentheses. Raises ValueError if not found. + """ + depth = 0 + for i in range(open_pos, len(src)): + ch = src[i] + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + return i + raise ValueError(f"No matching closing parenthesis found starting at offset {open_pos}") + + def build_injection(indent: str, app_var: str) -> str: lines = [ "", @@ -47,17 +79,39 @@ def main(target: str) -> int: print("[apply_patch] already applied, skipping") return 0 - match = APP_ASSIGN_RE.search(src) + # Phase 1 — find the assignment start + match = APP_ASSIGN_START_RE.search(src) if not match: + # Emit a diagnostic snippet to help debug future mismatches print( - "[apply_patch] ERROR: não encontrei `... = web.Application(...)` no api_server.py", + "[apply_patch] ERROR: não encontrei `... = web.Application(` no api_server.py", file=sys.stderr, ) + # Print lines that contain "Application" to aid debugging + for lineno, line in enumerate(src.splitlines(), 1): + if "Application" in line: + print(f"[apply_patch] hint line {lineno}: {line!r}", file=sys.stderr) return 2 indent = match.group("indent") app_var = match.group("lhs") - insert_at = match.end() + + # Phase 2 — walk forward to find the matching closing parenthesis + open_paren_pos = match.end() - 1 # position of the '(' in web.Application( + try: + close_paren_pos = _find_closing_paren(src, open_paren_pos) + except ValueError as exc: + print(f"[apply_patch] ERROR: {exc}", file=sys.stderr) + return 2 + + # Insert after the closing ')' (and any trailing whitespace on that line) + # so we land right after the complete statement. + insert_at = close_paren_pos + 1 + # Advance past an optional trailing comment / whitespace up to the newline + newline_pos = src.find("\n", insert_at) + if newline_pos != -1: + insert_at = newline_pos # inject before the newline so indentation stays clean + injection = build_injection(indent, app_var) new_src = src[:insert_at] + injection + src[insert_at:]