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.
This commit is contained in:
James R. Barlow
2026-07-06 23:35:52 -07:00
parent 5569d4db07
commit 273826377e
27 changed files with 282 additions and 170 deletions
+23
View File
@@ -14,8 +14,29 @@ on:
pull_request: pull_request:
jobs: 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: test_linux:
name: Test ${{ matrix.os }} with Python ${{ matrix.python }} name: Test ${{ matrix.os }} with Python ${{ matrix.python }}
needs: lint
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
@@ -96,6 +117,7 @@ jobs:
test_macos: test_macos:
name: Test macOS name: Test macOS
needs: lint
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
@@ -158,6 +180,7 @@ jobs:
test_windows: test_windows:
name: Test Windows name: Test Windows
needs: lint
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
-27
View File
@@ -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
+2 -1
View File
@@ -306,7 +306,8 @@ def bump_version() -> None:
if not found_at_least_one_file_needing_update: if not found_at_least_one_file_needing_update:
print( 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) sys.exit(1)
+1
View File
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
"""Helper script for bisecting PDFs to find a page with an issue.""" """Helper script for bisecting PDFs to find a page with an issue."""
from __future__ import annotations from __future__ import annotations
import sys import sys
+1 -1
View File
@@ -109,7 +109,7 @@ def main():
doc1 = pymupdf.open(os.path.join(d, "output1.pdf")) doc1 = pymupdf.open(os.path.join(d, "output1.pdf"))
doc2 = pymupdf.open(os.path.join(d, "output2.pdf")) doc2 = pymupdf.open(os.path.join(d, "output2.pdf"))
for i, page1_2 in enumerate(zip(doc1, doc2, strict=False)): 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 page1, page2 = page1_2
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
with col1, st.container(border=True): with col1, st.container(border=True):
+1 -1
View File
@@ -63,7 +63,7 @@ def main():
doc1 = pymupdf.open(os.path.join(d, "1.pdf")) doc1 = pymupdf.open(os.path.join(d, "1.pdf"))
doc2 = pymupdf.open(os.path.join(d, "2.pdf")) doc2 = pymupdf.open(os.path.join(d, "2.pdf"))
for i, page1_2 in enumerate(zip(doc1, doc2, strict=False)): 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 page1, page2 = page1_2
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
with col1, st.container(border=True): with col1, st.container(border=True):
+2 -2
View File
@@ -13,7 +13,7 @@ import logging
import shutil import shutil
import sys import sys
import time import time
from enum import Enum from enum import StrEnum
from pathlib import Path from pathlib import Path
from typing import Annotated, Any from typing import Annotated, Any
@@ -35,7 +35,7 @@ app = cyclopts.App(name="ocrmypdf-watcher")
log = logging.getLogger('ocrmypdf-watcher') log = logging.getLogger('ocrmypdf-watcher')
class LoggingLevelEnum(str, Enum): class LoggingLevelEnum(StrEnum):
"""Enum for logging levels.""" """Enum for logging levels."""
DEBUG = "DEBUG" DEBUG = "DEBUG"
+66
View File
@@ -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
+2
View File
@@ -158,6 +158,8 @@ quote-style = "preserve"
# Developer-only tools - use `uv sync --group <name>` # Developer-only tools - use `uv sync --group <name>`
dev = [ dev = [
"mypy>=1.13.0", "mypy>=1.13.0",
"ruff>=0.14.11",
"prek>=0.4.8",
"ipykernel>=6.29.5", "ipykernel>=6.29.5",
"reportlab>=4.4.4", "reportlab>=4.4.4",
"cyclopts>=4.5.1", "cyclopts>=4.5.1",
+1
View File
@@ -10,6 +10,7 @@ This module provides font infrastructure for the fpdf2 PDF renderer. It includes
- MultiFontManager: Automatic font selection for multilingual documents - MultiFontManager: Automatic font selection for multilingual documents
- SystemFontProvider: System font discovery - SystemFontProvider: System font discovery
""" """
from __future__ import annotations from __future__ import annotations
from ocrmypdf.font.font_manager import FontManager from ocrmypdf.font.font_manager import FontManager
+1 -3
View File
@@ -274,9 +274,7 @@ class SystemFontProvider:
try: try:
matches = list(font_dir.rglob(pattern)) matches = list(font_dir.rglob(pattern))
if matches: if matches:
log.debug( log.debug("Found system font %s at %s", font_name, matches[0])
"Found system font %s at %s", font_name, matches[0]
)
return matches[0] return matches[0]
except PermissionError: except PermissionError:
# Skip directories we can't read # Skip directories we can't read
+1
View File
@@ -6,6 +6,7 @@
This module provides the PDF renderer using fpdf2 for creating This module provides the PDF renderer using fpdf2 for creating
searchable OCR text layers. searchable OCR text layers.
""" """
from __future__ import annotations from __future__ import annotations
from ocrmypdf.fpdf_renderer.renderer import ( from ocrmypdf.fpdf_renderer.renderer import (
+24 -33
View File
@@ -448,8 +448,13 @@ class Fpdf2PdfRenderer:
# entirely (slope=0, no textangle) and produced garbage text in a # entirely (slope=0, no textangle) and produced garbage text in a
# bounding box whose shape doesn't match the text content at all. # bounding box whose shape doesn't match the text content at all.
if not self._check_aspect_ratio_plausible( if not self._check_aspect_ratio_plausible(
pdf, words, font_size, slope_angle_deg, pdf,
line_size_width, line_size_height, line_language, words,
font_size,
slope_angle_deg,
line_size_width,
line_size_height,
line_language,
): ):
return return
@@ -507,13 +512,15 @@ class Fpdf2PdfRenderer:
else: else:
word_tz = 100.0 word_tz = 100.0
word_render_data.append(WordRenderData( word_render_data.append(
text=word.text, WordRenderData(
x_baseline=box_llx, text=word.text,
font_family=font_family, x_baseline=box_llx,
word_tz=word_tz, font_family=font_family,
is_rtl=word_is_rtl, word_tz=word_tz,
)) is_rtl=word_is_rtl,
)
)
if not word_render_data: if not word_render_data:
return return
@@ -561,9 +568,7 @@ class Fpdf2PdfRenderer:
if line_size_width >= line_size_height: if line_size_width >= line_size_height:
return True return True
line_text = ' '.join( line_text = ' '.join(w.text for w in words if w is not None and w.text)
w.text for w in words if w is not None and w.text
)
if not line_text: if not line_text:
return True return True
@@ -603,9 +608,7 @@ class Fpdf2PdfRenderer:
line_text[:80], line_text[:80],
) )
if not self._logged_aspect_ratio_suppression: if not self._logged_aspect_ratio_suppression:
log.info( log.info("Suppressing OCR output text with improbable aspect ratio")
"Suppressing OCR output text with improbable aspect ratio"
)
self._logged_aspect_ratio_suppression = True self._logged_aspect_ratio_suppression = True
return False return False
@@ -679,9 +682,7 @@ class Fpdf2PdfRenderer:
ops.append(f'{first_x_baseline:.2f} 0 Td') ops.append(f'{first_x_baseline:.2f} 0 Td')
else: else:
# Direct PDF coordinates # Direct PDF coordinates
page_x, page_y_fpdf = transform_point( page_x, page_y_fpdf = transform_point(baseline_matrix, first_x_baseline, 0)
baseline_matrix, first_x_baseline, 0
)
page_y_pdf = page_height - page_y_fpdf page_y_pdf = page_height - page_y_fpdf
ops.append(f'{page_x:.2f} {page_y_pdf:.2f} Td') ops.append(f'{page_x:.2f} {page_y_pdf:.2f} Td')
@@ -698,9 +699,7 @@ class Fpdf2PdfRenderer:
pdf._resource_catalog.add( pdf._resource_catalog.add(
PDFResourceType.FONT, pdf.current_font.i, pdf.page PDFResourceType.FONT, pdf.current_font.i, pdf.page
) )
ops.append( ops.append(f'/F{pdf.current_font.i} {pdf.font_size_pt:.2f} Tf')
f'/F{pdf.current_font.i} {pdf.font_size_pt:.2f} Tf'
)
prev_font_family = word.font_family prev_font_family = word.font_family
# Relative positioning (for words after the first) # Relative positioning (for words after the first)
@@ -728,12 +727,8 @@ class Fpdf2PdfRenderer:
advance = next_word.x_baseline - word.x_baseline advance = next_word.x_baseline - word.x_baseline
# Add trailing space for text extraction unless both are CJK # Add trailing space for text extraction unless both are CJK
if ( if advance > 0 and not (
advance > 0 self._is_cjk_only(word.text) and self._is_cjk_only(next_word.text)
and not (
self._is_cjk_only(word.text)
and self._is_cjk_only(next_word.text)
)
): ):
text_to_render = word.text + ' ' text_to_render = word.text + ' '
else: else:
@@ -744,9 +739,7 @@ class Fpdf2PdfRenderer:
# Use word_tz (fits word into its hOCR bbox) — Td handles # Use word_tz (fits word into its hOCR bbox) — Td handles
# inter-word gaps, so Tz should not stretch to fill them. # inter-word gaps, so Tz should not stretch to fill them.
ops.append(f'{word.word_tz:.2f} Tz') ops.append(f'{word.word_tz:.2f} Tz')
ops.append( ops.append(self._encode_shaped_text(pdf, text_to_render, word.is_rtl))
self._encode_shaped_text(pdf, text_to_render, word.is_rtl)
)
prev_x_baseline = word.x_baseline prev_x_baseline = word.x_baseline
@@ -762,9 +755,7 @@ class Fpdf2PdfRenderer:
# don't think Tz is still set from our raw operators # don't think Tz is still set from our raw operators
pdf.font_stretching = 100 pdf.font_stretching = 100
def _encode_shaped_text( def _encode_shaped_text(self, pdf: FPDF, text: str, is_rtl: bool = False) -> str:
self, pdf: FPDF, text: str, is_rtl: bool = False
) -> str:
"""Encode text using HarfBuzz text shaping for complex script support. """Encode text using HarfBuzz text shaping for complex script support.
Unlike font.encode_text() which maps unicode characters one-by-one to Unlike font.encode_text() which maps unicode characters one-by-one to
+1
View File
@@ -6,6 +6,7 @@
Derived from Derived from
https://www.loc.gov/standards/iso639-2/ascii_8bits.html https://www.loc.gov/standards/iso639-2/ascii_8bits.html
""" """
from __future__ import annotations from __future__ import annotations
from typing import NamedTuple from typing import NamedTuple
+4 -4
View File
@@ -172,10 +172,10 @@ def _interpret_contents(
name_index = defaultdict(lambda: []) name_index = defaultdict(lambda: [])
found_vector = False found_vector = False
found_text = False found_text = False
vector_ops = set('S s f F f* B B* b b*'.split()) vector_ops = set(['S', 's', 'f', 'F', 'f*', 'B', 'B*', 'b', 'b*'])
text_showing_ops = set("""TJ Tj " '""".split()) text_showing_ops = set(["TJ", "Tj", '"', "'"])
image_ops = set('BI ID EI q Q Do cm'.split()) image_ops = set(['BI', 'ID', 'EI', 'q', 'Q', 'Do', 'cm'])
color_ops = set('g rg k cs sc scn'.split()) color_ops = set(['g', 'rg', 'k', 'cs', 'sc', 'scn'])
operator_whitelist = ' '.join(vector_ops | text_showing_ops | image_ops | color_ops) operator_whitelist = ' '.join(vector_ops | text_showing_ops | image_ops | color_ops)
for n, graphobj in enumerate( for n, graphobj in enumerate(
+1
View File
@@ -59,6 +59,7 @@ def pdfsimplefont__init__(
PDFSimpleFont.__init__ = pdfsimplefont__init__ PDFSimpleFont.__init__ = pdfsimplefont__init__
def pdftype3font__pscript5_get_height(self): def pdftype3font__pscript5_get_height(self):
"""Monkeypatch for PScript5.dll PDFs. """Monkeypatch for PScript5.dll PDFs.
-1
View File
@@ -28,7 +28,6 @@ def test_language_parameter_mapped_to_languages():
Regression test for GitHub issue #1640: the Python API ignored the language Regression test for GitHub issue #1640: the Python API ignored the language
parameter, always defaulting to 'eng'. parameter, always defaulting to 'eng'.
""" """
from ocrmypdf._options import OcrOptions
from ocrmypdf.api import create_options, setup_plugin_infrastructure from ocrmypdf.api import create_options, setup_plugin_infrastructure
from ocrmypdf.cli import get_parser from ocrmypdf.cli import get_parser
+1 -1
View File
@@ -219,7 +219,7 @@ class TestFpdf2MultiPageRenderer:
for i in range(3): for i in range(3):
word = OcrElement( word = OcrElement(
ocr_class=OcrClass.WORD, ocr_class=OcrClass.WORD,
text=f"Page{i+1}", text=f"Page{i + 1}",
bbox=BoundingBox(left=100, top=100, right=200, bottom=130), bbox=BoundingBox(left=100, top=100, right=200, bottom=130),
) )
line = OcrElement( line = OcrElement(
+6 -6
View File
@@ -78,9 +78,9 @@ def test_redo_ocr_with_offset_mediabox(resources, outdir):
mediabox = list(page.MediaBox) mediabox = list(page.MediaBox)
# MediaBox origin should be preserved # MediaBox origin should be preserved
assert ( assert float(mediabox[1]) == y_offset, (
float(mediabox[1]) == y_offset f"MediaBox Y origin should be preserved at {y_offset}, got {mediabox[1]}"
), 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. # 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. # 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) nr_visible_pre = count('visible', page)
ocrmypdf._graft.strip_invisible_text(pdf, page) ocrmypdf._graft.strip_invisible_text(pdf, page)
nr_visible_post = count('visible', page) nr_visible_post = count('visible', page)
assert ( assert nr_visible_pre == nr_visible_post, (
nr_visible_pre == nr_visible_post 'Number of visible text elements did not change'
), 'Number of visible text elements did not change' )
assert count('invisible', page) == 0, 'No invisible elems left' assert count('invisible', page) == 0, 'No invisible elems left'
+1
View File
@@ -1,4 +1,5 @@
"""Test JSON serialization of OcrOptions for multiprocessing compatibility.""" """Test JSON serialization of OcrOptions for multiprocessing compatibility."""
from __future__ import annotations from __future__ import annotations
import multiprocessing import multiprocessing
+2 -4
View File
@@ -43,7 +43,7 @@ def has_devanagari_font(manager: MultiFontManager) -> bool:
# Marker for tests that require CJK fonts # Marker for tests that require CJK fonts
requires_cjk = pytest.mark.skipif( requires_cjk = pytest.mark.skipif(
"not has_cjk_font(MultiFontManager())", "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: class MockFontProvider:
"""Mock FontProvider for testing missing fonts.""" """Mock FontProvider for testing missing fonts."""
def __init__( def __init__(self, available_fonts: dict[str, FontManager], fallback: FontManager):
self, available_fonts: dict[str, FontManager], fallback: FontManager
):
"""Initialize mock font provider with given fonts.""" """Initialize mock font provider with given fonts."""
self._fonts = available_fonts self._fonts = available_fonts
self._fallback = fallback self._fallback = fallback
+13 -12
View File
@@ -10,6 +10,7 @@ This tests the fpdf2 renderer with various language groups:
- CJK (Chinese Simplified/Traditional, Japanese, Korean) - CJK (Chinese Simplified/Traditional, Japanese, Korean)
- Devanagari (Hindi, Sanskrit) - Devanagari (Hindi, Sanskrit)
""" """
from __future__ import annotations from __future__ import annotations
import shutil import shutil
@@ -208,9 +209,9 @@ class TestArabicScript:
for para in page.paragraphs: for para in page.paragraphs:
if para.language in ('ara', 'per'): if para.language in ('ara', 'per'):
# Arabic paragraphs should have RTL direction # Arabic paragraphs should have RTL direction
assert ( assert para.direction == 'rtl', (
para.direction == 'rtl' "Arabic paragraph should have RTL direction"
), "Arabic paragraph should have RTL direction" )
# ============================================================================= # =============================================================================
@@ -529,9 +530,9 @@ class TestBaselineHandling:
for line in page.lines: for line in page.lines:
if line.baseline: if line.baseline:
# Baseline should be reasonable # Baseline should be reasonable
assert ( assert -1.0 <= line.baseline.slope <= 1.0, (
-1.0 <= line.baseline.slope <= 1.0 "Baseline slope should be reasonable"
), "Baseline slope should be reasonable" )
# ============================================================================= # =============================================================================
@@ -556,9 +557,9 @@ class TestFontCoverage:
] ]
for sample in latin_samples: for sample in latin_samples:
assert multi_font_manager.has_all_glyphs( assert multi_font_manager.has_all_glyphs('NotoSans-Regular', sample), (
'NotoSans-Regular', sample f"NotoSans should cover: {sample}"
), f"NotoSans should cover: {sample}" )
def test_noto_sans_arabic_coverage(self, multi_font_manager_arabic): def test_noto_sans_arabic_coverage(self, multi_font_manager_arabic):
"""Test NotoSansArabic covers Arabic characters.""" """Test NotoSansArabic covers Arabic characters."""
@@ -602,9 +603,9 @@ class TestFontCoverage:
] ]
for sample in cjk_samples: for sample in cjk_samples:
assert multi_font_manager.has_all_glyphs( assert multi_font_manager.has_all_glyphs('NotoSansCJK-Regular', sample), (
'NotoSansCJK-Regular', sample f"NotoSansCJK should cover: {sample}"
), f"NotoSansCJK should cover: {sample}" )
if __name__ == "__main__": if __name__ == "__main__":
+4 -6
View File
@@ -511,7 +511,8 @@ class TestFpdf2PdfRendererErrors:
def test_invalid_ocr_class(self, multi_font_manager): def test_invalid_ocr_class(self, multi_font_manager):
"""Test that non-page elements are rejected.""" """Test that non-page elements are rejected."""
line = OcrElement( 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"): with pytest.raises(ValueError, match="ocr_page"):
@@ -622,9 +623,7 @@ def create_rtl_page(
OcrElement( OcrElement(
ocr_class=OcrClass.WORD, ocr_class=OcrClass.WORD,
text=text, text=text,
bbox=BoundingBox( bbox=BoundingBox(left=bbox[0], top=bbox[1], right=bbox[2], bottom=bbox[3]),
left=bbox[0], top=bbox[1], right=bbox[2], bottom=bbox[3]
),
) )
for text, bbox in words for text, bbox in words
] ]
@@ -850,8 +849,7 @@ class TestRtlTextExtraction:
decoded = ''.join(cmap.get(g, '') for g in glyph_ids) decoded = ''.join(cmap.get(g, '') for g in glyph_ids)
logical = decoded[::-1] logical = decoded[::-1]
assert logical == 'שלום', ( assert logical == 'שלום', (
f"Expected logical text 'שלום', got {logical!r} " f"Expected logical text 'שלום', got {logical!r} (stream: {decoded!r})"
f"(stream: {decoded!r})"
) )
def test_rtl_tounicode_one_to_one(self, tmp_path, multi_font_manager): def test_rtl_tounicode_one_to_one(self, tmp_path, multi_font_manager):
+18 -18
View File
@@ -160,9 +160,9 @@ def test_rotated_skew_timeout(resources, outpdf, rasterizer):
input_file = resources / 'rotated_skew.pdf' input_file = resources / 'rotated_skew.pdf'
in_pageinfo = PdfInfo(input_file)[0] in_pageinfo = PdfInfo(input_file)[0]
assert ( assert in_pageinfo.height_pixels < in_pageinfo.width_pixels, (
in_pageinfo.height_pixels < in_pageinfo.width_pixels "Expected the input page to be landscape"
), "Expected the input page to be landscape" )
assert in_pageinfo.rotation == 90, "Expected a rotated page" assert in_pageinfo.rotation == 90, "Expected a rotated page"
out = check_ocrmypdf( 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 out_pageinfo.rotation == 0, "Expected no page rotation for output"
assert ( assert in_pageinfo.width_pixels == h and in_pageinfo.height_pixels == w, (
in_pageinfo.width_pixels == h and in_pageinfo.height_pixels == w "Expected page rotation to be baked in"
), "Expected page rotation to be baked in" )
@pytest.mark.parametrize('rasterizer', ['pypdfium', 'ghostscript']) @pytest.mark.parametrize('rasterizer', ['pypdfium', 'ghostscript'])
@@ -411,16 +411,16 @@ def test_simulated_scan(outdir):
) )
with pikepdf.open(outdir / 'out.pdf') as pdf: with pikepdf.open(outdir / 'out.pdf') as pdf:
assert ( assert pdf.pages[1].mediabox[2] > pdf.pages[1].mediabox[3], (
pdf.pages[1].mediabox[2] > pdf.pages[1].mediabox[3] "Wrong orientation: not landscape"
), "Wrong orientation: not landscape" )
assert ( assert pdf.pages[3].mediabox[2] > pdf.pages[3].mediabox[3], (
pdf.pages[3].mediabox[2] > pdf.pages[3].mediabox[3] "Wrong orientation: Not landscape"
), "Wrong orientation: Not landscape" )
assert ( assert pdf.pages[0].mediabox[2] < pdf.pages[0].mediabox[3], (
pdf.pages[0].mediabox[2] < pdf.pages[0].mediabox[3] "Wrong orientation: Not portrait"
), "Wrong orientation: Not portrait" )
assert ( assert pdf.pages[2].mediabox[2] < pdf.pages[2].mediabox[3], (
pdf.pages[2].mediabox[2] < pdf.pages[2].mediabox[3] "Wrong orientation: Not portrait"
), "Wrong orientation: Not portrait" )
+5 -3
View File
@@ -103,7 +103,10 @@ class TestSystemFontProviderDirectories:
patch.object(sys, 'platform', 'win32'), patch.object(sys, 'platform', 'win32'),
patch.dict( patch.dict(
'os.environ', '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 provider._font_dirs = None # Reset cache
@@ -113,8 +116,7 @@ class TestSystemFontProviderDirectories:
assert len(dirs) == 2 assert len(dirs) == 2
assert any('Windows' in d and 'Fonts' in d for d in dir_strs) assert any('Windows' in d and 'Fonts' in d for d in dir_strs)
assert any( assert any(
'AppData' in d and 'Local' in d and 'Fonts' in d 'AppData' in d and 'Local' in d and 'Fonts' in d for d in dir_strs
for d in dir_strs
) )
def test_font_dirs_cached(self): def test_font_dirs_cached(self):
+11 -10
View File
@@ -47,10 +47,13 @@ def make_ocr_opts(input_file='a.pdf', output_file='b.pdf', **kwargs):
def test_old_tesseract_error(): def test_old_tesseract_error():
with patch( with (
'ocrmypdf._exec.tesseract.version', patch(
return_value=TesseractVersion('4.00.00alpha'), 'ocrmypdf._exec.tesseract.version',
), pytest.raises(MissingDependencyError): return_value=TesseractVersion('4.00.00alpha'),
),
pytest.raises(MissingDependencyError),
):
vd.check_options(*make_opts_pm(pdf_renderer='sandwich', language='eng')) 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') not_found.side_effect = FileNotFoundError('tesseract')
with pytest.raises(MissingDependencyError, match="Could not find program"): with pytest.raises(MissingDependencyError, match="Could not find program"):
vd.check_options(*make_opts_pm()) vd.check_options(*make_opts_pm())
assert ( assert "'tesseract' could not be executed" in caplog.text, (
"'tesseract' could not be executed" in caplog.text "Error message not printed"
), "Error message not printed" )
assert 'install' in caplog.text, "Install advice not printed" assert 'install' in caplog.text, "Install advice not printed"
not_found.assert_called() not_found.assert_called()
@@ -87,9 +90,7 @@ def test_mutex_options():
def test_optimizing(caplog): def test_optimizing(caplog):
vd.check_options( vd.check_options(*make_opts_pm(optimize=0, png_quality=18, jpeg_quality=10))
*make_opts_pm(optimize=0, png_quality=18, jpeg_quality=10)
)
assert 'will be ignored because' in caplog.text assert 'will be ignored because' in caplog.text
Generated
+90 -37
View File
@@ -1434,7 +1434,7 @@ wheels = [
[[package]] [[package]]
name = "ocrmypdf" name = "ocrmypdf"
version = "17.7.0" version = "17.8.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "deprecation" }, { name = "deprecation" },
@@ -1467,8 +1467,10 @@ dev = [
{ name = "cyclopts" }, { name = "cyclopts" },
{ name = "ipykernel" }, { name = "ipykernel" },
{ name = "mypy" }, { name = "mypy" },
{ name = "prek" },
{ name = "pygithub" }, { name = "pygithub" },
{ name = "reportlab" }, { name = "reportlab" },
{ name = "ruff" },
] ]
docs = [ docs = [
{ name = "myst-parser" }, { name = "myst-parser" },
@@ -1503,7 +1505,7 @@ requires-dist = [
{ name = "fpdf2", specifier = ">=2.8.0" }, { name = "fpdf2", specifier = ">=2.8.0" },
{ name = "img2pdf", specifier = ">=0.5" }, { name = "img2pdf", specifier = ">=0.5" },
{ name = "packaging", specifier = ">=20" }, { name = "packaging", specifier = ">=20" },
{ name = "pdfminer-six", specifier = ">=20220319" }, { name = "pdfminer-six", specifier = ">=20260107" },
{ name = "pi-heif" }, { name = "pi-heif" },
{ name = "pikepdf", specifier = ">=10" }, { name = "pikepdf", specifier = ">=10" },
{ name = "pillow", specifier = ">=10.0.1" }, { name = "pillow", specifier = ">=10.0.1" },
@@ -1523,8 +1525,10 @@ dev = [
{ name = "cyclopts", specifier = ">=4.5.1" }, { name = "cyclopts", specifier = ">=4.5.1" },
{ name = "ipykernel", specifier = ">=6.29.5" }, { name = "ipykernel", specifier = ">=6.29.5" },
{ name = "mypy", specifier = ">=1.13.0" }, { name = "mypy", specifier = ">=1.13.0" },
{ name = "prek", specifier = ">=0.4.8" },
{ name = "pygithub", specifier = ">=2.9.1" }, { name = "pygithub", specifier = ">=2.9.1" },
{ name = "reportlab", specifier = ">=4.4.4" }, { name = "reportlab", specifier = ">=4.4.4" },
{ name = "ruff", specifier = ">=0.14.11" },
] ]
docs = [ docs = [
{ name = "myst-parser", specifier = ">=4.0.1" }, { name = "myst-parser", specifier = ">=4.0.1" },
@@ -1656,7 +1660,7 @@ name = "pexpect"
version = "4.9.0" version = "4.9.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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" } 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 = [ 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" }, { 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]] [[package]]
name = "prompt-toolkit" name = "prompt-toolkit"
version = "3.0.52" 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" }, { 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]] [[package]]
name = "six" name = "six"
version = "1.17.0" version = "1.17.0"
@@ -2679,23 +2732,23 @@ resolution-markers = [
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "alabaster", marker = "python_full_version < '3.12'" }, { name = "alabaster" },
{ name = "babel", marker = "python_full_version < '3.12'" }, { name = "babel" },
{ name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils", marker = "python_full_version < '3.12'" }, { name = "docutils" },
{ name = "imagesize", marker = "python_full_version < '3.12'" }, { name = "imagesize" },
{ name = "jinja2", marker = "python_full_version < '3.12'" }, { name = "jinja2" },
{ name = "packaging", marker = "python_full_version < '3.12'" }, { name = "packaging" },
{ name = "pygments", marker = "python_full_version < '3.12'" }, { name = "pygments" },
{ name = "requests", marker = "python_full_version < '3.12'" }, { name = "requests" },
{ name = "roman-numerals", marker = "python_full_version < '3.12'" }, { name = "roman-numerals" },
{ name = "snowballstemmer", marker = "python_full_version < '3.12'" }, { name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, { name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, { name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, { name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, { name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, { name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, { 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" } 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 = [ 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'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "alabaster", marker = "python_full_version >= '3.12'" }, { name = "alabaster" },
{ name = "babel", marker = "python_full_version >= '3.12'" }, { name = "babel" },
{ name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils", marker = "python_full_version >= '3.12'" }, { name = "docutils" },
{ name = "imagesize", marker = "python_full_version >= '3.12'" }, { name = "imagesize" },
{ name = "jinja2", marker = "python_full_version >= '3.12'" }, { name = "jinja2" },
{ name = "packaging", marker = "python_full_version >= '3.12'" }, { name = "packaging" },
{ name = "pygments", marker = "python_full_version >= '3.12'" }, { name = "pygments" },
{ name = "requests", marker = "python_full_version >= '3.12'" }, { name = "requests" },
{ name = "roman-numerals", marker = "python_full_version >= '3.12'" }, { name = "roman-numerals" },
{ name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, { name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, { 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" } 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 = [ wheels = [