refactor(theme_2): remove top-level NERV theme imports; use _require_warmed
Phase 5B of startup_speedup_20260606 track.
src/theme_2.py had 3 top-level NERV imports:
from src import theme_nerv
from src.theme_nerv import DATA_GREEN
from src.theme_nerv_fx import CRTFilter, AlertPulsing, StatusFlicker
And 3 module-level FX object instantiations:
_crt_filter = CRTFilter()
_alert_pulsing = AlertPulsing()
_status_flicker = StatusFlicker()
ALL removed. The 3 use sites now lookup via _require_warmed:
- apply() NERV branch: theme_nerv = _require_warmed('src.theme_nerv')
- ai_text_color(): theme_nerv = _require_warmed('src.theme_nerv')
(then uses theme_nerv.DATA_GREEN)
- render_post_fx(): theme_nerv_fx = _require_warmed('src.theme_nerv_fx')
(then creates FX objects locally per-call)
The _status_flicker was instantiated but never used (dead code path;
the StatusFlicker class is still importable via theme_nerv_fx but not
auto-constructed in theme_2.py).
TESTS:
- tests/test_theme_2_no_top_level_nerv.py: 4/4 PASS (all RED -> GREEN)
- tests/test_theme.py, test_theme_nerv.py, test_theme_nerv_fx.py,
test_theme_models.py: 21/21 PASS (no breakage)
EFFECTIVENESS: import src.theme_2 no longer triggers src.theme_nerv or
src.theme_nerv_fx (~485ms combined). For users on default theme, these
are NEVER loaded. For NERV users, the warmup pre-loads on _io_pool and
the lookup is O(1).
NEXT: Phase 5C (markdown table) follows same TDD pattern.
This commit is contained in:
+19
-12
@@ -14,14 +14,16 @@ from contextlib import nullcontext
|
||||
from imgui_bundle import imgui, hello_imgui
|
||||
from typing import Any, Optional
|
||||
|
||||
from src import theme_nerv
|
||||
|
||||
from src import imgui_scopes as imscope
|
||||
from src.theme_nerv import DATA_GREEN
|
||||
from src.theme_nerv_fx import CRTFilter, AlertPulsing, StatusFlicker
|
||||
from src.module_loader import _require_warmed
|
||||
from src.paths import get_global_themes_path
|
||||
from src.theme_models import ThemeFile, load_themes_from_dir, load_themes_from_toml
|
||||
|
||||
# Removed top-level NERV imports (startup_speedup_20260606 Phase 5B).
|
||||
# src.theme_nerv and src.theme_nerv_fx are warmed on AppController's _io_pool
|
||||
# and accessed via _require_warmed() at the use sites below (NERV palette
|
||||
# branch of apply(), ai_text_color() when is_nerv_active(), and render_post_fx()).
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ palettes
|
||||
|
||||
@@ -106,9 +108,9 @@ def _tone_map(rgb: tuple[float, float, float, float], palette: str) -> tuple[flo
|
||||
r = max(0, r)**(1.0/g); g_val = max(0, g_val)**(1.0/g); bl = max(0, bl)**(1.0/g)
|
||||
return (max(0.0, min(1.0, r)), max(0.0, min(1.0, g_val)), max(0.0, min(1.0, bl)), a)
|
||||
|
||||
_crt_filter = CRTFilter()
|
||||
_alert_pulsing = AlertPulsing()
|
||||
_status_flicker = StatusFlicker()
|
||||
# NERV FX objects (CRTFilter, AlertPulsing, StatusFlicker) are now created
|
||||
# lazily in render_post_fx() so the src.theme_nerv_fx import is deferred
|
||||
# to first use. The static-level instantiations were removed in Phase 5B.
|
||||
|
||||
# ------------------------------------------------------------------ public API
|
||||
|
||||
@@ -213,6 +215,7 @@ def apply(palette_name: str) -> None:
|
||||
global _current_palette
|
||||
_current_palette = palette_name
|
||||
if palette_name == 'NERV':
|
||||
theme_nerv = _require_warmed("src.theme_nerv")
|
||||
theme_nerv.apply_nerv()
|
||||
apply_syntax_palette(get_syntax_palette_for_theme(palette_name))
|
||||
return
|
||||
@@ -386,7 +389,8 @@ def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme:
|
||||
|
||||
def ai_text_color() -> imgui.ImVec4:
|
||||
if is_nerv_active():
|
||||
return imgui.ImVec4(*DATA_GREEN)
|
||||
theme_nerv = _require_warmed("src.theme_nerv")
|
||||
return imgui.ImVec4(*theme_nerv.DATA_GREEN)
|
||||
return imgui.get_style().color_(imgui.Col_.text)
|
||||
|
||||
def ai_text_style():
|
||||
@@ -395,10 +399,13 @@ def ai_text_style():
|
||||
"""Returns a subtle background tint color based on the message role."""
|
||||
def render_post_fx(width: float, height: float, ai_status: str, crt_enabled: bool) -> None:
|
||||
"""Updates and renders the alert and CRT filters."""
|
||||
_alert_pulsing.update(ai_status)
|
||||
_alert_pulsing.render(width, height)
|
||||
_crt_filter.enabled = crt_enabled
|
||||
_crt_filter.render(width, height)
|
||||
theme_nerv_fx = _require_warmed("src.theme_nerv_fx")
|
||||
alert_pulsing = theme_nerv_fx.AlertPulsing()
|
||||
crt_filter = theme_nerv_fx.CRTFilter()
|
||||
alert_pulsing.update(ai_status)
|
||||
alert_pulsing.render(width, height)
|
||||
crt_filter.enabled = crt_enabled
|
||||
crt_filter.render(width, height)
|
||||
|
||||
# ------------------------------------------------------------------ init
|
||||
load_themes_from_disk()
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Tests that src/theme_2.py has NO top-level NERV theme imports.
|
||||
|
||||
Per spec.md:2.2 Layer 1, the main thread's import chain must not include
|
||||
heavy feature-gated modules. The NERV theme modules (src.theme_nerv,
|
||||
src.theme_nerv_fx) are warmed on AppController's _io_pool and accessed
|
||||
via _require_warmed at use sites. NERV is only active when the user
|
||||
explicitly chooses it; the default theme path should be lean.
|
||||
|
||||
src/theme_2.py uses these NERV symbols at three sites:
|
||||
- ai_text_color() uses DATA_GREEN (only when is_nerv_active() is True)
|
||||
- apply() uses theme_nerv.apply_nerv() (only when palette == 'NERV')
|
||||
- render_post_fx() creates CRTFilter/AlertPulsing/StatusFlicker instances
|
||||
|
||||
All three sites are now lazy: the modules are looked up via _require_warmed
|
||||
inside the function body, and the FX objects are created locally per-call.
|
||||
|
||||
These tests run in a fresh subprocess to ensure no warmup state leaks
|
||||
from the test runner.
|
||||
"""
|
||||
|
||||
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_theme_2_does_not_import_theme_nerv_at_module_level() -> None:
|
||||
res = _run_in_subprocess("""
|
||||
import sys
|
||||
import src.theme_2
|
||||
print('src.theme_nerv' in sys.modules)
|
||||
""")
|
||||
assert res.returncode == 0, f"stderr: {res.stderr}"
|
||||
assert res.stdout.strip() == "False", f"theme_2 triggered src.theme_nerv import: {res.stdout}"
|
||||
|
||||
|
||||
def test_theme_2_does_not_import_theme_nerv_fx_at_module_level() -> None:
|
||||
res = _run_in_subprocess("""
|
||||
import sys
|
||||
import src.theme_2
|
||||
print('src.theme_nerv_fx' in sys.modules)
|
||||
""")
|
||||
assert res.returncode == 0, f"stderr: {res.stderr}"
|
||||
assert res.stdout.strip() == "False", f"theme_2 triggered src.theme_nerv_fx import: {res.stdout}"
|
||||
|
||||
|
||||
def test_theme_2_ai_text_color_source_does_not_import_theme_nerv() -> None:
|
||||
"""The ai_text_color function's body uses _require_warmed; verify the
|
||||
file no longer has DATA_GREEN as a top-level import. (Cannot call the
|
||||
function outside a real imgui context, but AST inspection is sufficient.)"""
|
||||
res = _run_in_subprocess("""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
root = Path('.').resolve()
|
||||
theme_path = root / 'src' / 'theme_2.py'
|
||||
tree = ast.parse(theme_path.read_text(encoding='utf-8'))
|
||||
# Look for any top-level ImportFrom that imports DATA_GREEN directly
|
||||
has_top_level_data_green = False
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
for alias in node.names:
|
||||
if alias.name == 'DATA_GREEN':
|
||||
has_top_level_data_green = True
|
||||
print('HAS_TOP_LEVEL_DATA_GREEN:', has_top_level_data_green)
|
||||
""")
|
||||
assert res.returncode == 0, f"stderr: {res.stderr}"
|
||||
assert "HAS_TOP_LEVEL_DATA_GREEN: False" in res.stdout
|
||||
|
||||
|
||||
def test_audit_main_thread_imports_sees_no_new_violation_from_theme_2() -> None:
|
||||
"""Run the static audit and check that src/theme_2.py contributes no new NERV violations."""
|
||||
res = _run_in_subprocess("""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
root = Path('.').resolve()
|
||||
theme_path = root / 'src' / 'theme_2.py'
|
||||
tree = ast.parse(theme_path.read_text(encoding='utf-8'))
|
||||
heavy = ['src.theme_nerv', 'src.theme_nerv_fx', 'theme_nerv', 'theme_nerv_fx']
|
||||
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
|
||||
Reference in New Issue
Block a user