Fix the SonarQube findings
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,36 +311,7 @@ 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:
|
||||||
path: Path = run_dir / "output.jsonl"
|
self.__load_run_ballots(run_dir, pooled)
|
||||||
text: str
|
|
||||||
try:
|
|
||||||
text = path.read_text(encoding="utf-8")
|
|
||||||
except OSError as error:
|
|
||||||
raise TallyError(str(error)) from error
|
|
||||||
line: str
|
|
||||||
for line in text.split("\n"):
|
|
||||||
if line.strip() == "":
|
|
||||||
continue
|
|
||||||
record: Any
|
|
||||||
try:
|
|
||||||
record = json.loads(line)
|
|
||||||
except json.JSONDecodeError as error:
|
|
||||||
raise TallyError(
|
|
||||||
f"{path}: malformed JSON: {error}") \
|
|
||||||
from error
|
|
||||||
song_id: int = self.__parse_song_id(
|
|
||||||
record["id"], run_dir)
|
|
||||||
if "text" not in record:
|
|
||||||
print(
|
|
||||||
f"warning: {run_dir}: song-{song_id}: no"
|
|
||||||
" \"text\" field, skipped",
|
|
||||||
file=sys.stderr)
|
|
||||||
continue
|
|
||||||
ballot: list[str] | None = self.__parse_ballot(
|
|
||||||
record["text"], run_dir, song_id)
|
|
||||||
if ballot is None:
|
|
||||||
continue
|
|
||||||
pooled.setdefault(song_id, []).append(ballot)
|
|
||||||
song_id: int
|
song_id: int
|
||||||
ballots: list[list[str]]
|
ballots: list[list[str]]
|
||||||
for song_id, ballots in pooled.items():
|
for song_id, ballots in pooled.items():
|
||||||
@@ -351,6 +322,74 @@ class AnnotationTallier:
|
|||||||
f" {self.__BALLOTS_PER_SONG}")
|
f" {self.__BALLOTS_PER_SONG}")
|
||||||
return pooled
|
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"
|
||||||
|
text: str
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
except OSError as error:
|
||||||
|
raise TallyError(str(error)) from error
|
||||||
|
line: str
|
||||||
|
for line in text.split("\n"):
|
||||||
|
if line.strip() == "":
|
||||||
|
continue
|
||||||
|
record: Any
|
||||||
|
try:
|
||||||
|
record = json.loads(line)
|
||||||
|
except json.JSONDecodeError as error:
|
||||||
|
raise TallyError(
|
||||||
|
f"{path}: malformed JSON: {error}") \
|
||||||
|
from error
|
||||||
|
cls.__load_ballot_record(record, run_dir, pooled)
|
||||||
|
|
||||||
|
@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:
|
||||||
|
print(
|
||||||
|
f"warning: {run_dir}: song-{song_id}: no"
|
||||||
|
" \"text\" field, skipped",
|
||||||
|
file=sys.stderr)
|
||||||
|
return
|
||||||
|
ballot: list[str] | None = cls.__parse_ballot(
|
||||||
|
record["text"], run_dir, song_id)
|
||||||
|
if ballot is None:
|
||||||
|
return
|
||||||
|
pooled.setdefault(song_id, []).append(ballot)
|
||||||
|
|
||||||
@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:
|
||||||
"""Parse the numeric song ID out of an annotation record
|
"""Parse the numeric song ID out of an annotation record
|
||||||
|
|||||||
+17
-17
@@ -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,8 +31,8 @@ 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.
|
||||||
|
|
||||||
:param custom_id: The custom ID of the entry.
|
:param custom_id: The custom ID of the entry.
|
||||||
@@ -47,8 +47,8 @@ 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.
|
||||||
|
|
||||||
The error object is shaped as the SDK envelope: the outer
|
The error object is shaped as the SDK envelope: the outer
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user