Tighten ruff rules and modernize style

This commit is contained in:
James R. Barlow
2026-01-27 14:04:52 -08:00
parent 6b37583674
commit c5d3ef4b17
22 changed files with 104 additions and 77 deletions
+3 -2
View File
@@ -25,10 +25,11 @@
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
from __future__ import annotations
needs_sphinx = '8'
import datetime
import datetime as dt
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@@ -63,7 +64,7 @@ master_doc = 'index'
# General information about the project.
project = 'ocrmypdf'
year = str(datetime.date.today().year)
year = str(dt.date.today().year)
copyright = (
f'{year}, James R. Barlow. '
+ 'Licensed under Creative Commons Attribution-ShareAlike 4.0'
+42 -38
View File
@@ -96,7 +96,9 @@ with st.expander("Optimization after OCR"):
png_quality = st.slider(
"PNG quality", min_value=0, max_value=100, value=75, key="png_quality"
)
jbig2_threshold = st.number_input("JBIG2 threshold", value=0.85, key="jbig2_threshold")
jbig2_threshold = st.number_input(
"JBIG2 threshold", value=0.85, key="jbig2_threshold"
)
with st.expander("Advanced options"):
jobs = st.slider(
@@ -192,45 +194,47 @@ if uploaded:
args.append(f"--jbig2-threshold={jbig2_threshold}")
if jobs:
args.append(f"--jobs={jobs}")
input_file = NamedTemporaryFile(delete=True, suffix=f"_{uploaded.name}")
input_file.write(uploaded.getvalue())
input_file.flush()
input_file.seek(0)
args.append(str(input_file.name))
output_file = NamedTemporaryFile(delete=True, suffix=".pdf")
args.append(str(output_file.name))
with NamedTemporaryFile(delete=True, suffix=f"_{uploaded.name}") as input_file:
input_file.write(uploaded.getvalue())
input_file.flush()
input_file.seek(0)
args.append(str(input_file.name))
with NamedTemporaryFile(delete=True, suffix=".pdf") as output_file:
args.append(str(output_file.name))
st.session_state['running'] = (
'run_button' in st.session_state and st.session_state.run_button
)
if st.button(
"Run OCRmyPDF",
disabled=st.session_state.get("running", False),
key='run_button',
):
st.session_state['running'] = True
args = [sys.executable, '-u', '-m', "ocrmypdf"] + args
st.session_state['running'] = (
'run_button' in st.session_state and st.session_state.run_button
)
if st.button(
"Run OCRmyPDF",
disabled=st.session_state.get("running", False),
key='run_button',
):
st.session_state['running'] = True
args = [sys.executable, '-u', '-m', "ocrmypdf"] + args
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
with st.container(border=True):
while proc.poll() is None:
line = proc.stderr.readline()
if line:
st.html("<code>" + line.decode().strip() + "</code>")
proc = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
with st.container(border=True):
while proc.poll() is None:
line = proc.stderr.readline()
if line:
st.html("<code>" + line.decode().strip() + "</code>")
if proc.returncode != 0:
st.error(f"ocrmypdf failed with exit code {proc.returncode}")
st.session_state['running'] = False
st.stop()
if proc.returncode != 0:
st.error(f"ocrmypdf failed with exit code {proc.returncode}")
st.session_state['running'] = False
st.stop()
if Path(output_file.name).stat().st_size == 0:
st.error("No output PDF file was generated")
st.stop()
if Path(output_file.name).stat().st_size == 0:
st.error("No output PDF file was generated")
st.stop()
st.download_button(
label="Download output PDF",
data=output_file.read(),
file_name=uploaded.name,
mime="application/pdf",
)
st.session_state['running'] = False
st.download_button(
label="Download output PDF",
data=output_file.read(),
file_name=uploaded.name,
mime="application/pdf",
)
st.session_state['running'] = False
+1 -4
View File
@@ -39,10 +39,7 @@ script_dir = Path(__file__).parent
# set archive_dir to a path for backup original documents. Leave empty if not required.
archive_dir = "/pdfbak"
if len(sys.argv) > 1:
start_dir = Path(sys.argv[1])
else:
start_dir = Path(".")
start_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
if len(sys.argv) > 2:
log_file = Path(sys.argv[2])
+1
View File
@@ -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
+5 -8
View File
@@ -7,12 +7,12 @@
from __future__ import annotations
import datetime as dt
import json
import logging
import shutil
import sys
import time
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Annotated, Any
@@ -48,7 +48,7 @@ class LoggingLevelEnum(str, Enum):
def get_output_path(root: Path, basename: str, output_dir_year_month: bool) -> Path:
assert '/' not in basename, "basename must not contain '/'"
if output_dir_year_month:
today = datetime.today()
today = dt.datetime.today()
output_directory_year_month = root / str(today.year) / f'{today.month:02d}'
if not output_directory_year_month.exists():
output_directory_year_month.mkdir(parents=True, exist_ok=True)
@@ -140,7 +140,7 @@ class HandleObserverEvent(PatternMatchingEventHandler):
ignore_patterns=None,
ignore_directories=False,
case_sensitive=False,
settings={},
settings=None,
):
super().__init__(
patterns=patterns,
@@ -148,7 +148,7 @@ class HandleObserverEvent(PatternMatchingEventHandler):
ignore_directories=ignore_directories,
case_sensitive=case_sensitive,
)
self._settings = settings
self._settings = settings if settings else {}
def on_any_event(self, event):
if event.event_type in ['created']:
@@ -302,10 +302,7 @@ def main(
'output_dir_year_month': output_dir_year_month,
},
)
if use_polling:
observer = PollingObserver()
else:
observer = Observer()
observer = PollingObserver() if use_polling else Observer()
observer.schedule(handler, input_dir, recursive=True)
observer.start()
print(f"Watching {input_dir} for new PDFs. Press Ctrl+C to exit.")
+3 -1
View File
@@ -4,6 +4,8 @@
"""Run the OCRmyPDF web service."""
from __future__ import annotations
import os
import sys
@@ -13,7 +15,7 @@ except ImportError:
raise ImportError(
'You need to install streamlit in the Python environment '
'to run the web service.\n'
)
) from None
if __name__ == '__main__':
os.execvp(
+17 -7
View File
@@ -130,6 +130,7 @@ exclude = ["src/ocrmypdf/_version.py"] # Autogenerated
"UP", # pyupgrade
"SIM", # simplify
"B", # flake8-bugbear
"ICN", # flake8-import-conventions
]
ignore = [
"B028", # warning with no explicit stacklevel
@@ -139,13 +140,20 @@ ignore = [
[tool.ruff.lint.isort]
known-first-party = ["ocrmypdf"]
required-imports = ["from __future__ import annotations"]
[tool.ruff.lint.flake8-import-conventions]
# Prohibit explicit imports from the 'datetime' module
banned-from = ["datetime"]
# Optionally, suggest an alias for 'import datetime' (e.g., as dt)
extend-aliases = { "datetime" = "dt" }
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.per-file-ignores]
"docs/conf.py" = ["D100", "D101", "D105"]
"tests/*.py" = ["D100", "D101", "D102", "D103", "D105"]
"tests/*.py" = ["D100", "D101", "D102", "D103", "D105", "E501"]
"misc/*.py" = ["D103", "D101", "D102"]
"src/ocrmypdf/builtin_plugins/*.py" = ["D103", "D102", "D105"]
@@ -154,11 +162,7 @@ quote-style = "preserve"
[dependency-groups]
# Developer-only tools - use `uv sync --group <name>`
dev = [
"mypy>=1.13.0",
"ipykernel>=6.29.5",
"reportlab>=4.4.4",
]
dev = ["mypy>=1.13.0", "ipykernel>=6.29.5", "reportlab>=4.4.4"]
test = [
# Core testing framework
"coverage[toml]>=6.2",
@@ -175,5 +179,11 @@ test = [
# Extended test capabilities (merged from extended_test)
"pymupdf>=1.24.14",
]
docs = ["myst-parser>=4.0.1", "sphinx", "sphinx-issues", "sphinx-rtd-theme", "sphinxcontrib-mermaid"]
docs = [
"myst-parser>=4.0.1",
"sphinx",
"sphinx-issues",
"sphinx-rtd-theme",
"sphinxcontrib-mermaid",
]
streamlit-dev = ["streamlit>=1.40.2", "streamlit-pdf-viewer>=0.0.19"]
+2
View File
@@ -2,6 +2,8 @@
# SPDX-License-Identifier: MPL-2.0
# Enforce English hegemony
from __future__ import annotations
DEFAULT_LANGUAGE = 'eng'
# Default rotation threshold
+9
View File
@@ -111,6 +111,15 @@ def rasterize_pdf(
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
Args:
input_file: The PDF file to rasterize.
output_file: The file to write the rasterized PDF to.
raster_device: The Ghostscript raster device to use to rasterize the PDF.
raster_dpi: Resolution in dots per inch at which to rasterize page.
pageno: Page number to rasterize (beginning at page 1).
page_dpi: Resolution, overriding output image DPI.
rotation: Cardinal angle, clockwise, to rotate page.
filter_vector: If True, remove vector graphics objects.
stop_on_error: If True, stop rasterizing on the first error.
use_cropbox: If True, rasterize the CropBox instead of MediaBox.
Default is False (use MediaBox).
"""
+2 -2
View File
@@ -5,9 +5,9 @@
from __future__ import annotations
import datetime as dt
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
@@ -53,7 +53,7 @@ def get_docinfo(base_pdf: Pdf, context: PdfContext) -> dict[str, str]:
pdfmark['/Creator'] = f'{PROGRAM_NAME} {OCRMYPF_VERSION} / {creator_tag}'
pdfmark['/Producer'] = f'pikepdf {PIKEPDF_VERSION}'
pdfmark['/ModDate'] = encode_pdf_date(datetime.now(timezone.utc))
pdfmark['/ModDate'] = encode_pdf_date(dt.datetime.now(dt.UTC))
return pdfmark
+1 -1
View File
@@ -50,7 +50,7 @@ from ocrmypdf._validation import (
)
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.helpers import available_cpu_count
from ocrmypdf.hocrtransform.ocr_element import OcrElement
from ocrmypdf.models.ocr_element import OcrElement
log = logging.getLogger(__name__)
+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
- SystemFontProvider: System font discovery
"""
from __future__ import annotations
from ocrmypdf.font.font_manager import FontManager
from ocrmypdf.font.font_provider import (
+1
View File
@@ -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 (
DebugRenderOptions,
-1
View File
@@ -25,7 +25,6 @@ from typing import (
import img2pdf
import pikepdf
from deprecation import deprecated
log = logging.getLogger(__name__)
+1
View File
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: MIT
"""Simple CLI for testing HOCR to PDF conversion using fpdf2 renderer."""
from __future__ import annotations
import argparse
from pathlib import Path
+1
View File
@@ -6,6 +6,7 @@
Derived from
https://www.loc.gov/standards/iso639-2/ascii_8bits.html
"""
from __future__ import annotations
from typing import NamedTuple
+4 -3
View File
@@ -23,9 +23,10 @@ class Gs106WarningFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
# Allow all records except the expected Ghostscript 10.6.x warning
if "Ghostscript 10.6.x contains JPEG encoding errors" in record.getMessage():
return False
return True
return (
"Ghostscript 10.6.x contains JPEG encoding errors"
not in record.getMessage()
)
@pytest.fixture(autouse=True)
+1
View File
@@ -1,4 +1,5 @@
"""Test JSON serialization of OcrOptions for multiprocessing compatibility."""
from __future__ import annotations
import multiprocessing
from io import BytesIO
+2 -6
View File
@@ -3,9 +3,8 @@
from __future__ import annotations
import datetime
import datetime as dt
import warnings
from datetime import timezone
from shutil import copyfile
import pikepdf
@@ -198,10 +197,7 @@ def test_creation_date_preserved(output_type, resources, infile, outpdf):
# We expect that the modified date is quite recent
date_after = decode_pdf_date(str(after['/ModDate']))
assert (
seconds_between_dates(date_after, datetime.datetime.now(timezone.utc))
< 1000
)
assert seconds_between_dates(date_after, dt.datetime.now(dt.UTC)) < 1000
@pytest.fixture
+1
View File
@@ -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
import subprocess
+1 -1
View File
@@ -119,7 +119,7 @@ def test_unpaper_args_invalid(resources, outpdf):
def test_unpaper_image_too_big(resources, outdir, caplog):
with patch('ocrmypdf._exec.unpaper.UNPAPER_IMAGE_PIXEL_LIMIT', 42):
infile = resources / 'crom.png'
unpaper.clean(infile, outdir / 'out.png', dpi=300) == infile
assert unpaper.clean(infile, outdir / 'out.png', dpi=300) == infile
assert any(
'too large for cleaning' in rec.message
+5 -3
View File
@@ -1,4 +1,6 @@
import datetime
from __future__ import annotations
import datetime as dt
import os
import shutil
import subprocess
@@ -43,8 +45,8 @@ def test_watcher(tmp_path, resources, year_month):
if year_month:
assert (
output_dir
/ f'{datetime.date.today().year}'
/ f'{datetime.date.today().month:02d}'
/ f'{dt.date.today().year}'
/ f'{dt.date.today().month:02d}'
/ 'trivial.pdf'
).exists()
else: