refactor: Create OCROptions model with Namespace compatibility

This commit introduces a new `OCROptions` class in `_options.py` that provides:
- Proper typing for OCRmyPDF options
- Pydantic validation
- Backward compatibility with `argparse.Namespace`
- Gradual migration support for the options system

Key changes:
- Added comprehensive option fields with type hints
- Implemented custom attribute access methods
- Created conversion methods between Namespace and OCROptions
- Updated type hints in multiple files to support both types
- Maintained existing validation logic

The new model allows for a step-by-step refactoring of the options handling throughout the project.

Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
James R. Barlow
2025-12-13 11:40:57 -08:00
co-authored by aider
parent 8d715c4157
commit 1579337ebe
4 changed files with 290 additions and 34 deletions
+23 -16
View File
@@ -15,6 +15,7 @@ from argparse import Namespace
from collections.abc import Sequence
from pathlib import Path
from shutil import copyfileobj
from typing import Union
import pikepdf
import PIL
@@ -22,6 +23,7 @@ from pluggy import PluginManager
from ocrmypdf._defaults import DEFAULT_LANGUAGE, DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._exec import unpaper
from ocrmypdf._options import OCROptions
from ocrmypdf.exceptions import (
BadArgsError,
InputFileError,
@@ -51,7 +53,7 @@ def check_platform() -> None:
def check_options_languages(
options: Namespace, ocr_engine_languages: list[str]
options: Union[Namespace, OCROptions], ocr_engine_languages: list[str]
) -> None:
if not options.languages:
options.languages = [DEFAULT_LANGUAGE]
@@ -81,7 +83,7 @@ def check_options_languages(
raise MissingDependencyError(msg)
def check_options_output(options: Namespace) -> None:
def check_options_output(options: Union[Namespace, OCROptions]) -> None:
if options.output_type == 'none' and options.output_file not in (os.devnull, '-'):
raise BadArgsError(
"Since you specified `--output-type none`, the output file "
@@ -90,7 +92,7 @@ def check_options_output(options: Namespace) -> None:
)
def set_lossless_reconstruction(options: Namespace) -> None:
def set_lossless_reconstruction(options: Union[Namespace, OCROptions]) -> None:
lossless_reconstruction = False
if not any(
(
@@ -110,7 +112,7 @@ def set_lossless_reconstruction(options: Namespace) -> None:
)
def check_options_sidecar(options: Namespace) -> None:
def check_options_sidecar(options: Union[Namespace, OCROptions]) -> None:
if options.sidecar == '\0':
if options.output_file == '-':
raise BadArgsError("--sidecar filename needed when output file is stdout.")
@@ -125,7 +127,7 @@ def check_options_sidecar(options: Namespace) -> None:
)
def check_options_preprocessing(options: Namespace) -> None:
def check_options_preprocessing(options: Union[Namespace, OCROptions]) -> None:
if options.clean_final:
options.clean = True
if options.unpaper_args and not options.clean:
@@ -191,7 +193,7 @@ def _pages_from_ranges(ranges: str) -> set[int]:
return set(pages)
def check_options_ocr_behavior(options: Namespace) -> None:
def check_options_ocr_behavior(options: Union[Namespace, OCROptions]) -> None:
exclusive_options = sum(
(1 if opt else 0)
for opt in (options.force_ocr, options.skip_text, options.redo_ocr)
@@ -202,7 +204,7 @@ def check_options_ocr_behavior(options: Namespace) -> None:
options.pages = _pages_from_ranges(options.pages)
def check_options_metadata(options: Namespace) -> None:
def check_options_metadata(options: Union[Namespace, OCROptions]) -> None:
docinfo = [options.title, options.author, options.keywords, options.subject]
for s in (m for m in docinfo if m):
for char in s:
@@ -215,13 +217,13 @@ def check_options_metadata(options: Namespace) -> None:
)
def check_options_pillow(options: Namespace) -> None:
def check_options_pillow(options: Union[Namespace, OCROptions]) -> None:
PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000)
if PIL.Image.MAX_IMAGE_PIXELS == 0:
PIL.Image.MAX_IMAGE_PIXELS = None # type: ignore
def _check_plugin_invariant_options(options: Namespace) -> None:
def _check_plugin_invariant_options(options: Union[Namespace, OCROptions]) -> None:
check_platform()
check_options_metadata(options)
check_options_output(options)
@@ -232,18 +234,23 @@ def _check_plugin_invariant_options(options: Namespace) -> None:
check_options_pillow(options)
def _check_plugin_options(options: Namespace, plugin_manager: PluginManager) -> None:
plugin_manager.hook.check_options(options=options)
ocr_engine_languages = plugin_manager.hook.get_ocr_engine().languages(options)
def _check_plugin_options(options: Union[Namespace, OCROptions], plugin_manager: PluginManager) -> None:
# Convert to Namespace for plugin compatibility during transition
if isinstance(options, OCROptions):
legacy_options = options.to_namespace()
else:
legacy_options = options
plugin_manager.hook.check_options(options=legacy_options)
ocr_engine_languages = plugin_manager.hook.get_ocr_engine().languages(legacy_options)
check_options_languages(options, ocr_engine_languages)
def check_options(options: Namespace, plugin_manager: PluginManager) -> None:
def check_options(options: Union[Namespace, OCROptions], plugin_manager: PluginManager) -> None:
_check_plugin_invariant_options(options)
_check_plugin_options(options, plugin_manager)
def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]:
def create_input_file(options: Union[Namespace, OCROptions], work_folder: Path) -> tuple[Path, str]:
if options.input_file == '-':
# stdin
log.info('reading file from standard input')
@@ -288,7 +295,7 @@ def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]
raise InputFileError(msg) from e
def check_requested_output_file(options: Namespace) -> None:
def check_requested_output_file(options: Union[Namespace, OCROptions]) -> None:
if options.output_file == '-':
if sys.stdout.isatty():
raise BadArgsError(
@@ -306,7 +313,7 @@ def check_requested_output_file(options: Namespace) -> None:
def report_output_file_size(
options: Namespace,
options: Union[Namespace, OCROptions],
input_file: Path,
output_file: Path,
optimize_messages: Sequence[str] | None = None,