diff --git a/src/newsdom_api/synthetic.py b/src/newsdom_api/synthetic.py index 8be4474a..bb41ae70 100644 --- a/src/newsdom_api/synthetic.py +++ b/src/newsdom_api/synthetic.py @@ -1,4 +1,7 @@ -"""Synthetic newspaper fixture generation for redistributable repository tests.""" +"""Synthetic newspaper fixture generation for redistributable repository tests. + +Provides programmatic tools to generate reliable deterministic fixture images, pdfs and truth files. +""" from __future__ import annotations @@ -16,7 +19,10 @@ def _font_candidates() -> list[str]: - """Return preferred macOS Japanese font candidates for fixture rendering.""" + """Return preferred macOS Japanese font candidates for fixture rendering. + + These fonts are used as fallback options to support rendering Japanese text correctly in synthetic images. + """ return [ "/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc", @@ -26,7 +32,10 @@ def _font_candidates() -> list[str]: def _load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: - """Load the first available Japanese-capable font at the requested size.""" + """Load the first available Japanese-capable font at the requested size. + + Checks availability of the fonts from the candidates list, and loads the first found font. Defaults to standard font. + """ for candidate in _font_candidates(): if Path(candidate).exists(): @@ -41,7 +50,10 @@ def _safe_draw_text( font: ImageFont.FreeTypeFont | ImageFont.ImageFont, fill: int | str = "black", ) -> None: - """Draw text with a fallback mechanism to prevent UnicodeEncodeError on default fonts.""" + """Draw text with a fallback mechanism to prevent UnicodeEncodeError on default fonts. + + Falls back to replacing unsupported unicode characters with question marks if standard drawing fails. + """ try: draw.text(xy, text, fill=fill, font=font) except UnicodeEncodeError: @@ -58,7 +70,10 @@ def _draw_vertical_text( font: ImageFont.FreeTypeFont | ImageFont.ImageFont, line_height: int, ) -> None: - """Render a string as simple top-to-bottom vertical glyph placement.""" + """Render a string as simple top-to-bottom vertical glyph placement. + + Draws characters one by one with a vertical offset calculated by line_height. + """ cursor_y = y for char in text: @@ -67,7 +82,10 @@ def _draw_vertical_text( def _split_vertical(text: str, max_chars: int) -> list[str]: - """Split text into vertical columns constrained by the page height budget.""" + """Split text into vertical columns constrained by the page height budget. + + The output list of strings can be mapped to individual vertical columns to fit within a given height limit. + """ return [text[idx : idx + max_chars] for idx in range(0, len(text), max_chars)] @@ -78,7 +96,10 @@ def _draw_vertical_columns( text: str, font: ImageFont.FreeTypeFont | ImageFont.ImageFont, ) -> None: - """Render multiple vertical columns of text inside the supplied bounding box.""" + """Render multiple vertical columns of text inside the supplied bounding box. + + The text content is first split into fitting columns, and each column is sequentially rendered. + """ x0, y0, x1, y1 = bbox font_size = int(getattr(font, "size", 24)) @@ -101,7 +122,10 @@ def _article_block( vertical: bool = True, page_number: int = 1, ) -> dict: - """Create one synthetic article descriptor for the fixture ground truth.""" + """Create one synthetic article descriptor for the fixture ground truth. + + The synthetic descriptor is primarily used to build validation metrics for parsing tasks. + """ return { "headline": headline, @@ -113,7 +137,10 @@ def _article_block( def _ground_truth() -> dict: - """Return the deterministic article/image/ad structure used for the fixture.""" + """Return the deterministic article/image/ad structure used for the fixture. + + The output includes predefined structure configuration blocks such as texts, layouts, and page parameters. + """ return { "page_size": [PAGE_WIDTH, PAGE_HEIGHT], @@ -160,7 +187,10 @@ def _ground_truth() -> dict: def generate_fixture(output_dir: Path, seed: int = 7) -> tuple[Path, Path]: - """Generate a synthetic scanned-newspaper PDF fixture and ground-truth JSON.""" + """Generate a synthetic scanned-newspaper PDF fixture and ground-truth JSON. + + A deterministic PDF and JSON structure configuration will be stored inside the target `output_dir` parameter. + """ resolved_dir = output_dir.resolve() try: diff --git a/tests/test_tools_export_csv.py b/tests/test_tools_export_csv.py index 2f066afe..3002b45a 100644 --- a/tests/test_tools_export_csv.py +++ b/tests/test_tools_export_csv.py @@ -119,3 +119,16 @@ def test_export_csv_cli_invalid_file( assert exc_info.value.code == 1 captured = capsys.readouterr() assert "Error exporting CSV:" in captured.err + + +def test_module_main(monkeypatch: pytest.MonkeyPatch) -> None: + import runpy + import sys + from unittest.mock import patch + + monkeypatch.delitem(sys.modules, "tools.export_csv", raising=False) + with patch("sys.argv", ["tools/export_csv.py", "-h"]): + with pytest.raises(SystemExit) as excinfo: + runpy.run_module("tools.export_csv", run_name="__main__") + + assert excinfo.value.code == 0 diff --git a/tests/test_tools_export_html.py b/tests/test_tools_export_html.py index 37445cdb..c7cda092 100644 --- a/tests/test_tools_export_html.py +++ b/tests/test_tools_export_html.py @@ -176,3 +176,16 @@ def test_main_file_output_error( assert excinfo.value.code == 1 assert "Error exporting HTML" in capsys.readouterr().err + + +def test_module_main(monkeypatch: pytest.MonkeyPatch) -> None: + import runpy + import sys + from unittest.mock import patch + + monkeypatch.delitem(sys.modules, "tools.export_html", raising=False) + with patch("sys.argv", ["tools/export_html.py", "-h"]): + with pytest.raises(SystemExit) as excinfo: + runpy.run_module("tools.export_html", run_name="__main__") + + assert excinfo.value.code == 0 diff --git a/tests/test_tools_export_markdown.py b/tests/test_tools_export_markdown.py index a076fcfa..5369ed41 100644 --- a/tests/test_tools_export_markdown.py +++ b/tests/test_tools_export_markdown.py @@ -146,3 +146,16 @@ def test_main_file_output_error(tmp_path, sample_json_data, capsys): assert excinfo.value.code == 1 assert "Error exporting Markdown" in capsys.readouterr().err + + +def test_module_main(monkeypatch: pytest.MonkeyPatch) -> None: + import runpy + import sys + from unittest.mock import patch + + monkeypatch.delitem(sys.modules, "tools.export_markdown", raising=False) + with patch("sys.argv", ["tools/export_markdown.py", "-h"]): + with pytest.raises(SystemExit) as excinfo: + runpy.run_module("tools.export_markdown", run_name="__main__") + + assert excinfo.value.code == 0 diff --git a/tests/test_tools_extract_text.py b/tests/test_tools_extract_text.py index 50776865..3441fcb2 100644 --- a/tests/test_tools_extract_text.py +++ b/tests/test_tools_extract_text.py @@ -94,3 +94,16 @@ def test_extract_text_wrong_ext(tmp_path, capsys): extract_text.main([str(txt)]) assert e.value.code == 1 assert "must be a .json file" in capsys.readouterr().err + + +def test_module_main(monkeypatch: pytest.MonkeyPatch) -> None: + import runpy + import sys + from unittest.mock import patch + + monkeypatch.delitem(sys.modules, "tools.extract_text", raising=False) + with patch("sys.argv", ["tools/extract_text.py", "-h"]): + with pytest.raises(SystemExit) as excinfo: + runpy.run_module("tools.extract_text", run_name="__main__") + + assert excinfo.value.code == 0 diff --git a/tools/export_csv.py b/tools/export_csv.py index 3f668da2..52b785f9 100644 --- a/tools/export_csv.py +++ b/tools/export_csv.py @@ -90,5 +90,5 @@ def main(argv: list[str] | None = None) -> None: sys.exit(1) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": main() diff --git a/tools/export_html.py b/tools/export_html.py index 2d55b9a0..bb20f35a 100644 --- a/tools/export_html.py +++ b/tools/export_html.py @@ -161,5 +161,5 @@ def main(argv: list[str] | None = None) -> None: sys.exit(1) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": main() diff --git a/tools/export_markdown.py b/tools/export_markdown.py index 26965272..6eed7385 100644 --- a/tools/export_markdown.py +++ b/tools/export_markdown.py @@ -115,5 +115,5 @@ def main(argv: list[str] | None = None) -> None: sys.exit(1) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": main() diff --git a/tools/extract_text.py b/tools/extract_text.py index 48f0c621..6bca444d 100644 --- a/tools/extract_text.py +++ b/tools/extract_text.py @@ -80,5 +80,5 @@ def main(argv: list[str] | None = None) -> None: sys.exit(1) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": main()