From f5bfd2fd3e45d51158816ab745670dd0811ec266 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Mon, 8 Dec 2025 17:33:45 -0800 Subject: [PATCH] Add compat function for pages_from_ranges fix: handle Pydantic validation errors with correct exit code Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) --- src/ocrmypdf/_jobcontext.py | 6 ++++++ src/ocrmypdf/_pipelines/_common.py | 12 +++++++++++- src/ocrmypdf/_pipelines/ocr.py | 18 ++++++++++++++++++ tests/test_page_numbers.py | 2 +- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/ocrmypdf/_jobcontext.py b/src/ocrmypdf/_jobcontext.py index 04df33b3..990b22a8 100644 --- a/src/ocrmypdf/_jobcontext.py +++ b/src/ocrmypdf/_jobcontext.py @@ -103,6 +103,8 @@ class PageContext: self.pageno = pageno self.pageinfo = pdf_context.pdfinfo[pageno] self.plugin_manager = pdf_context.plugin_manager + # Ensure no reference to PdfContext which contains OCROptions + self._pdf_context = None def get_path(self, name: str) -> Path: """Generate a ``Path`` for a file that is part of processing this page. @@ -115,6 +117,7 @@ class PageContext: def __getstate__(self): state = self.__dict__.copy() + # Ensure we only pickle the Namespace, not any Pydantic objects state['options'] = copy(self.options) # Handle stream inputs if hasattr(state['options'], 'input_file'): @@ -123,4 +126,7 @@ class PageContext: if hasattr(state['options'], 'output_file'): if not isinstance(state['options'].output_file, str | bytes | os.PathLike): state['options'].output_file = 'stream' + + # Remove any potential references to Pydantic objects + state.pop('_pdf_context', None) return state diff --git a/src/ocrmypdf/_pipelines/_common.py b/src/ocrmypdf/_pipelines/_common.py index d3c219e0..b39e1d30 100644 --- a/src/ocrmypdf/_pipelines/_common.py +++ b/src/ocrmypdf/_pipelines/_common.py @@ -50,7 +50,7 @@ from ocrmypdf._plugin_manager import OcrmypdfPluginManager from ocrmypdf._validation import ( report_output_file_size, ) -from ocrmypdf.exceptions import ExitCode, ExitCodeException +from ocrmypdf.exceptions import BadArgsError, ExitCode, ExitCodeException from ocrmypdf.helpers import ( available_cpu_count, check_pdf, @@ -275,6 +275,16 @@ def cli_exception_handler( else: log.error(type(e).__name__) return e.exit_code + except ValueError as e: + # Convert Pydantic validation errors to BadArgsError for proper exit code + if "validation error" in str(e).lower() or "value error" in str(e).lower(): + if options.verbose >= 1: + log.exception("Validation error") + else: + log.error("Invalid argument: %s", str(e)) + return ExitCode.bad_args + # Re-raise other ValueErrors to be caught by the general exception handler + raise except PIL.Image.DecompressionBombError: log.exception( "A decompression bomb error was encountered while executing the " diff --git a/src/ocrmypdf/_pipelines/ocr.py b/src/ocrmypdf/_pipelines/ocr.py index 907422fd..3c9b217d 100644 --- a/src/ocrmypdf/_pipelines/ocr.py +++ b/src/ocrmypdf/_pipelines/ocr.py @@ -48,6 +48,21 @@ from ocrmypdf._validation import ( check_requested_output_file, create_input_file, ) + + +def _convert_pages_field_for_legacy_compatibility(options: argparse.Namespace) -> None: + """Convert pages field from string to set if needed. + + This is a temporary shim to handle the transition from CLI string processing + to OCROptions Pydantic validation. The pages field needs to be converted + before calling do_get_pdfinfo() since PdfInfo expects a Container[int]. + + TODO: Remove this function when the refactoring plan is complete and all + pipeline functions work directly with OCROptions instead of Namespace. + """ + if hasattr(options, 'pages') and isinstance(options.pages, str): + from ocrmypdf._options import _pages_from_ranges + options.pages = _pages_from_ranges(options.pages) from ocrmypdf.exceptions import ExitCode log = logging.getLogger(__name__) @@ -175,6 +190,9 @@ def _run_pipeline( original_filename, start_input_file, work_folder / 'origin.pdf', options ) + # Convert pages field if needed before gathering pdfinfo + _convert_pages_field_for_legacy_compatibility(options) + # Gather pdfinfo and create context pdfinfo = do_get_pdfinfo(origin_pdf, executor, options) context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager) diff --git a/tests/test_page_numbers.py b/tests/test_page_numbers.py index aac80c0d..79b1acf0 100644 --- a/tests/test_page_numbers.py +++ b/tests/test_page_numbers.py @@ -6,7 +6,7 @@ from __future__ import annotations import pytest import ocrmypdf -from ocrmypdf._validation import _pages_from_ranges +from ocrmypdf._options import _pages_from_ranges from ocrmypdf.exceptions import BadArgsError from ocrmypdf.pdfinfo import PdfInfo