From 48c9649951b144da142e0c3882713c855c9a2962 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 6 Jun 2026 16:58:32 -0400 Subject: [PATCH] refactor(markdown_helper): remove top-level src.markdown_table import; use _require_warmed Phase 5C of startup_speedup_20260606 track. src/markdown_helper.py imported src.markdown_table at module level: from src.markdown_table import parse_tables, render_table Both parse_tables and render_table are only used inside MarkdownRenderer.render(). Removed the top-level import; the MarkdownRenderer.render() method now does: markdown_table = _require_warmed('src.markdown_table') parse_tables = markdown_table.parse_tables render_table = markdown_table.render_table at the top of its body, before any other logic. TESTS: - tests/test_markdown_helper_no_top_level_table.py: 3/3 PASS (all RED -> GREEN) - tests/test_markdown_table*.py (5 files) + test_markdown_helper_bullets.py + test_markdown_render_robust.py: 24/24 PASS (no breakage) EFFECTIVENESS: import src.markdown_helper no longer triggers src.markdown_table (~250ms). For renderers that never hit a GFM table, the import is never paid. For renderers that do, the warmup pre-loads it on _io_pool and the render() lookup is O(1). NEXT: Phase 5D - bulk refactor of src/gui_2.py feature-gated imports via scripts/audit_gui2_imports.py. --- src/markdown_helper.py | 14 ++- ...test_markdown_helper_no_top_level_table.py | 91 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/test_markdown_helper_no_top_level_table.py diff --git a/src/markdown_helper.py b/src/markdown_helper.py index a33c74cd..d03d347d 100644 --- a/src/markdown_helper.py +++ b/src/markdown_helper.py @@ -12,7 +12,14 @@ from typing import Optional, Dict, Callable from src import theme_2 -from src.markdown_table import parse_tables, render_table +from src.module_loader import _require_warmed + +# NOTE: src.markdown_table is NOT imported at module level. The GFM table +# rendering branch is feature-gated (startup_speedup_20260606 Phase 5C); +# the lookup happens inside MarkdownRenderer.render() via _require_warmed. +# If you need parse_tables/render_table elsewhere in this file, do the +# lookup there too. The warmup on AppController's _io_pool pre-loads +# src.markdown_table so the first render() call is O(1). def _get_language_id(name: str): @@ -121,6 +128,11 @@ class MarkdownRenderer: Render Markdown text with code block interception and GFM table substitution. [C: src/theme_2.py:render_post_fx, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_active, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_inactive, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_render, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_disabled, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_render] """ + # Lazy lookup of src.markdown_table (Phase 5C). Warmup has already loaded + # it on AppController's _io_pool, so this is an O(1) sys.modules get. + markdown_table = _require_warmed("src.markdown_table") + parse_tables = markdown_table.parse_tables + render_table = markdown_table.render_table if not text: return text = self._normalize_bullet_delimiters(text) text = self._normalize_nested_list_endings(text) diff --git a/tests/test_markdown_helper_no_top_level_table.py b/tests/test_markdown_helper_no_top_level_table.py new file mode 100644 index 00000000..4f17e40d --- /dev/null +++ b/tests/test_markdown_helper_no_top_level_table.py @@ -0,0 +1,91 @@ +"""Tests that src/markdown_helper.py has NO top-level src.markdown_table import. + +Per spec.md:2.2 Layer 1, the main thread's import chain must not include +heavy feature-gated modules. src.markdown_table (~250ms) is warmed on +AppController's _io_pool and accessed via _require_warmed at use sites. +The GFM table-rendering branch is optional (default markdown rendering +falls through to imgui_md). + +src/markdown_helper.py uses src.markdown_table only inside the +MarkdownRenderer.render() method (parse_tables at the top, render_table +in a nested code block). The lookup is deferred to the render() call. +""" + +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, + ) + + +def test_markdown_helper_does_not_import_markdown_table_at_module_level() -> None: + res = _run_in_subprocess(""" + import sys + import src.markdown_helper + print('src.markdown_table' in sys.modules) + """) + assert res.returncode == 0, f"stderr: {res.stderr}" + assert res.stdout.strip() == "False", f"markdown_helper triggered src.markdown_table import: {res.stdout}" + + +def test_markdown_helper_render_method_lazy_lookups() -> None: + """The render() method should resolve parse_tables and render_table + via _require_warmed at call time, not at module load.""" + res = _run_in_subprocess(""" + import ast + from pathlib import Path + root = Path('.').resolve() + helper_path = root / 'src' / 'markdown_helper.py' + tree = ast.parse(helper_path.read_text(encoding='utf-8')) + # Look for any top-level ImportFrom that imports parse_tables or render_table + has_top_level_table_import = False + for node in tree.body: + if isinstance(node, ast.ImportFrom): + for alias in node.names: + if alias.name in ('parse_tables', 'render_table'): + has_top_level_table_import = True + print('HAS_TOP_LEVEL_TABLE_IMPORT:', has_top_level_table_import) + """) + assert res.returncode == 0, f"stderr: {res.stderr}" + assert "HAS_TOP_LEVEL_TABLE_IMPORT: False" in res.stdout + + +def test_audit_main_thread_imports_sees_no_new_violation_from_markdown_helper() -> None: + """Run the static audit and check that src/markdown_helper.py contributes no + new markdown_table violations. + """ + res = _run_in_subprocess(""" + import ast + from pathlib import Path + root = Path('.').resolve() + helper_path = root / 'src' / 'markdown_helper.py' + tree = ast.parse(helper_path.read_text(encoding='utf-8')) + heavy = ['src.markdown_table', 'markdown_table'] + 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 + '.'): + print('VIOLATION:', alias.name) + elif isinstance(node, ast.ImportFrom): + if node.module: + for h in heavy: + if node.module == h or node.module.startswith(h + '.'): + print('VIOLATION:', node.module) + print('OK') + """) + assert res.returncode == 0, f"stderr: {res.stderr}" + assert "OK" in res.stdout + assert "VIOLATION" not in res.stdout