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:
@@ -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)
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user