Import each command module only when its subcommand runs

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 22:38:34 +08:00
co-authored by Claude Opus 5
parent 668f1bb118
commit 7008e14972
2 changed files with 132 additions and 11 deletions
@@ -2,11 +2,109 @@
# Copyright 2026 imacat. All rights reserved. # Copyright 2026 imacat. All rights reserved.
# Authors: # Authors:
# imacat@mail.imacat.idv.tw (imacat), 2026/8/4 # imacat@mail.imacat.idv.tw (imacat), 2026/8/4
"""The registry of the CLI subcommands.""" """The registry of the CLI subcommands.
from .build_db import main as build_db_command
from .cluster_keywords import main as cluster_keywords_command Each subcommand is a wrapper that imports its tool module on the
from .compare_codings import main as compare_codings_command call, so that importing the registry costs nothing but this
from .export_llm_input import main as export_llm_input_command module itself.
from .fetch_artists import main as fetch_artists_command """
from .fetch_lyrics import main as fetch_lyrics_command
from .run_llm import main as run_llm_command
def build_db_command(argv: list[str] | None = None) -> int:
"""Rebuild the SQLite working store from the inputs.
:param argv: The command-line arguments, or None for
``sys.argv``.
:return: The exit status: 0 on success, non-zero on failure.
"""
from .build_db import main
return main(argv)
def cluster_keywords_command(argv: list[str] | None = None) -> int:
"""Pool the two tagging runs' keywords and cluster them.
Writes the five fixed-named artifacts under the output
directory, creating it (with parents) if it does not exist:
the pooled keyword text file; then the group membership CSV
file, holding the clustering result alone; the group name
keyword text file, holding the same group names as a readable
list; the coding keyword set JSON file, holding the group
names plus every extra keyword given via ``--extra-keyword``;
and the run metadata JSON file, recording the command-line
choices and the environment. Each file is written as soon as
its content is computed, so when the input is rejected, or an
extra keyword duplicates a group name or another extra
keyword, the output directory holds whatever the steps before
the failing one produced, and the error message names what
failed.
:param argv: The command-line arguments, or None for
``sys.argv``.
:return: The exit status: 0 on success, non-zero on failure.
"""
from .cluster_keywords import main
return main(argv)
def compare_codings_command(argv: list[str] | None = None) -> int:
"""Compare the two coding runs and export the disagreements.
Writes the disagreement JSON file under the output directory,
creating it (with parents) if it does not exist. When the
input is rejected, the file is not written.
:param argv: The command-line arguments, or None for
``sys.argv``.
:return: The exit status: 0 on success, non-zero on failure.
"""
from .compare_codings import main
return main(argv)
def export_llm_input_command(argv: list[str] | None = None) -> int:
"""Export the LLM input JSONL file from the working store.
Every song is exported, unless ``--extras-per-id`` is given,
in which case only the songs its file names are.
:param argv: The command-line arguments, or None for
``sys.argv``.
:return: The exit status: 0 on success, non-zero on failure.
"""
from .export_llm_input import main
return main(argv)
def fetch_artists_command(argv: list[str] | None = None) -> int:
"""Fetch the artist metadata from Wikidata.
:param argv: The command-line arguments, or None for
``sys.argv``.
:return: The exit status: 0 on success, misses and errors
included, non-zero on a setup error.
"""
from .fetch_artists import main
return main(argv)
def fetch_lyrics_command(argv: list[str] | None = None) -> int:
"""Fetch the missing song lyrics from the public APIs.
:param argv: The command-line arguments, or None for
``sys.argv``.
:return: The exit status: 0 on success, misses included,
non-zero on a setup error.
"""
from .fetch_lyrics import main
return main(argv)
def run_llm_command(argv: list[str] | None = None) -> int:
"""Run one LLM definition file against one input and archive it.
:param argv: The command-line arguments, or None for ``sys.argv``.
:return: The exit status: 0 on success, non-zero on failure.
"""
from .run_llm import main
return main(argv)
+26 -3
View File
@@ -11,7 +11,7 @@ import unittest
from contextlib import redirect_stderr, redirect_stdout from contextlib import redirect_stderr, redirect_stdout
from unittest import mock from unittest import mock
from pop_fem_audit_tools import __main__ from pop_fem_audit_tools import __main__, commands
from pop_fem_audit_tools.commands import run_llm from pop_fem_audit_tools.commands import run_llm
@@ -33,8 +33,31 @@ class TestDispatcher(unittest.TestCase):
return status, stdout.getvalue(), stderr.getvalue() return status, stdout.getvalue(), stderr.getvalue()
def test_run_llm_registered(self) -> None: def test_run_llm_registered(self) -> None:
"""Test that run-llm is bound to the run_llm tool main.""" """Test that run-llm is bound to the run_llm wrapper."""
self.assertIs(__main__.SUBCOMMANDS["run-llm"], run_llm.main) self.assertIs(__main__.SUBCOMMANDS["run-llm"],
commands.run_llm_command)
def test_every_subcommand_registered(self) -> None:
"""Test that every subcommand is bound to its wrapper."""
for name, command in __main__.SUBCOMMANDS.items():
with self.subTest(subcommand=name):
wrapper: str = f"{name.replace('-', '_')}_command"
self.assertIs(command, getattr(commands, wrapper))
def test_run_llm_command_delegates(self) -> None:
"""Test that the run-llm wrapper calls the run_llm main."""
tool: mock.Mock = mock.Mock(return_value=7)
with mock.patch.object(run_llm, "main", tool):
status: int = commands.run_llm_command(["--input", "x"])
self.assertEqual(status, 7)
tool.assert_called_once_with(["--input", "x"])
def test_run_llm_command_defaults_to_none(self) -> None:
"""Test that the run-llm wrapper defaults the arguments."""
tool: mock.Mock = mock.Mock(return_value=0)
with mock.patch.object(run_llm, "main", tool):
commands.run_llm_command()
tool.assert_called_once_with(None)
def test_run_llm_dispatch(self) -> None: def test_run_llm_dispatch(self) -> None:
"""Test that run-llm forwards the arguments and the status.""" """Test that run-llm forwards the arguments and the status."""