settings_schema.py reaches into the tui extra at module scope. It is inert today only because of who imports it — nothing about the module itself keeps it off a core path.
The chain
# src/bmad_loop/settings_schema.py:39
from .tui.settings import STAGES
# src/bmad_loop/tui/settings.py:15
import tomlkit # declared only in the [tui] extra
Both are module-top imports, so any import of settings_schema on an install without the extra raises ModuleNotFoundError: No module named 'tomlkit'.
Why it does not fire today
settings_schema is currently reached only from src/bmad_loop/tui/screens/settings_screen.py:46 — a module that already requires the extra. The dependency is therefore satisfied by accident of the call graph, not by design.
Why it is worth a line of defense
This is the third instance of one pattern, and the other two are live:
tui/__init__.py states the invariant the family keeps violating:
data is the pure-stdlib observation layer and must stay importable without the extra
Nothing enforces it. There is no test anywhere in tests/ that imports the core CLI surface with the extra's modules blocked, which is why #650 shipped through 23 releases undetected.
Suggested handling
No behavior change is needed — the ask is coverage. When the guard test for #650/#678 lands (blocking pyte, rich, textual and tomlkit), extend it to assert that bmad_loop.settings_schema imports cleanly under the blocker, so a future core-path consumer of that module fails in CI rather than on a user's install.
If the import is instead made lazy inside the function that needs STAGES, the same test still pins it.
settings_schema.pyreaches into thetuiextra at module scope. It is inert today only because of who imports it — nothing about the module itself keeps it off a core path.The chain
Both are module-top imports, so any import of
settings_schemaon an install without the extra raisesModuleNotFoundError: No module named 'tomlkit'.Why it does not fire today
settings_schemais currently reached only fromsrc/bmad_loop/tui/screens/settings_screen.py:46— a module that already requires the extra. The dependency is therefore satisfied by accident of the call graph, not by design.Why it is worth a line of defense
This is the third instance of one pattern, and the other two are live:
cmd_list→tui.data→import pyte. A core command, broken since v0.6.1.richbeforetextual, so the friendly message never prints #678 —cmd_tui's guard missesrichbecausetui/app.pyimports it beforetextual.tui/__init__.pystates the invariant the family keeps violating:Nothing enforces it. There is no test anywhere in
tests/that imports the core CLI surface with the extra's modules blocked, which is why #650 shipped through 23 releases undetected.Suggested handling
No behavior change is needed — the ask is coverage. When the guard test for #650/#678 lands (blocking
pyte,rich,textualandtomlkit), extend it to assert thatbmad_loop.settings_schemaimports cleanly under the blocker, so a future core-path consumer of that module fails in CI rather than on a user's install.If the import is instead made lazy inside the function that needs
STAGES, the same test still pins it.