diff --git a/docs/api.md b/docs/api.md index 8defcbbe..b3f52954 100644 --- a/docs/api.md +++ b/docs/api.md @@ -122,10 +122,10 @@ When OCRmyPDF succeeds conditionally, it returns an integer exit code. Starting in OCRmyPDF v16.13.0, the plugin interface has been updated: -- Plugin hooks now receive `OCROptions` objects instead of `argparse.Namespace` -- `OCROptions` provides the same attribute access as `Namespace` (duck-typing compatible) -- Plugin developers should update type hints: `from ocrmypdf._options import OCROptions` +- Plugin hooks now receive `OcrOptions` objects instead of `argparse.Namespace` +- `OcrOptions` provides the same attribute access as `Namespace` (duck-typing compatible) +- Plugin developers should update type hints: `from ocrmypdf._options import OcrOptions` - Built-in plugins no longer modify options in-place for better immutability Most existing plugins will continue working without modification due to the -duck-typing compatibility between `OCROptions` and `Namespace`. +duck-typing compatibility between `OcrOptions` and `Namespace`. diff --git a/docs/apiref.md b/docs/apiref.md index c4728bc0..239b7d75 100644 --- a/docs/apiref.md +++ b/docs/apiref.md @@ -17,7 +17,7 @@ should be mainly of interest to plugin developers. ```{eval-rst} .. automodule:: ocrmypdf._options - :members: OCROptions + :members: OcrOptions ``` ## ocrmypdf.exceptions diff --git a/docs/plugins.md b/docs/plugins.md index 982a8a8e..8f3c3f28 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -185,7 +185,7 @@ Both access patterns are equivalent and return the same values. :::{note} **Plugin Interface Change**: Starting in OCRmyPDF v16.13.0, plugin hooks receive -`OCROptions` objects instead of `argparse.Namespace` objects. Most plugins will +`OcrOptions` objects instead of `argparse.Namespace` objects. Most plugins will continue working due to duck-typing compatibility, but plugin developers should update their type hints accordingly. ::: diff --git a/docs/release_notes.md b/docs/release_notes.md index e619cea6..2303c7e6 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -29,24 +29,24 @@ official when it's tagged and posted to PyPI. **Breaking changes** -- **Plugin interface migration**: Plugin hooks now receive `OCROptions` objects instead of +- **Plugin interface migration**: Plugin hooks now receive `OcrOptions` objects instead of `argparse.Namespace` objects. Most plugins will continue working due to duck-typing compatibility, but plugin developers should update their type hints from `Namespace` - to `OCROptions`. + to `OcrOptions`. - Built-in plugins no longer modify options in-place, improving immutability and code clarity. **API improvements** -- Centralized validation logic in the `OCROptions` Pydantic model +- Centralized validation logic in the `OcrOptions` Pydantic model - Removed scattered option mutation throughout the codebase - Better type safety for plugin development - Simplified plugin option handling **Migration guide for plugin developers** -- Update imports: `from ocrmypdf._options import OCROptions` -- Update type hints: `def check_options(options: OCROptions)` instead of `options: Namespace` +- Update imports: `from ocrmypdf._options import OcrOptions` +- Update type hints: `def check_options(options: OcrOptions)` instead of `options: Namespace` - Attribute access remains unchanged: `options.languages`, `options.output_type`, etc. - Remove any in-place option modifications - compute values at point of use instead - Most existing plugins will continue working without changes due to duck-typing diff --git a/src/ocrmypdf/_jobcontext.py b/src/ocrmypdf/_jobcontext.py index c680cc7c..15ebd5e6 100644 --- a/src/ocrmypdf/_jobcontext.py +++ b/src/ocrmypdf/_jobcontext.py @@ -9,7 +9,7 @@ from collections.abc import Iterator from pathlib import Path from typing import TYPE_CHECKING -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf.pdfinfo import PdfInfo from ocrmypdf.pdfinfo.info import PageInfo @@ -20,14 +20,16 @@ if TYPE_CHECKING: class PdfContext: """Holds the context for a particular run of the pipeline.""" - options: OCROptions #: The specified options for processing this PDF. + options: OcrOptions #: The specified options for processing this PDF. origin: Path #: The filename of the original input file. pdfinfo: PdfInfo #: Detailed data for this PDF. - plugin_manager: OcrmypdfPluginManager #: PluginManager for processing the current PDF. + plugin_manager: ( + OcrmypdfPluginManager #: PluginManager for processing the current PDF. + ) def __init__( self, - options: OCROptions, + options: OcrOptions, work_folder: Path, origin: Path, pdfinfo: PdfInfo, @@ -66,23 +68,25 @@ class PageContext: Must be pickle-able, so stores only intrinsic/simple data elements or those capable of their serializing themselves via ``__getstate__``. - Note: Uses OCROptions with JSON serialization for multiprocessing compatibility. + Note: Uses OcrOptions with JSON serialization for multiprocessing compatibility. """ origin: Path #: The filename of the original input file. pageno: int #: This page number (zero-based). pageinfo: PageInfo #: Information on this page. - plugin_manager: OcrmypdfPluginManager #: PluginManager for processing the current PDF. + plugin_manager: ( + OcrmypdfPluginManager #: PluginManager for processing the current PDF. + ) def __init__(self, pdf_context: PdfContext, pageno): self.work_folder = pdf_context.work_folder self.origin = pdf_context.origin - # Store OCROptions directly instead of Namespace + # Store OcrOptions directly instead of Namespace self.options = pdf_context.options self.pageno = pageno self.pageinfo = pdf_context.pdfinfo[pageno] self.plugin_manager = pdf_context.plugin_manager - # Ensure no reference to PdfContext which contains OCROptions + # Ensure no reference to PdfContext which contains OcrOptions self._pdf_context = None def get_path(self, name: str) -> Path: @@ -98,7 +102,7 @@ class PageContext: options_json = self.options.model_dump_json_safe() state['options_json'] = options_json - # Remove the OCROptions object to avoid pickle issues + # Remove the OcrOptions object to avoid pickle issues del state['options'] # Remove any potential references to Pydantic objects @@ -108,10 +112,10 @@ class PageContext: def __setstate__(self, state): self.__dict__.update(state) - # Reconstruct OCROptions from JSON if available + # Reconstruct OcrOptions from JSON if available if 'options_json' in state: - from ocrmypdf._options import OCROptions + from ocrmypdf._options import OcrOptions - self.options = OCROptions.model_validate_json_safe(state['options_json']) + self.options = OcrOptions.model_validate_json_safe(state['options_json']) # Otherwise, we have a fallback Namespace (shouldn't happen in normal operation) # Leave it as-is for compatibility diff --git a/src/ocrmypdf/_options.py b/src/ocrmypdf/_options.py index f2ab6552..9be9c85e 100644 --- a/src/ocrmypdf/_options.py +++ b/src/ocrmypdf/_options.py @@ -90,7 +90,7 @@ def _pages_from_ranges(ranges: str) -> set[int]: return set(pages) -class OCROptions(BaseModel): +class OcrOptions(BaseModel): """Internal options model that can masquerade as argparse.Namespace. This model provides proper typing and validation while maintaining @@ -210,7 +210,6 @@ class OCROptions(BaseModel): default_factory=dict, exclude=True, alias='_extra_attrs' ) - @field_validator('languages') @classmethod def validate_languages(cls, v): @@ -459,7 +458,7 @@ class OCROptions(BaseModel): return json.dumps(serializable_data) @classmethod - def model_validate_json_safe(cls, json_str: str) -> OCROptions: + def model_validate_json_safe(cls, json_str: str) -> OcrOptions: """Reconstruct from JSON with special handling for non-serializable types.""" data = json.loads(json_str) @@ -547,25 +546,25 @@ class OCROptions(BaseModel): for field_name in model_class.model_fields: # Try namespace_field pattern first (e.g., tesseract_timeout) flat_name = f"{namespace}_{field_name}" - if flat_name in OCROptions.model_fields: + if flat_name in OcrOptions.model_fields: value = getattr(self, flat_name) if value is not None: kwargs[field_name] = _convert_value(value) # Also check direct field name (for fields like jbig2_lossy) - elif field_name in OCROptions.model_fields: + elif field_name in OcrOptions.model_fields: value = getattr(self, field_name) if value is not None: kwargs[field_name] = _convert_value(value) # Check for special mappings elif namespace == 'optimize' and field_name == 'level': # 'optimize' field maps to 'level' in OptimizeOptions - if 'optimize' in OCROptions.model_fields: + if 'optimize' in OcrOptions.model_fields: value = getattr(self, 'optimize') if value is not None: kwargs[field_name] = _convert_value(value) elif namespace == 'optimize' and field_name == 'jpeg_quality': # jpg_quality maps to jpeg_quality - if 'jpg_quality' in OCROptions.model_fields: + if 'jpg_quality' in OcrOptions.model_fields: value = getattr(self, 'jpg_quality') if value is not None: kwargs[field_name] = _convert_value(value) diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index 75fbb8eb..75cf864f 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 +from ocrmypdf._options import OcrOptions, ProcessingMode from ocrmypdf.exceptions import ( DigitalSignatureError, DpiError, @@ -64,7 +64,7 @@ VECTOR_PAGE_DPI = 400 register_heif_opener() -def triage_image_file(input_file: Path, output_file: Path, options: OCROptions) -> None: +def triage_image_file(input_file: Path, output_file: Path, options: OcrOptions) -> None: """Triage the input image file. If the input file is an image, check its resolution and convert it to PDF. @@ -163,7 +163,7 @@ def _pdf_guess_version(input_file: Path, search_window=1024) -> str: def triage( - original_filename: str, input_file: Path, output_file: Path, options: OCROptions + original_filename: str, input_file: Path, output_file: Path, options: OcrOptions ) -> Path: """Triage the input file. We can handle PDFs and images.""" try: diff --git a/src/ocrmypdf/_pipelines/_common.py b/src/ocrmypdf/_pipelines/_common.py index 7dff4844..a129f780 100644 --- a/src/ocrmypdf/_pipelines/_common.py +++ b/src/ocrmypdf/_pipelines/_common.py @@ -30,7 +30,7 @@ from ocrmypdf._concurrent import Executor, setup_executor from ocrmypdf._jobcontext import PageContext, PdfContext from ocrmypdf._logging import PageNumberFilter from ocrmypdf._metadata import metadata_fixup -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf._pipeline import ( convert_to_pdfa, create_ocr_image, @@ -205,7 +205,7 @@ def worker_init(max_pixels: int | None) -> None: @contextmanager def manage_debug_log_handler( *, - options: OCROptions, + options: OcrOptions, work_folder: Path, ): remover = None @@ -254,8 +254,8 @@ def manage_work_folder(*, work_folder: Path, retain: bool, print_location: bool) def cli_exception_handler( - fn: Callable[[OCROptions, OcrmypdfPluginManager], ExitCode], - options: OCROptions, + fn: Callable[[OcrOptions, OcrmypdfPluginManager], ExitCode], + options: OcrOptions, plugin_manager: OcrmypdfPluginManager, ) -> ExitCode: """Convert exceptions into command line error messages and exit codes. @@ -319,14 +319,14 @@ def cli_exception_handler( def setup_pipeline( - options: OCROptions, + options: OcrOptions, plugin_manager: OcrmypdfPluginManager, ) -> Executor: # Any changes to options will not take effect for options that are already # bound to function parameters in the pipeline. (For example # options.input_file, options.pdf_renderer are already bound.) - # Note: OCROptions is immutable, so we can't modify options.jobs directly - # The jobs field should already be set correctly during OCROptions creation + # Note: OcrOptions is immutable, so we can't modify options.jobs directly + # The jobs field should already be set correctly during OcrOptions creation # Apply PIL max image pixels side effect PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000) diff --git a/src/ocrmypdf/_pipelines/hocr_to_ocr_pdf.py b/src/ocrmypdf/_pipelines/hocr_to_ocr_pdf.py index ded75f98..750901f3 100644 --- a/src/ocrmypdf/_pipelines/hocr_to_ocr_pdf.py +++ b/src/ocrmypdf/_pipelines/hocr_to_ocr_pdf.py @@ -16,7 +16,7 @@ import PIL from ocrmypdf._concurrent import Executor from ocrmypdf._graft import OcrGrafter from ocrmypdf._jobcontext import PageContext, PdfContext -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf._pipeline import copy_final from ocrmypdf._pipelines._common import ( HOCRResult, @@ -104,7 +104,7 @@ def exec_hocr_to_ocr_pdf(context: PdfContext, executor: Executor) -> Sequence[st def run_hocr_to_ocr_pdf_pipeline( - options: OCROptions, + options: OcrOptions, *, plugin_manager: OcrmypdfPluginManager, ) -> ExitCode: diff --git a/src/ocrmypdf/_pipelines/ocr.py b/src/ocrmypdf/_pipelines/ocr.py index 7ebccfbb..d875be0e 100644 --- a/src/ocrmypdf/_pipelines/ocr.py +++ b/src/ocrmypdf/_pipelines/ocr.py @@ -18,7 +18,7 @@ import PIL from ocrmypdf._concurrent import Executor from ocrmypdf._graft import OcrGrafter from ocrmypdf._jobcontext import PageContext, PdfContext -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf._pipeline import ( copy_final, is_ocr_required, @@ -162,7 +162,7 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]: def _run_pipeline( - options: OCROptions, + options: OcrOptions, plugin_manager: OcrmypdfPluginManager, ) -> ExitCode: with ( @@ -197,7 +197,7 @@ def _run_pipeline( def run_pipeline_cli( - options: OCROptions, + options: OcrOptions, *, plugin_manager: OcrmypdfPluginManager, ) -> ExitCode: @@ -212,7 +212,7 @@ def run_pipeline_cli( def run_pipeline( - options: OCROptions, + options: OcrOptions, *, plugin_manager: OcrmypdfPluginManager, ) -> ExitCode: diff --git a/src/ocrmypdf/_pipelines/pdf_to_hocr.py b/src/ocrmypdf/_pipelines/pdf_to_hocr.py index f91b0da2..b63f67d9 100644 --- a/src/ocrmypdf/_pipelines/pdf_to_hocr.py +++ b/src/ocrmypdf/_pipelines/pdf_to_hocr.py @@ -15,7 +15,7 @@ import PIL from ocrmypdf._concurrent import Executor from ocrmypdf._jobcontext import PageContext, PdfContext -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf._pipeline import ( is_ocr_required, ocr_engine_hocr, @@ -84,7 +84,7 @@ def exec_pdf_to_hocr(context: PdfContext, executor: Executor) -> None: def run_hocr_pipeline( - options: OCROptions, + options: OcrOptions, *, plugin_manager: OcrmypdfPluginManager, ) -> None: diff --git a/src/ocrmypdf/_plugin_manager.py b/src/ocrmypdf/_plugin_manager.py index a896c8dc..02999aa2 100644 --- a/src/ocrmypdf/_plugin_manager.py +++ b/src/ocrmypdf/_plugin_manager.py @@ -20,7 +20,7 @@ from pydantic import BaseModel import ocrmypdf.builtin_plugins from ocrmypdf import Executor, PdfContext, pluginspec -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf._progressbar import ProgressBar from ocrmypdf.helpers import Resolution from ocrmypdf.pluginspec import OcrEngine @@ -140,7 +140,7 @@ class OcrmypdfPluginManager: rotation: int | None, filter_vector: bool, stop_on_soft_error: bool, - options: OCROptions | None, + options: OcrOptions | None, use_cropbox: bool, ) -> Path | None: """Rasterize one page of a PDF at specified resolution.""" @@ -178,11 +178,11 @@ class OcrmypdfPluginManager: page=page, image_filename=image_filename, output_pdf=output_pdf ) - def get_ocr_engine(self, *, options: OCROptions | None = None) -> OcrEngine | None: + def get_ocr_engine(self, *, options: OcrOptions | None = None) -> OcrEngine | None: """Returns an OcrEngine to use for processing. Args: - options: OCROptions to pass to the hook for engine selection. + options: OcrOptions to pass to the hook for engine selection. """ return self._pm.hook.get_ocr_engine(options=options) @@ -251,11 +251,11 @@ class OcrmypdfPluginManager: """Returns plugin option models keyed by namespace.""" return self._pm.hook.register_options() - def check_options(self, *, options: OCROptions) -> list[None]: + def check_options(self, *, options: OcrOptions) -> list[None]: """Called to validate options after parsing.""" return self._pm.hook.check_options(options=options) - def validate(self, *, pdfinfo: PdfInfo, options: OCROptions) -> list[None]: + def validate(self, *, pdfinfo: PdfInfo, options: OcrOptions) -> list[None]: """Called to validate options and pdfinfo after PDF is loaded.""" return self._pm.hook.validate(pdfinfo=pdfinfo, options=options) diff --git a/src/ocrmypdf/_plugin_registry.py b/src/ocrmypdf/_plugin_registry.py index 8fa9d6e1..26829824 100644 --- a/src/ocrmypdf/_plugin_registry.py +++ b/src/ocrmypdf/_plugin_registry.py @@ -16,7 +16,7 @@ class PluginOptionRegistry: """Registry for plugin option models. This registry collects option models from plugins during initialization. - Plugin options can be accessed via nested namespaces on OCROptions + Plugin options can be accessed via nested namespaces on OcrOptions (e.g., options.tesseract.timeout) or via flat field names for backward compatibility (e.g., options.tesseract_timeout). """ diff --git a/src/ocrmypdf/_validation.py b/src/ocrmypdf/_validation.py index 0429bd27..b1d0ecdb 100644 --- a/src/ocrmypdf/_validation.py +++ b/src/ocrmypdf/_validation.py @@ -17,7 +17,7 @@ import pikepdf from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD from ocrmypdf._exec import unpaper -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf._plugin_manager import OcrmypdfPluginManager from ocrmypdf.exceptions import ( BadArgsError, @@ -47,7 +47,7 @@ def check_platform() -> None: def check_options_languages( - options: OCROptions, ocr_engine_languages: list[str] + options: OcrOptions, ocr_engine_languages: list[str] ) -> None: if not ocr_engine_languages: return @@ -72,7 +72,7 @@ def check_options_languages( raise MissingDependencyError(msg) -def check_options_sidecar(options: OCROptions) -> None: +def check_options_sidecar(options: OcrOptions) -> None: if options.sidecar == '\0': if options.output_file == '-': raise BadArgsError("--sidecar filename needed when output file is stdout.") @@ -87,7 +87,7 @@ def check_options_sidecar(options: OCROptions) -> None: ) -def check_options_preprocessing(options: OCROptions) -> None: +def check_options_preprocessing(options: OcrOptions) -> None: if options.clean_final: options.clean = True if options.unpaper_args and not options.clean: @@ -114,14 +114,14 @@ def check_options_preprocessing(options: OCROptions) -> None: raise BadArgsError("--unpaper-args: " + str(e)) from e -def _check_plugin_invariant_options(options: OCROptions) -> None: +def _check_plugin_invariant_options(options: OcrOptions) -> None: check_platform() check_options_sidecar(options) check_options_preprocessing(options) def _check_plugin_options( - options: OCROptions, plugin_manager: OcrmypdfPluginManager + options: OcrOptions, plugin_manager: OcrmypdfPluginManager ) -> None: # First, let plugins check their external dependencies plugin_manager.check_options(options=options) @@ -134,11 +134,12 @@ def _check_plugin_options( # Finally, run comprehensive validation using the coordinator from ocrmypdf._validation_coordinator import ValidationCoordinator + coordinator = ValidationCoordinator(plugin_manager) coordinator.validate_all_options(options) -def check_options(options: OCROptions, plugin_manager: OcrmypdfPluginManager) -> None: +def check_options(options: OcrOptions, plugin_manager: OcrmypdfPluginManager) -> None: """Check options for validity and consistency. This function coordinates validation across the entire system: @@ -151,7 +152,7 @@ def check_options(options: OCROptions, plugin_manager: OcrmypdfPluginManager) -> _check_plugin_options(options, plugin_manager) -def create_input_file(options: OCROptions, work_folder: Path) -> tuple[Path, str]: +def create_input_file(options: OcrOptions, work_folder: Path) -> tuple[Path, str]: if options.input_file == '-': # stdin log.info('reading file from standard input') @@ -196,7 +197,7 @@ def create_input_file(options: OCROptions, work_folder: Path) -> tuple[Path, str raise InputFileError(msg) from e -def check_requested_output_file(options: OCROptions) -> None: +def check_requested_output_file(options: OcrOptions) -> None: if options.output_file == '-': if sys.stdout.isatty(): raise BadArgsError( @@ -214,7 +215,7 @@ def check_requested_output_file(options: OCROptions) -> None: def report_output_file_size( - options: OCROptions, + options: OcrOptions, input_file: Path, output_file: Path, optimize_messages: Sequence[str] | None = None, diff --git a/src/ocrmypdf/_validation_coordinator.py b/src/ocrmypdf/_validation_coordinator.py index f82eedd2..bbe98223 100644 --- a/src/ocrmypdf/_validation_coordinator.py +++ b/src/ocrmypdf/_validation_coordinator.py @@ -12,7 +12,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: import pluggy - from ocrmypdf._options import OCROptions + from ocrmypdf._options import OcrOptions log = logging.getLogger(__name__) @@ -24,7 +24,7 @@ class ValidationCoordinator: self.plugin_manager = plugin_manager self.registry = getattr(plugin_manager, '_option_registry', None) - def validate_all_options(self, options: OCROptions) -> None: + def validate_all_options(self, options: OcrOptions) -> None: """Run comprehensive validation on all options. This runs validation in the correct order: @@ -41,7 +41,7 @@ class ValidationCoordinator: # Step 2: Cross-cutting validation self._validate_cross_cutting_concerns(options) - def _validate_plugin_contexts(self, options: OCROptions) -> None: + def _validate_plugin_contexts(self, options: OcrOptions) -> None: """Validate plugin options that require external context.""" # For now, we'll run the plugin validation directly since the models # are still being integrated. This ensures the validation warnings @@ -53,7 +53,7 @@ class ValidationCoordinator: # Run Optimize validation self._validate_optimize_options(options) - def _validate_tesseract_options(self, options: OCROptions) -> None: + def _validate_tesseract_options(self, options: OcrOptions) -> None: """Validate Tesseract options.""" # Check pagesegmode warning if options.tesseract.pagesegmode in (0, 2): @@ -74,6 +74,7 @@ class ValidationCoordinator: # Check for blocked languages from ocrmypdf.exceptions import BadArgsError + DENIED_LANGUAGES = {'equ', 'osd'} if DENIED_LANGUAGES & set(options.languages): raise BadArgsError( @@ -83,19 +84,21 @@ class ValidationCoordinator: "Remove them from the -l/--language argument." ) - def _validate_optimize_options(self, options: OCROptions) -> None: + def _validate_optimize_options(self, options: OcrOptions) -> None: """Validate optimization options.""" # Check optimization consistency - if options.optimize == 0 and any([ - options.png_quality and options.png_quality > 0, - options.jpeg_quality and options.jpeg_quality > 0 - ]): + if options.optimize == 0 and any( + [ + options.png_quality and options.png_quality > 0, + options.jpeg_quality and options.jpeg_quality > 0, + ] + ): log.warning( "The arguments --png-quality and --jpeg-quality " "will be ignored because --optimize=0." ) - def _validate_cross_cutting_concerns(self, options: OCROptions) -> None: + def _validate_cross_cutting_concerns(self, options: OcrOptions) -> None: """Validate cross-cutting concerns that span multiple plugins.""" from ocrmypdf._options import ProcessingMode @@ -115,7 +118,8 @@ class ValidationCoordinator: # Validate output type compatibility if options.output_type == 'none' and str(options.output_file) not in ( - os.devnull, '-' + os.devnull, + '-', ): raise ValueError( "Since you specified `--output-type none`, the output file " @@ -134,7 +138,7 @@ class ValidationCoordinator: "--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'" ) - def _handle_deprecated_pdf_renderer(self, options: OCROptions) -> None: + def _handle_deprecated_pdf_renderer(self, options: OcrOptions) -> None: """Handle deprecated pdf_renderer values by redirecting to fpdf2.""" if options.pdf_renderer in ('hocr', 'hocrdebug'): log.info( diff --git a/src/ocrmypdf/api.py b/src/ocrmypdf/api.py index c28b6004..93294f2c 100644 --- a/src/ocrmypdf/api.py +++ b/src/ocrmypdf/api.py @@ -51,7 +51,7 @@ from typing import BinaryIO from warnings import warn from ocrmypdf._logging import PageNumberFilter -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf._pipelines.hocr_to_ocr_pdf import run_hocr_to_ocr_pdf_pipeline from ocrmypdf._pipelines.ocr import run_pipeline, run_pipeline_cli from ocrmypdf._pipelines.pdf_to_hocr import run_hocr_pipeline @@ -120,8 +120,8 @@ def setup_plugin_infrastructure( registry.register_option_model(namespace, model_class) all_plugin_models[namespace] = model_class - # Register plugin models with OCROptions for dynamic nested access - OCROptions.register_plugin_models(all_plugin_models) + # Register plugin models with OcrOptions for dynamic nested access + OcrOptions.register_plugin_models(all_plugin_models) # Store registry in plugin manager for later access plugin_manager._option_registry = registry @@ -234,7 +234,7 @@ def configure_logging( def create_options( *, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs -) -> OCROptions: +) -> OcrOptions: """Construct an options object from the input/output files and keyword arguments. Args: @@ -244,12 +244,12 @@ def create_options( **kwargs: Keyword arguments. Returns: - OCROptions: An options object containing the parsed arguments. + OcrOptions: An options object containing the parsed arguments. Raises: TypeError: If the type of a keyword argument is not supported. """ - # Prepare kwargs for direct OCROptions construction + # Prepare kwargs for direct OcrOptions construction options_kwargs = kwargs.copy() # Set input and output files @@ -260,16 +260,16 @@ def create_options( if 'sidecar' in options_kwargs and isinstance( options_kwargs['sidecar'], BinaryIO | IOBase ): - # Keep the stream object as-is - OCROptions can handle it + # Keep the stream object as-is - OcrOptions can handle it pass - # Remove None values to let OCROptions use its defaults + # Remove None values to let OcrOptions use its defaults options_kwargs = {k: v for k, v in options_kwargs.items() if v is not None} - # Remove any kwargs that aren't OCROptions fields and store in extra_attrs + # Remove any kwargs that aren't OcrOptions fields and store in extra_attrs extra_attrs = {} - ocr_fields = set(OCROptions.model_fields.keys()) - # Legacy mode flags are handled by OCROptions model validator + ocr_fields = set(OcrOptions.model_fields.keys()) + # Legacy mode flags are handled by OcrOptions model validator legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'} # Known extra attributes that should be preserved @@ -280,16 +280,16 @@ def create_options( continue extra_attrs[key] = options_kwargs.pop(key) - # Create OCROptions directly + # Create OcrOptions directly try: - options = OCROptions(**options_kwargs) + options = OcrOptions(**options_kwargs) # Add any extra attributes if extra_attrs: options.extra_attrs.update(extra_attrs) return options except Exception as e: # If direct construction fails, provide a helpful error message - raise TypeError(f"Failed to create OCROptions: {e}") from e + raise TypeError(f"Failed to create OcrOptions: {e}") from e def ocr( # noqa: D417 @@ -538,7 +538,7 @@ def _pdf_to_hocr( # noqa: D417 else: plugins = list(plugins) - # Prepare kwargs for direct OCROptions construction + # Prepare kwargs for direct OcrOptions construction options_kwargs = kwargs.copy() # Set input file and handle special output_folder case @@ -558,16 +558,16 @@ def _pdf_to_hocr( # noqa: D417 if plugins: options_kwargs['plugins'] = plugins - # Remove None values to let OCROptions use its defaults + # Remove None values to let OcrOptions use its defaults options_kwargs = {k: v for k, v in options_kwargs.items() if v is not None} # Add output_folder to options_kwargs since it's now a proper field options_kwargs['output_folder'] = output_folder - # Remove any kwargs that aren't OCROptions fields and store in extra_attrs + # Remove any kwargs that aren't OcrOptions fields and store in extra_attrs extra_attrs = {} - ocr_fields = set(OCROptions.model_fields.keys()) - # Legacy mode flags are handled by OCROptions model validator + ocr_fields = set(OcrOptions.model_fields.keys()) + # Legacy mode flags are handled by OcrOptions model validator legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'} known_extra = {'progress_bar', 'plugins'} @@ -584,15 +584,15 @@ def _pdf_to_hocr( # noqa: D417 plugin_manager.add_options(parser=get_parser()) - # Create OCROptions directly + # Create OcrOptions directly try: - options = OCROptions(**options_kwargs) + options = OcrOptions(**options_kwargs) # Add any extra attributes if extra_attrs: options.extra_attrs.update(extra_attrs) except Exception as e: raise TypeError( - f"Failed to create OCROptions for hOCR pipeline: {e}" + f"Failed to create OcrOptions for hOCR pipeline: {e}" ) from e return run_hocr_pipeline(options=options, plugin_manager=plugin_manager) @@ -643,7 +643,7 @@ def _hocr_to_ocr_pdf( # noqa: D417 else: plugins = list(plugins) - # Prepare kwargs for direct OCROptions construction + # Prepare kwargs for direct OcrOptions construction options_kwargs = kwargs.copy() # Set output file and handle special work_folder case @@ -663,7 +663,7 @@ def _hocr_to_ocr_pdf( # noqa: D417 if plugins: options_kwargs['plugins'] = plugins - # Remove None values to let OCROptions use its defaults + # Remove None values to let OcrOptions use its defaults options_kwargs = {k: v for k, v in options_kwargs.items() if v is not None} # Warn about deprecated jbig2 options and remove from kwargs @@ -680,10 +680,10 @@ def _hocr_to_ocr_pdf( # noqa: D417 # Add work_folder to options_kwargs since it's now a proper field options_kwargs['work_folder'] = work_folder - # Remove any kwargs that aren't OCROptions fields and store in extra_attrs + # Remove any kwargs that aren't OcrOptions fields and store in extra_attrs extra_attrs = {} - ocr_fields = set(OCROptions.model_fields.keys()) - # Legacy mode flags are handled by OCROptions model validator + ocr_fields = set(OcrOptions.model_fields.keys()) + # Legacy mode flags are handled by OcrOptions model validator legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'} known_extra = {'progress_bar', 'plugins'} @@ -700,15 +700,15 @@ def _hocr_to_ocr_pdf( # noqa: D417 plugin_manager.add_options(parser=get_parser()) - # Create OCROptions directly + # Create OcrOptions directly try: - options = OCROptions(**options_kwargs) + options = OcrOptions(**options_kwargs) # Add any extra attributes if extra_attrs: options.extra_attrs.update(extra_attrs) except Exception as e: raise TypeError( - f"Failed to create OCROptions for hOCR to PDF pipeline: {e}" + f"Failed to create OcrOptions for hOCR to PDF pipeline: {e}" ) from e return run_hocr_to_ocr_pdf_pipeline( diff --git a/src/ocrmypdf/builtin_plugins/null_ocr.py b/src/ocrmypdf/builtin_plugins/null_ocr.py index 409f6f71..3017a9ae 100644 --- a/src/ocrmypdf/builtin_plugins/null_ocr.py +++ b/src/ocrmypdf/builtin_plugins/null_ocr.py @@ -23,7 +23,7 @@ from ocrmypdf.hocrtransform import BoundingBox, OcrClass, OcrElement from ocrmypdf.pluginspec import OcrEngine, OrientationConfidence if TYPE_CHECKING: - from ocrmypdf._options import OCROptions + from ocrmypdf._options import OcrOptions class NullOcrEngine(OcrEngine): @@ -39,7 +39,7 @@ class NullOcrEngine(OcrEngine): return "none" @staticmethod - def creator_tag(options: OCROptions) -> str: + def creator_tag(options: OcrOptions) -> str: """Return creator tag for PDF metadata.""" return "OCRmyPDF (no OCR)" @@ -48,17 +48,17 @@ class NullOcrEngine(OcrEngine): return "No OCR engine" @staticmethod - def languages(options: OCROptions) -> set[str]: + def languages(options: OcrOptions) -> set[str]: """Return supported languages (empty set for null engine).""" return set() @staticmethod - def get_orientation(input_file: Path, options: OCROptions) -> OrientationConfidence: + def get_orientation(input_file: Path, options: OcrOptions) -> OrientationConfidence: """Return neutral orientation (no rotation detected).""" return OrientationConfidence(angle=0, confidence=0.0) @staticmethod - def get_deskew(input_file: Path, options: OCROptions) -> float: + def get_deskew(input_file: Path, options: OcrOptions) -> float: """Return zero deskew angle.""" return 0.0 @@ -70,7 +70,7 @@ class NullOcrEngine(OcrEngine): @staticmethod def generate_ocr( input_file: Path, - options: OCROptions, + options: OcrOptions, page_number: int = 0, ) -> tuple[OcrElement, str]: """Generate empty OCR results. @@ -104,7 +104,7 @@ class NullOcrEngine(OcrEngine): input_file: Path, output_hocr: Path, output_text: Path, - options: OCROptions, + options: OcrOptions, ) -> None: """Generate empty hOCR file. @@ -137,7 +137,7 @@ class NullOcrEngine(OcrEngine): input_file: Path, output_pdf: Path, output_text: Path, - options: OCROptions, + options: OcrOptions, ) -> None: """NullOcrEngine cannot generate PDFs directly. diff --git a/src/ocrmypdf/cli.py b/src/ocrmypdf/cli.py index df2d694a..f23696dd 100644 --- a/src/ocrmypdf/cli.py +++ b/src/ocrmypdf/cli.py @@ -12,7 +12,7 @@ from typing import Any, TypeVar from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD from ocrmypdf._defaults import PROGRAM_NAME as _PROGRAM_NAME -from ocrmypdf._options import OCROptions, ProcessingMode +from ocrmypdf._options import OcrOptions, ProcessingMode from ocrmypdf._plugin_manager import OcrmypdfPluginManager from ocrmypdf._version import __version__ as _VERSION @@ -473,8 +473,8 @@ plugins_only_parser.add_argument( ) -def namespace_to_options(ns) -> OCROptions: - """Convert argparse.Namespace to OCROptions. +def namespace_to_options(ns) -> OcrOptions: + """Convert argparse.Namespace to OcrOptions. This function encapsulates CLI-specific knowledge of how command line arguments map to our internal options model. @@ -483,14 +483,14 @@ def namespace_to_options(ns) -> OCROptions: known_fields = {} extra_attrs = {} - # Legacy boolean flags that map to mode - handled by OCROptions model validator + # Legacy boolean flags that map to mode - handled by OcrOptions model validator legacy_mode_flags = {'force_ocr', 'skip_text', 'redo_ocr'} for key, value in vars(ns).items(): - if key in OCROptions.model_fields: + if key in OcrOptions.model_fields: known_fields[key] = value elif key in legacy_mode_flags: - # Pass legacy flags to OCROptions for conversion to mode + # Pass legacy flags to OcrOptions for conversion to mode known_fields[key] = value else: extra_attrs[key] = value @@ -503,15 +503,15 @@ def namespace_to_options(ns) -> OCROptions: if 'work_folder' in extra_attrs and 'input_file' not in known_fields: known_fields['input_file'] = '/dev/null' # Placeholder - instance = OCROptions(**known_fields) + instance = OcrOptions(**known_fields) instance.extra_attrs = extra_attrs return instance def get_options_and_plugins( args=None, -) -> tuple[OCROptions, OcrmypdfPluginManager]: - """Parse command line arguments and return OCROptions and plugin manager. +) -> tuple[OcrOptions, OcrmypdfPluginManager]: + """Parse command line arguments and return OcrOptions and plugin manager. This is the main entry point for CLI argument processing. It handles plugin discovery, argument parsing, and conversion to our internal @@ -521,7 +521,7 @@ def get_options_and_plugins( args: Command line arguments. If None, uses sys.argv. Returns: - Tuple of (OCROptions, PluginManager) + Tuple of (OcrOptions, PluginManager) """ # Import here to avoid circular imports from ocrmypdf.api import setup_plugin_infrastructure @@ -539,7 +539,7 @@ def get_options_and_plugins( # Parse all arguments namespace = parser.parse_args(args=args) - # Convert to OCROptions + # Convert to OcrOptions options = namespace_to_options(namespace) return options, plugin_manager diff --git a/src/ocrmypdf/optimize.py b/src/ocrmypdf/optimize.py index 93f0c105..e5a543e4 100644 --- a/src/ocrmypdf/optimize.py +++ b/src/ocrmypdf/optimize.py @@ -721,12 +721,12 @@ def main(infile, outfile, level, jobs=1): from shutil import copy # pylint: disable=import-outside-toplevel from tempfile import TemporaryDirectory # pylint: disable=import-outside-toplevel - from ocrmypdf._options import OCROptions # pylint: disable=import-outside-toplevel + from ocrmypdf._options import OcrOptions # pylint: disable=import-outside-toplevel infile = Path(infile) - # Create OCROptions with optimization-specific settings - options = OCROptions( + # Create OcrOptions with optimization-specific settings + options = OcrOptions( input_file=infile, output_file=outfile, # Required field jobs=jobs, diff --git a/src/ocrmypdf/pluginspec.py b/src/ocrmypdf/pluginspec.py index 6b366fe2..86270a76 100644 --- a/src/ocrmypdf/pluginspec.py +++ b/src/ocrmypdf/pluginspec.py @@ -16,7 +16,7 @@ import pluggy from pydantic import BaseModel from ocrmypdf import Executor, PdfContext -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf._progressbar import ProgressBar from ocrmypdf.helpers import Resolution @@ -112,7 +112,7 @@ def register_options() -> dict[str, type[BaseModel]]: @hookspec -def check_options(options: OCROptions) -> None: +def check_options(options: OcrOptions) -> None: """Called to ask the plugin to check all of the options. The plugin may check if options that it added are valid. @@ -183,7 +183,7 @@ def get_progressbar_class() -> type[ProgressBar]: # type: ignore[return-value] @hookspec -def validate(pdfinfo: PdfInfo, options: OCROptions) -> None: +def validate(pdfinfo: PdfInfo, options: OcrOptions) -> None: """Called to give a plugin an opportunity to review *options* and *pdfinfo*. *options* contains the "work order" to process a particular file. *pdfinfo* @@ -214,7 +214,7 @@ def rasterize_pdf_page( rotation: int | None, filter_vector: bool, stop_on_soft_error: bool, - options: OCROptions | None, + options: OcrOptions | None, use_cropbox: bool, ) -> Path: # type: ignore[return-value] """Rasterize one page of a PDF at resolution raster_dpi in canvas units. @@ -401,7 +401,7 @@ class OcrEngine(ABC): @staticmethod @abstractmethod - def creator_tag(options: OCROptions) -> str: + def creator_tag(options: OcrOptions) -> str: """Returns the creator tag to identify this software's role in creating the PDF. This tag will be inserted in the XMP metadata and DocumentInfo dictionary @@ -422,7 +422,7 @@ class OcrEngine(ABC): @staticmethod @abstractmethod - def languages(options: OCROptions) -> Set[str]: + def languages(options: OcrOptions) -> Set[str]: """Returns the set of all languages that are supported by the engine. Languages are typically given in 3-letter ISO 3166-1 codes, but actually @@ -431,18 +431,18 @@ class OcrEngine(ABC): @staticmethod @abstractmethod - def get_orientation(input_file: Path, options: OCROptions) -> OrientationConfidence: + def get_orientation(input_file: Path, options: OcrOptions) -> OrientationConfidence: """Returns the orientation of the image.""" @staticmethod - def get_deskew(input_file: Path, options: OCROptions) -> float: + def get_deskew(input_file: Path, options: OcrOptions) -> float: """Returns the deskew angle of the image, in degrees.""" return 0.0 @staticmethod @abstractmethod def generate_hocr( - input_file: Path, output_hocr: Path, output_text: Path, options: OCROptions + input_file: Path, output_hocr: Path, output_text: Path, options: OcrOptions ) -> None: """Called to produce a hOCR file from a page image and sidecar text file. @@ -465,7 +465,7 @@ class OcrEngine(ABC): @staticmethod @abstractmethod def generate_pdf( - input_file: Path, output_pdf: Path, output_text: Path, options: OCROptions + input_file: Path, output_pdf: Path, output_text: Path, options: OcrOptions ) -> None: """Called to produce a text only PDF from a page image. @@ -501,7 +501,7 @@ class OcrEngine(ABC): @staticmethod def generate_ocr( input_file: Path, - options: OCROptions, + options: OcrOptions, page_number: int = 0, ) -> tuple[OcrElement, str]: """Generate OCR results as an OcrElement tree. @@ -531,7 +531,7 @@ class OcrEngine(ABC): @hookspec(firstresult=True) -def get_ocr_engine(options: OCROptions | None) -> OcrEngine: # type: ignore[return-value] +def get_ocr_engine(options: OcrOptions | None) -> OcrEngine: # type: ignore[return-value] """Returns an OcrEngine to use for processing this file. The OcrEngine may be instantiated multiple times, by both the main process @@ -542,7 +542,7 @@ def get_ocr_engine(options: OCROptions | None) -> OcrEngine: # type: ignore[ret engine. The hook caller will then try the next plugin. Args: - options: The current OCROptions, used to determine which engine + options: The current OcrOptions, used to determine which engine to select. May be None for backward compatibility with external plugins. diff --git a/tests/test_api.py b/tests/test_api.py index 86c8f688..9eebc0f2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -107,14 +107,14 @@ def test_hocr_result_pickle(): def test_nested_plugin_option_access(): """Test that plugin options can be accessed via nested namespaces.""" - from ocrmypdf._options import OCROptions + from ocrmypdf._options import OcrOptions from ocrmypdf.api import setup_plugin_infrastructure # Set up plugin infrastructure to register plugin models setup_plugin_infrastructure() # Create options with tesseract settings - options = OCROptions( + options = OcrOptions( input_file='test.pdf', output_file='output.pdf', tesseract_timeout=120.0, diff --git a/tests/test_json_serialization.py b/tests/test_json_serialization.py index 4240ae33..d7ee6a78 100644 --- a/tests/test_json_serialization.py +++ b/tests/test_json_serialization.py @@ -1,4 +1,4 @@ -"""Test JSON serialization of OCROptions for multiprocessing compatibility.""" +"""Test JSON serialization of OcrOptions for multiprocessing compatibility.""" import multiprocessing from io import BytesIO @@ -6,28 +6,28 @@ from pathlib import Path import pytest -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOptions @pytest.fixture(autouse=True) def register_plugin_models(): """Register plugin models for tests.""" - OCROptions.register_plugin_models({'tesseract': TesseractOptions}) + OcrOptions.register_plugin_models({'tesseract': TesseractOptions}) yield # Clean up after test (optional, but good practice) def worker_function(options_json: str) -> str: - """Worker function that deserializes OCROptions from JSON and returns a result.""" + """Worker function that deserializes OcrOptions from JSON and returns a result.""" # Register plugin models in worker process - from ocrmypdf._options import OCROptions + from ocrmypdf._options import OcrOptions from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOptions - OCROptions.register_plugin_models({'tesseract': TesseractOptions}) + OcrOptions.register_plugin_models({'tesseract': TesseractOptions}) - # Reconstruct OCROptions from JSON in worker process - options = OCROptions.model_validate_json_safe(options_json) + # Reconstruct OcrOptions from JSON in worker process + options = OcrOptions.model_validate_json_safe(options_json) # Verify we can access various option types # Count only user-added extra_attrs (exclude plugin cache keys starting with '_') @@ -51,9 +51,9 @@ def worker_function(options_json: str) -> str: def test_json_serialization_multiprocessing(): - """Test that OCROptions can be JSON serialized and used in multiprocessing.""" - # Create OCROptions with various field types - options = OCROptions( + """Test that OcrOptions can be JSON serialized and used in multiprocessing.""" + # Create OcrOptions with various field types + options = OcrOptions( input_file=Path('/test/input.pdf'), output_file=Path('/test/output.pdf'), languages=['eng', 'deu'], @@ -72,7 +72,7 @@ def test_json_serialization_multiprocessing(): options_json = options.model_dump_json_safe() # Test that we can deserialize in the main process - reconstructed = OCROptions.model_validate_json_safe(options_json) + reconstructed = OcrOptions.model_validate_json_safe(options_json) assert reconstructed.input_file == options.input_file assert reconstructed.output_file == options.output_file assert reconstructed.languages == options.languages @@ -112,7 +112,7 @@ def test_json_serialization_with_streams(): input_stream = BytesIO(b'fake pdf data') output_stream = BytesIO() - options = OCROptions( + options = OcrOptions( input_file=input_stream, output_file=output_stream, languages=['eng'], @@ -123,7 +123,7 @@ def test_json_serialization_with_streams(): options_json = options.model_dump_json_safe() # Deserialize (streams will be placeholder strings) - reconstructed = OCROptions.model_validate_json_safe(options_json) + reconstructed = OcrOptions.model_validate_json_safe(options_json) # Streams should be converted to placeholder strings assert reconstructed.input_file == 'stream' @@ -134,7 +134,7 @@ def test_json_serialization_with_streams(): def test_json_serialization_with_none_values(): """Test JSON serialization handles None values correctly.""" - options = OCROptions( + options = OcrOptions( input_file=Path('/test/input.pdf'), output_file=Path('/test/output.pdf'), languages=['eng'], @@ -145,7 +145,7 @@ def test_json_serialization_with_none_values(): options_json = options.model_dump_json_safe() # Deserialize - reconstructed = OCROptions.model_validate_json_safe(options_json) + reconstructed = OcrOptions.model_validate_json_safe(options_json) # Verify None values are preserved (check actual defaults from model) assert reconstructed.tesseract_timeout == 0.0 # Default value, not None diff --git a/tests/test_ocr_engine_selection.py b/tests/test_ocr_engine_selection.py index ac7db0e4..265f74d1 100644 --- a/tests/test_ocr_engine_selection.py +++ b/tests/test_ocr_engine_selection.py @@ -74,14 +74,14 @@ class TestOcrEngineCliOption: class TestOcrEngineOptionsModel: - """Test OCROptions has ocr_engine field.""" + """Test OcrOptions has ocr_engine field.""" def test_ocr_options_has_ocr_engine_field(self): - """OCROptions should have ocr_engine field.""" - from ocrmypdf._options import OCROptions + """OcrOptions should have ocr_engine field.""" + from ocrmypdf._options import OcrOptions # Check field exists in model - assert 'ocr_engine' in OCROptions.model_fields + assert 'ocr_engine' in OcrOptions.model_fields class TestOcrEnginePluginSelection: @@ -91,8 +91,8 @@ class TestOcrEnginePluginSelection: """TesseractOcrEngine should be returned when ocr_engine='auto'.""" from unittest.mock import MagicMock - from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine from ocrmypdf.builtin_plugins import tesseract_ocr + from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine options = MagicMock() options.ocr_engine = 'auto' @@ -104,8 +104,8 @@ class TestOcrEnginePluginSelection: """TesseractOcrEngine should be returned when ocr_engine='tesseract'.""" from unittest.mock import MagicMock - from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine from ocrmypdf.builtin_plugins import tesseract_ocr + from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine options = MagicMock() options.ocr_engine = 'tesseract' @@ -117,8 +117,8 @@ class TestOcrEnginePluginSelection: """NullOcrEngine should be returned when ocr_engine='none'.""" from unittest.mock import MagicMock - from ocrmypdf.builtin_plugins.null_ocr import NullOcrEngine from ocrmypdf.builtin_plugins import null_ocr + from ocrmypdf.builtin_plugins.null_ocr import NullOcrEngine options = MagicMock() options.ocr_engine = 'none' diff --git a/tests/test_rasterizer.py b/tests/test_rasterizer.py index f144f944..dec805c1 100644 --- a/tests/test_rasterizer.py +++ b/tests/test_rasterizer.py @@ -12,7 +12,7 @@ import pikepdf import pytest from PIL import Image -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf._plugin_manager import get_plugin_manager from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution @@ -67,7 +67,7 @@ class TestRasterizerOption: def test_rasterizer_invalid(self): """Test that an invalid rasterizer value is rejected.""" with pytest.raises(ValueError, match="rasterizer must be one of"): - OCROptions( + OcrOptions( input_file='test.pdf', output_file='out.pdf', rasterizer='invalid' ) @@ -127,7 +127,7 @@ class TestRasterizerHookDirect: pm = get_plugin_manager([]) # Create options requesting pypdfium - options = OCROptions( + options = OcrOptions( input_file=resources / 'graph.pdf', output_file=tmp_path / 'out.pdf', rasterizer='pypdfium', @@ -162,7 +162,7 @@ class TestRasterizerHookDirect: pm = get_plugin_manager([]) # Create options requesting ghostscript - options = OCROptions( + options = OcrOptions( input_file=resources / 'graph.pdf', output_file=tmp_path / 'out.pdf', rasterizer='ghostscript', @@ -190,7 +190,7 @@ class TestRasterizerHookDirect: """Test that auto mode uses pypdfium when available.""" pm = get_plugin_manager([]) - options = OCROptions( + options = OcrOptions( input_file=resources / 'graph.pdf', output_file=tmp_path / 'out.pdf', rasterizer='auto', @@ -363,7 +363,7 @@ class TestRasterizerWithNonStandardBoxes: """Compare output dimensions between rasterizers for nonstandard boxes.""" pm = get_plugin_manager([]) - options_gs = OCROptions( + options_gs = OcrOptions( input_file=pdf_with_nonstandard_boxes, output_file=tmp_path / 'out_gs.pdf', rasterizer='ghostscript', @@ -388,7 +388,7 @@ class TestRasterizerWithNonStandardBoxes: gs_size = im_gs.size if PYPDFIUM_AVAILABLE: - options_pdfium = OCROptions( + options_pdfium = OcrOptions( input_file=pdf_with_nonstandard_boxes, output_file=tmp_path / 'out_pdfium.pdf', rasterizer='pypdfium', @@ -445,7 +445,7 @@ class TestRasterizerWithRotationAndBoxes: """Test Ghostscript produces correct dimensions with rotation.""" pm = get_plugin_manager([]) - options = OCROptions( + options = OcrOptions( input_file=pdf_with_nonstandard_boxes, output_file=tmp_path / 'out.pdf', rasterizer='ghostscript', @@ -481,13 +481,11 @@ class TestRasterizerWithRotationAndBoxes: ) @pytest.mark.skipif(not PYPDFIUM_AVAILABLE, reason="pypdfium2 not installed") - def test_pypdfium_rotation_dimensions( - self, pdf_with_nonstandard_boxes, tmp_path - ): + def test_pypdfium_rotation_dimensions(self, pdf_with_nonstandard_boxes, tmp_path): """Test pypdfium produces correct dimensions with rotation.""" pm = get_plugin_manager([]) - options = OCROptions( + options = OcrOptions( input_file=pdf_with_nonstandard_boxes, output_file=tmp_path / 'out.pdf', rasterizer='pypdfium', @@ -535,7 +533,7 @@ class TestRasterizerWithRotationAndBoxes: for rotation in [0, 90, 180, 270]: # Rasterize with Ghostscript - gs_options = OCROptions( + gs_options = OcrOptions( input_file=pdf_with_nonstandard_boxes, output_file=tmp_path / 'out.pdf', rasterizer='ghostscript', @@ -556,7 +554,7 @@ class TestRasterizerWithRotationAndBoxes: ) # Rasterize with pypdfium - pdfium_options = OCROptions( + pdfium_options = OcrOptions( input_file=pdf_with_nonstandard_boxes, output_file=tmp_path / 'out.pdf', rasterizer='pypdfium', @@ -577,9 +575,10 @@ class TestRasterizerWithRotationAndBoxes: ) # Verify both produce the same MediaBox dimensions - with Image.open(gs_img_path) as gs_img, Image.open( - pdfium_img_path - ) as pdfium_img: + with ( + Image.open(gs_img_path) as gs_img, + Image.open(pdfium_img_path) as pdfium_img, + ): expected = self._get_expected_size(rotation) assert abs(gs_img.size[0] - expected[0]) <= 2, ( diff --git a/tests/test_rotation.py b/tests/test_rotation.py index 3d5820d6..e4dfe712 100644 --- a/tests/test_rotation.py +++ b/tests/test_rotation.py @@ -329,11 +329,11 @@ def test_rotate_and_crop( def test_rasterize_rotates(resources, tmp_path): - from ocrmypdf._options import OCROptions + from ocrmypdf._options import OcrOptions pm = get_plugin_manager([]) - options = OCROptions( + options = OcrOptions( input_file=resources / 'graph.pdf', output_file=tmp_path / 'out.pdf', rasterizer='ghostscript', # Use Ghostscript for consistent dimensions diff --git a/tests/test_validation.py b/tests/test_validation.py index 038da945..535f013f 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -13,7 +13,7 @@ import pytest from ocrmypdf import _validation as vd from ocrmypdf._concurrent import NullProgressBar, SerialExecutor from ocrmypdf._exec.tesseract import TesseractVersion -from ocrmypdf._options import OCROptions +from ocrmypdf._options import OcrOptions from ocrmypdf.api import create_options, setup_plugin_infrastructure from ocrmypdf.cli import get_parser from ocrmypdf.exceptions import BadArgsError, MissingDependencyError @@ -42,8 +42,8 @@ def make_opts(*args, **kwargs): def make_ocr_opts(input_file='a.pdf', output_file='b.pdf', **kwargs): - """Create OCROptions directly for testing Pydantic validation.""" - return OCROptions(input_file=input_file, output_file=output_file, **kwargs) + """Create OcrOptions directly for testing Pydantic validation.""" + return OcrOptions(input_file=input_file, output_file=output_file, **kwargs) def test_old_tesseract_error(): @@ -95,7 +95,7 @@ def test_optimizing(caplog): def test_pillow_options(): - # Test that max_image_mpixels=0 is valid (validation now in OCROptions) + # Test that max_image_mpixels=0 is valid (validation now in OcrOptions) opts = make_ocr_opts(max_image_mpixels=0) assert opts.max_image_mpixels == 0