From 2ecdee28cf11ddc413509ecd4f9211b49136fe12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Wed, 4 Mar 2026 23:31:15 +0100 Subject: [PATCH 1/8] make code compliant with ruff lint --- PythonScripts/audit_translations/__init__.py | 2 +- PythonScripts/audit_translations/auditor.py | 10 ++++---- PythonScripts/audit_translations/cli.py | 4 ++-- .../audit_translations/line_resolver.py | 2 +- PythonScripts/audit_translations/parsers.py | 23 ++++++++----------- PythonScripts/audit_translations/renderer.py | 2 +- .../tests/test_line_resolver.py | 2 +- .../audit_translations/tests/test_parsers.py | 13 +++++------ PythonScripts/pyproject.toml | 12 ++++++++++ 9 files changed, 38 insertions(+), 32 deletions(-) diff --git a/PythonScripts/audit_translations/__init__.py b/PythonScripts/audit_translations/__init__.py index d17aae02..cd6dab62 100644 --- a/PythonScripts/audit_translations/__init__.py +++ b/PythonScripts/audit_translations/__init__.py @@ -11,7 +11,7 @@ import sys sys.stdout.reconfigure(encoding="utf-8") -from .cli import main +from .cli import main # noqa: E402 __all__ = [ "main", diff --git a/PythonScripts/audit_translations/auditor.py b/PythonScripts/audit_translations/auditor.py index 7ad8a216..25e1ab67 100644 --- a/PythonScripts/audit_translations/auditor.py +++ b/PythonScripts/audit_translations/auditor.py @@ -13,8 +13,8 @@ from rich.panel import Panel from rich.table import Table -from .dataclasses import RuleInfo, ComparisonResult -from .parsers import parse_yaml_file, diff_rules +from .dataclasses import ComparisonResult, RuleInfo +from .parsers import diff_rules, parse_yaml_file from .renderer import collect_issues, console, print_warnings # Re-export console so existing `from .auditor import console` callers keep working. @@ -181,12 +181,12 @@ def audit_language( if output_format == "rich": # Print header console.print(Panel(f"MathCAT Translation Audit: {language.upper()}", style="bold cyan")) - console.print(f"\n [dim]Comparing against English (en) reference files[/]") + console.print("\n [dim]Comparing against English (en) reference files[/]") console.print(f" [dim]Files to check: {len(files)}[/]") out_stream: TextIO = sys.stdout if output_path: - out_stream = open(output_path, "w", encoding="utf-8", newline="") + out_stream = open(output_path, "w", encoding="utf-8", newline="") # noqa: SIM115 total_issues = 0 total_missing = 0 @@ -259,7 +259,7 @@ def audit_language( return total_issues -def list_languages(rules_dir: str | None = None): +def list_languages(rules_dir: str | None = None) -> None: """List available languages for auditing""" console.print(Panel("Available Languages", style="bold cyan")) diff --git a/PythonScripts/audit_translations/cli.py b/PythonScripts/audit_translations/cli.py index edeb190d..85db07a0 100644 --- a/PythonScripts/audit_translations/cli.py +++ b/PythonScripts/audit_translations/cli.py @@ -7,10 +7,10 @@ import argparse import sys -from .auditor import audit_language, list_languages, console +from .auditor import audit_language, console, list_languages -def main(): +def main() -> None: """Main entry point for the audit tool""" parser = argparse.ArgumentParser( diff --git a/PythonScripts/audit_translations/line_resolver.py b/PythonScripts/audit_translations/line_resolver.py index e440c5f6..1222a6cd 100644 --- a/PythonScripts/audit_translations/line_resolver.py +++ b/PythonScripts/audit_translations/line_resolver.py @@ -4,7 +4,7 @@ Maps rule diff types and structure tokens to precise YAML source line numbers. """ -from .dataclasses import RuleInfo, RuleDifference +from .dataclasses import RuleDifference, RuleInfo from .parsers import extract_structure_elements diff --git a/PythonScripts/audit_translations/parsers.py b/PythonScripts/audit_translations/parsers.py index 591d9605..6f1e3967 100644 --- a/PythonScripts/audit_translations/parsers.py +++ b/PythonScripts/audit_translations/parsers.py @@ -4,15 +4,16 @@ Handles parsing of rule files and unicode files to extract rule information. """ +from collections.abc import Iterator from pathlib import Path -from typing import Any, Iterator +from typing import Any from jsonpath_ng.ext import parse from jsonpath_ng.jsonpath import Fields from ruamel.yaml import YAML from ruamel.yaml.scanner import ScannerError -from .dataclasses import RuleInfo, RuleDifference +from .dataclasses import RuleDifference, RuleInfo _yaml = YAML() _yaml.preserve_quotes = True @@ -35,7 +36,7 @@ def parse_yaml_file(file_path: str, strict: bool = False) -> tuple[list[RuleInfo For standard rule files: extracts rules with name/tag For unicode files: extracts entries with character/range keys """ - with open(file_path, "r", encoding="utf-8") as f: + with open(file_path, encoding="utf-8") as f: content = f.read() try: @@ -49,10 +50,7 @@ def parse_yaml_file(file_path: str, strict: bool = False) -> tuple[list[RuleInfo else: raise exc - if is_unicode_file(file_path): - rules = parse_unicode_file(content, data) - else: - rules = parse_rules_file(content, data) + rules = parse_unicode_file(content, data) if is_unicode_file(file_path) else parse_rules_file(content, data) return rules, content @@ -130,7 +128,7 @@ def _build_rule_items(content: str, data: Any, is_unicode_file: bool) -> list[Ru raw_blocks = build_raw_blocks(lines, start_lines) rules: list[RuleInfo] = [] - for (key, name, tag, item_data), raw_content, line_idx in zip(extracted, raw_blocks, start_lines): + for (key, name, tag, item_data), raw_content, line_idx in zip(extracted, raw_blocks, start_lines, strict=True): rules.append( RuleInfo( name=name, @@ -183,14 +181,11 @@ def should_add(text: str) -> bool: return False if len(text) == 1 and not text.isalpha(): return False - if text.startswith("$") or text.startswith("@"): - return False - return True + return not (text.startswith("$") or text.startswith("@")) for key, child, parent in iter_field_matches(node): - if key.lower() in translation_keys and not key.isupper() and isinstance(child, str): - if should_add(child): - entries.append((key, child, mapping_key_line(parent, key))) + if key.lower() in translation_keys and not key.isupper() and isinstance(child, str) and should_add(child): + entries.append((key, child, mapping_key_line(parent, key))) return entries diff --git a/PythonScripts/audit_translations/renderer.py b/PythonScripts/audit_translations/renderer.py index 14779df8..9d411696 100644 --- a/PythonScripts/audit_translations/renderer.py +++ b/PythonScripts/audit_translations/renderer.py @@ -119,7 +119,7 @@ def collect_issues( issues.append(issue) for rule, entries in result.untranslated_text: - for key, text, line in entries: + for _key, text, line in entries: issue = issue_base(rule, file_name, language) issue.update( issue_type="untranslated_text", diff --git a/PythonScripts/audit_translations/tests/test_line_resolver.py b/PythonScripts/audit_translations/tests/test_line_resolver.py index 9c48ccee..e5f1eb0a 100644 --- a/PythonScripts/audit_translations/tests/test_line_resolver.py +++ b/PythonScripts/audit_translations/tests/test_line_resolver.py @@ -2,7 +2,7 @@ Unit tests for line_resolver.py. """ -from ..dataclasses import RuleInfo, RuleDifference +from ..dataclasses import RuleDifference, RuleInfo from ..line_resolver import first_structure_mismatch, resolve_diff_lines diff --git a/PythonScripts/audit_translations/tests/test_parsers.py b/PythonScripts/audit_translations/tests/test_parsers.py index 83de6439..ca848c86 100644 --- a/PythonScripts/audit_translations/tests/test_parsers.py +++ b/PythonScripts/audit_translations/tests/test_parsers.py @@ -2,14 +2,14 @@ Tests for parsers.py. """ -from typing import List import pytest from ruamel.yaml import YAML from ruamel.yaml.scanner import ScannerError -from ..dataclasses import RuleInfo, RuleDifference +from ..dataclasses import RuleDifference, RuleInfo from ..parsers import ( + build_line_map, diff_rules, extract_conditions, extract_match_pattern, @@ -18,7 +18,6 @@ find_untranslated_text_entries, find_untranslated_text_values, has_audit_ignore, - build_line_map, parse_rules_file, parse_unicode_file, ) @@ -488,8 +487,8 @@ def test_condition_snippet_preserves_rule_order(self): }, ) tr = make_rule("test", "mo", {"if": "condition_c"}) - diffs: List[RuleDifference] = diff_rules(en, tr) - cond_diff: RuleDifference = [d for d in diffs if d.diff_type == "condition"][0] + diffs: list[RuleDifference] = diff_rules(en, tr) + cond_diff: RuleDifference = next(d for d in diffs if d.diff_type == "condition") assert cond_diff.english_snippet == "condition_b, condition_a" assert cond_diff.translated_snippet == "condition_c" @@ -521,8 +520,8 @@ def test_condition_snippet_deduplicates_repeated_conditions(self): }, ) tr = make_rule("test", "mo", {"if": "condition_c"}) - diffs: List[RuleDifference] = diff_rules(en, tr) - cond_diff: RuleDifference = [d for d in diffs if d.diff_type == "condition"][0] + diffs: list[RuleDifference] = diff_rules(en, tr) + cond_diff: RuleDifference = next(d for d in diffs if d.diff_type == "condition") # without deduplication, we'd have "condition_a" repeated. assert cond_diff.english_snippet == "condition_a, condition_b" diff --git a/PythonScripts/pyproject.toml b/PythonScripts/pyproject.toml index 13d0b08e..b4d422a3 100644 --- a/PythonScripts/pyproject.toml +++ b/PythonScripts/pyproject.toml @@ -42,5 +42,17 @@ module-root = "" target-version = "py314" line-length = 130 # easier for some files. maybe decrease in the future +[tool.ruff.lint] +select = [ + "F", # pyflakes — unused imports, undefined names, etc. + "E", # pycodestyle errors + "W", # pycodestyle warnings + "I", # isort — import ordering + "UP", # pyupgrade — modernize syntax for target Python version + "B", # flake8-bugbear — common bug patterns + "SIM", # flake8-simplify — unnecessary complexity + "RUF", # ruff-specific rules +] + [tool.ruff.format] docstring-code-format = true From 617d016e29c66fa6953b041a50ee20b98b9e4443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Wed, 4 Mar 2026 23:53:42 +0100 Subject: [PATCH 2/8] fix "audit-translations --list" --- PythonScripts/audit_translations/auditor.py | 11 ++++---- .../audit_translations/tests/test_auditor.py | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/PythonScripts/audit_translations/auditor.py b/PythonScripts/audit_translations/auditor.py index 25e1ab67..afcc147e 100644 --- a/PythonScripts/audit_translations/auditor.py +++ b/PythonScripts/audit_translations/auditor.py @@ -276,11 +276,12 @@ def list_languages(rules_dir: str | None = None) -> None: table.add_row(lang_dir.name, f"[{color}]{base_count}[/] files") for region_dir in sorted(lang_dir.iterdir()): - if region_dir.is_dir(): - code = f"{lang_dir.name}-{region_dir.name}" - count = len(get_yaml_files(lang_dir, region_dir)) - region_color = "green" if count >= 7 else "yellow" if count >= 4 else "red" - table.add_row(code, f"[{region_color}]{count}[/] files") + if not region_dir.is_dir() or region_dir.name.lower() == "sharedrules": + continue + code = f"{lang_dir.name}-{region_dir.name}" + count = len(get_yaml_files(lang_dir, region_dir)) + region_color = "green" if count >= 7 else "yellow" if count >= 4 else "red" + table.add_row(code, f"[{region_color}]{count}[/] files") console.print(table) console.print("\n [dim]Reference: en (English) - base translation[/]\n") diff --git a/PythonScripts/audit_translations/tests/test_auditor.py b/PythonScripts/audit_translations/tests/test_auditor.py index 7e00ae3f..34ccbe48 100644 --- a/PythonScripts/audit_translations/tests/test_auditor.py +++ b/PythonScripts/audit_translations/tests/test_auditor.py @@ -234,6 +234,31 @@ def test_list_languages_includes_region_codes(tmp_path) -> None: assert "zz-aa" in output +def test_list_languages_ignores_sharedrules_as_region(tmp_path) -> None: + """ + Ensures SharedRules is not misreported as a language-region variant. + """ + rules_dir = tmp_path / "Rules" / "Languages" + (rules_dir / "en").mkdir(parents=True) + lang_dir = rules_dir / "zz" + region_dir = lang_dir / "aa" + shared_rules_dir = lang_dir / "SharedRules" + lang_dir.mkdir(parents=True) + region_dir.mkdir(parents=True) + shared_rules_dir.mkdir(parents=True) + + (lang_dir / "file.yaml").write_text("---", encoding="utf-8") + (region_dir / "region.yaml").write_text("---", encoding="utf-8") + (shared_rules_dir / "shared.yaml").write_text("---", encoding="utf-8") + + with console.capture() as capture: + list_languages(str(rules_dir)) + output = capture.get() + + assert "zz-aa" in output + assert "zz-SharedRules" not in output + + def test_print_warnings_omits_snippets_when_not_verbose(fixed_console_width) -> None: """ Ensure the print_warnings output matches the non-verbose golden snapshot. From e8e8056247b3bbd1f5e004780773c3013b3eed1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Thu, 5 Mar 2026 00:11:48 +0100 Subject: [PATCH 3/8] simplify print_warnings() --- PythonScripts/audit_translations/renderer.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/PythonScripts/audit_translations/renderer.py b/PythonScripts/audit_translations/renderer.py index 9d411696..69e50528 100644 --- a/PythonScripts/audit_translations/renderer.py +++ b/PythonScripts/audit_translations/renderer.py @@ -227,16 +227,13 @@ def add_issue(rule: RuleInfo, issue_type: str, payload: dict[str, Any]) -> None: for entry in entries: if issue_type == "missing_rule": console.print(f" [dim]•[/] [dim](line {entry['line_en']} in English)[/]") - issues += 1 elif issue_type == "extra_rule": console.print(f" [dim]•[/] [dim](line {entry['line_tr']} in {target_label})[/]") - issues += 1 elif issue_type == "untranslated_text": console.print( f" [dim]•[/] [dim](line {entry['line_tr']} {target_label})[/] " f'[yellow]"{escape(entry["text"])}"[/]' ) - issues += 1 else: diff: RuleDifference = entry["diff"] console.print( @@ -246,6 +243,6 @@ def add_issue(rule: RuleInfo, issue_type: str, payload: dict[str, Any]) -> None: if verbose: console.print(f" [green]en:[/] {escape(diff.english_snippet)}") console.print(f" [red]{target_label}:[/] {escape(diff.translated_snippet)}") - issues += 1 + issues += len(entries) return issues From 8174f4f81218c685f7a2004a0cd4ceafe11b1442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Thu, 5 Mar 2026 00:22:20 +0100 Subject: [PATCH 4/8] extract magic numbers in rendering --- PythonScripts/audit_translations/auditor.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/PythonScripts/audit_translations/auditor.py b/PythonScripts/audit_translations/auditor.py index afcc147e..9fdc1632 100644 --- a/PythonScripts/audit_translations/auditor.py +++ b/PythonScripts/audit_translations/auditor.py @@ -20,6 +20,18 @@ # Re-export console so existing `from .auditor import console` callers keep working. __all__ = ["console"] +GREEN_FILE_COUNT_THRESHOLD = 7 +YELLOW_FILE_COUNT_THRESHOLD = 4 + + +def file_count_color(file_count: int) -> str: + """Map number of translated YAML files to a display color.""" + if file_count >= GREEN_FILE_COUNT_THRESHOLD: + return "green" + if file_count >= YELLOW_FILE_COUNT_THRESHOLD: + return "yellow" + return "red" + def split_language_into_base_and_region(language: str) -> tuple[str, str | None]: """Split a language code into base and optional region.""" @@ -272,7 +284,7 @@ def list_languages(rules_dir: str | None = None) -> None: if not lang_dir.is_dir() or lang_dir.name == "en": continue base_count = len(get_yaml_files(lang_dir)) - color = "green" if base_count >= 7 else "yellow" if base_count >= 4 else "red" + color = file_count_color(base_count) table.add_row(lang_dir.name, f"[{color}]{base_count}[/] files") for region_dir in sorted(lang_dir.iterdir()): @@ -280,7 +292,7 @@ def list_languages(rules_dir: str | None = None) -> None: continue code = f"{lang_dir.name}-{region_dir.name}" count = len(get_yaml_files(lang_dir, region_dir)) - region_color = "green" if count >= 7 else "yellow" if count >= 4 else "red" + region_color = file_count_color(count) table.add_row(code, f"[{region_color}]{count}[/] files") console.print(table) From 87f753b31db91bfa74bf249fd90c0cfaf56e6d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Thu, 5 Mar 2026 01:54:33 +0100 Subject: [PATCH 5/8] add enums for IssueType and DiffType --- .../audit_translations/dataclasses.py | 25 ++++- PythonScripts/audit_translations/parsers.py | 10 +- PythonScripts/audit_translations/renderer.py | 96 ++++++++----------- 3 files changed, 68 insertions(+), 63 deletions(-) diff --git a/PythonScripts/audit_translations/dataclasses.py b/PythonScripts/audit_translations/dataclasses.py index 98a7098e..7205a3e0 100644 --- a/PythonScripts/audit_translations/dataclasses.py +++ b/PythonScripts/audit_translations/dataclasses.py @@ -5,9 +5,28 @@ """ from dataclasses import dataclass, field +from enum import StrEnum from typing import Any +class IssueType(StrEnum): + """Top-level issue categories serialized to JSONL.""" + + MISSING_RULE = "missing_rule" + UNTRANSLATED_TEXT = "untranslated_text" + RULE_DIFFERENCE = "rule_difference" + EXTRA_RULE = "extra_rule" + + +class DiffType(StrEnum): + """Rule-difference subcategories used for fine-grained diagnostics.""" + + MATCH = "match" # `match` XPath differs between English and translation. + CONDITION = "condition" # `if` / `test` condition expressions differ. + VARIABLES = "variables" # Variable names defined in `variables` differ. + STRUCTURE = "structure" # Control-flow block shape/order differs (if/then/else/with/replace). + + @dataclass class RuleInfo: """ @@ -64,11 +83,15 @@ class RuleDifference: english_rule: RuleInfo translated_rule: RuleInfo - diff_type: str # 'match', 'condition', 'structure', 'variables' + diff_type: DiffType description: str english_snippet: str translated_snippet: str + def __post_init__(self) -> None: + if isinstance(self.diff_type, str): + self.diff_type = DiffType(self.diff_type) + @dataclass class ComparisonResult: diff --git a/PythonScripts/audit_translations/parsers.py b/PythonScripts/audit_translations/parsers.py index 6f1e3967..18154f83 100644 --- a/PythonScripts/audit_translations/parsers.py +++ b/PythonScripts/audit_translations/parsers.py @@ -13,7 +13,7 @@ from ruamel.yaml import YAML from ruamel.yaml.scanner import ScannerError -from .dataclasses import RuleDifference, RuleInfo +from .dataclasses import DiffType, RuleDifference, RuleInfo _yaml = YAML() _yaml.preserve_quotes = True @@ -313,7 +313,7 @@ def diff_rules(english_rule: RuleInfo, translated_rule: RuleInfo) -> list[RuleDi RuleDifference( english_rule=english_rule, translated_rule=translated_rule, - diff_type="match", + diff_type=DiffType.MATCH, description="Match pattern differs", english_snippet=en_match, translated_snippet=translated_match, @@ -333,7 +333,7 @@ def diff_rules(english_rule: RuleInfo, translated_rule: RuleInfo) -> list[RuleDi RuleDifference( english_rule=english_rule, translated_rule=translated_rule, - diff_type="condition", + diff_type=DiffType.CONDITION, description="Conditions differ", english_snippet=", ".join(dedup_list(en_conditions)) or "(none)", translated_snippet=", ".join(dedup_list(tr_conditions)) or "(none)", @@ -351,7 +351,7 @@ def diff_rules(english_rule: RuleInfo, translated_rule: RuleInfo) -> list[RuleDi RuleDifference( english_rule=english_rule, translated_rule=translated_rule, - diff_type="variables", + diff_type=DiffType.VARIABLES, description="Variable definitions differ", english_snippet=", ".join(sorted(en_var_names)) or "(none)", translated_snippet=", ".join(sorted(tr_var_names)) or "(none)", @@ -366,7 +366,7 @@ def diff_rules(english_rule: RuleInfo, translated_rule: RuleInfo) -> list[RuleDi RuleDifference( english_rule=english_rule, translated_rule=translated_rule, - diff_type="structure", + diff_type=DiffType.STRUCTURE, description="Rule structure differs (test/if/then/else blocks)", english_snippet=" ".join(en_structure), translated_snippet=" ".join(tr_structure), diff --git a/PythonScripts/audit_translations/renderer.py b/PythonScripts/audit_translations/renderer.py index 69e50528..03253a26 100644 --- a/PythonScripts/audit_translations/renderer.py +++ b/PythonScripts/audit_translations/renderer.py @@ -11,56 +11,36 @@ from rich.console import Console from rich.markup import escape -from .dataclasses import ComparisonResult, RuleDifference, RuleInfo +from .dataclasses import ComparisonResult, DiffType, IssueType, RuleDifference, RuleInfo from .line_resolver import resolve_diff_lines console = Console() -def rule_label(rule: RuleInfo) -> str: - if rule.name is None: - return f'[yellow]"{escape(rule.key)}"[/]' - tag = rule.tag or "unknown" - return f"[cyan]{escape(rule.name)}[/] [dim]({escape(tag)})[/]" +type IssueGroupKey = tuple[IssueType, DiffType | None] -def issue_type_sort_key(issue_type: str) -> tuple[int, str]: - """ - Stable ordering for per-rule issue groups. +def issue_group_key(issue_type: IssueType, diff_type: DiffType | None = None) -> IssueGroupKey: + return issue_type, diff_type - The first tuple element defines user-facing priority (missing/untranslated/ - match/condition/variables/structure/extra). The second element keeps sorting - deterministic for unknown keys. - """ - order = { - "missing_rule": 0, - "untranslated_text": 1, - "rule_difference:match": 2, - "rule_difference:condition": 3, - "rule_difference:variables": 4, - "rule_difference:structure": 5, - "extra_rule": 6, - } - return order.get(issue_type, 99), issue_type +ISSUE_GROUP_SPECS: list[tuple[IssueGroupKey, str]] = [ + (issue_group_key(IssueType.MISSING_RULE), "Missing in Translation"), + (issue_group_key(IssueType.UNTRANSLATED_TEXT), "Untranslated Text"), + (issue_group_key(IssueType.RULE_DIFFERENCE, DiffType.MATCH), "Match Pattern Differences"), + (issue_group_key(IssueType.RULE_DIFFERENCE, DiffType.CONDITION), "Condition Differences"), + (issue_group_key(IssueType.RULE_DIFFERENCE, DiffType.VARIABLES), "Variable Differences"), + (issue_group_key(IssueType.RULE_DIFFERENCE, DiffType.STRUCTURE), "Structure Differences"), + (issue_group_key(IssueType.EXTRA_RULE), "Extra in Translation"), +] +ISSUE_GROUP_LABELS = {key: label for key, label in ISSUE_GROUP_SPECS} -def issue_type_label(issue_type: str) -> str: - """ - Return the display label used in rich grouped output. - Unknown issue types fall back to their raw key so renderer behavior remains - robust when new categories are introduced. - """ - labels = { - "missing_rule": "Missing in Translation", - "untranslated_text": "Untranslated Text", - "rule_difference:match": "Match Pattern Differences", - "rule_difference:condition": "Condition Differences", - "rule_difference:variables": "Variable Differences", - "rule_difference:structure": "Structure Differences", - "extra_rule": "Extra in Translation", - } - return labels.get(issue_type, issue_type) +def rule_label(rule: RuleInfo) -> str: + if rule.name is None: + return f'[yellow]"{escape(rule.key)}"[/]' + tag = rule.tag or "unknown" + return f"[cyan]{escape(rule.name)}[/] [dim]({escape(tag)})[/]" def issue_base(rule: RuleInfo, file_name: str, language: str) -> dict: @@ -93,7 +73,7 @@ def collect_issues( for rule in result.missing_rules: issue = issue_base(rule, file_name, language) issue.update( - issue_type="missing_rule", + issue_type=IssueType.MISSING_RULE.value, diff_type="", issue_line_en=rule.line_number, rule_line_en=rule.line_number, @@ -107,7 +87,7 @@ def collect_issues( for rule in result.extra_rules: issue = issue_base(rule, file_name, language) issue.update( - issue_type="extra_rule", + issue_type=IssueType.EXTRA_RULE.value, diff_type="", issue_line_tr=rule.line_number, rule_line_tr=rule.line_number, @@ -122,7 +102,7 @@ def collect_issues( for _key, text, line in entries: issue = issue_base(rule, file_name, language) issue.update( - issue_type="untranslated_text", + issue_type=IssueType.UNTRANSLATED_TEXT.value, diff_type="", issue_line_tr=line or rule.line_number, rule_line_tr=rule.line_number, @@ -140,8 +120,8 @@ def collect_issues( issue_line_en, issue_line_tr = lines issue = issue_base(diff.english_rule, file_name, language) issue.update( - issue_type="rule_difference", - diff_type=diff.diff_type, + issue_type=IssueType.RULE_DIFFERENCE.value, + diff_type=diff.diff_type.value, issue_line_en=issue_line_en, issue_line_tr=issue_line_tr, rule_line_en=diff.english_rule.line_number, @@ -185,17 +165,17 @@ def print_warnings( grouped_issues: dict[str, dict[str, Any]] = {} - def add_issue(rule: RuleInfo, issue_type: str, payload: dict[str, Any]) -> None: + def add_issue(rule: RuleInfo, group_key: IssueGroupKey, payload: dict[str, Any]) -> None: if rule.key not in grouped_issues: grouped_issues[rule.key] = {"rule": rule, "by_type": {}} - grouped_issues[rule.key]["by_type"].setdefault(issue_type, []).append(payload) + grouped_issues[rule.key]["by_type"].setdefault(group_key, []).append(payload) for rule in result.missing_rules: - add_issue(rule, "missing_rule", {"line_en": rule.line_number}) + add_issue(rule, issue_group_key(IssueType.MISSING_RULE), {"line_en": rule.line_number}) for rule, entries in result.untranslated_text: for _, text, line in entries: - add_issue(rule, "untranslated_text", {"line_tr": line or rule.line_number, "text": text}) + add_issue(rule, issue_group_key(IssueType.UNTRANSLATED_TEXT), {"line_tr": line or rule.line_number, "text": text}) for diff in result.rule_differences: lines = resolve_diff_lines(diff) @@ -204,12 +184,12 @@ def add_issue(rule: RuleInfo, issue_type: str, payload: dict[str, Any]) -> None: line_en, line_tr = lines add_issue( diff.english_rule, - f"rule_difference:{diff.diff_type}", + issue_group_key(IssueType.RULE_DIFFERENCE, diff.diff_type), {"line_en": line_en, "line_tr": line_tr, "diff": diff}, ) for rule in result.extra_rules: - add_issue(rule, "extra_rule", {"line_tr": rule.line_number}) + add_issue(rule, issue_group_key(IssueType.EXTRA_RULE), {"line_tr": rule.line_number}) if grouped_issues: total_grouped_issues = sum(len(entries) for group in grouped_issues.values() for entries in group["by_type"].values()) @@ -219,17 +199,19 @@ def add_issue(rule: RuleInfo, issue_type: str, payload: dict[str, Any]) -> None: ) for group in grouped_issues.values(): rule = group["rule"] - by_type: dict[str, list[dict[str, Any]]] = group["by_type"] + by_type: dict[IssueGroupKey, list[dict[str, Any]]] = group["by_type"] console.print(f" [dim]•[/] {rule_label(rule)}") - for issue_type in sorted(by_type.keys(), key=issue_type_sort_key): - entries = by_type[issue_type] - console.print(f" [dim]{issue_type_label(issue_type)} [{len(entries)}][/]") + ordered_group_keys = [group_key for group_key, _ in ISSUE_GROUP_SPECS if group_key in by_type] + for group_key in ordered_group_keys: + entries = by_type[group_key] + issue_type, _diff_type = group_key + console.print(f" [dim]{ISSUE_GROUP_LABELS[group_key]} [{len(entries)}][/]") for entry in entries: - if issue_type == "missing_rule": + if issue_type is IssueType.MISSING_RULE: console.print(f" [dim]•[/] [dim](line {entry['line_en']} in English)[/]") - elif issue_type == "extra_rule": + elif issue_type is IssueType.EXTRA_RULE: console.print(f" [dim]•[/] [dim](line {entry['line_tr']} in {target_label})[/]") - elif issue_type == "untranslated_text": + elif issue_type is IssueType.UNTRANSLATED_TEXT: console.print( f" [dim]•[/] [dim](line {entry['line_tr']} {target_label})[/] " f'[yellow]"{escape(entry["text"])}"[/]' From bc1501df42aa978883c79ca03669c86e13207608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Thu, 5 Mar 2026 02:19:26 +0100 Subject: [PATCH 6/8] remove JSONL functionality --- PythonScripts/audit_translations/README.md | 6 +- PythonScripts/audit_translations/auditor.py | 76 ++--- PythonScripts/audit_translations/cli.py | 11 +- .../audit_translations/dataclasses.py | 5 +- .../audit_translations/line_resolver.py | 4 +- PythonScripts/audit_translations/renderer.py | 98 +----- .../tests/golden/jsonl/de.json | 294 ------------------ .../audit_translations/tests/test_auditor.py | 164 +++++----- .../tests/test_cli_end_to_end.py | 71 ----- .../tests/test_output_jsonl.py | 45 --- .../tests/test_output_objects.py | 74 +++++ 11 files changed, 199 insertions(+), 649 deletions(-) delete mode 100644 PythonScripts/audit_translations/tests/golden/jsonl/de.json delete mode 100644 PythonScripts/audit_translations/tests/test_output_jsonl.py create mode 100644 PythonScripts/audit_translations/tests/test_output_objects.py diff --git a/PythonScripts/audit_translations/README.md b/PythonScripts/audit_translations/README.md index ef084922..ee4af211 100644 --- a/PythonScripts/audit_translations/README.md +++ b/PythonScripts/audit_translations/README.md @@ -68,10 +68,9 @@ uv run --project PythonScripts audit-translations --list * `--list`: Displays all available languages. * Region variants are shown as `lang-region` (e.g., `zz-aa`) based on subdirectories under `Rules/Languages/`. * `--file`: Audits a single specific file instead of the whole directory. -* `--format`: Output format (`rich`, `jsonl`). `--output` is honored only for `jsonl`; rich output always prints to the console. * `--rules-dir`: Override the Rules/Languages directory path. * `--only`: Filter issue types (comma-separated): `missing`, `untranslated`, `extra`, `diffs`, `all`. -* `--verbose`: Show detailed output including English/translated snippets for rule differences (only affects rich format; default shows summary only). +* `--verbose`: Show detailed output including English/translated snippets for rule differences. * **Summary Stats:** Provides a statistical summary after every run. **Examples:** @@ -92,9 +91,6 @@ uv run audit-translations de # Audit only a specific file uv run audit-translations es --file SharedRules/default.yaml -# Produce JSONL output for automation or AI workflows -uv run audit-translations es --format jsonl --output es-issues.jsonl - # Audit a regional variant (merges Rules/Languages/de and Rules/Languages/de/CH) uv run audit-translations de-CH diff --git a/PythonScripts/audit_translations/auditor.py b/PythonScripts/audit_translations/auditor.py index 9fdc1632..d30126c5 100644 --- a/PythonScripts/audit_translations/auditor.py +++ b/PythonScripts/audit_translations/auditor.py @@ -5,17 +5,15 @@ and for performing full language audits. """ -import json import sys from pathlib import Path -from typing import TextIO from rich.panel import Panel from rich.table import Table from .dataclasses import ComparisonResult, RuleInfo from .parsers import diff_rules, parse_yaml_file -from .renderer import collect_issues, console, print_warnings +from .renderer import console, print_warnings # Re-export console so existing `from .auditor import console` callers keep working. __all__ = ["console"] @@ -160,8 +158,6 @@ def merge_rules(base_rules: list[RuleInfo], region_rules: list[RuleInfo]) -> lis def audit_language( language: str, specific_file: str | None = None, - output_format: str = "rich", - output_path: str | None = None, rules_dir: str | None = None, issue_filter: set[str] | None = None, verbose: bool = False, @@ -190,15 +186,10 @@ def audit_language( # Get list of files to audit files = [specific_file] if specific_file else get_yaml_files(english_dir, english_region_dir) - if output_format == "rich": - # Print header - console.print(Panel(f"MathCAT Translation Audit: {language.upper()}", style="bold cyan")) - console.print("\n [dim]Comparing against English (en) reference files[/]") - console.print(f" [dim]Files to check: {len(files)}[/]") - - out_stream: TextIO = sys.stdout - if output_path: - out_stream = open(output_path, "w", encoding="utf-8", newline="") # noqa: SIM115 + # Print header + console.print(Panel(f"MathCAT Translation Audit: {language.upper()}", style="bold cyan")) + console.print("\n [dim]Comparing against English (en) reference files[/]") + console.print(f" [dim]Files to check: {len(files)}[/]") total_issues = 0 total_missing = 0 @@ -226,48 +217,35 @@ def audit_language( str(english_region_path) if english_region_path and english_region_path.exists() else None, ) - if output_format == "rich": - if result.has_issues: - issues = print_warnings(result, file_name, verbose, language) - if issues > 0: - files_with_issues += 1 - total_issues += issues - else: - files_ok += 1 - else: - issues_list = collect_issues(result, file_name, language) - for issue in issues_list: - out_stream.write(json.dumps(issue, ensure_ascii=False) + "\n") - if issues_list: + if result.has_issues: + issues = print_warnings(result, file_name, verbose, language) + if issues > 0: files_with_issues += 1 - total_issues += len(issues_list) - else: - files_ok += 1 + total_issues += issues + else: + files_ok += 1 total_missing += len(result.missing_rules) total_untranslated += sum(len(entries) for _, entries in result.untranslated_text) total_extra += len(result.extra_rules) total_differences += len(result.rule_differences) - if output_format == "rich": - # Summary - table = Table(title="SUMMARY", title_style="bold", box=None, show_header=False, padding=(0, 2)) - table.add_column(width=30) - table.add_column() - for label, value, color in [ - ("Files checked", len(files), None), - ("Files with issues", files_with_issues, "yellow" if files_with_issues else "green"), - ("Files OK", files_ok, "green" if files_ok else None), - ("Missing rules", total_missing, "red" if total_missing else "green"), - ("Untranslated text", total_untranslated, "yellow" if total_untranslated else "green"), - ("Rule differences", total_differences, "magenta" if total_differences else "green"), - ("Extra rules", total_extra, "blue" if total_extra else None), - ]: - table.add_row(label, f"[{color}]{value}[/]" if color else str(value)) - console.print(Panel(table, style="cyan")) - - if output_path: - out_stream.close() + # Summary + table = Table(title="SUMMARY", title_style="bold", box=None, show_header=False, padding=(0, 2)) + table.add_column(width=30) + table.add_column() + for label, value, color in [ + ("Files checked", len(files), None), + ("Files with issues", files_with_issues, "yellow" if files_with_issues else "green"), + ("Files OK", files_ok, "green" if files_ok else None), + ("Missing rules", total_missing, "red" if total_missing else "green"), + ("Untranslated text", total_untranslated, "yellow" if total_untranslated else "green"), + ("Rule differences", total_differences, "magenta" if total_differences else "green"), + ("Extra rules", total_extra, "blue" if total_extra else None), + ]: + table.add_row(label, f"[{color}]{value}[/]" if color else str(value)) + console.print(Panel(table, style="cyan")) + return total_issues diff --git a/PythonScripts/audit_translations/cli.py b/PythonScripts/audit_translations/cli.py index 85db07a0..1ae382a0 100644 --- a/PythonScripts/audit_translations/cli.py +++ b/PythonScripts/audit_translations/cli.py @@ -28,13 +28,6 @@ def main() -> None: parser.add_argument("--file", dest="specific_file", help="Audit only a specific file (e.g., 'SharedRules/default.yaml')") parser.add_argument("--list", action="store_true", help="List available languages") parser.add_argument("--rules-dir", help="Override Rules/Languages directory path") - parser.add_argument( - "--format", - choices=["rich", "jsonl"], - default="rich", - help="Output format (default: rich)", - ) - parser.add_argument("--output", help="Write output to a file instead of stdout") parser.add_argument( "--only", help="Comma-separated issue types: missing, untranslated, extra, diffs, all", @@ -42,7 +35,7 @@ def main() -> None: parser.add_argument( "--verbose", action="store_true", - help="Show detailed output including rule snippets (only affects rich format)", + help="Show detailed output including rule snippets", ) args = parser.parse_args() @@ -68,8 +61,6 @@ def main() -> None: audit_language( args.language, args.specific_file, - args.format, - args.output, args.rules_dir, issue_filter, args.verbose, diff --git a/PythonScripts/audit_translations/dataclasses.py b/PythonScripts/audit_translations/dataclasses.py index 7205a3e0..03b27996 100644 --- a/PythonScripts/audit_translations/dataclasses.py +++ b/PythonScripts/audit_translations/dataclasses.py @@ -10,7 +10,7 @@ class IssueType(StrEnum): - """Top-level issue categories serialized to JSONL.""" + """Top-level issue categories used by the audit renderer.""" MISSING_RULE = "missing_rule" UNTRANSLATED_TEXT = "untranslated_text" @@ -48,8 +48,7 @@ class RuleInfo: Parsed YAML node for the rule; used for structural diffs. untranslated_entries : list[tuple[str, str, int | None]] List of (key, text, line) entries extracted from lowercase translation keys. - This drives per-issue JSONL output so each untranslated string can report - the specific YAML line number where it appears. + This preserves exact text fragments and YAML line numbers for diagnostics. line_map : dict[str, list[int]] Mapping of element type to line numbers for rule components like match, conditions, variables, and structural tokens. This is used to point diff --git a/PythonScripts/audit_translations/line_resolver.py b/PythonScripts/audit_translations/line_resolver.py index 1222a6cd..5d8a4076 100644 --- a/PythonScripts/audit_translations/line_resolver.py +++ b/PythonScripts/audit_translations/line_resolver.py @@ -150,8 +150,8 @@ def resolve_diff_lines(diff: RuleDifference) -> tuple[int | None, int | None] | Resolve issue line numbers for a rule difference. Returns (line_en, line_tr), or None only for unresolvable structure diffs. - This is the single entry point used by both collect_issues and print_warnings - to avoid duplicating the structure vs non-structure branching logic. + This is the single entry point used by the renderer to avoid duplicating + the structure vs non-structure branching logic. """ if diff.diff_type == "structure": return resolve_structure_issue_lines(diff) diff --git a/PythonScripts/audit_translations/renderer.py b/PythonScripts/audit_translations/renderer.py index 03253a26..aa4135c1 100644 --- a/PythonScripts/audit_translations/renderer.py +++ b/PythonScripts/audit_translations/renderer.py @@ -1,8 +1,7 @@ """ -Rich console rendering and issue serialization. +Rich console rendering. -Handles all output concerns: rich terminal display, JSONL issue normalization, -and the IssueWriter interface. +Handles rich terminal display for grouped translation issues. """ from pathlib import Path @@ -43,99 +42,6 @@ def rule_label(rule: RuleInfo) -> str: return f"[cyan]{escape(rule.name)}[/] [dim]({escape(tag)})[/]" -def issue_base(rule: RuleInfo, file_name: str, language: str) -> dict: - return { - "language": language, - "file": Path(file_name).as_posix(), - "rule_name": rule.name or "", - "rule_tag": rule.tag or "", - "rule_key": rule.key, - "issue_line_en": None, - "issue_line_tr": None, - "rule_line_en": None, - "rule_line_tr": None, - } - - -def collect_issues( - result: ComparisonResult, - file_name: str, - language: str, -) -> list[dict]: - """ - Flatten a ComparisonResult into one normalized dictionary per issue. - - This is the canonical bridge from parser/diff objects to serializable - records consumed by JSONL output, snapshot tests, and line-level assertions. - """ - issues = [] - - for rule in result.missing_rules: - issue = issue_base(rule, file_name, language) - issue.update( - issue_type=IssueType.MISSING_RULE.value, - diff_type="", - issue_line_en=rule.line_number, - rule_line_en=rule.line_number, - description="Rule present in English but missing in translation", - english_snippet="", - translated_snippet="", - untranslated_texts=[], - ) - issues.append(issue) - - for rule in result.extra_rules: - issue = issue_base(rule, file_name, language) - issue.update( - issue_type=IssueType.EXTRA_RULE.value, - diff_type="", - issue_line_tr=rule.line_number, - rule_line_tr=rule.line_number, - description="Rule present in translation but missing in English", - english_snippet="", - translated_snippet="", - untranslated_texts=[], - ) - issues.append(issue) - - for rule, entries in result.untranslated_text: - for _key, text, line in entries: - issue = issue_base(rule, file_name, language) - issue.update( - issue_type=IssueType.UNTRANSLATED_TEXT.value, - diff_type="", - issue_line_tr=line or rule.line_number, - rule_line_tr=rule.line_number, - description="Lowercase t/ot/ct keys indicate untranslated text", - english_snippet="", - translated_snippet="", - untranslated_texts=[text], - ) - issues.append(issue) - - for diff in result.rule_differences: - lines = resolve_diff_lines(diff) - if lines is None: - continue - issue_line_en, issue_line_tr = lines - issue = issue_base(diff.english_rule, file_name, language) - issue.update( - issue_type=IssueType.RULE_DIFFERENCE.value, - diff_type=diff.diff_type.value, - issue_line_en=issue_line_en, - issue_line_tr=issue_line_tr, - rule_line_en=diff.english_rule.line_number, - rule_line_tr=diff.translated_rule.line_number, - description=diff.description, - english_snippet=diff.english_snippet, - translated_snippet=diff.translated_snippet, - untranslated_texts=[], - ) - issues.append(issue) - - return issues - - def print_warnings( result: ComparisonResult, file_name: str, diff --git a/PythonScripts/audit_translations/tests/golden/jsonl/de.json b/PythonScripts/audit_translations/tests/golden/jsonl/de.json deleted file mode 100644 index 17cb6a0e..00000000 --- a/PythonScripts/audit_translations/tests/golden/jsonl/de.json +++ /dev/null @@ -1,294 +0,0 @@ -[ - { - "language": "de", - "file": "basic.yaml", - "rule_name": "rule-2", - "rule_tag": "mn", - "rule_key": "rule-2|mn", - "issue_line_en": 7, - "issue_line_tr": null, - "rule_line_en": 7, - "rule_line_tr": null, - "issue_type": "missing_rule", - "diff_type": "", - "description": "Rule present in English but missing in translation", - "english_snippet": "", - "translated_snippet": "", - "untranslated_texts": [], - "_explanation": "missing_rule basic.yaml rule-2|mn" - }, - { - "language": "de", - "file": "basic.yaml", - "rule_name": "rule-3", - "rule_tag": "mo", - "rule_key": "rule-3|mo", - "issue_line_en": null, - "issue_line_tr": 7, - "rule_line_en": null, - "rule_line_tr": 7, - "issue_type": "extra_rule", - "diff_type": "", - "description": "Rule present in translation but missing in English", - "english_snippet": "", - "translated_snippet": "", - "untranslated_texts": [], - "_explanation": "extra_rule basic.yaml rule-3|mo" - }, - { - "language": "de", - "file": "basic.yaml", - "rule_name": "rule-1", - "rule_tag": "mo", - "rule_key": "rule-1|mo", - "issue_line_en": null, - "issue_line_tr": 5, - "rule_line_en": null, - "rule_line_tr": 1, - "issue_type": "untranslated_text", - "diff_type": "", - "description": "Lowercase t/ot/ct keys indicate untranslated text", - "english_snippet": "", - "translated_snippet": "", - "untranslated_texts": [ - "equals" - ], - "_explanation": "untranslated_text basic.yaml rule-1|mo" - }, - { - "language": "de", - "file": "condition_none.yaml", - "rule_name": "condition-none", - "rule_tag": "mi", - "rule_key": "condition-none|mi", - "issue_line_en": 6, - "issue_line_tr": 6, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "condition", - "description": "Conditions differ", - "english_snippet": "@intent='foo'", - "translated_snippet": "(none)", - "untranslated_texts": [], - "_explanation": "rule_difference condition_none.yaml condition-none|mi" - }, - { - "language": "de", - "file": "match.yaml", - "rule_name": "match-rule", - "rule_tag": "mo", - "rule_key": "match-rule|mo", - "issue_line_en": 3, - "issue_line_tr": 3, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "match", - "description": "Match pattern differs", - "english_snippet": ".//m:mi", - "translated_snippet": ".//m:mo", - "untranslated_texts": [], - "_explanation": "rule_difference match.yaml match-rule|mo" - }, - { - "language": "de", - "file": "structure.yaml", - "rule_name": "cond-rule", - "rule_tag": "mi", - "rule_key": "cond-rule|mi", - "issue_line_en": 6, - "issue_line_tr": 6, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "condition", - "description": "Conditions differ", - "english_snippet": "@intent='foo'", - "translated_snippet": "@intent='bar'", - "untranslated_texts": [], - "_explanation": "rule_difference structure.yaml cond-rule|mi" - }, - { - "language": "de", - "file": "structure_diff.yaml", - "rule_name": "struct-rule", - "rule_tag": "mi", - "rule_key": "struct-rule|mi", - "issue_line_en": 7, - "issue_line_tr": 7, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "structure", - "description": "Rule structure differs (test/if/then/else blocks)", - "english_snippet": "replace: test: if: then: else:", - "translated_snippet": "replace: test: if: then:", - "untranslated_texts": [], - "_explanation": "rule_difference structure_diff.yaml struct-rule|mi" - }, - { - "language": "de", - "file": "structure_empty.yaml", - "rule_name": "struct-empty", - "rule_tag": "mi", - "rule_key": "struct-empty|mi", - "issue_line_en": 4, - "issue_line_tr": 1, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "structure", - "description": "Rule structure differs (test/if/then/else blocks)", - "english_snippet": "replace:", - "translated_snippet": "", - "untranslated_texts": [], - "_explanation": "rule_difference structure_empty.yaml struct-empty|mi" - }, - { - "language": "de", - "file": "structure_misaligned.yaml", - "rule_name": "misaligned-structure", - "rule_tag": "root", - "rule_key": "misaligned-structure|root", - "issue_line_en": 6, - "issue_line_tr": 6, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "condition", - "description": "Conditions differ", - "english_snippet": "$Verbosity!='Terse', $Setting = 'Value', parent::m:minus, *[2][.='2']", - "translated_snippet": "$Setting = 'Value', parent::m:minus, *[2][.='2']", - "untranslated_texts": [], - "_explanation": "structure_misaligned.yaml: condition difference remains reported." - }, - { - "language": "de", - "file": "structure_misaligned.yaml", - "rule_name": "misaligned-structure", - "rule_tag": "root", - "rule_key": "misaligned-structure|root", - "issue_line_en": 11, - "issue_line_tr": 11, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "structure", - "description": "Rule structure differs (test/if/then/else blocks)", - "english_snippet": "replace: test: if: then: test: if: then: test: if: then: else: test: if: then: else:", - "translated_snippet": "replace: test: if: then: test: if: then: else: test: if: then: else:", - "untranslated_texts": [], - "_explanation": "structure_misaligned.yaml: structure substitutions/realignments are now reported with position-aware anchors." - }, - { - "language": "de", - "file": "structure_missing_else.yaml", - "rule_name": "missing-else-block", - "rule_tag": "root", - "rule_key": "missing-else-block|root", - "issue_line_en": 7, - "issue_line_tr": 7, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "structure", - "description": "Rule structure differs (test/if/then/else blocks)", - "english_snippet": "replace: test: if: then: else:", - "translated_snippet": "replace: test: if: then:", - "untranslated_texts": [], - "_explanation": "structure_missing_else.yaml: Verifies fix still reports legitimate missing else blocks. Unlike misaligned case, this has clear missing element." - }, - { - "language": "de", - "file": "unicode.yaml", - "rule_name": "", - "rule_tag": "", - "rule_key": "b", - "issue_line_en": 3, - "issue_line_tr": null, - "rule_line_en": 3, - "rule_line_tr": null, - "issue_type": "missing_rule", - "diff_type": "", - "description": "Rule present in English but missing in translation", - "english_snippet": "", - "translated_snippet": "", - "untranslated_texts": [], - "_explanation": "missing_rule unicode.yaml b" - }, - { - "language": "de", - "file": "unicode.yaml", - "rule_name": "", - "rule_tag": "", - "rule_key": "c", - "issue_line_en": null, - "issue_line_tr": 5, - "rule_line_en": null, - "rule_line_tr": 5, - "issue_type": "extra_rule", - "diff_type": "", - "description": "Rule present in translation but missing in English", - "english_snippet": "", - "translated_snippet": "", - "untranslated_texts": [], - "_explanation": "extra_rule unicode.yaml c" - }, - { - "language": "de", - "file": "unicode.yaml", - "rule_name": "", - "rule_tag": "", - "rule_key": "a", - "issue_line_en": null, - "issue_line_tr": 2, - "rule_line_en": null, - "rule_line_tr": 1, - "issue_type": "untranslated_text", - "diff_type": "", - "description": "Lowercase t/ot/ct keys indicate untranslated text", - "english_snippet": "", - "translated_snippet": "", - "untranslated_texts": [ - "a" - ], - "_explanation": "untranslated_text unicode.yaml a" - }, - { - "language": "de", - "file": "variables.yaml", - "rule_name": "vars-rule", - "rule_tag": "mo", - "rule_key": "vars-rule|mo", - "issue_line_en": 4, - "issue_line_tr": 4, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "variables", - "description": "Variable definitions differ", - "english_snippet": "v1", - "translated_snippet": "v2", - "untranslated_texts": [], - "_explanation": "rule_difference variables.yaml vars-rule|mo" - }, - { - "language": "de", - "file": "variables_none.yaml", - "rule_name": "vars-none", - "rule_tag": "mo", - "rule_key": "vars-none|mo", - "issue_line_en": 4, - "issue_line_tr": 1, - "rule_line_en": 1, - "rule_line_tr": 1, - "issue_type": "rule_difference", - "diff_type": "variables", - "description": "Variable definitions differ", - "english_snippet": "v1", - "translated_snippet": "(none)", - "untranslated_texts": [], - "_explanation": "rule_difference variables_none.yaml vars-none|mo" - } -] diff --git a/PythonScripts/audit_translations/tests/test_auditor.py b/PythonScripts/audit_translations/tests/test_auditor.py index 34ccbe48..0909182d 100644 --- a/PythonScripts/audit_translations/tests/test_auditor.py +++ b/PythonScripts/audit_translations/tests/test_auditor.py @@ -7,8 +7,9 @@ import pytest from ..auditor import compare_files, console, get_yaml_files, list_languages -from ..dataclasses import ComparisonResult, RuleDifference, RuleInfo -from ..renderer import collect_issues, print_warnings +from ..dataclasses import ComparisonResult, DiffType, RuleDifference, RuleInfo +from ..line_resolver import resolve_diff_lines +from ..renderer import print_warnings @pytest.fixture() @@ -30,13 +31,54 @@ def make_rule(name: str, tag: str, line: int, raw: str) -> RuleInfo: ) -def test_collect_issues_fields() -> None: - """Ensure collect issues fields.""" +def resolved_diff_lines_by_type(result: ComparisonResult) -> dict[str, list[tuple[int | None, int | None]]]: + lines_by_type: dict[str, list[tuple[int | None, int | None]]] = {} + for diff in result.rule_differences: + lines = resolve_diff_lines(diff) + if lines is None: + continue + lines_by_type.setdefault(diff.diff_type.value, []).append(lines) + return lines_by_type + + +def fixture_rules_dir() -> Path: + return Path(__file__).resolve().parent / "fixtures" / "Rules" / "Languages" + + +def aggregate_issue_counts( + language: str, + issue_filter: set[str] | None = None, +) -> tuple[int, int, int, int, int]: + rules_dir = fixture_rules_dir() + english_dir = rules_dir / "en" + translated_dir = rules_dir / language + files = get_yaml_files(english_dir) + + missing = untranslated = extra = diffs = total = 0 + for file_name in files: + result = compare_files( + str(english_dir / file_name), + str(translated_dir / file_name), + issue_filter, + ) + missing += len(result.missing_rules) + untranslated += sum(len(entries) for _, entries in result.untranslated_text) + extra += len(result.extra_rules) + diffs += len(result.rule_differences) + total += len(result.missing_rules) + len(result.extra_rules) + len(result.rule_differences) + total += sum(len(entries) for _, entries in result.untranslated_text) + return missing, untranslated, extra, diffs, total + + +def test_comparison_result_object_fields() -> None: + """Ensure comparison objects keep expected field-level values.""" missing = make_rule("missing", "mo", 10, "missing raw") extra = make_rule("extra", "mi", 20, "extra raw") untranslated = make_rule("untranslated", "mn", 30, "untranslated raw") diff_en = make_rule("diff", "mrow", 40, "diff en raw") diff_tr = make_rule("diff", "mrow", 41, "diff tr raw") + diff_en.line_map = {"match": [40]} + diff_tr.line_map = {"match": [41]} diff = RuleDifference( english_rule=diff_en, @@ -46,7 +88,6 @@ def test_collect_issues_fields() -> None: english_snippet="a", translated_snippet="b", ) - result = ComparisonResult( missing_rules=[missing], extra_rules=[extra], @@ -57,32 +98,38 @@ def test_collect_issues_fields() -> None: translated_rule_count=1, ) - issues = collect_issues(result, "file.yaml", "xx") - by_type = {issue["issue_type"]: issue for issue in issues} + assert result.missing_rules[0].line_number == 10 + assert result.extra_rules[0].line_number == 20 + assert result.untranslated_text[0][0].line_number == 30 + assert result.untranslated_text[0][1] == [("t", "x", 31)] + assert result.rule_differences[0].diff_type is DiffType.MATCH + assert resolve_diff_lines(result.rule_differences[0]) == (40, 41) + - assert by_type["missing_rule"]["issue_line_en"] == 10 - assert by_type["missing_rule"]["issue_line_tr"] is None - assert by_type["missing_rule"]["rule_line_en"] == 10 - assert by_type["missing_rule"]["rule_line_tr"] is None - assert "english_raw" not in by_type["missing_rule"] +def test_compare_files_fixture_issue_counts_match_expected() -> None: + """ + Ensure object-level findings on fixture rules match expected totals. - assert by_type["extra_rule"]["issue_line_tr"] == 20 - assert by_type["extra_rule"]["rule_line_tr"] == 20 - assert "translated_raw" not in by_type["extra_rule"] + Replaces removed JSONL count coverage with direct ComparisonResult checks. + """ + missing, untranslated, extra, diffs, total = aggregate_issue_counts("es") + assert total == 19 + assert missing == 4 + assert extra == 3 + assert untranslated == 6 + assert diffs == 6 - assert by_type["untranslated_text"]["untranslated_texts"] == ["x"] - assert by_type["untranslated_text"]["issue_line_tr"] == 31 - assert by_type["untranslated_text"]["rule_line_tr"] == 30 - assert "translated_raw" not in by_type["untranslated_text"] - assert by_type["rule_difference"]["diff_type"] == "match" - assert by_type["rule_difference"]["english_snippet"] == "a" - assert by_type["rule_difference"]["translated_snippet"] == "b" - assert by_type["rule_difference"]["issue_line_en"] == 40 - assert by_type["rule_difference"]["issue_line_tr"] == 41 - assert by_type["rule_difference"]["rule_line_en"] == 40 - assert by_type["rule_difference"]["rule_line_tr"] == 41 - assert "english_raw" not in by_type["rule_difference"] +def test_compare_files_fixture_filter_missing_extra_matches_expected() -> None: + """ + Ensure object-level filtering mirrors the old missing/extra JSONL scenario. + """ + missing, untranslated, extra, diffs, total = aggregate_issue_counts("es", {"missing", "extra"}) + assert total == 7 + assert missing == 4 + assert extra == 3 + assert untranslated == 0 + assert diffs == 0 def test_compare_files_merges_region_rules(tmp_path) -> None: @@ -186,7 +233,6 @@ def test_compare_files_skips_untranslated_and_diffs_when_audit_ignored(tmp_path) assert result.extra_rules == [] assert result.untranslated_text == [] assert result.rule_differences == [] - assert collect_issues(result, "de.yaml", "de") == [] def test_get_yaml_files_includes_region(tmp_path) -> None: @@ -321,18 +367,10 @@ def test_misaligned_structure_differences_are_reported() -> None: assert len(result.rule_differences) > 0 assert any(diff.diff_type == "structure" for diff in result.rule_differences) - # Collecting issues should include a structure issue. - issues = collect_issues(result, "structure_misaligned.yaml", "de") - structure_issues = [i for i in issues if i["diff_type"] == "structure"] - - assert len(structure_issues) == 1 - issue = structure_issues[0] - assert issue["issue_line_en"] == 11 - assert issue["issue_line_tr"] == 11 - - # Other differences (like conditions) should still be reported - condition_issues = [i for i in issues if i["diff_type"] == "condition"] - assert len(condition_issues) > 0, "Expected condition differences to still be reported" + lines_by_type = resolved_diff_lines_by_type(result) + assert len(lines_by_type.get("structure", [])) == 1 + assert lines_by_type["structure"][0] == (11, 11) + assert len(lines_by_type.get("condition", [])) > 0, "Expected condition differences to still be reported" def test_missing_else_block_is_still_reported() -> None: @@ -355,20 +393,9 @@ def test_missing_else_block_is_still_reported() -> None: structure_diffs = [diff for diff in result.rule_differences if diff.diff_type == "structure"] assert len(structure_diffs) == 1 - # This case has one token None (missing else), so it should still be reported - issues = collect_issues(result, "structure_missing_else.yaml", "de") - structure_issues = [i for i in issues if i["diff_type"] == "structure"] - - # CRITICAL: This legitimate difference should still be reported - # One file has else:, the other doesn't - a clear missing element - assert len(structure_issues) == 1, ( - f"Expected missing else block to be reported, but found {len(structure_issues)} structure issues" - ) - - # Verify the issue anchors to the last shared structure token ('then:') - issue = structure_issues[0] - assert issue["issue_line_en"] == 7 - assert issue["issue_line_tr"] == 7 + lines_by_type = resolved_diff_lines_by_type(result) + assert len(lines_by_type.get("structure", [])) == 1, "Expected missing else block to be reported" + assert lines_by_type["structure"][0] == (7, 7) def test_structure_diff_uses_position_aware_token_occurrence_for_missing_block(tmp_path) -> None: @@ -408,13 +435,9 @@ def test_structure_diff_uses_position_aware_token_occurrence_for_missing_block(t ) result = compare_files(str(english_file), str(translated_file)) - issues = collect_issues(result, "repeated-structure.yaml", "tr") - structure_issues = [i for i in issues if i["diff_type"] == "structure"] - - assert len(structure_issues) == 1 - issue = structure_issues[0] - assert issue["issue_line_en"] == 7 - assert issue["issue_line_tr"] == 7 + lines_by_type = resolved_diff_lines_by_type(result) + assert len(lines_by_type.get("structure", [])) == 1 + assert lines_by_type["structure"][0] == (7, 7) def test_structure_substitution_diff_is_reported(tmp_path) -> None: @@ -449,12 +472,9 @@ def test_structure_substitution_diff_is_reported(tmp_path) -> None: result = compare_files(str(english_file), str(translated_file)) assert any(diff.diff_type == "structure" for diff in result.rule_differences) - issues = collect_issues(result, "substitution-structure.yaml", "tr") - structure_issues = [i for i in issues if i["diff_type"] == "structure"] - assert len(structure_issues) == 1 - issue = structure_issues[0] - assert issue["issue_line_en"] == 7 - assert issue["issue_line_tr"] == 7 + lines_by_type = resolved_diff_lines_by_type(result) + assert len(lines_by_type.get("structure", [])) == 1 + assert lines_by_type["structure"][0] == (7, 7) def test_structure_per_fraction_should_anchor_to_replace_lines_expected_behavior() -> None: @@ -469,13 +489,9 @@ def test_structure_per_fraction_should_anchor_to_replace_lines_expected_behavior path = base_dir / "fixtures" / "repro" result = compare_files(str(path / "en" / "per_fraction.yaml"), str(path / "nb" / "per_fraction.yaml")) - issues = collect_issues(result, "per_fraction.yaml", "nb") - structure_issues = [i for i in issues if i["diff_type"] == "structure"] - - assert len(structure_issues) == 1 - issue = structure_issues[0] - assert issue["issue_line_en"] == 8 - assert issue["issue_line_tr"] == 8 + lines_by_type = resolved_diff_lines_by_type(result) + assert len(lines_by_type.get("structure", [])) == 1 + assert lines_by_type["structure"][0] == (8, 8) def test_print_warnings_shows_misaligned_structures() -> None: diff --git a/PythonScripts/audit_translations/tests/test_cli_end_to_end.py b/PythonScripts/audit_translations/tests/test_cli_end_to_end.py index 5fa6f137..3c9a10fc 100644 --- a/PythonScripts/audit_translations/tests/test_cli_end_to_end.py +++ b/PythonScripts/audit_translations/tests/test_cli_end_to_end.py @@ -4,11 +4,9 @@ from __future__ import annotations -import json import os import subprocess import sys -from collections import Counter from pathlib import Path import pytest @@ -21,75 +19,6 @@ def fixture_rules_dir() -> Path: return Path(__file__).resolve().parent / "fixtures" / "Rules" / "Languages" -def parse_jsonl(output: str) -> list[dict]: - return [json.loads(line) for line in output.splitlines() if line.strip()] - - -def assert_issue_counts(issues: list[dict]) -> None: - counts = Counter(issue["issue_type"] for issue in issues) - assert len(issues) == 19 - assert counts["missing_rule"] == 4 - assert counts["extra_rule"] == 3 - assert counts["untranslated_text"] == 6 - assert counts["rule_difference"] == 6 - - -def test_cli_main_jsonl_output_matches_fixture(capsys, monkeypatch) -> None: - """ - Exercise the CLI entrypoint in-process by patching sys.argv. - - This validates argparse wiring and output formatting without spawning a new process. - """ - args = ["es", "--format", "jsonl", "--rules-dir", str(fixture_rules_dir())] - - monkeypatch.setattr(sys, "argv", ["audit_translations", *args]) - audit_cli.main() - in_process_output = capsys.readouterr().out - assert_issue_counts(parse_jsonl(in_process_output)) - - -def test_cli_module_jsonl_output_matches_fixture() -> None: - """ - Exercise the CLI via python -m audit_translations in a subprocess. - - This validates module execution, environment wiring, and exit behavior. - """ - args = ["es", "--format", "jsonl", "--rules-dir", str(fixture_rules_dir())] - - python_scripts_dir = Path(__file__).resolve().parents[2] - env = os.environ.copy() - env["PYTHONPATH"] = os.pathsep.join([str(python_scripts_dir), env.get("PYTHONPATH", "")]).strip(os.pathsep) - - result = subprocess.run( - [sys.executable, "-m", "audit_translations", *args], - capture_output=True, - text=True, - cwd=str(python_scripts_dir), - env=env, - check=True, - ) - assert_issue_counts(parse_jsonl(result.stdout)) - - -def test_cli_main_jsonl_only_filters_issue_types(capsys, monkeypatch) -> None: - """ - Ensure --only limits JSONL output to the requested categories. - - Uses in-process CLI invocation so argparse parsing and filter plumbing - are both exercised without subprocess overhead. - """ - args = ["es", "--format", "jsonl", "--rules-dir", str(fixture_rules_dir()), "--only", "missing,extra"] - - monkeypatch.setattr(sys, "argv", ["audit_translations", *args]) - audit_cli.main() - issues = parse_jsonl(capsys.readouterr().out) - - counts = Counter(issue["issue_type"] for issue in issues) - assert set(counts) == {"missing_rule", "extra_rule"} - assert counts["missing_rule"] == 4 - assert counts["extra_rule"] == 3 - - def test_cli_main_rich_only_filters_issue_groups(capsys, monkeypatch) -> None: """ Ensure --only also filters visible rich subgroup sections. diff --git a/PythonScripts/audit_translations/tests/test_output_jsonl.py b/PythonScripts/audit_translations/tests/test_output_jsonl.py deleted file mode 100644 index a3baa3d4..00000000 --- a/PythonScripts/audit_translations/tests/test_output_jsonl.py +++ /dev/null @@ -1,45 +0,0 @@ -"""JSONL output snapshot tests.""" - -import json -from io import StringIO -from pathlib import Path - -from ..auditor import compare_files -from ..renderer import collect_issues - - -def load_jsonl(text: str) -> list[dict]: - return [json.loads(line) for line in text.splitlines() if line.strip()] - - -def load_json_array(path: Path) -> list[dict]: - items = json.loads(path.read_text(encoding="utf-8")) - for item in items: - item.pop("_explanation", None) # only for humans - return items - - -def test_jsonl_output_matches_golden(): - """Ensure jsonl output matches golden.""" - base_dir = Path(__file__).parent - fixtures_dir = base_dir / "fixtures" - english_dir = fixtures_dir / "en" - translated_dir = fixtures_dir / "de" - files = sorted(path.name for path in english_dir.glob("*.yaml")) - - stream = StringIO() - - for file_name in files: - result = compare_files( - str(english_dir / file_name), - str(translated_dir / file_name), - ) - for issue in collect_issues(result, file_name, "de"): - stream.write(json.dumps(issue, ensure_ascii=False) + "\n") - - actual = load_jsonl(stream.getvalue()) - - golden_path = base_dir / "golden" / "jsonl" / "de.json" - expected = load_json_array(golden_path) - - assert actual == expected diff --git a/PythonScripts/audit_translations/tests/test_output_objects.py b/PythonScripts/audit_translations/tests/test_output_objects.py new file mode 100644 index 00000000..316bff06 --- /dev/null +++ b/PythonScripts/audit_translations/tests/test_output_objects.py @@ -0,0 +1,74 @@ +"""Object-level golden tests for audit findings.""" + +from pathlib import Path + +from ..auditor import compare_files +from ..line_resolver import resolve_diff_lines + + +def collect_issue_tuples(language: str = "de", issue_filter: set[str] | None = None) -> list[tuple]: + base_dir = Path(__file__).parent + fixtures_dir = base_dir / "fixtures" + english_dir = fixtures_dir / "en" + translated_dir = fixtures_dir / language + rows: list[tuple] = [] + + for english_path in sorted(english_dir.glob("*.yaml")): + file_name = english_path.name + result = compare_files( + str(english_path), + str(translated_dir / file_name), + issue_filter, + ) + + for rule in result.missing_rules: + rows.append((file_name, "missing_rule", rule.key, "", rule.line_number, None, "")) + + for rule in result.extra_rules: + rows.append((file_name, "extra_rule", rule.key, "", None, rule.line_number, "")) + + for rule, entries in result.untranslated_text: + for _key, text, line in entries: + rows.append((file_name, "untranslated_text", rule.key, "", None, line or rule.line_number, text)) + + for diff in result.rule_differences: + lines = resolve_diff_lines(diff) + if lines is None: + continue + line_en, line_tr = lines + rows.append((file_name, "rule_difference", diff.english_rule.key, diff.diff_type.value, line_en, line_tr, "")) + + return rows + + +def test_object_findings_match_golden_fixture() -> None: + expected = [ + ("basic.yaml", "missing_rule", "rule-2|mn", "", 7, None, ""), + ("basic.yaml", "extra_rule", "rule-3|mo", "", None, 7, ""), + ("basic.yaml", "untranslated_text", "rule-1|mo", "", None, 5, "equals"), + ("condition_none.yaml", "rule_difference", "condition-none|mi", "condition", 6, 6, ""), + ("match.yaml", "rule_difference", "match-rule|mo", "match", 3, 3, ""), + ("structure.yaml", "rule_difference", "cond-rule|mi", "condition", 6, 6, ""), + ("structure_diff.yaml", "rule_difference", "struct-rule|mi", "structure", 7, 7, ""), + ("structure_empty.yaml", "rule_difference", "struct-empty|mi", "structure", 4, 1, ""), + ("structure_misaligned.yaml", "rule_difference", "misaligned-structure|root", "condition", 6, 6, ""), + ("structure_misaligned.yaml", "rule_difference", "misaligned-structure|root", "structure", 11, 11, ""), + ("structure_missing_else.yaml", "rule_difference", "missing-else-block|root", "structure", 7, 7, ""), + ("unicode.yaml", "missing_rule", "b", "", 3, None, ""), + ("unicode.yaml", "extra_rule", "c", "", None, 5, ""), + ("unicode.yaml", "untranslated_text", "a", "", None, 2, "a"), + ("variables.yaml", "rule_difference", "vars-rule|mo", "variables", 4, 4, ""), + ("variables_none.yaml", "rule_difference", "vars-none|mo", "variables", 4, 1, ""), + ] + + assert collect_issue_tuples("de") == expected + + +def test_object_findings_filter_missing_extra_only() -> None: + expected = [ + ("basic.yaml", "missing_rule", "rule-2|mn", "", 7, None, ""), + ("basic.yaml", "extra_rule", "rule-3|mo", "", None, 7, ""), + ("unicode.yaml", "missing_rule", "b", "", 3, None, ""), + ("unicode.yaml", "extra_rule", "c", "", None, 5, ""), + ] + assert collect_issue_tuples("de", {"missing", "extra"}) == expected From 281976cf41d6d2b129917d5a4789054a82893c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Thu, 5 Mar 2026 02:23:55 +0100 Subject: [PATCH 7/8] simplify _get_line_map_lines --- .../audit_translations/line_resolver.py | 16 ++++++---------- .../audit_translations/tests/test_parsers.py | 2 -- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/PythonScripts/audit_translations/line_resolver.py b/PythonScripts/audit_translations/line_resolver.py index 5d8a4076..df867fd6 100644 --- a/PythonScripts/audit_translations/line_resolver.py +++ b/PythonScripts/audit_translations/line_resolver.py @@ -4,18 +4,14 @@ Maps rule diff types and structure tokens to precise YAML source line numbers. """ -from .dataclasses import RuleDifference, RuleInfo +from .dataclasses import DiffType, RuleDifference, RuleInfo from .parsers import extract_structure_elements -def _get_line_map_lines(rule: RuleInfo, kind: str, token: str | None = None) -> list[int]: +def _get_line_map_lines(rule: RuleInfo, kind: DiffType | str, token: str | None = None) -> list[int]: """Return the line-number list for a given element kind from the rule's line map.""" - if kind == "match": - return rule.line_map.get("match", []) - if kind == "condition": - return rule.line_map.get("condition", []) - if kind == "variables": - return rule.line_map.get("variables", []) + if kind in ("match", "condition", "variables"): + return rule.line_map.get(kind, []) if kind == "structure" and token: return rule.line_map.get(f"structure:{token.rstrip(':')}", []) return [] @@ -44,7 +40,7 @@ def first_structure_mismatch( def resolve_issue_line_at_position( rule: RuleInfo, - kind: str, + kind: DiffType | str, token: str | None = None, position: int = 0, ) -> int | None: @@ -64,7 +60,7 @@ def resolve_issue_line_at_position( return lines[position] if position < len(lines) else None -def resolve_issue_line(rule: RuleInfo, kind: str, token: str | None = None) -> int | None: +def resolve_issue_line(rule: RuleInfo, kind: DiffType | str, token: str | None = None) -> int | None: """ Resolve the line number for an issue within a rule. diff --git a/PythonScripts/audit_translations/tests/test_parsers.py b/PythonScripts/audit_translations/tests/test_parsers.py index ca848c86..2463209a 100644 --- a/PythonScripts/audit_translations/tests/test_parsers.py +++ b/PythonScripts/audit_translations/tests/test_parsers.py @@ -2,7 +2,6 @@ Tests for parsers.py. """ - import pytest from ruamel.yaml import YAML from ruamel.yaml.scanner import ScannerError @@ -305,7 +304,6 @@ def test_parses_multiple_entries(self): rules = parse_unicode_file(content, data) assert len(rules) == 2 - def test_returns_empty_for_non_list_data(self): """Non-list YAML data returns no rules.""" rules = parse_unicode_file("key: value", {"key": "value"}) From d7a48f15da6a9d44afab8ea37ac1e6ba344fb636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gro=C3=9F?= Date: Thu, 5 Mar 2026 02:31:59 +0100 Subject: [PATCH 8/8] add ruff to CI --- .github/workflows/python.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index a89265c7..2562deb0 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -9,6 +9,12 @@ jobs: steps: - uses: actions/checkout@v3 - uses: astral-sh/setup-uv@v6 + - name: Run ruff lint + working-directory: PythonScripts + run: uv run ruff check audit_translations/ + - name: Run ruff format check + working-directory: PythonScripts + run: uv run ruff format --check audit_translations/ - name: Run tests working-directory: PythonScripts run: uv run pytest