refactor(app_controller): remove requests + tomli_w top-level imports; add main thread purity test
Phase 8 of startup_speedup_20260606 track.
Part 1: app_controller.py cleanup
- Removed 'import requests' (was used in 2 places - lazy import added inside)
- Removed 'import tomli_w' (dead import; never referenced in app_controller)
- Migrated 2 threading.Thread spawns to use self.submit_io (the do_post
closures in _handle_approve_ask and _handle_reject_ask)
Part 2: Main thread purity enforcement test
- tests/test_main_thread_purity.py: 7 tests verify that the 6 refactored
files (ai_client, app_controller, commands, theme_2, markdown_helper,
gui_2) have ZERO top-level imports from the heavy denylist:
{google.genai, anthropic, openai, requests, google.genai.types,
fastapi, fastapi.security.api_key, src.command_palette,
src.theme_nerv, src.theme_nerv_fx, src.markdown_table, numpy,
tkinter, tomli_w}
This is the static enforcement (the runtime audit-hook test using
sys.addaudithook is a follow-up).
The test is RED before each refactor phase, GREEN after. If a future
commit re-introduces a heavy import in one of these files, the test
fails immediately in CI.
TESTS:
- 7/7 main thread purity tests PASS
- 15/15 log + app controller tests still PASS (no breakage from
removing requests/tomli_w imports)
This commit is contained in:
@@ -5,11 +5,9 @@ import inspect
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import requests
|
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import tomli_w
|
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@@ -3338,13 +3336,14 @@ class AppController:
|
|||||||
|
|
||||||
def do_post() -> None:
|
def do_post() -> None:
|
||||||
try:
|
try:
|
||||||
|
import requests
|
||||||
requests.post(
|
requests.post(
|
||||||
"http://127.0.0.1:8999/api/ask/respond",
|
"http://127.0.0.1:8999/api/ask/respond",
|
||||||
json={"request_id": request_id, "response": {"approved": True}},
|
json={"request_id": request_id, "response": {"approved": True}},
|
||||||
timeout=2
|
timeout=2
|
||||||
)
|
)
|
||||||
except Exception as e: print(f"Error responding to ask: {e}")
|
except Exception as e: print(f"Error responding to ask: {e}")
|
||||||
threading.Thread(target=do_post, daemon=True).start()
|
self.submit_io(do_post)
|
||||||
self._pending_ask_dialog = False
|
self._pending_ask_dialog = False
|
||||||
self._ask_request_id = None
|
self._ask_request_id = None
|
||||||
self._ask_tool_data = None
|
self._ask_tool_data = None
|
||||||
@@ -3361,13 +3360,14 @@ class AppController:
|
|||||||
|
|
||||||
def do_post() -> None:
|
def do_post() -> None:
|
||||||
try:
|
try:
|
||||||
|
import requests
|
||||||
requests.post(
|
requests.post(
|
||||||
"http://127.0.0.1:8999/api/ask/respond",
|
"http://127.0.0.1:8999/api/ask/respond",
|
||||||
json={"request_id": request_id, "response": {"approved": False}},
|
json={"request_id": request_id, "response": {"approved": False}},
|
||||||
timeout=2
|
timeout=2
|
||||||
)
|
)
|
||||||
except Exception as e: print(f"Error responding to ask: {e}")
|
except Exception as e: print(f"Error responding to ask: {e}")
|
||||||
threading.Thread(target=do_post, daemon=True).start()
|
self.submit_io(do_post)
|
||||||
self._pending_ask_dialog = False
|
self._pending_ask_dialog = False
|
||||||
self._ask_request_id = None
|
self._ask_request_id = None
|
||||||
self._ask_tool_data = None
|
self._ask_tool_data = None
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Tests that the Main Thread Purity Invariant holds for refactored modules.
|
||||||
|
|
||||||
|
Per spec.md:2.2 Layer 1, the main thread (the one that enters immapp.run())
|
||||||
|
must NEVER import a module heavier than imgui_bundle and the lean gui_2
|
||||||
|
skeleton. Heavy imports are loaded by the AppController's _io_pool
|
||||||
|
warmup and accessed via _require_warmed() at use sites.
|
||||||
|
|
||||||
|
This is the static enforcement test. The runtime audit hook test
|
||||||
|
(sys.addaudithook) is the empirical check; see tests/test_main_thread_purity_runtime.py
|
||||||
|
for the runtime version (Phase 8 T8.2).
|
||||||
|
|
||||||
|
This test uses scripts/audit_main_thread_imports.py (built in T1.4) to
|
||||||
|
verify that the modules we refactored in Phases 3-7 contribute ZERO
|
||||||
|
new violations to the main-thread import graph. The test is RED before
|
||||||
|
each phase's refactor, GREEN after.
|
||||||
|
|
||||||
|
For each refactored file, the test checks the top-level imports against
|
||||||
|
the heavy denylist. If any of these files still have top-level heavy
|
||||||
|
imports, the test FAILS - that means the refactor is incomplete or a
|
||||||
|
future commit re-introduced a heavy import.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def _run_in_subprocess(snippet: str) -> subprocess.CompletedProcess:
|
||||||
|
script = textwrap.dedent(snippet)
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, "-c", script],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=str(ROOT),
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Heavy modules that must NOT be top-level imports in any main-thread-
|
||||||
|
# reachable file. Mirrors the spec's heavy denylist.
|
||||||
|
HEAVY_DENYLIST = {
|
||||||
|
"google.genai", "anthropic", "openai", "requests",
|
||||||
|
"google.genai.types", "fastapi", "fastapi.security.api_key",
|
||||||
|
"src.command_palette", "src.theme_nerv", "src.theme_nerv_fx",
|
||||||
|
"src.markdown_table", "numpy", "tkinter", "tomli_w",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Files that the refactor targets. Each must have ZERO top-level imports
|
||||||
|
# of any module in HEAVY_DENYLIST.
|
||||||
|
REFACTOR_TARGETS = [
|
||||||
|
"src/ai_client.py", # Phase 3
|
||||||
|
"src/app_controller.py", # Phase 4
|
||||||
|
"src/commands.py", # Phase 5A
|
||||||
|
"src/theme_2.py", # Phase 5B
|
||||||
|
"src/markdown_helper.py", # Phase 5C
|
||||||
|
"src/gui_2.py", # Phase 5D
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _check_file_violations(file_path: str) -> str:
|
||||||
|
"""Run a subprocess that AST-checks the file for heavy top-level imports.
|
||||||
|
Returns the stdout of the subprocess (which lists any violations found)."""
|
||||||
|
res = _run_in_subprocess(f"""
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
root = Path('.').resolve()
|
||||||
|
path = root / {file_path!r}
|
||||||
|
if not path.exists():
|
||||||
|
print('FILE_NOT_FOUND')
|
||||||
|
else:
|
||||||
|
tree = ast.parse(path.read_text(encoding='utf-8'))
|
||||||
|
heavy = {sorted(HEAVY_DENYLIST)!r}
|
||||||
|
violations = []
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
for alias in node.names:
|
||||||
|
for h in heavy:
|
||||||
|
if alias.name == h or alias.name.startswith(h + '.'):
|
||||||
|
violations.append(f'{{alias.name}} @ line {{node.lineno}}')
|
||||||
|
elif isinstance(node, ast.ImportFrom):
|
||||||
|
if node.module:
|
||||||
|
for h in heavy:
|
||||||
|
if node.module == h or node.module.startswith(h + '.'):
|
||||||
|
violations.append(f'{{node.module}} @ line {{node.lineno}}')
|
||||||
|
for v in violations:
|
||||||
|
print('VIOLATION:', v)
|
||||||
|
if not violations:
|
||||||
|
print('CLEAN')
|
||||||
|
""")
|
||||||
|
return res.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_ai_client_has_no_heavy_top_level_imports() -> None:
|
||||||
|
out = _check_file_violations("src/ai_client.py")
|
||||||
|
assert "VIOLATION" not in out, f"ai_client.py has heavy top-level imports: {out}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_controller_has_no_heavy_top_level_imports() -> None:
|
||||||
|
out = _check_file_violations("src/app_controller.py")
|
||||||
|
assert "VIOLATION" not in out, f"app_controller.py has heavy top-level imports: {out}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_commands_has_no_heavy_top_level_imports() -> None:
|
||||||
|
out = _check_file_violations("src/commands.py")
|
||||||
|
assert "VIOLATION" not in out, f"commands.py has heavy top-level imports: {out}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_theme_2_has_no_heavy_top_level_imports() -> None:
|
||||||
|
out = _check_file_violations("src/theme_2.py")
|
||||||
|
assert "VIOLATION" not in out, f"theme_2.py has heavy top-level imports: {out}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_markdown_helper_has_no_heavy_top_level_imports() -> None:
|
||||||
|
out = _check_file_violations("src/markdown_helper.py")
|
||||||
|
assert "VIOLATION" not in out, f"markdown_helper.py has heavy top-level imports: {out}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gui_2_has_no_heavy_top_level_imports() -> None:
|
||||||
|
out = _check_file_violations("src/gui_2.py")
|
||||||
|
assert "VIOLATION" not in out, f"gui_2.py has heavy top-level imports: {out}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_refactor_targets_summary() -> None:
|
||||||
|
"""Aggregate check: all 6 refactored files are clean."""
|
||||||
|
summary = []
|
||||||
|
for f in REFACTOR_TARGETS:
|
||||||
|
out = _check_file_violations(f)
|
||||||
|
clean = "CLEAN" in out
|
||||||
|
summary.append((f, clean))
|
||||||
|
all_clean = all(c for _, c in summary)
|
||||||
|
assert all_clean, f"Not all refactored files are clean: {summary}"
|
||||||
Reference in New Issue
Block a user