From ceb037d89112b05eeab1738382b9f5393c081c4c Mon Sep 17 00:00:00 2001 From: Shane Hutchins Date: Thu, 9 Apr 2026 00:33:11 -0400 Subject: [PATCH 1/3] Improve PPTX converter and fix lint/type errors repo-wide - Aggregate PPTX warnings by type instead of per-shape for cleaner output - Add image alt text from shape name in PPTX converter - Preserve bold/italic formatting on hyperlinks in PPTX converter - Add ty.toml config to handle third-party stub false positives - Fix type annotations in source.py and test helper functions - Fix import sorting and unused variable lint errors across test files - Remove error-suppressing prefix from typecheck Makefile target --- Makefile | 2 +- src/leafpress/importer/converter_pptx.py | 47 ++++++++----- src/leafpress/source.py | 2 +- tests/test_cli.py | 3 +- tests/test_docx_renderer.py | 1 + tests/test_docx_styles.py | 2 +- tests/test_html_styles.py | 2 +- tests/test_import_pptx.py | 84 +++++++++++++++++++----- tests/test_markdown_renderer.py | 2 +- tests/test_odt_renderer.py | 1 + tests/test_pdf_renderer.py | 1 + tests/test_pdf_styles.py | 4 +- tests/test_pipeline_monorepo.py | 5 +- ty.toml | 17 +++++ uv.lock | 2 +- 15 files changed, 128 insertions(+), 47 deletions(-) create mode 100644 ty.toml diff --git a/Makefile b/Makefile index be67a47..0d1da53 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ format: uv run ruff format src/ typecheck: - -uv run ty check + uv run ty check setup-hooks: git config core.hooksPath .githooks diff --git a/src/leafpress/importer/converter_pptx.py b/src/leafpress/importer/converter_pptx.py index 8cedfe0..9868d14 100644 --- a/src/leafpress/importer/converter_pptx.py +++ b/src/leafpress/importer/converter_pptx.py @@ -2,7 +2,7 @@ from __future__ import annotations -import re +from collections import Counter from pathlib import Path from pptx import Presentation @@ -71,12 +71,15 @@ def import_pptx( except Exception as e: raise PptxImportError(f"Failed to open PPTX: {e}") from e - warnings: list[str] = [] + skipped_counts: Counter[str] = Counter() sections: list[str] = [] for slide_num, slide in enumerate(prs.slides, start=1): - slide_md = _convert_slide(slide, slide_num, image_handler, include_notes, warnings) + slide_md = _convert_slide( + slide, slide_num, image_handler, include_notes, skipped_counts + ) sections.append(slide_md) + warnings = _aggregate_warnings(skipped_counts) markdown = "\n\n".join(sections) markdown = postprocess_markdown(markdown) @@ -91,12 +94,21 @@ def import_pptx( ) +def _aggregate_warnings(skipped_counts: Counter[str]) -> list[str]: + """Build aggregated warning messages from per-type skip counts.""" + warnings: list[str] = [] + for label, count in sorted(skipped_counts.items()): + plural = "s" if count > 1 else "" + warnings.append(f"Presentation has {count} unsupported {label}{plural} — skipped") + return warnings + + def _convert_slide( slide, slide_num: int, image_handler: ImageHandler | None, include_notes: bool, - warnings: list[str], + skipped_counts: Counter[str], ) -> str: """Convert a single slide to markdown.""" parts: list[str] = [] @@ -115,7 +127,7 @@ def _convert_slide( for shape in slide.shapes: if shape is title_shape: continue - shape_md = _convert_shape(shape, image_handler, slide_label, warnings) + shape_md = _convert_shape(shape, image_handler, slide_label, skipped_counts) if shape_md: parts.append(shape_md) @@ -140,11 +152,11 @@ def _convert_shape( shape, image_handler: ImageHandler | None, slide_label: str, - warnings: list[str], + skipped_counts: Counter[str], ) -> str: """Convert a single shape to markdown.""" if shape.shape_type == MSO_SHAPE_TYPE.GROUP: - return _convert_group(shape, image_handler, slide_label, warnings) + return _convert_group(shape, image_handler, slide_label, skipped_counts) if shape.shape_type == MSO_SHAPE_TYPE.TABLE: return _table_to_markdown(shape.table) @@ -156,12 +168,11 @@ def _convert_shape( return _text_frame_to_markdown(shape.text_frame) # Shapes with no text, table, image, or group content are skipped. - # Warn for shape types that may contain meaningful content. + # Count per type for aggregated warnings. shape_type = shape.shape_type if shape_type in _WARN_SHAPE_TYPES: label = _WARN_SHAPE_TYPES[shape_type] - name = shape.name or "unnamed" - warnings.append(f"Unsupported {label} '{name}' on slide '{slide_label}' — skipped") + skipped_counts[label] += 1 return "" @@ -170,12 +181,12 @@ def _convert_group( group_shape, image_handler: ImageHandler | None, slide_label: str, - warnings: list[str], + skipped_counts: Counter[str], ) -> str: """Recursively convert shapes inside a group.""" parts: list[str] = [] for shape in group_shape.shapes: - md = _convert_shape(shape, image_handler, slide_label, warnings) + md = _convert_shape(shape, image_handler, slide_label, skipped_counts) if md: parts.append(md) return "\n\n".join(parts) @@ -187,7 +198,8 @@ def _convert_image(shape, image_handler: ImageHandler) -> str: image_bytes = image.blob content_type = image.content_type src = image_handler.save_image(image_bytes, content_type) - return f"![]({src})" + alt = shape.name or "" + return f"![{alt}]({src})" def _text_frame_to_markdown(text_frame) -> str: @@ -214,15 +226,14 @@ def _runs_to_markdown(paragraph) -> str: if not text: continue - # Apply formatting + # Apply hyperlink first, then wrap with formatting so + # bold/italic are preserved: **[text](url)** instead of lost. + if run.hyperlink and run.hyperlink.address: + text = f"[{text}]({run.hyperlink.address})" if run.font.bold: text = f"**{text}**" if run.font.italic: text = f"*{text}*" - if run.hyperlink and run.hyperlink.address: - # Strip formatting wrappers to put inside link text - display = re.sub(r"^\*{1,2}(.*?)\*{1,2}$", r"\1", text) - text = f"[{display}]({run.hyperlink.address})" parts.append(text) return "".join(parts) diff --git a/src/leafpress/source.py b/src/leafpress/source.py index 7bf361f..1026446 100644 --- a/src/leafpress/source.py +++ b/src/leafpress/source.py @@ -60,7 +60,7 @@ def resolve_source( def _clone_repo(url: str, branch: str | None) -> Path: """Clone a git repo to a temporary directory.""" tmp_dir = Path(tempfile.mkdtemp(prefix="leafpress_")) - clone_kwargs: dict[str, object] = {"depth": 50} + clone_kwargs: dict[str, str | int] = {"depth": 50} if branch: clone_kwargs["branch"] = branch diff --git a/tests/test_cli.py b/tests/test_cli.py index 96dc899..42b6e4b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,10 +3,11 @@ from pathlib import Path from docx import Document as DocxDocument -from leafpress.cli import cli from pptx import Presentation from typer.testing import CliRunner +from leafpress.cli import cli + runner = CliRunner() diff --git a/tests/test_docx_renderer.py b/tests/test_docx_renderer.py index c5ef643..9d3dffd 100644 --- a/tests/test_docx_renderer.py +++ b/tests/test_docx_renderer.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest + from leafpress.config import WatermarkConfig, load_config from leafpress.docx.renderer import DocxRenderer from leafpress.git_info import extract_git_info diff --git a/tests/test_docx_styles.py b/tests/test_docx_styles.py index af8f4c2..9b89e7b 100644 --- a/tests/test_docx_styles.py +++ b/tests/test_docx_styles.py @@ -8,7 +8,7 @@ def _make_branding(**kwargs: object) -> BrandingConfig: - defaults = {"company_name": "TestCo", "project_name": "TestProject"} + defaults: dict[str, object] = {"company_name": "TestCo", "project_name": "TestProject"} defaults.update(kwargs) return BrandingConfig(**defaults) diff --git a/tests/test_html_styles.py b/tests/test_html_styles.py index 49b2bc3..dec60c7 100644 --- a/tests/test_html_styles.py +++ b/tests/test_html_styles.py @@ -5,7 +5,7 @@ def _make_branding(**kwargs: object) -> BrandingConfig: - defaults = {"company_name": "TestCo", "project_name": "TestProject"} + defaults: dict[str, object] = {"company_name": "TestCo", "project_name": "TestProject"} defaults.update(kwargs) return BrandingConfig(**defaults) diff --git a/tests/test_import_pptx.py b/tests/test_import_pptx.py index 842162e..6d428fe 100644 --- a/tests/test_import_pptx.py +++ b/tests/test_import_pptx.py @@ -210,11 +210,12 @@ def test_table_extraction(tmp_path: Path) -> None: def test_image_extraction(tmp_path: Path) -> None: - """Embedded images are extracted to assets/ and referenced in markdown.""" + """Embedded images are extracted to assets/ with alt text from shape name.""" pptx_path = _make_image_pptx(tmp_path) result = import_pptx(pptx_path) md = result.markdown_path.read_text() - assert "![](assets/" in md + assert "![" in md + assert "](assets/" in md assert len(result.images) == 1 assert result.images[0].exists() @@ -329,8 +330,9 @@ def test_runs_to_markdown_plain() -> None: assert _runs_to_markdown(para) == "hello world" -def test_convert_shape_warns_on_chart() -> None: - """Chart shapes produce a warning.""" +def test_convert_shape_counts_chart() -> None: + """Chart shapes increment the skipped counter.""" + from collections import Counter from unittest.mock import MagicMock from leafpress.importer.converter_pptx import _convert_shape @@ -339,19 +341,16 @@ def test_convert_shape_warns_on_chart() -> None: shape.shape_type = MSO_SHAPE_TYPE.CHART shape.name = "Chart 1" shape.has_text_frame = False - warnings: list[str] = [] + skipped_counts: Counter[str] = Counter() - result = _convert_shape(shape, None, "Revenue Slide", warnings) + result = _convert_shape(shape, None, "Revenue Slide", skipped_counts) assert result == "" - assert len(warnings) == 1 - assert "chart" in warnings[0].lower() - assert "Chart 1" in warnings[0] - assert "Revenue Slide" in warnings[0] - assert "skipped" in warnings[0].lower() + assert skipped_counts["chart"] == 1 -def test_convert_shape_no_warn_on_freeform() -> None: +def test_convert_shape_no_count_on_freeform() -> None: """Freeform shapes (decorative lines, etc.) are silently skipped.""" + from collections import Counter from unittest.mock import MagicMock from leafpress.importer.converter_pptx import _convert_shape @@ -359,10 +358,10 @@ def test_convert_shape_no_warn_on_freeform() -> None: shape = MagicMock() shape.shape_type = MSO_SHAPE_TYPE.FREEFORM shape.has_text_frame = False - warnings: list[str] = [] + skipped_counts: Counter[str] = Counter() - _convert_shape(shape, None, "Slide 1", warnings) - assert len(warnings) == 0 + _convert_shape(shape, None, "Slide 1", skipped_counts) + assert len(skipped_counts) == 0 def test_runs_to_markdown_hyperlink() -> None: @@ -382,7 +381,7 @@ def test_runs_to_markdown_hyperlink() -> None: def test_runs_to_markdown_bold_hyperlink() -> None: - """Bold text with a hyperlink unwraps formatting for the link text.""" + """Bold text with a hyperlink preserves both formatting and link.""" from unittest.mock import MagicMock para = MagicMock() @@ -394,7 +393,56 @@ def test_runs_to_markdown_bold_hyperlink() -> None: para.runs = [run] md = _runs_to_markdown(para) - assert "[bold link](https://example.com)" in md + assert "**[bold link](https://example.com)**" in md + + +def test_runs_to_markdown_italic_hyperlink() -> None: + """Italic text with a hyperlink preserves both formatting and link.""" + from unittest.mock import MagicMock + + para = MagicMock() + run = MagicMock() + run.text = "italic link" + run.font.bold = False + run.font.italic = True + run.hyperlink.address = "https://example.com" + para.runs = [run] + + md = _runs_to_markdown(para) + assert "*[italic link](https://example.com)*" in md + + +def test_aggregate_warnings_multiple_charts() -> None: + """Multiple charts across slides produce a single aggregated warning.""" + from collections import Counter + from unittest.mock import MagicMock + + from leafpress.importer.converter_pptx import _aggregate_warnings, _convert_shape + + skipped_counts: Counter[str] = Counter() + for i in range(3): + shape = MagicMock() + shape.shape_type = MSO_SHAPE_TYPE.CHART + shape.name = f"Chart {i}" + shape.has_text_frame = False + _convert_shape(shape, None, f"Slide {i}", skipped_counts) + + warnings = _aggregate_warnings(skipped_counts) + assert len(warnings) == 1 + assert "3 unsupported charts" in warnings[0] + + +def test_aggregate_warnings_mixed_types() -> None: + """Different unsupported types each get their own aggregated warning.""" + from collections import Counter + + from leafpress.importer.converter_pptx import _aggregate_warnings + + skipped_counts: Counter[str] = Counter({"chart": 2, "media": 1}) + warnings = _aggregate_warnings(skipped_counts) + assert len(warnings) == 2 + assert any("2 unsupported charts" in w for w in warnings) + assert any("1 unsupported media" in w for w in warnings) def test_runs_to_markdown_empty_run() -> None: @@ -587,7 +635,7 @@ def test_image_extracted(self) -> None: """Embedded image extracted to assets/.""" assert len(self.result.images) >= 1 assert self.result.images[0].exists() - assert "![](assets/" in self.content + assert "](assets/" in self.content def test_slide_count(self) -> None: """All 5 slides appear in output.""" diff --git a/tests/test_markdown_renderer.py b/tests/test_markdown_renderer.py index 6f7571a..3b166e2 100644 --- a/tests/test_markdown_renderer.py +++ b/tests/test_markdown_renderer.py @@ -56,7 +56,7 @@ def test_extension_load_failure_includes_error_message(sample_mkdocs_dir: Path) ) failed = [(ext, ok, msg) for ext, ok, msg in renderer.extension_load_results if not ok] assert len(failed) >= 1 - ext, ok, msg = failed[0] + ext, _ok, msg = failed[0] assert ext == "nonexistent_extension_xyz" assert msg != "" diff --git a/tests/test_odt_renderer.py b/tests/test_odt_renderer.py index 4c1789e..b77486a 100644 --- a/tests/test_odt_renderer.py +++ b/tests/test_odt_renderer.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest + from leafpress.config import WatermarkConfig, load_config from leafpress.git_info import extract_git_info from leafpress.markdown_renderer import MarkdownRenderer diff --git a/tests/test_pdf_renderer.py b/tests/test_pdf_renderer.py index 16a3fb8..719247a 100644 --- a/tests/test_pdf_renderer.py +++ b/tests/test_pdf_renderer.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest + from leafpress.config import load_config from leafpress.exceptions import RenderError from leafpress.git_info import extract_git_info diff --git a/tests/test_pdf_styles.py b/tests/test_pdf_styles.py index b5afe60..628b255 100644 --- a/tests/test_pdf_styles.py +++ b/tests/test_pdf_styles.py @@ -8,13 +8,13 @@ def _make_branding(**kwargs: object) -> BrandingConfig: - defaults = {"company_name": "TestCo", "project_name": "TestProject"} + defaults: dict[str, object] = {"company_name": "TestCo", "project_name": "TestProject"} defaults.update(kwargs) return BrandingConfig(**defaults) def _make_git_info(**kwargs: object) -> GitVersion: - defaults = { + defaults: dict[str, object] = { "branch": "main", "commit_hash": "abc1234", "commit_hash_full": "abc1234567890abcdef1234567890abcdef123456", diff --git a/tests/test_pipeline_monorepo.py b/tests/test_pipeline_monorepo.py index bfa2af4..8a79dbc 100644 --- a/tests/test_pipeline_monorepo.py +++ b/tests/test_pipeline_monorepo.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest + from leafpress.config import BrandingConfig, ProjectEntry from leafpress.exceptions import SourceError from leafpress.pipeline import _build_chapter_cover, _collect_monorepo_pages @@ -450,7 +451,7 @@ def test_monorepo_root_used_for_version_detection(monorepo: Path) -> None: ProjectEntry(path="services/api/docs", root="services/api"), ] - pages, count = _collect_monorepo_pages( + pages, _count = _collect_monorepo_pages( projects, monorepo, monorepo / "mermaid", @@ -477,7 +478,7 @@ def test_monorepo_no_root_uses_path_for_version(monorepo: Path) -> None: ProjectEntry(path="services/api"), # no root set ] - pages, count = _collect_monorepo_pages( + pages, _count = _collect_monorepo_pages( projects, monorepo, monorepo / "mermaid", diff --git a/ty.toml b/ty.toml new file mode 100644 index 0000000..f6648a2 --- /dev/null +++ b/ty.toml @@ -0,0 +1,17 @@ +[environment] +python-version = "3.13" + +[src] +root = "src" + +[rules] +# Third-party libraries (BeautifulSoup, python-docx, python-pptx, odfpy, +# markdownify, PyQt6) ship incomplete or inaccurate type stubs that trigger +# false positives on perfectly valid code. Suppress the rules that are +# overwhelmingly caused by stub gaps rather than real bugs. +invalid-argument-type = "warn" # bs4 Tag.get(), Repo.clone_from(), etc. +invalid-assignment = "warn" # bs4 Tag["class"] = [...] +unsupported-operator = "warn" # "x" in tag.get(...) +unresolved-attribute = "warn" # python-docx Paragraph.add_run(), markdownify +invalid-type-form = "warn" # python-docx Document, odfpy OpenDocumentText +unresolved-import = "ignore" # optional deps: PyQt6, tomli, AppKit, helpers (test) diff --git a/uv.lock b/uv.lock index 5df5f8a..4422012 100644 --- a/uv.lock +++ b/uv.lock @@ -416,7 +416,7 @@ wheels = [ [[package]] name = "leafpress" -version = "0.7.0" +version = "0.8.0" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" }, From ebfcd59bf0056cef3d3a97c1a0781a6c2b2b936b Mon Sep 17 00:00:00 2001 From: Shane Hutchins Date: Thu, 9 Apr 2026 07:35:23 -0400 Subject: [PATCH 2/3] Fix release workflow and replace dict-unpacking test helpers - Handle pre-existing GitHub releases in release workflow by uploading assets instead of failing on duplicate tag - Replace **kwargs dict-unpacking helpers in test files with explicit typed parameters to eliminate type checker warnings --- .github/workflows/release.yml | 8 ++++++- tests/test_docx_styles.py | 12 ++++++---- tests/test_html_styles.py | 14 +++++++---- tests/test_pdf_styles.py | 41 ++++++++++++++++++--------------- tests/test_pipeline_monorepo.py | 14 +++++++---- tests/test_watermark.py | 20 ++++++++++++---- 6 files changed, 74 insertions(+), 35 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae0f458..3d8c4e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,4 +30,10 @@ jobs: - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} - run: gh release create ${{ github.ref_name }} dist/* --generate-notes + TAG_NAME: ${{ github.ref_name }} + run: | + if gh release view "$TAG_NAME" > /dev/null 2>&1; then + gh release upload "$TAG_NAME" dist/* --clobber + else + gh release create "$TAG_NAME" dist/* --generate-notes + fi diff --git a/tests/test_docx_styles.py b/tests/test_docx_styles.py index 9b89e7b..e5693b8 100644 --- a/tests/test_docx_styles.py +++ b/tests/test_docx_styles.py @@ -7,10 +7,14 @@ from leafpress.docx.styles import _parse_hex_color, apply_branding_styles -def _make_branding(**kwargs: object) -> BrandingConfig: - defaults: dict[str, object] = {"company_name": "TestCo", "project_name": "TestProject"} - defaults.update(kwargs) - return BrandingConfig(**defaults) +def _make_branding( + primary_color: str = "#1a73e8", +) -> BrandingConfig: + return BrandingConfig( + company_name="TestCo", + project_name="TestProject", + primary_color=primary_color, + ) def test_apply_branding_styles() -> None: diff --git a/tests/test_html_styles.py b/tests/test_html_styles.py index dec60c7..dcbe96b 100644 --- a/tests/test_html_styles.py +++ b/tests/test_html_styles.py @@ -4,10 +4,16 @@ from leafpress.html.styles import generate_html_css -def _make_branding(**kwargs: object) -> BrandingConfig: - defaults: dict[str, object] = {"company_name": "TestCo", "project_name": "TestProject"} - defaults.update(kwargs) - return BrandingConfig(**defaults) +def _make_branding( + primary_color: str = "#1a73e8", + accent_color: str = "#ffffff", +) -> BrandingConfig: + return BrandingConfig( + company_name="TestCo", + project_name="TestProject", + primary_color=primary_color, + accent_color=accent_color, + ) def test_generate_css_with_branding() -> None: diff --git a/tests/test_pdf_styles.py b/tests/test_pdf_styles.py index 628b255..87088e5 100644 --- a/tests/test_pdf_styles.py +++ b/tests/test_pdf_styles.py @@ -7,24 +7,29 @@ from leafpress.pdf.styles import generate_pdf_css -def _make_branding(**kwargs: object) -> BrandingConfig: - defaults: dict[str, object] = {"company_name": "TestCo", "project_name": "TestProject"} - defaults.update(kwargs) - return BrandingConfig(**defaults) - - -def _make_git_info(**kwargs: object) -> GitVersion: - defaults: dict[str, object] = { - "branch": "main", - "commit_hash": "abc1234", - "commit_hash_full": "abc1234567890abcdef1234567890abcdef123456", - "commit_date": datetime(2025, 1, 15, tzinfo=timezone.utc), - "is_dirty": False, - "tag": "v1.0.0", - "tag_distance": 0, - } - defaults.update(kwargs) - return GitVersion(**defaults) +def _make_branding( + primary_color: str = "#1a73e8", +) -> BrandingConfig: + return BrandingConfig( + company_name="TestCo", + project_name="TestProject", + primary_color=primary_color, + ) + + +def _make_git_info( + tag: str = "v1.0.0", + tag_distance: int = 0, +) -> GitVersion: + return GitVersion( + branch="main", + commit_hash="abc1234", + commit_hash_full="abc1234567890abcdef1234567890abcdef123456", + commit_date=datetime(2025, 1, 15, tzinfo=timezone.utc), + is_dirty=False, + tag=tag, + tag_distance=tag_distance, + ) def test_generate_css_with_branding() -> None: diff --git a/tests/test_pipeline_monorepo.py b/tests/test_pipeline_monorepo.py index 8a79dbc..e115781 100644 --- a/tests/test_pipeline_monorepo.py +++ b/tests/test_pipeline_monorepo.py @@ -48,10 +48,16 @@ def monorepo(tmp_path: Path) -> Path: return tmp_path -def _branding(**kwargs) -> BrandingConfig: - defaults = {"company_name": "Test Corp", "project_name": "Platform Docs"} - defaults.update(kwargs) - return BrandingConfig(**defaults) +def _branding( + author: str | None = None, + document_owner: str | None = None, +) -> BrandingConfig: + return BrandingConfig( + company_name="Test Corp", + project_name="Platform Docs", + author=author, + document_owner=document_owner, + ) # --- _collect_monorepo_pages --- diff --git a/tests/test_watermark.py b/tests/test_watermark.py index dfb5333..4fcd186 100644 --- a/tests/test_watermark.py +++ b/tests/test_watermark.py @@ -101,11 +101,17 @@ def test_watermark_angle_validation(self) -> None: class TestWatermarkPdfCss: """Test watermark CSS generation for PDF.""" - def _make_branding(self, **wm_kwargs: object) -> BrandingConfig: + def _make_branding( + self, + text: str | None = None, + color: str = "#cccccc", + opacity: float = 0.15, + angle: int = -45, + ) -> BrandingConfig: return BrandingConfig( company_name="Test", project_name="Test", - watermark=WatermarkConfig(**wm_kwargs), + watermark=WatermarkConfig(text=text, color=color, opacity=opacity, angle=angle), ) def test_no_watermark_css_when_disabled(self) -> None: @@ -140,11 +146,17 @@ def test_watermark_css_custom_values(self) -> None: class TestWatermarkHtmlCss: """Test watermark CSS generation for HTML.""" - def _make_branding(self, **wm_kwargs: object) -> BrandingConfig: + def _make_branding( + self, + text: str | None = None, + color: str = "#cccccc", + opacity: float = 0.15, + angle: int = -45, + ) -> BrandingConfig: return BrandingConfig( company_name="Test", project_name="Test", - watermark=WatermarkConfig(**wm_kwargs), + watermark=WatermarkConfig(text=text, color=color, opacity=opacity, angle=angle), ) def test_no_watermark_display_none(self) -> None: From f51af5e5745458e2446d89a079f5809576a2c063 Mon Sep 17 00:00:00 2001 From: Shane Hutchins Date: Thu, 9 Apr 2026 07:47:04 -0400 Subject: [PATCH 3/3] Bump version to 0.8.1, suppress ty stub warnings, fix integration test path - Bump version to 0.8.1 - Suppress third-party type stub warnings in ty.toml instead of demoting - Update CIButler integration test path --- pyproject.toml | 2 +- tests/test_cibutler_integration.py | 4 ++-- ty.toml | 15 +++++++-------- uv.lock | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ef5564d..7ac56cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "uv_build" [project] name = "leafpress" -version = "0.8.0" +version = "0.8.1" description = "Convert MkDocs sites to PDF, Word, HTML, ODT, EPUB, and Markdown documents with branding" readme = "README.md" authors = [{name = "Shane Hutchins", email = "hutchins@users.noreply.github.com"}] diff --git a/tests/test_cibutler_integration.py b/tests/test_cibutler_integration.py index cdd1842..40beee9 100644 --- a/tests/test_cibutler_integration.py +++ b/tests/test_cibutler_integration.py @@ -15,8 +15,8 @@ from leafpress.mkdocs_parser import MkDocsConfig, flatten_nav, parse_mkdocs_config from leafpress.pdf.renderer import PdfRenderer -CIBUTLER_DOCS = Path("/Users/hutchins/projects/osdu/cibutler/docs") -CIBUTLER_REPO = Path("/Users/hutchins/projects/osdu/cibutler") +CIBUTLER_DOCS = Path("/Users/hutchins/projects/cibutler/docs") +CIBUTLER_REPO = Path("/Users/hutchins/projects/cibutler") pytestmark = pytest.mark.skipif( not CIBUTLER_DOCS.exists(), diff --git a/ty.toml b/ty.toml index f6648a2..66b809d 100644 --- a/ty.toml +++ b/ty.toml @@ -1,17 +1,16 @@ [environment] python-version = "3.13" - -[src] -root = "src" +root = ["src"] [rules] # Third-party libraries (BeautifulSoup, python-docx, python-pptx, odfpy, # markdownify, PyQt6) ship incomplete or inaccurate type stubs that trigger # false positives on perfectly valid code. Suppress the rules that are # overwhelmingly caused by stub gaps rather than real bugs. -invalid-argument-type = "warn" # bs4 Tag.get(), Repo.clone_from(), etc. -invalid-assignment = "warn" # bs4 Tag["class"] = [...] -unsupported-operator = "warn" # "x" in tag.get(...) -unresolved-attribute = "warn" # python-docx Paragraph.add_run(), markdownify -invalid-type-form = "warn" # python-docx Document, odfpy OpenDocumentText +invalid-argument-type = "ignore" # bs4 Tag.get(), Repo.clone_from(), etc. +invalid-assignment = "ignore" # bs4 Tag["class"] = [...] +unsupported-operator = "ignore" # "x" in tag.get(...) +unresolved-attribute = "ignore" # python-docx Paragraph.add_run(), markdownify +invalid-type-form = "ignore" # python-docx Document, odfpy OpenDocumentText unresolved-import = "ignore" # optional deps: PyQt6, tomli, AppKit, helpers (test) +no-matching-overload = "ignore" # TypedDict.update() with kwargs pattern diff --git a/uv.lock b/uv.lock index 4422012..5abaea1 100644 --- a/uv.lock +++ b/uv.lock @@ -416,7 +416,7 @@ wheels = [ [[package]] name = "leafpress" -version = "0.8.0" +version = "0.8.1" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" },