refactor(cli): extract interactive menu into cli/interactive.py with CliContext#229
Merged
Merged
Conversation
…CliContext Splits the interactive-menu/prompt subsystem (_interactive_menu, _ask_bench_profile/_ask_micro_profile/_ask_number, _print_menu, _pick_path_via_dialog/_prompt_path/_resolve_cli_path_or_error, _BENCH_PROFILES/_MICRO_PROFILES) out of cli/__init__.py into a new leaf module cli/interactive.py, following the CliContext-based DI pattern #120 already proved (842/842 tests passed there unmodified). Grepped every test file to find exactly which of these names are directly monkeypatched via cli.<name> (expecting effect through the call chain) versus never patched: _ask_bench_profile, _ask_micro_profile, _pick_path_via_dialog, and _interactive_menu are; _print_menu, _ask_number, _prompt_path, _resolve_cli_path_or_error, and the two profile dicts are not. CliContext (cli/context.py) gains 7 new fields (pick_path_via_dialog, ask_bench_profile, ask_micro_profile, run_mode_1..4) for exactly the directly-patched names; _print_menu and _prompt_path stay as ordinary same-module bare calls inside interactive.py since nothing independently patches them (confirmed, not assumed) -- they still correctly observe patches on their own dependencies since those flow through ctx. _LANG/_MESSAGES/_LOCALE_MESSAGES/_t deliberately stay in cli/__init__.py, unmoved -- main() does `global _LANG; _LANG = args.lang`, a real runtime mutation. Moving _LANG would make cli/__init__.py's re-export a one-time snapshot instead of a live alias, silently breaking monkeypatch.setattr(cli, "_LANG", "en") (an autouse fixture covering ~68 tests in test_cli.py) without any test failing loudly at the call site that actually broke. This scope boundary was independently re-verified by a Plan agent before implementation and is treated as settled, not just deferred. cli/__init__.py keeps thin wrapper functions with the original public signatures for every directly-patched name (matching #120's _run_mode_1..4 pattern) and plain re-exports (no wrapper) for _ask_number/ _BENCH_PROFILES/_MICRO_PROFILES, matching cli/options.py's precedent. grader.py needs zero changes -- all 7 names it imports from stepik_grader.cli remain resolvable there. Zero test files changed -- the full suite (842 tests) passes unmodified. Updated docs/architecture.md and docs/project-structure.md for the new cli/interactive.py leaf module and CliContext's extended field list. Issue #121 (Phase 2). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Jul 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Issue #121 Phase 2, part of the
cli.pydecomposition epic (#117). Phase 1 (rendering extraction) already merged. Branched from freshmain(#118-#120/#121-Phase-1 all merged).Extracts the interactive-menu/prompt subsystem —
_interactive_menu,_ask_bench_profile/_ask_micro_profile/_ask_number,_print_menu,_pick_path_via_dialog/_prompt_path/_resolve_cli_path_or_error,_BENCH_PROFILES/_MICRO_PROFILES— out ofcli/__init__.pyinto a new leaf modulecli/interactive.py, following theCliContext-based DI pattern #120 already proved (842/842 tests passed there with zero test changes).What I verified before designing
Grepped every test file to find exactly which of these names are directly monkeypatched via
cli.<name>(expecting effect to propagate through the call chain) vs. never patched:_ask_bench_profile,_ask_micro_profile,_pick_path_via_dialog, and_interactive_menuare;_print_menu,_ask_number,_prompt_path,_resolve_cli_path_or_error, and the two profile dicts are not.CliContextgains 7 new fields for exactly the directly-patched names;_print_menu/_prompt_pathstay as ordinary same-module bare calls insideinteractive.pysince nothing independently patches them — confirmed, not assumed — and they still correctly propagate patches on their own dependencies (e.g._prompt_path→ctx.pick_path_via_dialog).The i18n-state scope decision
The issue's Phase 2 text groups these functions with
_LANG/_MESSAGES/_LOCALE_MESSAGES/_t. I deliberately did not move the i18n state —main()doesglobal _LANG; _LANG = args.lang, a real runtime mutation, not just monkeypatch plumbing. If_LANGphysically moved to a new module,cli/__init__.py's re-export would become a one-time snapshot instead of a live alias, silently breakingmonkeypatch.setattr(cli, "_LANG", "en")(an autouse fixture covering ~68 tests intest_cli.py) — the exact class of hazard #120 was designed to avoid, but worse here because it's real state, not just test plumbing. A Plan agent independently re-verified this before implementation and confirmed it as a hard blocker, not a cautious guess._LANG/_MESSAGES/_LOCALE_MESSAGES/_tstay incli/__init__.py, unmoved — treated as a settled architectural boundary, not a "later" TODO.Result
Zero test files changed. The full suite (842 tests) passes unmodified.
This design was validated by a Plan agent before implementation (independently re-verified the monkeypatch inventory, the context-field-vs-bare-call rule, and the
_LANGmutation hazard) and approved via plan mode before any code was written.Test plan
pytest tests/ -q— 842 passed, 3 skipped, 0 test files modified (deselected the 2 pre-existing unrelated failures already failing onmain)ruff check src tests docs/ruff format --check src tests— cleanmypy src/stepik_grader --ignore-missing-imports— clean--mode 1 --file ... --output json, interactive-menu-driven mode 1 path prompt — all exercised the new wrapper →interactive.py→CliContextchain end-to-endfrom stepik_grader.cli import main,import stepik_grader.grader— facade re-exports intactdocs/architecture.md(module table + DAG) anddocs/project-structure.mdfor the newcli/interactive.pyleaf module andCliContext's extended field list🤖 Generated with Claude Code