From 273826377e7bd336efd8769e3217d40e57152a4d Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Mon, 6 Jul 2026 23:35:52 -0700 Subject: [PATCH] Migrate pre-commit to prek prek runs local hooks as plain execs against tools uv already provisions, so ruff/mypy can never drift from the versions/config uv.lock pins elsewhere and CI needs no separate hook-cache download. - Add ruff and prek to the uv dev dependency group (ruff wasn't a uv-managed dependency before; pre-commit silently vendored its own). - Replace .pre-commit-config.yaml with prek.toml: keep the pre-commit-hooks repo for generic file checks, convert ruff-format/ ruff-check to local `uv run ruff ...` hooks, and add a local mypy hook that reports but never fails (87 pre-existing errors need a separate cleanup before it can be made blocking). - Add a `lint` job to CI that runs `prek run --all-files` and gate the OS/Python test matrix on it so lint issues fail fast. - Fix the ruff debt (format + lint) uncovered by actually running it, since it was small and mechanical, so the new CI gate starts green. --- .github/workflows/build.yml | 23 ++++ .pre-commit-config.yaml | 27 ----- bin/bump_version.py | 3 +- misc/bisect_pdf.py | 1 + misc/ocrmypdf_compare.py | 2 +- misc/pdf_compare.py | 2 +- misc/watcher.py | 4 +- prek.toml | 66 +++++++++++ pyproject.toml | 2 + src/ocrmypdf/font/__init__.py | 1 + src/ocrmypdf/font/system_font_provider.py | 4 +- src/ocrmypdf/fpdf_renderer/__init__.py | 1 + src/ocrmypdf/fpdf_renderer/renderer.py | 57 ++++------ src/ocrmypdf/languages.py | 1 + src/ocrmypdf/pdfinfo/_contentstream.py | 8 +- src/ocrmypdf/pdfinfo/layout.py | 1 + tests/test_api.py | 1 - tests/test_fpdf_renderer.py | 2 +- tests/test_graft.py | 12 +- tests/test_json_serialization.py | 1 + tests/test_multi_font_manager.py | 6 +- tests/test_multilingual_direct.py | 25 +++-- tests/test_pdf_renderer.py | 10 +- tests/test_rotation.py | 36 +++--- tests/test_system_font_provider.py | 8 +- tests/test_validation.py | 21 ++-- uv.lock | 127 +++++++++++++++------- 27 files changed, 282 insertions(+), 170 deletions(-) delete mode 100644 .pre-commit-config.yaml create mode 100644 prek.toml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a2b394d..5c26336b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,8 +14,29 @@ on: pull_request: jobs: + lint: + name: Lint (prek) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.9.x" + + - name: "Set up Python" + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Run prek + run: | + uv run prek run --all-files + test_linux: name: Test ${{ matrix.os }} with Python ${{ matrix.python }} + needs: lint runs-on: ${{ matrix.os }} strategy: matrix: @@ -96,6 +117,7 @@ jobs: test_macos: name: Test macOS + needs: lint runs-on: ${{ matrix.os }} strategy: matrix: @@ -158,6 +180,7 @@ jobs: test_windows: name: Test Windows + needs: lint runs-on: ${{ matrix.os }} strategy: matrix: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 5150ebe3..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-FileCopyrightText: 2022 James R. Barlow -# SPDX-License-Identifier: MPL-2.0 - -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: check-case-conflict - - id: check-merge-conflict - - id: check-toml - - id: check-yaml - - id: debug-statements - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.14.11" - hooks: - - id: ruff-check - args: [--fix] - - id: ruff-format - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.2.0 - hooks: - - id: mypy - additional_dependencies: - - types-toml - - types-setuptools - - types-requests - - types-Pillow diff --git a/bin/bump_version.py b/bin/bump_version.py index d075782a..02591311 100644 --- a/bin/bump_version.py +++ b/bin/bump_version.py @@ -306,7 +306,8 @@ def bump_version() -> None: if not found_at_least_one_file_needing_update: print( - f'''error: Didn't find any occurrences of "{find_pattern}" in "{path_pattern}"''' + f'''error: Didn't find any occurrences of "{find_pattern}" ''' + f'''in "{path_pattern}"''' ) sys.exit(1) diff --git a/misc/bisect_pdf.py b/misc/bisect_pdf.py index b50aec07..e6023216 100644 --- a/misc/bisect_pdf.py +++ b/misc/bisect_pdf.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: MIT """Helper script for bisecting PDFs to find a page with an issue.""" + from __future__ import annotations import sys diff --git a/misc/ocrmypdf_compare.py b/misc/ocrmypdf_compare.py index a292456a..64d46e31 100644 --- a/misc/ocrmypdf_compare.py +++ b/misc/ocrmypdf_compare.py @@ -109,7 +109,7 @@ def main(): doc1 = pymupdf.open(os.path.join(d, "output1.pdf")) doc2 = pymupdf.open(os.path.join(d, "output2.pdf")) for i, page1_2 in enumerate(zip(doc1, doc2, strict=False)): - st.write(f"Page {i+1}") + st.write(f"Page {i + 1}") page1, page2 = page1_2 col1, col2 = st.columns(2) with col1, st.container(border=True): diff --git a/misc/pdf_compare.py b/misc/pdf_compare.py index a49ed6b8..a43fe38c 100644 --- a/misc/pdf_compare.py +++ b/misc/pdf_compare.py @@ -63,7 +63,7 @@ def main(): doc1 = pymupdf.open(os.path.join(d, "1.pdf")) doc2 = pymupdf.open(os.path.join(d, "2.pdf")) for i, page1_2 in enumerate(zip(doc1, doc2, strict=False)): - st.write(f"Page {i+1}") + st.write(f"Page {i + 1}") page1, page2 = page1_2 col1, col2 = st.columns(2) with col1, st.container(border=True): diff --git a/misc/watcher.py b/misc/watcher.py index 7d1f421f..d552f430 100644 --- a/misc/watcher.py +++ b/misc/watcher.py @@ -13,7 +13,7 @@ import logging import shutil import sys import time -from enum import Enum +from enum import StrEnum from pathlib import Path from typing import Annotated, Any @@ -35,7 +35,7 @@ app = cyclopts.App(name="ocrmypdf-watcher") log = logging.getLogger('ocrmypdf-watcher') -class LoggingLevelEnum(str, Enum): +class LoggingLevelEnum(StrEnum): """Enum for logging levels.""" DEBUG = "DEBUG" diff --git a/prek.toml b/prek.toml new file mode 100644 index 00000000..8bdbfd5f --- /dev/null +++ b/prek.toml @@ -0,0 +1,66 @@ +# prek pre-commit configuration — https://prek.j178.dev +# +# The local/system hooks below invoke the project's OWN pinned tools (ruff/mypy +# from uv.lock) and mirror .github/workflows/build.yml's lint job exactly, so +# they can never drift from CI's versions or rules. prek installs nothing of +# its own for them — "system" language just execs whatever `uv run` resolves. +# +# The pre-commit/pre-commit-hooks repo hooks below are generic file checks with +# no project-local tool equivalent, so they're kept as a normal (non-local) repo. +# +# Run all checks manually: `uv run prek run --all-files` +# Install the git hooks: `uv run prek install` + +default_install_hook_types = ["pre-commit", "pre-push"] +default_stages = ["pre-commit"] + +[[repos]] +repo = "https://github.com/pre-commit/pre-commit-hooks" +rev = "v4.4.0" + +[[repos.hooks]] +id = "check-case-conflict" + +[[repos.hooks]] +id = "check-merge-conflict" + +[[repos.hooks]] +id = "check-toml" + +[[repos.hooks]] +id = "check-yaml" + +[[repos.hooks]] +id = "debug-statements" + +[[repos]] +repo = "local" + +[[repos.hooks]] +id = "ruff-format" +name = "ruff format (check)" +language = "system" +entry = "uv run ruff format --check ." +types = ["python"] +pass_filenames = false +require_serial = true + +[[repos.hooks]] +id = "ruff-check" +name = "ruff check" +language = "system" +entry = "uv run ruff check ." +types = ["python"] +pass_filenames = false +require_serial = true + +[[repos.hooks]] +id = "mypy" +name = "mypy (advisory — reports errors, does not fail)" +language = "system" +# Errors are pre-existing debt (87 across 22 files as of 2026-07); reports but +# never fails the hook until that debt is fixed and this can be made blocking. +entry = "bash -c 'uv run mypy src/ocrmypdf; exit 0'" +types = ["python"] +pass_filenames = false +require_serial = true diff --git a/pyproject.toml b/pyproject.toml index cdd63b80..773afa47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,6 +158,8 @@ quote-style = "preserve" # Developer-only tools - use `uv sync --group ` dev = [ "mypy>=1.13.0", + "ruff>=0.14.11", + "prek>=0.4.8", "ipykernel>=6.29.5", "reportlab>=4.4.4", "cyclopts>=4.5.1", diff --git a/src/ocrmypdf/font/__init__.py b/src/ocrmypdf/font/__init__.py index f94cc3cb..d39dc0a3 100644 --- a/src/ocrmypdf/font/__init__.py +++ b/src/ocrmypdf/font/__init__.py @@ -10,6 +10,7 @@ This module provides font infrastructure for the fpdf2 PDF renderer. It includes - MultiFontManager: Automatic font selection for multilingual documents - SystemFontProvider: System font discovery """ + from __future__ import annotations from ocrmypdf.font.font_manager import FontManager diff --git a/src/ocrmypdf/font/system_font_provider.py b/src/ocrmypdf/font/system_font_provider.py index 72132361..f491a7bb 100644 --- a/src/ocrmypdf/font/system_font_provider.py +++ b/src/ocrmypdf/font/system_font_provider.py @@ -274,9 +274,7 @@ class SystemFontProvider: try: matches = list(font_dir.rglob(pattern)) if matches: - log.debug( - "Found system font %s at %s", font_name, matches[0] - ) + log.debug("Found system font %s at %s", font_name, matches[0]) return matches[0] except PermissionError: # Skip directories we can't read diff --git a/src/ocrmypdf/fpdf_renderer/__init__.py b/src/ocrmypdf/fpdf_renderer/__init__.py index db039f9e..31394f07 100644 --- a/src/ocrmypdf/fpdf_renderer/__init__.py +++ b/src/ocrmypdf/fpdf_renderer/__init__.py @@ -6,6 +6,7 @@ This module provides the PDF renderer using fpdf2 for creating searchable OCR text layers. """ + from __future__ import annotations from ocrmypdf.fpdf_renderer.renderer import ( diff --git a/src/ocrmypdf/fpdf_renderer/renderer.py b/src/ocrmypdf/fpdf_renderer/renderer.py index 6f6ff715..54279a50 100644 --- a/src/ocrmypdf/fpdf_renderer/renderer.py +++ b/src/ocrmypdf/fpdf_renderer/renderer.py @@ -448,8 +448,13 @@ class Fpdf2PdfRenderer: # entirely (slope=0, no textangle) and produced garbage text in a # bounding box whose shape doesn't match the text content at all. if not self._check_aspect_ratio_plausible( - pdf, words, font_size, slope_angle_deg, - line_size_width, line_size_height, line_language, + pdf, + words, + font_size, + slope_angle_deg, + line_size_width, + line_size_height, + line_language, ): return @@ -507,13 +512,15 @@ class Fpdf2PdfRenderer: else: word_tz = 100.0 - word_render_data.append(WordRenderData( - text=word.text, - x_baseline=box_llx, - font_family=font_family, - word_tz=word_tz, - is_rtl=word_is_rtl, - )) + word_render_data.append( + WordRenderData( + text=word.text, + x_baseline=box_llx, + font_family=font_family, + word_tz=word_tz, + is_rtl=word_is_rtl, + ) + ) if not word_render_data: return @@ -561,9 +568,7 @@ class Fpdf2PdfRenderer: if line_size_width >= line_size_height: return True - line_text = ' '.join( - w.text for w in words if w is not None and w.text - ) + line_text = ' '.join(w.text for w in words if w is not None and w.text) if not line_text: return True @@ -603,9 +608,7 @@ class Fpdf2PdfRenderer: line_text[:80], ) if not self._logged_aspect_ratio_suppression: - log.info( - "Suppressing OCR output text with improbable aspect ratio" - ) + log.info("Suppressing OCR output text with improbable aspect ratio") self._logged_aspect_ratio_suppression = True return False @@ -679,9 +682,7 @@ class Fpdf2PdfRenderer: ops.append(f'{first_x_baseline:.2f} 0 Td') else: # Direct PDF coordinates - page_x, page_y_fpdf = transform_point( - baseline_matrix, first_x_baseline, 0 - ) + page_x, page_y_fpdf = transform_point(baseline_matrix, first_x_baseline, 0) page_y_pdf = page_height - page_y_fpdf ops.append(f'{page_x:.2f} {page_y_pdf:.2f} Td') @@ -698,9 +699,7 @@ class Fpdf2PdfRenderer: pdf._resource_catalog.add( PDFResourceType.FONT, pdf.current_font.i, pdf.page ) - ops.append( - f'/F{pdf.current_font.i} {pdf.font_size_pt:.2f} Tf' - ) + ops.append(f'/F{pdf.current_font.i} {pdf.font_size_pt:.2f} Tf') prev_font_family = word.font_family # Relative positioning (for words after the first) @@ -728,12 +727,8 @@ class Fpdf2PdfRenderer: advance = next_word.x_baseline - word.x_baseline # Add trailing space for text extraction unless both are CJK - if ( - advance > 0 - and not ( - self._is_cjk_only(word.text) - and self._is_cjk_only(next_word.text) - ) + if advance > 0 and not ( + self._is_cjk_only(word.text) and self._is_cjk_only(next_word.text) ): text_to_render = word.text + ' ' else: @@ -744,9 +739,7 @@ class Fpdf2PdfRenderer: # Use word_tz (fits word into its hOCR bbox) — Td handles # inter-word gaps, so Tz should not stretch to fill them. ops.append(f'{word.word_tz:.2f} Tz') - ops.append( - self._encode_shaped_text(pdf, text_to_render, word.is_rtl) - ) + ops.append(self._encode_shaped_text(pdf, text_to_render, word.is_rtl)) prev_x_baseline = word.x_baseline @@ -762,9 +755,7 @@ class Fpdf2PdfRenderer: # don't think Tz is still set from our raw operators pdf.font_stretching = 100 - def _encode_shaped_text( - self, pdf: FPDF, text: str, is_rtl: bool = False - ) -> str: + def _encode_shaped_text(self, pdf: FPDF, text: str, is_rtl: bool = False) -> str: """Encode text using HarfBuzz text shaping for complex script support. Unlike font.encode_text() which maps unicode characters one-by-one to diff --git a/src/ocrmypdf/languages.py b/src/ocrmypdf/languages.py index 0702fe53..4db3810b 100644 --- a/src/ocrmypdf/languages.py +++ b/src/ocrmypdf/languages.py @@ -6,6 +6,7 @@ Derived from https://www.loc.gov/standards/iso639-2/ascii_8bits.html """ + from __future__ import annotations from typing import NamedTuple diff --git a/src/ocrmypdf/pdfinfo/_contentstream.py b/src/ocrmypdf/pdfinfo/_contentstream.py index e97133fe..12af2c86 100644 --- a/src/ocrmypdf/pdfinfo/_contentstream.py +++ b/src/ocrmypdf/pdfinfo/_contentstream.py @@ -172,10 +172,10 @@ def _interpret_contents( name_index = defaultdict(lambda: []) found_vector = False found_text = False - vector_ops = set('S s f F f* B B* b b*'.split()) - text_showing_ops = set("""TJ Tj " '""".split()) - image_ops = set('BI ID EI q Q Do cm'.split()) - color_ops = set('g rg k cs sc scn'.split()) + vector_ops = set(['S', 's', 'f', 'F', 'f*', 'B', 'B*', 'b', 'b*']) + text_showing_ops = set(["TJ", "Tj", '"', "'"]) + image_ops = set(['BI', 'ID', 'EI', 'q', 'Q', 'Do', 'cm']) + color_ops = set(['g', 'rg', 'k', 'cs', 'sc', 'scn']) operator_whitelist = ' '.join(vector_ops | text_showing_ops | image_ops | color_ops) for n, graphobj in enumerate( diff --git a/src/ocrmypdf/pdfinfo/layout.py b/src/ocrmypdf/pdfinfo/layout.py index d679f2b7..a7290c60 100644 --- a/src/ocrmypdf/pdfinfo/layout.py +++ b/src/ocrmypdf/pdfinfo/layout.py @@ -59,6 +59,7 @@ def pdfsimplefont__init__( PDFSimpleFont.__init__ = pdfsimplefont__init__ + def pdftype3font__pscript5_get_height(self): """Monkeypatch for PScript5.dll PDFs. diff --git a/tests/test_api.py b/tests/test_api.py index 03db01db..9e4b5ab1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -28,7 +28,6 @@ def test_language_parameter_mapped_to_languages(): Regression test for GitHub issue #1640: the Python API ignored the language parameter, always defaulting to 'eng'. """ - from ocrmypdf._options import OcrOptions from ocrmypdf.api import create_options, setup_plugin_infrastructure from ocrmypdf.cli import get_parser diff --git a/tests/test_fpdf_renderer.py b/tests/test_fpdf_renderer.py index 540aa680..1103d5cb 100644 --- a/tests/test_fpdf_renderer.py +++ b/tests/test_fpdf_renderer.py @@ -219,7 +219,7 @@ class TestFpdf2MultiPageRenderer: for i in range(3): word = OcrElement( ocr_class=OcrClass.WORD, - text=f"Page{i+1}", + text=f"Page{i + 1}", bbox=BoundingBox(left=100, top=100, right=200, bottom=130), ) line = OcrElement( diff --git a/tests/test_graft.py b/tests/test_graft.py index 5c0fbbe1..4f86bc90 100644 --- a/tests/test_graft.py +++ b/tests/test_graft.py @@ -78,9 +78,9 @@ def test_redo_ocr_with_offset_mediabox(resources, outdir): mediabox = list(page.MediaBox) # MediaBox origin should be preserved - assert ( - float(mediabox[1]) == y_offset - ), f"MediaBox Y origin should be preserved at {y_offset}, got {mediabox[1]}" + assert float(mediabox[1]) == y_offset, ( + f"MediaBox Y origin should be preserved at {y_offset}, got {mediabox[1]}" + ) # The content stream should include a CTM with the Y origin translation. # Without the fix, the CTM was omitted for rotation==0, causing a shift. @@ -153,7 +153,7 @@ def test_strip_invisble_text(): nr_visible_pre = count('visible', page) ocrmypdf._graft.strip_invisible_text(pdf, page) nr_visible_post = count('visible', page) - assert ( - nr_visible_pre == nr_visible_post - ), 'Number of visible text elements did not change' + assert nr_visible_pre == nr_visible_post, ( + 'Number of visible text elements did not change' + ) assert count('invisible', page) == 0, 'No invisible elems left' diff --git a/tests/test_json_serialization.py b/tests/test_json_serialization.py index b0ed87c3..ed45b56b 100644 --- a/tests/test_json_serialization.py +++ b/tests/test_json_serialization.py @@ -1,4 +1,5 @@ """Test JSON serialization of OcrOptions for multiprocessing compatibility.""" + from __future__ import annotations import multiprocessing diff --git a/tests/test_multi_font_manager.py b/tests/test_multi_font_manager.py index 6bf9abd2..70db34c4 100644 --- a/tests/test_multi_font_manager.py +++ b/tests/test_multi_font_manager.py @@ -43,7 +43,7 @@ def has_devanagari_font(manager: MultiFontManager) -> bool: # Marker for tests that require CJK fonts requires_cjk = pytest.mark.skipif( "not has_cjk_font(MultiFontManager())", - reason="CJK font not available (not installed on system)" + reason="CJK font not available (not installed on system)", ) @@ -356,9 +356,7 @@ def test_get_all_fonts(multi_font_manager): class MockFontProvider: """Mock FontProvider for testing missing fonts.""" - def __init__( - self, available_fonts: dict[str, FontManager], fallback: FontManager - ): + def __init__(self, available_fonts: dict[str, FontManager], fallback: FontManager): """Initialize mock font provider with given fonts.""" self._fonts = available_fonts self._fallback = fallback diff --git a/tests/test_multilingual_direct.py b/tests/test_multilingual_direct.py index 113e29d2..3b8e3b19 100644 --- a/tests/test_multilingual_direct.py +++ b/tests/test_multilingual_direct.py @@ -10,6 +10,7 @@ This tests the fpdf2 renderer with various language groups: - CJK (Chinese Simplified/Traditional, Japanese, Korean) - Devanagari (Hindi, Sanskrit) """ + from __future__ import annotations import shutil @@ -208,9 +209,9 @@ class TestArabicScript: for para in page.paragraphs: if para.language in ('ara', 'per'): # Arabic paragraphs should have RTL direction - assert ( - para.direction == 'rtl' - ), "Arabic paragraph should have RTL direction" + assert para.direction == 'rtl', ( + "Arabic paragraph should have RTL direction" + ) # ============================================================================= @@ -529,9 +530,9 @@ class TestBaselineHandling: for line in page.lines: if line.baseline: # Baseline should be reasonable - assert ( - -1.0 <= line.baseline.slope <= 1.0 - ), "Baseline slope should be reasonable" + assert -1.0 <= line.baseline.slope <= 1.0, ( + "Baseline slope should be reasonable" + ) # ============================================================================= @@ -556,9 +557,9 @@ class TestFontCoverage: ] for sample in latin_samples: - assert multi_font_manager.has_all_glyphs( - 'NotoSans-Regular', sample - ), f"NotoSans should cover: {sample}" + assert multi_font_manager.has_all_glyphs('NotoSans-Regular', sample), ( + f"NotoSans should cover: {sample}" + ) def test_noto_sans_arabic_coverage(self, multi_font_manager_arabic): """Test NotoSansArabic covers Arabic characters.""" @@ -602,9 +603,9 @@ class TestFontCoverage: ] for sample in cjk_samples: - assert multi_font_manager.has_all_glyphs( - 'NotoSansCJK-Regular', sample - ), f"NotoSansCJK should cover: {sample}" + assert multi_font_manager.has_all_glyphs('NotoSansCJK-Regular', sample), ( + f"NotoSansCJK should cover: {sample}" + ) if __name__ == "__main__": diff --git a/tests/test_pdf_renderer.py b/tests/test_pdf_renderer.py index e316bd6e..218b7df7 100644 --- a/tests/test_pdf_renderer.py +++ b/tests/test_pdf_renderer.py @@ -511,7 +511,8 @@ class TestFpdf2PdfRendererErrors: def test_invalid_ocr_class(self, multi_font_manager): """Test that non-page elements are rejected.""" line = OcrElement( - ocr_class=OcrClass.LINE, bbox=BoundingBox(left=0, top=0, right=100, bottom=50) + ocr_class=OcrClass.LINE, + bbox=BoundingBox(left=0, top=0, right=100, bottom=50), ) with pytest.raises(ValueError, match="ocr_page"): @@ -622,9 +623,7 @@ def create_rtl_page( OcrElement( ocr_class=OcrClass.WORD, text=text, - bbox=BoundingBox( - left=bbox[0], top=bbox[1], right=bbox[2], bottom=bbox[3] - ), + bbox=BoundingBox(left=bbox[0], top=bbox[1], right=bbox[2], bottom=bbox[3]), ) for text, bbox in words ] @@ -850,8 +849,7 @@ class TestRtlTextExtraction: decoded = ''.join(cmap.get(g, '') for g in glyph_ids) logical = decoded[::-1] assert logical == 'שלום', ( - f"Expected logical text 'שלום', got {logical!r} " - f"(stream: {decoded!r})" + f"Expected logical text 'שלום', got {logical!r} (stream: {decoded!r})" ) def test_rtl_tounicode_one_to_one(self, tmp_path, multi_font_manager): diff --git a/tests/test_rotation.py b/tests/test_rotation.py index b23f6d0f..779a4176 100644 --- a/tests/test_rotation.py +++ b/tests/test_rotation.py @@ -160,9 +160,9 @@ def test_rotated_skew_timeout(resources, outpdf, rasterizer): input_file = resources / 'rotated_skew.pdf' in_pageinfo = PdfInfo(input_file)[0] - assert ( - in_pageinfo.height_pixels < in_pageinfo.width_pixels - ), "Expected the input page to be landscape" + assert in_pageinfo.height_pixels < in_pageinfo.width_pixels, ( + "Expected the input page to be landscape" + ) assert in_pageinfo.rotation == 90, "Expected a rotated page" out = check_ocrmypdf( @@ -184,9 +184,9 @@ def test_rotated_skew_timeout(resources, outpdf, rasterizer): assert out_pageinfo.rotation == 0, "Expected no page rotation for output" - assert ( - in_pageinfo.width_pixels == h and in_pageinfo.height_pixels == w - ), "Expected page rotation to be baked in" + assert in_pageinfo.width_pixels == h and in_pageinfo.height_pixels == w, ( + "Expected page rotation to be baked in" + ) @pytest.mark.parametrize('rasterizer', ['pypdfium', 'ghostscript']) @@ -411,16 +411,16 @@ def test_simulated_scan(outdir): ) with pikepdf.open(outdir / 'out.pdf') as pdf: - assert ( - pdf.pages[1].mediabox[2] > pdf.pages[1].mediabox[3] - ), "Wrong orientation: not landscape" - assert ( - pdf.pages[3].mediabox[2] > pdf.pages[3].mediabox[3] - ), "Wrong orientation: Not landscape" + assert pdf.pages[1].mediabox[2] > pdf.pages[1].mediabox[3], ( + "Wrong orientation: not landscape" + ) + assert pdf.pages[3].mediabox[2] > pdf.pages[3].mediabox[3], ( + "Wrong orientation: Not landscape" + ) - assert ( - pdf.pages[0].mediabox[2] < pdf.pages[0].mediabox[3] - ), "Wrong orientation: Not portrait" - assert ( - pdf.pages[2].mediabox[2] < pdf.pages[2].mediabox[3] - ), "Wrong orientation: Not portrait" + assert pdf.pages[0].mediabox[2] < pdf.pages[0].mediabox[3], ( + "Wrong orientation: Not portrait" + ) + assert pdf.pages[2].mediabox[2] < pdf.pages[2].mediabox[3], ( + "Wrong orientation: Not portrait" + ) diff --git a/tests/test_system_font_provider.py b/tests/test_system_font_provider.py index b4939a2d..2dc67f65 100644 --- a/tests/test_system_font_provider.py +++ b/tests/test_system_font_provider.py @@ -103,7 +103,10 @@ class TestSystemFontProviderDirectories: patch.object(sys, 'platform', 'win32'), patch.dict( 'os.environ', - {'WINDIR': r'C:\Windows', 'LOCALAPPDATA': r'C:\Users\Test\AppData\Local'}, + { + 'WINDIR': r'C:\Windows', + 'LOCALAPPDATA': r'C:\Users\Test\AppData\Local', + }, ), ): provider._font_dirs = None # Reset cache @@ -113,8 +116,7 @@ class TestSystemFontProviderDirectories: assert len(dirs) == 2 assert any('Windows' in d and 'Fonts' in d for d in dir_strs) assert any( - 'AppData' in d and 'Local' in d and 'Fonts' in d - for d in dir_strs + 'AppData' in d and 'Local' in d and 'Fonts' in d for d in dir_strs ) def test_font_dirs_cached(self): diff --git a/tests/test_validation.py b/tests/test_validation.py index 1fee81e6..edda4c91 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -47,10 +47,13 @@ def make_ocr_opts(input_file='a.pdf', output_file='b.pdf', **kwargs): def test_old_tesseract_error(): - with patch( - 'ocrmypdf._exec.tesseract.version', - return_value=TesseractVersion('4.00.00alpha'), - ), pytest.raises(MissingDependencyError): + with ( + patch( + 'ocrmypdf._exec.tesseract.version', + return_value=TesseractVersion('4.00.00alpha'), + ), + pytest.raises(MissingDependencyError), + ): vd.check_options(*make_opts_pm(pdf_renderer='sandwich', language='eng')) @@ -59,9 +62,9 @@ def test_tesseract_not_installed(caplog): not_found.side_effect = FileNotFoundError('tesseract') with pytest.raises(MissingDependencyError, match="Could not find program"): vd.check_options(*make_opts_pm()) - assert ( - "'tesseract' could not be executed" in caplog.text - ), "Error message not printed" + assert "'tesseract' could not be executed" in caplog.text, ( + "Error message not printed" + ) assert 'install' in caplog.text, "Install advice not printed" not_found.assert_called() @@ -87,9 +90,7 @@ def test_mutex_options(): def test_optimizing(caplog): - vd.check_options( - *make_opts_pm(optimize=0, png_quality=18, jpeg_quality=10) - ) + vd.check_options(*make_opts_pm(optimize=0, png_quality=18, jpeg_quality=10)) assert 'will be ignored because' in caplog.text diff --git a/uv.lock b/uv.lock index 8b4b8878..5fce9aad 100644 --- a/uv.lock +++ b/uv.lock @@ -1434,7 +1434,7 @@ wheels = [ [[package]] name = "ocrmypdf" -version = "17.7.0" +version = "17.8.0" source = { editable = "." } dependencies = [ { name = "deprecation" }, @@ -1467,8 +1467,10 @@ dev = [ { name = "cyclopts" }, { name = "ipykernel" }, { name = "mypy" }, + { name = "prek" }, { name = "pygithub" }, { name = "reportlab" }, + { name = "ruff" }, ] docs = [ { name = "myst-parser" }, @@ -1503,7 +1505,7 @@ requires-dist = [ { name = "fpdf2", specifier = ">=2.8.0" }, { name = "img2pdf", specifier = ">=0.5" }, { name = "packaging", specifier = ">=20" }, - { name = "pdfminer-six", specifier = ">=20220319" }, + { name = "pdfminer-six", specifier = ">=20260107" }, { name = "pi-heif" }, { name = "pikepdf", specifier = ">=10" }, { name = "pillow", specifier = ">=10.0.1" }, @@ -1523,8 +1525,10 @@ dev = [ { name = "cyclopts", specifier = ">=4.5.1" }, { name = "ipykernel", specifier = ">=6.29.5" }, { name = "mypy", specifier = ">=1.13.0" }, + { name = "prek", specifier = ">=0.4.8" }, { name = "pygithub", specifier = ">=2.9.1" }, { name = "reportlab", specifier = ">=4.4.4" }, + { name = "ruff", specifier = ">=0.14.11" }, ] docs = [ { name = "myst-parser", specifier = ">=4.0.1" }, @@ -1656,7 +1660,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1860,6 +1864,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "prek" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/46/e436a6eb9fdb4d3fd08d0ab7fdba19fe03a9e994ec810de57869b853bd8e/prek-0.4.8.tar.gz", hash = "sha256:d15d8bef72ab7b02c7dc01458ac9e05b3131534492b5ce9bb11c4f6f636fa868", size = 494570, upload-time = "2026-07-04T12:05:10.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/78/b4149c8913ced2e42debb49e261c4788a1ce431e84226921c2e1a7ea8545/prek-0.4.8-py3-none-linux_armv6l.whl", hash = "sha256:1f8f8cdc65836b571824c965daebb81b449f7e4a43894c58621f5708d5a185ed", size = 5668955, upload-time = "2026-07-04T12:04:41.588Z" }, + { url = "https://files.pythonhosted.org/packages/76/5f/7f54a0087b6b2f1751aeb41266d9c15e66fd0055492814798ab818cd0414/prek-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bce1798e96d9e3a6e6abf435da7107e81452f69edb3ca7c6f90a457355ea46e2", size = 6030947, upload-time = "2026-07-04T12:04:43.8Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/f2829fc3902920c36b764a386fa303e71a8219dac25cb3827c575e84199a/prek-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab3a52db17254d701c3cebb7eea58c8230aa7c1959aacfd5b5f25de18edb15d1", size = 5572593, upload-time = "2026-07-04T12:04:45.763Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/c5589955bcd5e3e33b67d8bc3110818cecac82a38fd6bc8b5dfdc5de421c/prek-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b3fcfd620523bbc3f51a21d7cd63449f659b9e2cf3582de12dd5949e23227b8f", size = 5847150, upload-time = "2026-07-04T12:04:47.419Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9d/1f2dc91bdb79d2c4714b27eac9477a51490fba5b4731330dbbebc76bd345/prek-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42e65bc8425e9d7f1691a13ca1da2e07807d1ba76c35740833354b945131689e", size = 5573738, upload-time = "2026-07-04T12:04:49.125Z" }, + { url = "https://files.pythonhosted.org/packages/81/29/69a7b58e16ecbc5f3989bf4b028018d11a82dcdd320b93d6588d72f32aa7/prek-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f578492a8e0c9bc6b4bf6dfbba8716f647d4cd0769bf10ad6cf336e3096fd392", size = 5981054, upload-time = "2026-07-04T12:04:50.842Z" }, + { url = "https://files.pythonhosted.org/packages/63/cc/9b9850a60c22ed18c7755ebd2d72c6eefb37fac58149d09f6adc4691c2cf/prek-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4335f9d5beb123a3884a7fe34f57c9f0828f4fbb7666beab4298833459b104f", size = 6751350, upload-time = "2026-07-04T12:04:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/01/e5/c425aa7272b430630119e6757def3a2007555ba8cbeb2630e0448e7a8b7f/prek-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18a8747df9c602e052881d3efb14dd7f7d62a59bd7277ae5171c9e7661d59d84", size = 6243881, upload-time = "2026-07-04T12:04:54.703Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/accd3ad07fd2891d3c2777eb42435439fdf11982c51d60f087c0b6b6e102/prek-0.4.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4db639db481d5f854eff9b3d2108889e613b8c15868bcf6bdd777c7cee577436", size = 5848846, upload-time = "2026-07-04T12:04:56.402Z" }, + { url = "https://files.pythonhosted.org/packages/15/00/3477704635249f21f5f98ce444cd7690c2aa9dc8d146a045db88ef2cd8c5/prek-0.4.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c3890a6f92316d2cf44eb50584e8d2b23a596dd70487022e61186a71a2ac0900", size = 5713942, upload-time = "2026-07-04T12:04:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e6/3ca4fabaebeadc976d9a92d1d9130674265355ea3b728418bad61583b097/prek-0.4.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:fc7e15c24c591a37c6ffce5b25a021b16c299ac2649f183d812b67d665cd6551", size = 5554725, upload-time = "2026-07-04T12:04:59.96Z" }, + { url = "https://files.pythonhosted.org/packages/a5/46/2ab6aaaeff0cedb8955b2e4032071c8712382bdd423bb849718c3720180d/prek-0.4.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:36fe721704ff0c7624c1167639e23a5fe658bfd38c314f487219c9afd1eeb733", size = 5838595, upload-time = "2026-07-04T12:05:01.861Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8b/91398f2b6cd1629d5d8ca8c85b08eca500814a374313b0193f4aaf6ab6c4/prek-0.4.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:162e544abc394a8124f3a4ad68efee116bad09440e679dbd1675177335c2a432", size = 6357222, upload-time = "2026-07-04T12:05:03.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/ce5cbfaad36866134a21754640a05ecdba641fcd7ad15aa74cf3443f34f6/prek-0.4.8-py3-none-win32.whl", hash = "sha256:2602e46c8c5da7dfa69f60fcf88c2b57132ac623f49fb08bfb3094298c5f07e3", size = 5354388, upload-time = "2026-07-04T12:05:05.587Z" }, + { url = "https://files.pythonhosted.org/packages/df/03/3bc908bc5f7e430315553e47dfa055f19923a3888f9afe4da19f244b5cbf/prek-0.4.8-py3-none-win_amd64.whl", hash = "sha256:7cb22da60bee41b89c4978c0bea7126a3c0ccc003dae6748cf29b53947815edc", size = 5748221, upload-time = "2026-07-04T12:05:07.559Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/4295e6d5f5028171dfeb115ad38ab76bf3fe0c8df91b70d73c79aa760a94/prek-0.4.8-py3-none-win_arm64.whl", hash = "sha256:da70057f577b15d4bd121bf9dd29ee205fd4b4d75a0cafba062e84d7e8b4378b", size = 5574425, upload-time = "2026-07-04T12:05:09.595Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -2633,6 +2661,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] +[[package]] +name = "ruff" +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -2679,23 +2732,23 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.12'" }, - { name = "babel", marker = "python_full_version < '3.12'" }, - { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.12'" }, - { name = "imagesize", marker = "python_full_version < '3.12'" }, - { name = "jinja2", marker = "python_full_version < '3.12'" }, - { name = "packaging", marker = "python_full_version < '3.12'" }, - { name = "pygments", marker = "python_full_version < '3.12'" }, - { name = "requests", marker = "python_full_version < '3.12'" }, - { name = "roman-numerals", marker = "python_full_version < '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -2718,23 +2771,23 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [