From 49bb64618089bf216cfe089354d044f1271847d6 Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic <44276455+microsasa@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:21:29 -0700 Subject: [PATCH 1/2] perf: lazy-import vscode_parser and vscode_report in CLI (#890) Move the vscode_parser and vscode_report imports from module level into the vscode() command function so they are only loaded when that subcommand is actually invoked. This avoids loading re, stat, types, and executing re.compile() on every CLI invocation. Update two existing tests that patched the imports at cli module level to patch at the source module instead. Add a regression test verifying the lazy-import invariant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/copilot_usage/cli.py | 5 +++-- tests/copilot_usage/test_vscode_parser.py | 10 +++++++-- tests/test_packaging.py | 26 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/copilot_usage/cli.py b/src/copilot_usage/cli.py index 669635bf..434802de 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 2fef52c0..3122b10f 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 c9a002d3..8d762c50 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -99,3 +99,29 @@ 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 + + # Purge any previously imported copilot_usage modules so we get a + # clean import of cli. + for mod_name in list(sys.modules): + if "copilot_usage" in mod_name: + 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" + ) From b749621d5c9e6a5480e3e40083b3843212572b5f Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic <44276455+microsasa@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:32:42 -0700 Subject: [PATCH 2/2] fix: address review comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_packaging.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 8d762c50..63a3ab81 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -111,17 +111,22 @@ def test_cli_does_not_import_vscode_modules_at_module_level() -> None: """ import sys - # Purge any previously imported copilot_usage modules so we get a - # clean import of cli. - for mod_name in list(sys.modules): - if "copilot_usage" in mod_name: - 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" - ) + 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)