Additional ruff fixes

This commit is contained in:
James R. Barlow
2023-04-14 01:25:16 -07:00
parent 1d0e4e7c9f
commit 4924b11b6b
21 changed files with 113 additions and 22 deletions
+30
View File
@@ -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',
]
+6
View File
@@ -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):
+1 -1
View File
@@ -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.
+6 -4
View File
@@ -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
+2 -2
View File
@@ -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
+1
View File
@@ -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 = []
@@ -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))
@@ -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,
+9
View File
@@ -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
+23 -1
View File
@@ -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")
+3 -8
View File
@@ -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(
+8
View File
@@ -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)
+2
View File
@@ -7,3 +7,5 @@
from __future__ import annotations
from ocrmypdf.pdfinfo.info import Colorspace, Encoding, PageInfo, PdfInfo
__all__ = ["Colorspace", "Encoding", "PageInfo", "PdfInfo"]
+3
View File
@@ -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.
"""
+1 -1
View File
@@ -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()