diff --git a/src/copilot_usage/cli.py b/src/copilot_usage/cli.py index 669635b..434802d 100644 --- a/src/copilot_usage/cli.py +++ b/src/copilot_usage/cli.py @@ -35,8 +35,6 @@ render_summary, session_display_name, ) -from copilot_usage.vscode_parser import get_vscode_summary -from copilot_usage.vscode_report import render_vscode_summary type _View = Literal["home", "detail", "cost"] @@ -661,6 +659,9 @@ def live(ctx: click.Context, path: Path | None) -> None: ) def vscode(vscode_logs: Path | None) -> None: """Show usage from VS Code Copilot Chat logs.""" + from copilot_usage.vscode_parser import get_vscode_summary + from copilot_usage.vscode_report import render_vscode_summary + _print_version_header() summary = get_vscode_summary(vscode_logs) if summary.total_requests == 0: diff --git a/tests/copilot_usage/test_vscode_parser.py b/tests/copilot_usage/test_vscode_parser.py index 2fef52c..3122b10 100644 --- a/tests/copilot_usage/test_vscode_parser.py +++ b/tests/copilot_usage/test_vscode_parser.py @@ -859,7 +859,10 @@ def test_all_files_error_shows_correct_message(self) -> None: summary = VSCodeLogSummary( log_files_found=2, log_files_parsed=0, total_requests=0 ) - with patch("copilot_usage.cli.get_vscode_summary", return_value=summary): + with patch( + "copilot_usage.vscode_parser.get_vscode_summary", + return_value=summary, + ): runner = CliRunner() result = runner.invoke(main, ["vscode"]) assert result.exit_code == 1 @@ -870,7 +873,10 @@ def test_no_files_shows_no_requests_message(self) -> None: summary = VSCodeLogSummary( log_files_found=0, log_files_parsed=0, total_requests=0 ) - with patch("copilot_usage.cli.get_vscode_summary", return_value=summary): + with patch( + "copilot_usage.vscode_parser.get_vscode_summary", + return_value=summary, + ): runner = CliRunner() result = runner.invoke(main, ["vscode"]) assert result.exit_code == 1 diff --git a/tests/test_packaging.py b/tests/test_packaging.py index c9a002d..63a3ab8 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -99,3 +99,34 @@ def test_ccreq_re_not_in_vscode_parser_all() -> None: dunder_all = vscode_mod.__all__ assert "CCREQ_RE" not in dunder_all, "CCREQ_RE must not be in vscode_parser.__all__" + + +def test_cli_does_not_import_vscode_modules_at_module_level() -> None: + """``vscode_parser`` and ``vscode_report`` must be lazy-imported. + + Regression guard for issue #890: these modules are only needed by the + ``vscode`` subcommand and should not be imported at ``cli`` module level + to avoid loading ``re``, ``stat``, ``types``, and running ``re.compile`` + on every invocation. + """ + import sys + + original_sys_modules = sys.modules.copy() + try: + # Purge any previously imported copilot_usage modules so we get a + # clean import of cli. + for mod_name in list(sys.modules): + if mod_name == "copilot_usage" or mod_name.startswith("copilot_usage."): + del sys.modules[mod_name] + + importlib.import_module("copilot_usage.cli") + + assert "copilot_usage.vscode_parser" not in sys.modules, ( + "vscode_parser must not be imported at cli module level" + ) + assert "copilot_usage.vscode_report" not in sys.modules, ( + "vscode_report must not be imported at cli module level" + ) + finally: + sys.modules.clear() + sys.modules.update(original_sys_modules)