From 4924b11b6b248e5bd8de016cc702bfe57081e2eb Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Fri, 14 Apr 2023 01:23:57 -0700 Subject: [PATCH] Additional ruff fixes --- misc/watcher.py | 2 ++ misc/webservice.py | 2 +- pyproject.toml | 7 ++++- src/ocrmypdf/__init__.py | 30 +++++++++++++++++++++ src/ocrmypdf/__main__.py | 6 +++++ src/ocrmypdf/_concurrent.py | 2 +- src/ocrmypdf/_exec/tesseract.py | 10 ++++--- src/ocrmypdf/_pipeline.py | 4 +-- src/ocrmypdf/api.py | 1 + src/ocrmypdf/builtin_plugins/concurrency.py | 5 ++++ src/ocrmypdf/builtin_plugins/ghostscript.py | 3 +++ src/ocrmypdf/extra_plugins/semfree.py | 9 +++++++ src/ocrmypdf/helpers.py | 24 ++++++++++++++++- src/ocrmypdf/hocrtransform.py | 11 +++----- src/ocrmypdf/optimize.py | 8 ++++++ src/ocrmypdf/pdfinfo/__init__.py | 2 ++ src/ocrmypdf/pluginspec.py | 3 +++ src/ocrmypdf/subprocess/_windows.py | 2 +- tests/test_main.py | 2 +- tests/test_metadata.py | 1 - tests/test_pipeline.py | 1 - 21 files changed, 113 insertions(+), 22 deletions(-) diff --git a/misc/watcher.py b/misc/watcher.py index 9e5ea4d8..a04960b4 100644 --- a/misc/watcher.py +++ b/misc/watcher.py @@ -3,6 +3,8 @@ # SPDX-FileCopyrightText: 2020 James R Barlow # SPDX-License-Identifier: MIT +"""Watch a directory for new PDFs and OCR them.""" + from __future__ import annotations import json diff --git a/misc/webservice.py b/misc/webservice.py index 4d126a44..2675a670 100644 --- a/misc/webservice.py +++ b/misc/webservice.py @@ -48,7 +48,7 @@ def do_ocrmypdf(file): return Response("--sidecar not supported", 501, mimetype='text/plain') ocrmypdf_args = ["ocrmypdf", *cmd_args, up_file, down_file] - proc = run(ocrmypdf_args, capture_output=True, encoding="utf-8") + proc = run(ocrmypdf_args, capture_output=True, encoding="utf-8", check=False) if proc.returncode != 0: stderr = proc.stderr return Response(stderr, 400, mimetype='text/plain') diff --git a/pyproject.toml b/pyproject.toml index ac42a43c..28121056 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -200,4 +200,9 @@ known-first-party = ["ocrmypdf"] required-imports = ["from __future__ import annotations"] [tool.ruff.pydocstyle] -convention = "google" \ No newline at end of file +convention = "google" + +[tool.ruff.per-file-ignores] +"docs/conf.py" = ["D100", "D101", "D105"] +"tests/*.py" = ["D100", "D101", "D102", "D103", "D105"] +"misc/*.py" = ["D103", "D101", "D102"] \ No newline at end of file diff --git a/src/ocrmypdf/__init__.py b/src/ocrmypdf/__init__.py index 25c3039d..2098958a 100644 --- a/src/ocrmypdf/__init__.py +++ b/src/ocrmypdf/__init__.py @@ -29,3 +29,33 @@ from ocrmypdf.exceptions import ( from ocrmypdf.pluginspec import OcrEngine, OrientationConfidence hookimpl = _HookimplMarker('ocrmypdf') + +__all__ = [ + '__version__', + 'BadArgsError', + 'configure_logging', + 'DpiError', + 'EncryptedPdfError', + 'Executor', + 'ExitCode', + 'ExitCodeException', + 'helpers', + 'hocrtransform', + 'hookimpl', + 'InputFileError', + 'MissingDependencyError', + 'ocr', + 'OcrEngine', + 'OrientationConfidence', + 'OutputFileAccessError', + 'PageContext', + 'pdfa', + 'PdfContext', + 'pdfinfo', + 'PriorOcrFoundError', + 'PROGRAM_NAME', + 'SubprocessOutputError', + 'TesseractConfigError', + 'UnsupportedImageFormatError', + 'Verbosity', +] diff --git a/src/ocrmypdf/__main__.py b/src/ocrmypdf/__main__.py index 759307bc..eb570d18 100755 --- a/src/ocrmypdf/__main__.py +++ b/src/ocrmypdf/__main__.py @@ -28,10 +28,16 @@ log = logging.getLogger('ocrmypdf') def sigbus(*args): + """Handle SIGBUS signals. + + pikepdf, depending on configuration, may use mmap so SIGBUS is a + possibility. + """ raise InputFileError("Lost access to the input file") def run(args=None): + """Run the ocrmypdf command line interface.""" _parser, options, plugin_manager = get_parser_options_plugins(args=args) with suppress(AttributeError, PermissionError): diff --git a/src/ocrmypdf/_concurrent.py b/src/ocrmypdf/_concurrent.py index 51aa994b..643d18e4 100644 --- a/src/ocrmypdf/_concurrent.py +++ b/src/ocrmypdf/_concurrent.py @@ -59,7 +59,7 @@ class Executor(ABC): heavily, and parallelizing it with threads is not expected to be performant). max_workers: The maximum number of workers that should be run. - tdqm_kwargs: Arguments to set up the progress bar. + tqdm_kwargs: Arguments to set up the progress bar. worker_initializer: Called when a worker is initialized, in the worker's execution context. If the child workers are processes, it must be possible to marshall/pickle the worker initializer. diff --git a/src/ocrmypdf/_exec/tesseract.py b/src/ocrmypdf/_exec/tesseract.py index cafd66a0..b22d7782 100644 --- a/src/ocrmypdf/_exec/tesseract.py +++ b/src/ocrmypdf/_exec/tesseract.py @@ -54,7 +54,7 @@ TESSERACT_THRESHOLDING_METHODS: dict[str, int] = { class TesseractLoggerAdapter(logging.LoggerAdapter): - "Prepend [tesseract] to messages emitted from tesseract." + """Prepend [tesseract] to messages emitted from tesseract.""" def process(self, msg, kwargs): kwargs['extra'] = self.extra @@ -106,7 +106,8 @@ TESSERACT_VERSION_PATTERN = r""" class TesseractVersion(Version): - "Modify standard packaging.Version regex to support Tesseract idiosyncrasies." + """Modify standard packaging.Version regex to support Tesseract idiosyncrasies.""" + _regex = re.compile( r"^\s*" + TESSERACT_VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE ) @@ -282,8 +283,9 @@ def page_timedout(timeout: float) -> None: def _generate_null_hocr(output_hocr: Path, output_text: Path, image: Path) -> None: - """Produce a .hocr file that reports no text detected on a page that is - the same size as the input image. + """Produce a .hocr file that reports no text detected. + + Ensures page is the same size as the input image. """ with Image.open(image) as im: w, h = im.size diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index 121c4c49..c0333704 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -221,7 +221,7 @@ def _vector_page_dpi(pageinfo: PageInfo) -> int: def get_page_dpi(pageinfo: PageInfo, options) -> Resolution: - "Get the DPI when nonsquare DPI is tolerable." + """Get the DPI when nonsquare DPI is tolerable.""" xres = max( pageinfo.dpi.x or VECTOR_PAGE_DPI, options.oversample or 0.0, @@ -236,7 +236,7 @@ def get_page_dpi(pageinfo: PageInfo, options) -> Resolution: def get_page_square_dpi(pageinfo: PageInfo, options) -> Resolution: - "Get the DPI when we require xres == yres, scaled to physical units." + """Get the DPI when we require xres == yres, scaled to physical units.""" xres = pageinfo.dpi.x or 0.0 yres = pageinfo.dpi.y or 0.0 userunit = float(pageinfo.userunit) or 1.0 diff --git a/src/ocrmypdf/api.py b/src/ocrmypdf/api.py index f0bd7175..e27a2c22 100644 --- a/src/ocrmypdf/api.py +++ b/src/ocrmypdf/api.py @@ -142,6 +142,7 @@ def configure_logging( def create_options( *, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs ): + """Construct an options object from the input/output files and keyword arguments.""" cmdline = [] deferred = [] diff --git a/src/ocrmypdf/builtin_plugins/concurrency.py b/src/ocrmypdf/builtin_plugins/concurrency.py index f5e69f0e..d9b186f8 100644 --- a/src/ocrmypdf/builtin_plugins/concurrency.py +++ b/src/ocrmypdf/builtin_plugins/concurrency.py @@ -54,6 +54,7 @@ def log_listener(q: Queue): def process_sigbus(*args): + """Handle SIGBUS signal at the worker level.""" raise InputFileError("A worker process lost access to an input file") @@ -80,6 +81,7 @@ def process_init(q: Queue, user_init: UserInit, loglevel) -> None: def thread_init(q: Queue, user_init: UserInit, loglevel) -> None: + """Begin a thread pool worker.""" del q # unused but required argument del loglevel # unused but required argument # As a thread, block SIGBUS so the main thread deals with it... @@ -162,14 +164,17 @@ class StandardExecutor(Executor): @hookimpl def get_executor(progressbar_class): + """Return the default executor.""" return StandardExecutor(pbar_class=progressbar_class) @hookimpl def get_progressbar_class(): + """Return the default progress bar class.""" return tqdm @hookimpl def get_logging_console(): + """Return the default logging console handler.""" return logging.StreamHandler(stream=TqdmConsole(sys.stderr)) diff --git a/src/ocrmypdf/builtin_plugins/ghostscript.py b/src/ocrmypdf/builtin_plugins/ghostscript.py index 60a3d52b..aa3e5f44 100644 --- a/src/ocrmypdf/builtin_plugins/ghostscript.py +++ b/src/ocrmypdf/builtin_plugins/ghostscript.py @@ -16,6 +16,7 @@ log = logging.getLogger(__name__) @hookimpl def check_options(options): + """Check that the options are valid for this plugin.""" check_external_program( program='gs', package='ghostscript', @@ -45,6 +46,7 @@ def rasterize_pdf_page( rotation, filter_vector, ): + """Rasterize a single page of a PDF file using Ghostscript.""" ghostscript.rasterize_pdf( input_file, output_file, @@ -68,6 +70,7 @@ def generate_pdfa( pdfa_part, progressbar_class, ): + """Generate a PDF/A from the list of PDF pages and PDF/A metadata.""" ghostscript.generate_pdfa( pdf_pages=[*pdf_pages, pdfmark], output_file=output_file, diff --git a/src/ocrmypdf/extra_plugins/semfree.py b/src/ocrmypdf/extra_plugins/semfree.py index 89ff3a0c..231f5f24 100644 --- a/src/ocrmypdf/extra_plugins/semfree.py +++ b/src/ocrmypdf/extra_plugins/semfree.py @@ -54,6 +54,7 @@ def split_every(n: int, iterable: Iterable) -> Iterator: def process_sigbus(*args): + """Handle SIGBUS signal at the worker level.""" raise InputFileError("A worker process lost access to an input file") @@ -61,12 +62,14 @@ class ConnectionLogHandler(logging.handlers.QueueHandler): """Handler used by child processes to forward log messages to parent.""" def __init__(self, conn: Connection) -> None: + """Initialize the handler.""" # sets the parent's queue to None - parent only touches queue # in enqueue() which we override super().__init__(None) # type: ignore self.conn = conn def enqueue(self, record): + """Enqueue a log message.""" self.conn.send(('log', record)) @@ -184,14 +187,20 @@ class LambdaExecutor(Executor): @hookimpl def get_executor(progressbar_class): + """Return a LambdaExecutor instance.""" return LambdaExecutor(pbar_class=progressbar_class) @hookimpl def get_logging_console(): + """Return a logging.StreamHandler instance.""" return logging.StreamHandler() @hookimpl def get_progressbar_class(): + """Return a NullProgressBar instance. + + This executor cannot use a progress bar. + """ return NullProgressBar diff --git a/src/ocrmypdf/helpers.py b/src/ocrmypdf/helpers.py index 965cc973..8a8e0c40 100644 --- a/src/ocrmypdf/helpers.py +++ b/src/ocrmypdf/helpers.py @@ -49,6 +49,7 @@ class Resolution(Generic[T]): __slots__ = ('x', 'y') def __init__(self, x: T, y: T): + """Construct a Resolution object.""" self.x = x self.y = y @@ -57,9 +58,11 @@ class Resolution(Generic[T]): CONVERSION_ERROR = 0.002 def round(self, ndigits: int) -> Resolution: + """Round to ndigits after the decimal point.""" return Resolution(round(self.x, ndigits), round(self.y, ndigits)) def to_int(self) -> Resolution[int]: + """Round to nearest integer.""" return Resolution(int(round(self.x)), int(round(self.y))) @classmethod @@ -68,10 +71,12 @@ class Resolution(Generic[T]): @property def is_square(self) -> bool: + """True if the resolution is square (x == y).""" return self._isclose(self.x, self.y) @property def is_finite(self) -> bool: + """True if both x and y are finite numbers.""" if isinstance(self.x, SupportsFloat) and isinstance(self.y, SupportsFloat): return isfinite(self.x) and isfinite(self.y) return True @@ -79,6 +84,7 @@ class Resolution(Generic[T]): def take_max( self, vals: Iterable[Any], yvals: Iterable[Any] | None = None ) -> Resolution: + """Return a new Resolution object with the maximum resolution of inputs.""" if yvals is not None: return Resolution(max(self.x, *vals), max(self.y, *yvals)) max_x, max_y = self.x, self.y @@ -88,18 +94,23 @@ class Resolution(Generic[T]): return Resolution(max_x, max_y) def flip_axis(self) -> Resolution[T]: + """Return a new Resolution object with x and y swapped.""" return Resolution(self.y, self.x) def __getitem__(self, idx: int | slice) -> T: + """Support [0] and [1] indexing.""" return (self.x, self.y)[idx] def __str__(self): + """Return a string representation of the resolution.""" return f"{self.x:f}x{self.y:f}" def __repr__(self): # pragma: no cover + """Return a repr() of the resolution.""" return f"Resolution({self.x}x{self.y} dpi)" def __eq__(self, other): + """Return True if the resolution is equal to another resolution.""" if isinstance(other, tuple) and len(other) == 2: other = Resolution(*other) if not isinstance(other, Resolution): @@ -153,6 +164,10 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike): def samefile(file1: os.PathLike, file2: os.PathLike): + """Return True if two files are the same file. + + Attempts to account for different relative paths to the same file. + """ if os.name == 'nt': return file1 == file2 else: @@ -273,13 +288,20 @@ def clamp(n, smallest, largest): # mypy doesn't understand types for this def remove_all_log_handlers(logger): - "Remove all log handlers, usually used in a child process." + """Remove all log handlers, usually used in a child process. + + The child process inherits the log handlers from the parent process when + a fork occurs. Typically we want to remove all log handlers in the child + process so that the child process can set up a single queue handler to + forward log messages to the parent process. + """ for handler in logger.handlers[:]: logger.removeHandler(handler) handler.close() # To ensure handlers with opened resources are released def pikepdf_enable_mmap(): + """Enable pikepdf mmap.""" # try: # if pikepdf._qpdf.set_access_default_mmap(True): # log.debug("pikepdf mmap enabled") diff --git a/src/ocrmypdf/hocrtransform.py b/src/ocrmypdf/hocrtransform.py index 28e33c01..0380d90f 100755 --- a/src/ocrmypdf/hocrtransform.py +++ b/src/ocrmypdf/hocrtransform.py @@ -100,6 +100,7 @@ class HocrTransformError(Exception): class HocrTransform: """A class for converting documents from the hOCR format. + For details of the hOCR format, see: http://kba.cloud/hocr-spec/. """ @@ -117,6 +118,7 @@ class HocrTransform: ) def __init__(self, *, hocr_filename: str | Path, dpi: float): + """Initialize the HocrTransform object.""" self.dpi = dpi self.hocr = ElementTree.parse(os.fspath(hocr_filename)) @@ -196,14 +198,6 @@ class HocrTransform: """Replaces characters with those available in the Helvetica typeface.""" return s.translate(cls.ligatures) - def topdown_position(self, element): - pxl_line_coords = self.element_coordinates(element) - line_box = self.pt_from_pixel(pxl_line_coords) - # Coordinates here are still in the hocr coordinate system, so 0 on the y axis - # is the top of the page and increasing values of y will move towards the - # bottom of the page. - return line_box.y2 - def to_pdf( self, *, @@ -306,6 +300,7 @@ class HocrTransform: @classmethod def polyval(cls, poly, x): # pragma: no cover + """Calculate the value of a polynomial at a point.""" return x * poly[0] + poly[1] def _do_line( diff --git a/src/ocrmypdf/optimize.py b/src/ocrmypdf/optimize.py index 264f6277..47c4f130 100644 --- a/src/ocrmypdf/optimize.py +++ b/src/ocrmypdf/optimize.py @@ -53,20 +53,24 @@ class XrefExt(NamedTuple): def img_name(root: Path, xref: Xref, ext: str) -> Path: + """Return the name of an image file for a given xref and extension.""" return root / f'{xref:08d}{ext}' def png_name(root: Path, xref: Xref) -> Path: + """Return the name of a PNG file for a given xref.""" return img_name(root, xref, '.png') def jpg_name(root: Path, xref: Xref) -> Path: + """Return the name of a JPEG file for a given xref.""" return img_name(root, xref, '.jpg') def extract_image_filter( pike: Pdf, root: Path, image: Stream, xref: Xref ) -> tuple[PdfImage, tuple[Name, Object]] | None: + """Determine if an image is extractable.""" del pike # unused args del root @@ -124,6 +128,7 @@ def extract_image_filter( def extract_image_jbig2( *, pike: Pdf, root: Path, image: Stream, xref: Xref, options ) -> XrefExt | None: + """Extract an image, saving it as a JBIG2 file.""" del options # unused arg result = extract_image_filter(pike, root, image, xref) @@ -165,6 +170,7 @@ def extract_image_jbig2( def extract_image_generic( *, pike: Pdf, root: Path, image: Stream, xref: Xref, options ) -> XrefExt | None: + """Generic image extraction.""" result = extract_image_filter(pike, root, image, xref) if result is None: return None @@ -420,6 +426,8 @@ def _optimize_jpeg(args: tuple[Xref, Path, Path, int]) -> tuple[Xref, Path | Non def transcode_jpegs( pike: Pdf, jpegs: Sequence[Xref], root: Path, options, executor: Executor ) -> None: + """Optimize JPEGs according to optimization settings.""" + def jpeg_args() -> Iterator[tuple[Xref, Path, Path, int]]: for xref in jpegs: in_jpg = jpg_name(root, xref) diff --git a/src/ocrmypdf/pdfinfo/__init__.py b/src/ocrmypdf/pdfinfo/__init__.py index 0ee64401..fe896337 100644 --- a/src/ocrmypdf/pdfinfo/__init__.py +++ b/src/ocrmypdf/pdfinfo/__init__.py @@ -7,3 +7,5 @@ from __future__ import annotations from ocrmypdf.pdfinfo.info import Colorspace, Encoding, PageInfo, PdfInfo + +__all__ = ["Colorspace", "Encoding", "PageInfo", "PdfInfo"] diff --git a/src/ocrmypdf/pluginspec.py b/src/ocrmypdf/pluginspec.py index acbbc07a..de7c19fe 100644 --- a/src/ocrmypdf/pluginspec.py +++ b/src/ocrmypdf/pluginspec.py @@ -427,6 +427,9 @@ class OcrEngine(ABC): a single page PDF with no visible content of any kind, sized to the dimensions implied by the input_file's width, height and DPI. The image will be grafted onto the input PDF page. + output_text: The expected name of a text file containing the + recognized text. + options: The command line options. """ diff --git a/src/ocrmypdf/subprocess/_windows.py b/src/ocrmypdf/subprocess/_windows.py index b199427d..a792bdc8 100644 --- a/src/ocrmypdf/subprocess/_windows.py +++ b/src/ocrmypdf/subprocess/_windows.py @@ -187,7 +187,7 @@ def fix_windows_args(program: str, args, env): def unique_everseen(iterable: Iterable[T], key: Callable[[T], Tkey]) -> Iterator[T]: - "List unique elements, preserving order." + """List unique elements, preserving order.""" # unique_everseen('AAAABBBCCDAABBB') --> A B C D # unique_everseen('ABBCcAD', str.lower) --> A B C D seen: set[Tkey] = set() diff --git a/tests/test_main.py b/tests/test_main.py index 06038809..8f310059 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -446,7 +446,7 @@ def test_linearized_pdf_and_indirect_object(resources, outpdf): def test_very_high_dpi(resources, outpdf): - "Checks for a Decimal quantize error with high DPI, etc." + """Checks for a Decimal quantize error with high DPI, etc.""" check_ocrmypdf( resources / '2400dpi.pdf', outpdf, diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 1adb60fd..8ed5d6aa 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -87,7 +87,6 @@ def test_override_metadata(output_type, resources, outpdf): def test_high_unicode(resources, no_outpdf): - # Ghostscript doesn't support high Unicode, so neither do we, to be # safe input_file = resources / 'c02-22.pdf' diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index b3fd7d08..518daf35 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -42,7 +42,6 @@ VECTOR_RESOLUTION = Resolution(_pipeline.VECTOR_PAGE_DPI, _pipeline.VECTOR_PAGE_ ], ) def test_dpi_needed(image, text, vector, result, rgb_image, outdir): - c = Canvas(str(outdir / 'dpi.pdf'), pagesize=(5 * inch, 5 * inch)) if image: c.drawImage(rgb_image, 1 * inch, 1 * inch, width=1 * inch, height=1 * inch)