diff --git a/pyproject.toml b/pyproject.toml index 773afa47..76aeceb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ readme = "README.md" license = "MPL-2.0" requires-python = ">=3.11" dependencies = [ - "deprecation>=2.1.0", "fpdf2>=2.8.0", "img2pdf>=0.5", "packaging>=20", @@ -24,6 +23,7 @@ dependencies = [ "pydantic>=2.12.5", "pypdfium2>=5.0.0", "rich>=13", + "typing-extensions>=4.12; python_version < '3.13'", "uharfbuzz>=0.53.2", ] authors = [{ name = "James R. Barlow", email = "james@purplerock.ca" }] @@ -101,15 +101,24 @@ filterwarnings = [ [[tool.mypy.overrides]] module = [ - 'pluggy', 'img2pdf', 'pdfminer.*', 'reportlab.*', 'fitz', 'libxmp.utils', + 'pypdfium2', + 'uharfbuzz', + 'pi_heif', ] ignore_missing_imports = true +[[tool.mypy.overrides]] +# Test functions are not required to annotate their return type (almost +# always None); it's a low-value hint that would just be noise here. +module = 'tests.*' +disallow_untyped_defs = false +disallow_incomplete_defs = false + [tool.ruff] target-version = "py311" exclude = ["src/ocrmypdf/_version.py"] # Autogenerated diff --git a/src/ocrmypdf/_exec/ghostscript.py b/src/ocrmypdf/_exec/ghostscript.py index d9fc9dc1..4dc98d72 100644 --- a/src/ocrmypdf/_exec/ghostscript.py +++ b/src/ocrmypdf/_exec/ghostscript.py @@ -105,8 +105,8 @@ def _gs_devicen_reported(stream) -> bool: def rasterize_pdf( - input_file: os.PathLike, - output_file: os.PathLike, + input_file: Path, + output_file: Path, *, raster_device: GhostscriptRasterDevice, raster_dpi: Resolution, @@ -288,7 +288,7 @@ class GhostscriptFollower: def generate_pdfa( pdf_pages, - output_file: os.PathLike, + output_file: Path, *, compression: str, color_conversion_strategy: str, diff --git a/src/ocrmypdf/_options.py b/src/ocrmypdf/_options.py index 3e956135..9018eabb 100644 --- a/src/ocrmypdf/_options.py +++ b/src/ocrmypdf/_options.py @@ -29,7 +29,7 @@ log = logging.getLogger(__name__) # Module-level registry for plugin option models # This is populated by setup_plugin_infrastructure() after plugins are loaded -_plugin_option_models: dict[str, type] = {} +_plugin_option_models: dict[str, type[BaseModel]] = {} PathOrIO = BinaryIO | IOBase | Path | str | bytes @@ -209,7 +209,6 @@ class OcrOptions(BaseModel): optimize: int = 1 jpg_quality: int | None = None png_quality: int | None = None - jbig2_threshold: float = 0.85 # Compatibility alias for plugins that expect jpeg_quality @property @@ -576,7 +575,7 @@ class OcrOptions(BaseModel): ) @classmethod - def register_plugin_models(cls, models: dict[str, type]) -> None: + def register_plugin_models(cls, models: dict[str, type[BaseModel]]) -> None: """Register plugin option model classes for nested access. Args: diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index 2da12769..0f568a39 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -28,7 +28,7 @@ from ocrmypdf._concurrent import Executor from ocrmypdf._exec import unpaper from ocrmypdf._jobcontext import PageContext, PdfContext from ocrmypdf._metadata import repair_docinfo_nuls -from ocrmypdf._options import OcrOptions, ProcessingMode, TaggedPdfMode +from ocrmypdf._options import OcrOptions, PathOrIO, ProcessingMode, TaggedPdfMode from ocrmypdf._pageboxes import log_box_repairs, repair_page_boxes from ocrmypdf._stdoutprotect import get_protected_stdout_fd from ocrmypdf.exceptions import ( @@ -1284,7 +1284,8 @@ def enumerate_compress_ranges( A tuple containing a range of indices and the corresponding element. If the element is None, the range represents a skipped range of indices. """ - skipped_from, index = None, None + skipped_from: int | None = None + index: int | None = None for index, txt_file in enumerate(iterable): index += 1 if txt_file: @@ -1296,6 +1297,9 @@ def enumerate_compress_ranges( if skipped_from is None: skipped_from = index if skipped_from is not None: + # skipped_from can only be set inside the loop above, so the loop + # must have run at least once and index is guaranteed to be an int. + assert index is not None yield (skipped_from, index), None @@ -1322,18 +1326,12 @@ def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext) -> Pat return output_file -def copy_final( - input_file: Path, output_file: str | Path | BinaryIO, original_file: Path | None -) -> None: +def copy_final(input_file: Path, output_file: PathOrIO) -> None: """Copy the final temporary file to the output destination. Args: - input_file (Path): The intermediate input file to copy. - output_file (str | Path | BinaryIO): The output file to copy to. - original_file: The original file to copy attributes from. - - Returns: - None + input_file: The intermediate input file to copy. + output_file: The output file to copy to. """ log.debug('%s -> %s', input_file, output_file) with input_file.open('rb') as input_stream: @@ -1359,5 +1357,7 @@ def copy_final( # At this point we overwrite the output_file specified by the user # use copyfileobj because then we use open() to create the file and # get the appropriate umask, ownership, etc. + # The `hasattr` check above already ruled out stream-like objects. + assert isinstance(output_file, str | bytes | os.PathLike) with open(output_file, 'w+b') as output_stream: copyfileobj(input_stream, output_stream) diff --git a/src/ocrmypdf/_pipelines/hocr_to_ocr_pdf.py b/src/ocrmypdf/_pipelines/hocr_to_ocr_pdf.py index 750901f3..a09dd259 100644 --- a/src/ocrmypdf/_pipelines/hocr_to_ocr_pdf.py +++ b/src/ocrmypdf/_pipelines/hocr_to_ocr_pdf.py @@ -98,8 +98,8 @@ def exec_hocr_to_ocr_pdf(context: PdfContext, executor: Executor) -> Sequence[st log.info("Postprocessing...") pdf, messages = postprocess(pdf, context, executor) - # Copy PDF file to destination (we don't know the input PDF file name) - copy_final(pdf, options.output_file, None) + # Copy PDF file to destination + copy_final(pdf, options.output_file) return messages @@ -109,6 +109,9 @@ def run_hocr_to_ocr_pdf_pipeline( plugin_manager: OcrmypdfPluginManager, ) -> ExitCode: """Run pipeline to convert hOCR to final output PDF.""" + # The _hocr_to_ocr_pdf() API requires work_folder: Path and stores it on + # options before this pipeline runs, so it is always set at this point. + assert options.work_folder is not None with manage_work_folder( work_folder=options.work_folder, retain=True, print_location=False ) as work_folder: diff --git a/src/ocrmypdf/_pipelines/ocr.py b/src/ocrmypdf/_pipelines/ocr.py index dd0288e7..f15f2ad6 100644 --- a/src/ocrmypdf/_pipelines/ocr.py +++ b/src/ocrmypdf/_pipelines/ocr.py @@ -145,7 +145,7 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]: if options.sidecar: text = merge_sidecars(sidecars, context) # Copy text file to destination - copy_final(text, options.sidecar, options.input_file) + copy_final(text, options.sidecar) # Merge layers to one single pdf pdf = ocrgraft.finalize() @@ -157,7 +157,7 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]: pdf, messages = postprocess(pdf, context, executor) # Copy PDF file to destination - copy_final(pdf, options.output_file, options.input_file) + copy_final(pdf, options.output_file) return messages diff --git a/src/ocrmypdf/_pipelines/pdf_to_hocr.py b/src/ocrmypdf/_pipelines/pdf_to_hocr.py index b63f67d9..eca8d703 100644 --- a/src/ocrmypdf/_pipelines/pdf_to_hocr.py +++ b/src/ocrmypdf/_pipelines/pdf_to_hocr.py @@ -8,6 +8,7 @@ from __future__ import annotations import logging import logging.handlers +import os import shutil from functools import partial @@ -91,6 +92,9 @@ def run_hocr_pipeline( """Run pipeline to output hOCR.""" if options.output_folder is None: raise ValueError("output_folder must be specified for hOCR pipeline") + # This pipeline is only reachable via the _pdf_to_hocr() API, which + # declares input_pdf: Path - streams and raw bytes paths are not supported. + assert isinstance(options.input_file, str | os.PathLike) with manage_work_folder( work_folder=options.output_folder, retain=True, print_location=False ) as work_folder: @@ -100,9 +104,7 @@ def run_hocr_pipeline( # Gather pdfinfo and create context pdfinfo = do_get_pdfinfo(origin_pdf, executor, options) - context = PdfContext( - options, work_folder, options.input_file, pdfinfo, plugin_manager - ) + context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager) # Validate options are okay for this pdf validate_pdfinfo_options(context) exec_pdf_to_hocr(context, executor) diff --git a/src/ocrmypdf/_plugin_manager.py b/src/ocrmypdf/_plugin_manager.py index f6f1abb7..c128766e 100644 --- a/src/ocrmypdf/_plugin_manager.py +++ b/src/ocrmypdf/_plugin_manager.py @@ -21,6 +21,7 @@ from pydantic import BaseModel import ocrmypdf.builtin_plugins from ocrmypdf import Executor, PdfContext, pluginspec from ocrmypdf._options import OcrOptions +from ocrmypdf._plugin_registry import PluginOptionRegistry from ocrmypdf._progressbar import ProgressBar from ocrmypdf.helpers import Resolution from ocrmypdf.pluginspec import OcrEngine @@ -53,10 +54,11 @@ class OcrmypdfPluginManager: self._plugins = plugins self._builtins = builtins self._pm = pluggy.PluginManager(*args, **kwargs) + self._option_registry: PluginOptionRegistry | None = None self._setup_plugins() @property - def pluggy(self) -> pluggy.PluginManager: + def pluggy_manager(self) -> pluggy.PluginManager: """Access the underlying pluggy.PluginManager for advanced use cases. This is useful for plugins that need to call methods like set_blocked() diff --git a/src/ocrmypdf/_validation.py b/src/ocrmypdf/_validation.py index 1f3c6116..1bfeb278 100644 --- a/src/ocrmypdf/_validation.py +++ b/src/ocrmypdf/_validation.py @@ -10,8 +10,10 @@ import logging import os import sys from collections.abc import Sequence +from collections.abc import Set as AbstractSet from pathlib import Path from shutil import copyfileobj +from typing import BinaryIO, cast import pikepdf @@ -48,7 +50,7 @@ def check_platform() -> None: def check_options_languages( - options: OcrOptions, ocr_engine_languages: list[str] + options: OcrOptions, ocr_engine_languages: AbstractSet[str] ) -> None: # Check for blocked languages first, before checking if they're installed DENIED_LANGUAGES = {'equ', 'osd'} @@ -92,7 +94,15 @@ def check_options_sidecar(options: OcrOptions) -> None: raise BadArgsError( "--sidecar filename needed when output file is /dev/null or NUL." ) - options.sidecar = options.output_file + '.txt' + elif not isinstance(options.output_file, str | Path): + # The '\0' sentinel is only ever set by the CLI, which always + # supplies output_file as a plain path - not a stream. If this + # somehow fires, the caller mixed a CLI-only sentinel with the + # stream-based API. + raise BadArgsError( + "--sidecar filename needed when output file is not a path." + ) + options.sidecar = os.fspath(options.output_file) + '.txt' if options.sidecar == options.input_file or options.sidecar == options.output_file: raise BadArgsError( "--sidecar file must be different from the input and output files" @@ -194,20 +204,24 @@ def create_input_file(options: OcrOptions, work_folder: Path) -> tuple[Path, str copyfileobj(sys.stdin.buffer, stream_buffer) return target, "stdin" elif hasattr(options.input_file, 'readable'): - if not options.input_file.readable(): + input_stream = cast(BinaryIO, options.input_file) + if not input_stream.readable(): raise InputFileError("Input file stream is not readable") log.info('reading file from input stream') target = work_folder / 'stream' with open(target, 'wb') as stream_buffer: - copyfileobj(options.input_file, stream_buffer) + copyfileobj(input_stream, stream_buffer) return target, "stream" else: + # The branches above already ruled out the stdin sentinel and + # stream-like objects, so this must be a filesystem path. + assert isinstance(options.input_file, str | bytes | os.PathLike) try: target = work_folder / 'origin' safe_symlink(options.input_file, target) - return target, os.fspath(options.input_file) + return target, os.fsdecode(options.input_file) except FileNotFoundError as e: - msg = f"File not found - {options.input_file}" + msg = f"File not found - {os.fsdecode(options.input_file)}" if running_in_docker(): # pragma: no cover msg += ( "\nDocker cannot access your working directory unless you " @@ -249,7 +263,8 @@ def check_requested_output_file(options: OcrOptions) -> None: raise OutputFileAccessError("Output stream is not writable") elif not is_file_writable(options.output_file): raise OutputFileAccessError( - f"Output file location ({options.output_file}) is not a writable file." + f"Output file location ({os.fsdecode(options.output_file)}) is not a " + "writable file." ) if ( @@ -259,7 +274,7 @@ def check_requested_output_file(options: OcrOptions) -> None: and Path(str(options.output_file)).exists() ): raise OutputFileAccessError( - f"Output file already exists: {options.output_file}\n" + f"Output file already exists: {os.fsdecode(options.output_file)}\n" "To overwrite it, omit the --no-overwrite / -n option." ) diff --git a/src/ocrmypdf/_validation_coordinator.py b/src/ocrmypdf/_validation_coordinator.py index d5440475..1d4ae23f 100644 --- a/src/ocrmypdf/_validation_coordinator.py +++ b/src/ocrmypdf/_validation_coordinator.py @@ -10,9 +10,8 @@ import os from typing import TYPE_CHECKING if TYPE_CHECKING: - import pluggy - from ocrmypdf._options import OcrOptions + from ocrmypdf._plugin_manager import OcrmypdfPluginManager log = logging.getLogger(__name__) @@ -20,9 +19,9 @@ log = logging.getLogger(__name__) class ValidationCoordinator: """Coordinates validation across plugin models and core options.""" - def __init__(self, plugin_manager: pluggy.PluginManager): + def __init__(self, plugin_manager: OcrmypdfPluginManager): self.plugin_manager = plugin_manager - self.registry = getattr(plugin_manager, '_option_registry', None) + self.registry = plugin_manager._option_registry def validate_all_options(self, options: OcrOptions) -> None: """Run comprehensive validation on all options. @@ -110,13 +109,18 @@ class ValidationCoordinator: ) # Validate output type compatibility - if options.output_type == 'none' and str(options.output_file) not in ( + output_file_display = ( + os.fsdecode(options.output_file) + if isinstance(options.output_file, bytes) + else str(options.output_file) + ) + if options.output_type == 'none' and output_file_display not in ( os.devnull, '-', ): raise ValueError( "Since you specified `--output-type none`, the output file " - f"{options.output_file} cannot be produced. Set the output file to " + f"{output_file_display} cannot be produced. Set the output file to " "`-` to suppress this message." ) diff --git a/src/ocrmypdf/api.py b/src/ocrmypdf/api.py index 9a0073b7..92b5f7fe 100644 --- a/src/ocrmypdf/api.py +++ b/src/ocrmypdf/api.py @@ -50,6 +50,8 @@ from pathlib import Path from typing import BinaryIO, overload from warnings import warn +from pydantic import BaseModel + from ocrmypdf._logging import PageNumberFilter from ocrmypdf._options import OcrOptions from ocrmypdf._pipelines.hocr_to_ocr_pdf import run_hocr_to_ocr_pdf_pipeline @@ -106,7 +108,7 @@ def setup_plugin_infrastructure( plugin_manager = get_plugin_manager(plugins) # Initialize plugins (pass the underlying pluggy manager) - plugin_manager.initialize(plugin_manager=plugin_manager.pluggy) + plugin_manager.initialize(plugin_manager=plugin_manager.pluggy_manager) # Initialize plugin option registry from ocrmypdf._plugin_registry import PluginOptionRegistry @@ -115,7 +117,7 @@ def setup_plugin_infrastructure( # Let plugins register their option models option_models = plugin_manager.register_options() - all_plugin_models: dict[str, type] = {} + all_plugin_models: dict[str, type[BaseModel]] = {} for plugin_options in option_models: if plugin_options: # Skip None returns for namespace, model_class in plugin_options.items(): diff --git a/src/ocrmypdf/builtin_plugins/pypdfium.py b/src/ocrmypdf/builtin_plugins/pypdfium.py index 58764a6d..dff51360 100644 --- a/src/ocrmypdf/builtin_plugins/pypdfium.py +++ b/src/ocrmypdf/builtin_plugins/pypdfium.py @@ -198,6 +198,7 @@ def _process_image_for_output( 'png16m', 'pngalpha', ) + format_name: Literal['PNG', 'TIFF', 'JPEG'] if raster_device_lower in png_devices: format_name = 'PNG' elif raster_device_lower in ('jpeg', 'jpeggray', 'jpg'): diff --git a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py index 42be9cf7..11b81b39 100644 --- a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py +++ b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py @@ -168,7 +168,7 @@ class TesseractOptions(BaseModel): tess.add_argument( f'--{namespace}-timeout', default=180.0, - type=numeric(float, 0), + type=numeric(float, 0.0), metavar='SECONDS', dest=f'{namespace}_timeout', help=( @@ -183,7 +183,7 @@ class TesseractOptions(BaseModel): tess.add_argument( f'--{namespace}-non-ocr-timeout', default=180.0, - type=numeric(float, 0), + type=numeric(float, 0.0), metavar='SECONDS', dest=f'{namespace}_non_ocr_timeout', help=( diff --git a/src/ocrmypdf/cli.py b/src/ocrmypdf/cli.py index 1cff9db6..df754b9d 100644 --- a/src/ocrmypdf/cli.py +++ b/src/ocrmypdf/cli.py @@ -362,7 +362,7 @@ Online documentation is located at: ) ocrsettings.add_argument( '--skip-big', - type=numeric(float, 0, 5000), + type=numeric(float, 0.0, 5000.0), metavar='MPixels', help="Skip OCR on pages larger than the specified amount of megapixels, " "but include skipped pages in final output", @@ -398,7 +398,7 @@ Online documentation is located at: advanced.add_argument( '--max-image-mpixels', action='store', - type=numeric(float, 0), + type=numeric(float, 0.0), metavar='MPixels', help="Set maximum number of megapixels to unpack before treating an image as a " "decompression bomb", @@ -438,14 +438,14 @@ Online documentation is located at: advanced.add_argument( '--rotate-pages-threshold', default=DEFAULT_ROTATE_PAGES_THRESHOLD, - type=numeric(float, 0, 1000), + type=numeric(float, 0.0, 1000.0), metavar='CONFIDENCE', help="Only rotate pages when confidence is above this value (arbitrary " "units reported by tesseract)", ) advanced.add_argument( '--fast-web-view', - type=numeric(float, 0), + type=numeric(float, 0.0), default=1.0, metavar="MEGABYTES", help="If the size of file is more than this threshold (in MB), then " diff --git a/src/ocrmypdf/helpers.py b/src/ocrmypdf/helpers.py index 9884e245..94cdc657 100644 --- a/src/ocrmypdf/helpers.py +++ b/src/ocrmypdf/helpers.py @@ -18,6 +18,7 @@ from math import isclose, isfinite from pathlib import Path from statistics import harmonic_mean from typing import ( + TYPE_CHECKING, Any, Generic, TypeVar, @@ -26,6 +27,9 @@ from typing import ( import img2pdf import pikepdf +if TYPE_CHECKING: + from _typeshed import StrOrBytesPath + log = logging.getLogger(__name__) IMG2PDF_KWARGS = dict(engine=img2pdf.Engine.pikepdf, rotation=img2pdf.Rotation.ifvalid) @@ -135,7 +139,7 @@ class Resolution(Generic[T]): return self._isclose(self.x, other.x) and self._isclose(self.y, other.y) -def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike) -> None: +def safe_symlink(input_file: StrOrBytesPath, soft_link_name: StrOrBytesPath) -> None: """Create a symbolic link at ``soft_link_name``, which references ``input_file``. Think of this as copying ``input_file`` to ``soft_link_name`` with less overhead. @@ -159,11 +163,15 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike) -> None: if os.path.lexists(soft_link_name): # do not delete or overwrite real (non-soft link) file if not os.path.islink(soft_link_name): - raise FileExistsError(f"{soft_link_name} exists and is not a link") + raise FileExistsError( + f"{os.fsdecode(soft_link_name)} exists and is not a link" + ) os.unlink(soft_link_name) if not os.path.exists(input_file): - raise FileNotFoundError(f"trying to create a broken symlink to {input_file}") + raise FileNotFoundError( + f"trying to create a broken symlink to {os.fsdecode(input_file)}" + ) if os.name == 'nt': # Don't actually use symlinks on Windows due to permission issues @@ -214,7 +222,7 @@ def available_cpu_count() -> int: return 1 -def is_file_writable(test_file: os.PathLike) -> bool: +def is_file_writable(test_file: StrOrBytesPath) -> bool: """Intentionally racy test if target is writable. We intend to write to the output file if and only if we succeed and @@ -222,7 +230,7 @@ def is_file_writable(test_file: os.PathLike) -> bool: the location is writable. """ try: - p = Path(test_file) + p = Path(os.fsdecode(test_file)) if p.is_symlink(): p = p.resolve(strict=False) diff --git a/src/ocrmypdf/pdfinfo/_worker.py b/src/ocrmypdf/pdfinfo/_worker.py index f0da8926..01c4b458 100644 --- a/src/ocrmypdf/pdfinfo/_worker.py +++ b/src/ocrmypdf/pdfinfo/_worker.py @@ -28,7 +28,7 @@ logger = logging.getLogger() worker_pdf = None # pylint: disable=invalid-name -def _pdf_pageinfo_sync_init(pdf: Pdf, infile: Path, pdfminer_loglevel): +def _pdf_pageinfo_sync_init(pdf: Pdf | None, infile: Path, pdfminer_loglevel): global worker_pdf # pylint: disable=global-statement,invalid-name pikepdf_enable_mmap() @@ -75,13 +75,13 @@ def _pdf_pageinfo_sync( def _pdf_pageinfo_concurrent( - pdf, + pdf: Pdf, executor: Executor, - max_workers: int, + max_workers: int | None, use_threads: bool, - infile, - progbar, - check_pages, + infile: Path, + progbar: bool, + check_pages: Container[int], detailed_analysis: bool = False, miner_state: PdfMinerState | None = None, ) -> Sequence[PageInfo | None]: diff --git a/src/ocrmypdf/pdfinfo/layout.py b/src/ocrmypdf/pdfinfo/layout.py index a7290c60..15ad1bfd 100644 --- a/src/ocrmypdf/pdfinfo/layout.py +++ b/src/ocrmypdf/pdfinfo/layout.py @@ -5,19 +5,19 @@ from __future__ import annotations import re +import sys from collections.abc import Iterator, Mapping from contextlib import contextmanager from math import copysign from os import PathLike from pathlib import Path -from typing import Any +from typing import Any, BinaryIO from unittest.mock import patch import pdfminer import pdfminer.encodingdb import pdfminer.pdfdevice import pdfminer.pdfinterp -from deprecation import deprecated from pdfminer.converter import PDFLayoutAnalyzer from pdfminer.layout import LAParams, LTChar, LTPage, LTTextBox from pdfminer.pdfcolor import PDFColorSpace @@ -30,6 +30,11 @@ from pdfminer.utils import Matrix, bbox2str, matrix2str from ocrmypdf.exceptions import EncryptedPdfError, InputFileError +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated + STRIP_NAME = re.compile(r'[0-9]+') @@ -57,7 +62,7 @@ def pdfsimplefont__init__( return -PDFSimpleFont.__init__ = pdfsimplefont__init__ +PDFSimpleFont.__init__ = pdfsimplefont__init__ # type: ignore[method-assign] def pdftype3font__pscript5_get_height(self): @@ -284,7 +289,7 @@ def patch_pdfminer(pscript5_mode: bool): yield -@deprecated(deprecated_in='16.6.0', details='Use PdfMinerState instead.') +@deprecated('Deprecated since 16.6.0; use PdfMinerState instead.') def get_page_analysis( infile: PathLike, pageno: int, pscript5_mode: bool ) -> LTPage | None: @@ -332,10 +337,10 @@ class PdfMinerState: self.infile = infile self.rman = pdfminer.pdfinterp.PDFResourceManager(caching=True) self.disable_boxes_flow = None - self.page_iter = None + self.page_iter: Iterator[PDFPage] | None = None self.page_cache: list[PDFPage] = [] self.pscript5_mode = pscript5_mode - self.file = None + self.file: BinaryIO | None = None def __enter__(self): """Enter the context manager.""" @@ -351,6 +356,7 @@ class PdfMinerState: def get_page_analysis(self, pageno: int): """Get the page analysis for a given page.""" + assert self.page_iter is not None, "must be used as a context manager" while len(self.page_cache) <= pageno: try: self.page_cache.append(next(self.page_iter)) diff --git a/uv.lock b/uv.lock index 5fce9aad..48fc5556 100644 --- a/uv.lock +++ b/uv.lock @@ -576,18 +576,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] -[[package]] -name = "deprecation" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, -] - [[package]] name = "docstring-parser" version = "0.18.0" @@ -1437,7 +1425,6 @@ name = "ocrmypdf" version = "17.8.0" source = { editable = "." } dependencies = [ - { name = "deprecation" }, { name = "fpdf2" }, { name = "img2pdf" }, { name = "packaging" }, @@ -1449,6 +1436,7 @@ dependencies = [ { name = "pydantic" }, { name = "pypdfium2" }, { name = "rich" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "uharfbuzz" }, ] @@ -1501,7 +1489,6 @@ test = [ [package.metadata] requires-dist = [ { name = "cyclopts", marker = "extra == 'watcher'", specifier = ">=3" }, - { name = "deprecation", specifier = ">=2.1.0" }, { name = "fpdf2", specifier = ">=2.8.0" }, { name = "img2pdf", specifier = ">=0.5" }, { name = "packaging", specifier = ">=20" }, @@ -1515,6 +1502,7 @@ requires-dist = [ { name = "python-dotenv", marker = "extra == 'watcher'" }, { name = "rich", specifier = ">=13" }, { name = "streamlit", marker = "extra == 'webservice'", specifier = ">=1.41.0" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'", specifier = ">=4.12" }, { name = "uharfbuzz", specifier = ">=0.53.2" }, { name = "watchdog", marker = "extra == 'watcher'", specifier = ">=1.0.2" }, ]