Fix mypy errors: drop deprecation dep, fix PathOrIO union bugs

Reduces mypy errors in src/ocrmypdf and tests from 89 to 51.

- Replace the `deprecation` package with stdlib `warnings.deprecated`
  (falling back to typing_extensions on <3.13); drop the dependency.
- Add pypdfium2/uharfbuzz/pi_heif to mypy's ignore_missing_imports
  overrides (no upstream stubs); drop pluggy, which now ships py.typed.
- Add a tests.* mypy override so test functions aren't required to
  annotate -> None.

Real bugs found and fixed along the way, not just annotations:
- OcrmypdfPluginManager had a `pluggy` property shadowing the `pluggy`
  module import within its own class body, breaking every
  `pluggy.PluginManager` annotation below it; renamed to
  `pluggy_manager`.
- `_option_registry` was bolted onto OcrmypdfPluginManager from outside
  and read via `getattr(..., None)` instead of being a declared
  attribute; declared it properly.
- ValidationCoordinator.__init__ was typed to accept a raw
  pluggy.PluginManager, but every caller passes the OcrmypdfPluginManager
  wrapper.
- check_options_sidecar() did `options.output_file + '.txt'`, assuming
  output_file is always a str; would raise a raw TypeError if ever hit
  with a stream/bytes output. Added an explicit guard.
- is_file_writable() called Path(test_file), which raises TypeError on
  a bytes path; fixed via os.fsdecode().
- copy_final() had a dead, unused `original_file` parameter; removed it.
- run_hocr_pipeline() constructed PdfContext with the raw, untriaged
  input_file instead of the locally-copied origin_pdf, inconsistent
  with the other two pipelines.
- _options.py had jbig2_threshold declared twice in the same model.
This commit is contained in:
James R. Barlow
2026-07-07 00:25:33 -07:00
parent 273826377e
commit dfdb32995e
18 changed files with 119 additions and 80 deletions
+11 -2
View File
@@ -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
+3 -3
View File
@@ -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,
+2 -3
View File
@@ -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:
+11 -11
View File
@@ -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)
+5 -2
View File
@@ -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:
+2 -2
View File
@@ -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
+5 -3
View File
@@ -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)
+3 -1
View File
@@ -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()
+23 -8
View File
@@ -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."
)
+10 -6
View File
@@ -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."
)
+4 -2
View File
@@ -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():
+1
View File
@@ -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'):
@@ -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=(
+4 -4
View File
@@ -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 "
+13 -5
View File
@@ -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)
+6 -6
View File
@@ -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]:
+12 -6
View File
@@ -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))
Generated
+2 -14
View File
@@ -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" },
]