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.
|
||||
# Authors:
|
||||
# imacat@mail.imacat.idv.tw (imacat), 2026/8/4
|
||||
"""The registry of the CLI subcommands."""
|
||||
from .build_db import main as build_db_command
|
||||
from .cluster_keywords import main as cluster_keywords_command
|
||||
from .compare_codings import main as compare_codings_command
|
||||
from .export_llm_input import main as export_llm_input_command
|
||||
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
|
||||
"""The registry of the CLI subcommands.
|
||||
|
||||
Each subcommand is a wrapper that imports its tool module on the
|
||||
call, so that importing the registry costs nothing but this
|
||||
module itself.
|
||||
"""
|
||||
|
||||
|
||||
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 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
|
||||
|
||||
|
||||
@@ -33,8 +33,31 @@ class TestDispatcher(unittest.TestCase):
|
||||
return status, stdout.getvalue(), stderr.getvalue()
|
||||
|
||||
def test_run_llm_registered(self) -> None:
|
||||
"""Test that run-llm is bound to the run_llm tool main."""
|
||||
self.assertIs(__main__.SUBCOMMANDS["run-llm"], run_llm.main)
|
||||
"""Test that run-llm is bound to the run_llm wrapper."""
|
||||
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:
|
||||
"""Test that run-llm forwards the arguments and the status."""
|
||||
|
||||
Reference in New Issue
Block a user