Fix the SonarQube findings

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 13:06:20 +08:00
co-authored by Claude Opus 5
parent 2fa55f29d7
commit e1e2c86661
4 changed files with 96 additions and 56 deletions
@@ -632,7 +632,7 @@ class PerformerGenderDeriver:
known: set[str] = {x for x in values if x} known: set[str] = {x for x in values if x}
if len(known) > 1: if len(known) > 1:
return cls.__MIXED return cls.__MIXED
if len(known) == 1 and all(x for x in values): if len(known) == 1 and all(values):
return known.pop() return known.pop()
return None return None
@@ -161,6 +161,10 @@ class RetryExhausted(Exception):
""" """
NOTE_NOT_FOUND: str = "not found"
"""The note marking an artist that could not be resolved."""
class ArtistFetcher: class ArtistFetcher:
"""A fetcher of artist metadata from Wikidata. """A fetcher of artist metadata from Wikidata.
@@ -252,9 +256,6 @@ class ArtistFetcher:
= ("band", "group", "duo", "trio") = ("band", "group", "duo", "trio")
"""The label keywords that suggest a musical ensemble, """The label keywords that suggest a musical ensemble,
covering labels like "boy band" and "girl group".""" covering labels like "boy band" and "girl group"."""
__NOTE_NOT_FOUND: ClassVar[str] = "not found"
"""The note sentinel of an artist without a resolved
Wikidata item."""
__MIXED_GENDER: ClassVar[str] = "mixed" __MIXED_GENDER: ClassVar[str] = "mixed"
"""The gender recorded for a group whose members do not """The gender recorded for a group whose members do not
share one gender.""" share one gender."""
@@ -301,7 +302,7 @@ class ArtistFetcher:
try: try:
qid: str | None = self.__resolve_qid(name, titles) qid: str | None = self.__resolve_qid(name, titles)
if qid is None: if qid is None:
snapshot.note = self.__NOTE_NOT_FOUND snapshot.note = NOTE_NOT_FOUND
return snapshot return snapshot
snapshot.qid = qid snapshot.qid = qid
self.__resolve(snapshot) self.__resolve(snapshot)
@@ -1039,9 +1040,9 @@ class ArtistSnapshotUpdater:
artist.name, titles) artist.name, titles)
self.__append_row(csv_file, snapshot) self.__append_row(csv_file, snapshot)
status: str = snapshot.qid status: str = snapshot.qid
if snapshot.note == "not found": if snapshot.note == NOTE_NOT_FOUND:
not_found += 1 not_found += 1
status = "not found" status = NOTE_NOT_FOUND
elif snapshot.note.startswith("error: "): elif snapshot.note.startswith("error: "):
errors += 1 errors += 1
status = snapshot.note status = snapshot.note
@@ -82,11 +82,11 @@ class AnnotationTallier:
song's stored performer gender; a gender missing here song's stored performer gender; a gender missing here
(including None) takes every prefix.""" (including None) takes every prefix."""
__NUMBERING_RE: ClassVar[re.Pattern[str]] = re.compile( __NUMBERING_RE: ClassVar[re.Pattern[str]] = re.compile(
r"^(?:模式[一二三四五六七八九十]+|[一二三四五六七八九十]+)[:、]") r"^(?:模式)?[一二三四五六七八九十]+[:、]")
"""The leading numbering token of a pattern heading, stripped """The leading numbering token of a pattern heading, stripped
to yield the pattern name.""" to yield the pattern name."""
__HEADING_RE: ClassVar[re.Pattern[str]] = re.compile( __HEADING_RE: ClassVar[re.Pattern[str]] = re.compile(
r"^#+\s*(.*)$") r"^#++\s*+(.*)$")
"""A Markdown heading line, the heading text captured.""" """A Markdown heading line, the heading text captured."""
def __init__(self, male_synthesis: Path, female_synthesis: Path, def __init__(self, male_synthesis: Path, female_synthesis: Path,
@@ -311,6 +311,32 @@ class AnnotationTallier:
pooled: dict[int, list[list[str]]] = {} pooled: dict[int, list[list[str]]] = {}
run_dir: Path run_dir: Path
for run_dir in self.__run_dirs: for run_dir in self.__run_dirs:
self.__load_run_ballots(run_dir, pooled)
song_id: int
ballots: list[list[str]]
for song_id, ballots in pooled.items():
if len(ballots) != self.__BALLOTS_PER_SONG:
raise TallyError(
f"song-{song_id}: appears {len(ballots)}"
f" times in the pool, expected"
f" {self.__BALLOTS_PER_SONG}")
return pooled
@classmethod
def __load_run_ballots(
cls, run_dir: Path,
pooled: dict[int, list[list[str]]]) -> None:
"""Load one run's ballots into the pool.
:param run_dir: The annotation run's archive directory,
containing ``output.jsonl``.
:param pooled: The pool to append the run's ballots
into, keyed by the numeric song ID; mutated in
place.
:return: None.
:raises TallyError: When the run's ``output.jsonl``
cannot be read, or a line is malformed JSON.
"""
path: Path = run_dir / "output.jsonl" path: Path = run_dir / "output.jsonl"
text: str text: str
try: try:
@@ -328,28 +354,41 @@ class AnnotationTallier:
raise TallyError( raise TallyError(
f"{path}: malformed JSON: {error}") \ f"{path}: malformed JSON: {error}") \
from error from error
song_id: int = self.__parse_song_id( cls.__load_ballot_record(record, run_dir, pooled)
record["id"], run_dir)
@classmethod
def __load_ballot_record(
cls, record: Any, run_dir: Path,
pooled: dict[int, list[list[str]]]) -> None:
"""Parse and pool one ballot record.
A record whose "text" field is missing, or does not
parse to a JSON array of strings, is skipped, a warning
naming the run directory and song reported on standard
error, as an observable side effect.
:param record: The parsed JSON record.
:param run_dir: The run directory the record came from,
for the warning and error messages.
:param pooled: The pool to append the record's ballot
into, keyed by the numeric song ID; mutated in
place.
:return: None.
:raises TallyError: When the record's "id" field is not
in the ``song-<ID>`` form.
"""
song_id: int = cls.__parse_song_id(record["id"], run_dir)
if "text" not in record: if "text" not in record:
print( print(
f"warning: {run_dir}: song-{song_id}: no" f"warning: {run_dir}: song-{song_id}: no"
" \"text\" field, skipped", " \"text\" field, skipped",
file=sys.stderr) file=sys.stderr)
continue return
ballot: list[str] | None = self.__parse_ballot( ballot: list[str] | None = cls.__parse_ballot(
record["text"], run_dir, song_id) record["text"], run_dir, song_id)
if ballot is None: if ballot is None:
continue return
pooled.setdefault(song_id, []).append(ballot) pooled.setdefault(song_id, []).append(ballot)
song_id: int
ballots: list[list[str]]
for song_id, ballots in pooled.items():
if len(ballots) != self.__BALLOTS_PER_SONG:
raise TallyError(
f"song-{song_id}: appears {len(ballots)}"
f" times in the pool, expected"
f" {self.__BALLOTS_PER_SONG}")
return pooled
@classmethod @classmethod
def __parse_song_id(cls, item_id: Any, run_dir: Path) -> int: def __parse_song_id(cls, item_id: Any, run_dir: Path) -> int:
+15 -15
View File
@@ -21,7 +21,7 @@ from pop_fem_audit_tools.commands import run_llm
class RunLLMTestCase(unittest.TestCase): class RunLLMTestCase(unittest.TestCase):
"""The common base test case with the shared helpers.""" """The common base test case with the shared helpers."""
def make_temp_dir(self) -> Path: def _make_temp_dir(self) -> Path:
"""Create a temporary directory removed on test cleanup. """Create a temporary directory removed on test cleanup.
:return: The path of the temporary directory. :return: The path of the temporary directory.
@@ -31,7 +31,7 @@ class RunLLMTestCase(unittest.TestCase):
self.addCleanup(tmp.cleanup) self.addCleanup(tmp.cleanup)
return Path(tmp.name) return Path(tmp.name)
def make_success_entry(self, custom_id: str, def _make_success_entry(self, custom_id: str,
text: str) -> mock.Mock: text: str) -> mock.Mock:
"""Create a mock succeeded batch result entry. """Create a mock succeeded batch result entry.
@@ -47,7 +47,7 @@ class RunLLMTestCase(unittest.TestCase):
return entry return entry
@staticmethod @staticmethod
def make_error_entry(custom_id: str, def _make_error_entry(custom_id: str,
error_type: str) -> mock.Mock: error_type: str) -> mock.Mock:
"""Create a mock errored batch result entry. """Create a mock errored batch result entry.
@@ -95,7 +95,7 @@ class TestLoadItems(RunLLMTestCase):
def setUp(self) -> None: def setUp(self) -> None:
"""Create the prompt and input paths for a dry run.""" """Create the prompt and input paths for a dry run."""
directory: Path = self.make_temp_dir() directory: Path = self._make_temp_dir()
self.__prompt: Path = directory / "task.md" self.__prompt: Path = directory / "task.md"
self.__prompt.write_text("The task.\n", encoding="utf-8") self.__prompt.write_text("The task.\n", encoding="utf-8")
self.__input: Path = directory / "items.jsonl" self.__input: Path = directory / "items.jsonl"
@@ -171,7 +171,7 @@ class TestRequestBuilding(RunLLMTestCase):
def setUp(self) -> None: def setUp(self) -> None:
"""Create the prompt and input files for a dry run.""" """Create the prompt and input files for a dry run."""
directory: Path = self.make_temp_dir() directory: Path = self._make_temp_dir()
self.__prompt: Path = directory / "task.md" self.__prompt: Path = directory / "task.md"
self.__prompt.write_text( self.__prompt.write_text(
"the system prompt", encoding="utf-8") "the system prompt", encoding="utf-8")
@@ -224,7 +224,7 @@ class TestMainFlow(RunLLMTestCase):
def setUp(self) -> None: def setUp(self) -> None:
"""Create a temporary directory with the input files.""" """Create a temporary directory with the input files."""
directory: Path = self.make_temp_dir() directory: Path = self._make_temp_dir()
self.__runs: Path = directory / "runs" self.__runs: Path = directory / "runs"
self.__archive_dir: Path = self.__runs / "task_v1" / "run1" self.__archive_dir: Path = self.__runs / "task_v1" / "run1"
self.__prompt: Path = directory / "task_v1.md" self.__prompt: Path = directory / "task_v1.md"
@@ -310,8 +310,8 @@ class TestMainFlow(RunLLMTestCase):
the item order preserved and the token usage summed, the the item order preserved and the token usage summed, the
output text written unescaped.""" output text written unescaped."""
client: mock.Mock = self.__make_client( client: mock.Mock = self.__make_client(
[self.make_success_entry("a", "answer 中文 a"), [self._make_success_entry("a", "answer 中文 a"),
self.make_success_entry("b", "answer b")]) self._make_success_entry("b", "answer b")])
status: int status: int
stderr: str stderr: str
with mock.patch( with mock.patch(
@@ -356,8 +356,8 @@ class TestMainFlow(RunLLMTestCase):
(run_dir / "stale.jsonl").write_text( (run_dir / "stale.jsonl").write_text(
"stale", encoding="utf-8") "stale", encoding="utf-8")
client: mock.Mock = self.__make_client( client: mock.Mock = self.__make_client(
[self.make_success_entry("a", "answer a"), [self._make_success_entry("a", "answer a"),
self.make_success_entry("b", "answer b")]) self._make_success_entry("b", "answer b")])
status: int = self.__run_main( status: int = self.__run_main(
self.__argv + ["--replace"], client)[0] self.__argv + ["--replace"], client)[0]
self.assertEqual(status, 0) self.assertEqual(status, 0)
@@ -375,8 +375,8 @@ class TestMainFlow(RunLLMTestCase):
(run2_dir / "output.jsonl").write_text( (run2_dir / "output.jsonl").write_text(
"stale run2 data", encoding="utf-8") "stale run2 data", encoding="utf-8")
client: mock.Mock = self.__make_client( client: mock.Mock = self.__make_client(
[self.make_success_entry("a", "answer a"), [self._make_success_entry("a", "answer a"),
self.make_success_entry("b", "answer b")]) self._make_success_entry("b", "answer b")])
argv: list[str] = [ argv: list[str] = [
str(self.__prompt), str(self.__input), str(self.__prompt), str(self.__input),
str(run2_dir), "--replace"] str(run2_dir), "--replace"]
@@ -393,8 +393,8 @@ class TestMainFlow(RunLLMTestCase):
"""Test that a failed item aborts with a non-zero status, """Test that a failed item aborts with a non-zero status,
the summed usage counting only the succeeded item.""" the summed usage counting only the succeeded item."""
client: mock.Mock = self.__make_client( client: mock.Mock = self.__make_client(
[self.make_success_entry("a", "answer a"), [self._make_success_entry("a", "answer a"),
self.make_error_entry("b", "invalid_request_error")]) self._make_error_entry("b", "invalid_request_error")])
status: int status: int
stderr: str stderr: str
status, _, stderr = self.__run_main(self.__argv, client) status, _, stderr = self.__run_main(self.__argv, client)
@@ -417,7 +417,7 @@ class TestMainFlow(RunLLMTestCase):
"""Test that an item missing from the batch results is """Test that an item missing from the batch results is
reported as a failed item.""" reported as a failed item."""
client: mock.Mock = self.__make_client( client: mock.Mock = self.__make_client(
[self.make_success_entry("a", "answer a")]) [self._make_success_entry("a", "answer a")])
status: int status: int
stderr: str stderr: str
status, _, stderr = self.__run_main(self.__argv, client) status, _, stderr = self.__run_main(self.__argv, client)