From 4f9c4c3e52813890cdcbf67b0f204bade3a6d15f Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Fri, 12 Dec 2025 01:42:23 -0800 Subject: [PATCH] refactor: reorganize CLI and options initialization Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) --- src/ocrmypdf/__main__.py | 8 +--- src/ocrmypdf/_jobcontext.py | 11 +----- src/ocrmypdf/_options.py | 25 ------------ src/ocrmypdf/_plugin_manager.py | 20 +--------- src/ocrmypdf/cli.py | 69 ++++++++++++++++++++++++++++++++- 5 files changed, 74 insertions(+), 59 deletions(-) diff --git a/src/ocrmypdf/__main__.py b/src/ocrmypdf/__main__.py index 24a567e7..b1514a86 100755 --- a/src/ocrmypdf/__main__.py +++ b/src/ocrmypdf/__main__.py @@ -14,10 +14,9 @@ import sys from contextlib import suppress from ocrmypdf import __version__ -from ocrmypdf._options import OCROptions from ocrmypdf._pipelines.ocr import run_pipeline_cli -from ocrmypdf._plugin_manager import get_parser_options_plugins from ocrmypdf._validation import check_options +from ocrmypdf.cli import get_options_and_plugins from ocrmypdf.api import Verbosity, configure_logging from ocrmypdf.exceptions import ( BadArgsError, @@ -40,10 +39,7 @@ def sigbus(*args): def run(args=None): """Run the ocrmypdf command line interface.""" - _parser, namespace_options, plugin_manager = get_parser_options_plugins(args=args) - - # Convert Namespace to OCROptions - options = OCROptions.from_namespace(namespace_options) + options, plugin_manager = get_options_and_plugins(args=args) with suppress(AttributeError, PermissionError): os.nice(5) diff --git a/src/ocrmypdf/_jobcontext.py b/src/ocrmypdf/_jobcontext.py index 7558538e..10a51f42 100644 --- a/src/ocrmypdf/_jobcontext.py +++ b/src/ocrmypdf/_jobcontext.py @@ -5,7 +5,6 @@ from __future__ import annotations -from argparse import Namespace from collections.abc import Iterator from pathlib import Path @@ -26,19 +25,13 @@ class PdfContext: def __init__( self, - options: OCROptions | Namespace, + options: OCROptions, work_folder: Path, origin: Path, pdfinfo: PdfInfo, plugin_manager, ): - # Handle both OCROptions and Namespace during transition - if isinstance(options, OCROptions): - self.options = options - else: - # Convert Namespace to OCROptions - self.options = OCROptions.from_namespace(options) - + self.options = options self.work_folder = work_folder self.origin = origin self.pdfinfo = pdfinfo diff --git a/src/ocrmypdf/_options.py b/src/ocrmypdf/_options.py index 15493e70..1353abab 100644 --- a/src/ocrmypdf/_options.py +++ b/src/ocrmypdf/_options.py @@ -9,7 +9,6 @@ import json import logging import os import unicodedata -from argparse import Namespace from collections.abc import Sequence from io import IOBase from pathlib import Path @@ -165,30 +164,6 @@ class OCROptions(BaseModel): default_factory=dict, exclude=True, alias='_extra_attrs' ) - @classmethod - def from_namespace(cls, ns: Namespace) -> OCROptions: - """Convert argparse.Namespace to OCROptions.""" - # Extract known fields - known_fields = {} - extra_attrs = {} - - for key, value in vars(ns).items(): - if key in cls.model_fields: - known_fields[key] = value - else: - extra_attrs[key] = value - - # Handle special cases for hOCR API - if 'output_folder' in extra_attrs and 'output_file' not in known_fields: - known_fields['output_file'] = '/dev/null' # Placeholder - - # Handle case where input_file is missing (e.g., in _hocr_to_ocr_pdf) - if 'work_folder' in extra_attrs and 'input_file' not in known_fields: - known_fields['input_file'] = '/dev/null' # Placeholder - - instance = cls(**known_fields) - instance.extra_attrs = extra_attrs - return instance @field_validator('languages') @classmethod diff --git a/src/ocrmypdf/_plugin_manager.py b/src/ocrmypdf/_plugin_manager.py index 090aaf27..146704fc 100644 --- a/src/ocrmypdf/_plugin_manager.py +++ b/src/ocrmypdf/_plugin_manager.py @@ -17,7 +17,7 @@ import pluggy import ocrmypdf.builtin_plugins from ocrmypdf import pluginspec -from ocrmypdf.cli import get_parser, plugins_only_parser +from ocrmypdf.cli import get_parser class OcrmypdfPluginManager(pluggy.PluginManager): @@ -101,20 +101,4 @@ def get_plugin_manager( ) -def get_parser_options_plugins( - args: Sequence[str], -) -> tuple[argparse.ArgumentParser, argparse.Namespace, pluggy.PluginManager]: - pre_options, _unused = plugins_only_parser.parse_known_args(args=args) - plugin_manager = get_plugin_manager(pre_options.plugins) - - parser = get_parser() - plugin_manager.hook.initialize( # pylint: disable=no-member - plugin_manager=plugin_manager - ) - plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member - - options = parser.parse_args(args=args) - return parser, options, plugin_manager - - -__all__ = ['OcrmypdfPluginManager', 'get_plugin_manager', 'get_parser_options_plugins'] +__all__ = ['OcrmypdfPluginManager', 'get_plugin_manager'] diff --git a/src/ocrmypdf/cli.py b/src/ocrmypdf/cli.py index f3bd50cd..4da0c250 100644 --- a/src/ocrmypdf/cli.py +++ b/src/ocrmypdf/cli.py @@ -9,7 +9,7 @@ import argparse from collections.abc import Callable, Mapping from typing import Any, TypeVar -from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD +from ocrmypdf._defaults import DEFAULT_LANGUAGE, DEFAULT_ROTATE_PAGES_THRESHOLD from ocrmypdf._defaults import PROGRAM_NAME as _PROGRAM_NAME from ocrmypdf._version import __version__ as _VERSION @@ -465,3 +465,70 @@ plugins_only_parser.add_argument( default=[], help="Name of plugin to import.", ) + + +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. + """ + from ocrmypdf._options import OCROptions + + # Extract known fields + known_fields = {} + extra_attrs = {} + + for key, value in vars(ns).items(): + if key in OCROptions.model_fields: + known_fields[key] = value + else: + extra_attrs[key] = value + + # Handle special cases for hOCR API + if 'output_folder' in extra_attrs and 'output_file' not in known_fields: + known_fields['output_file'] = '/dev/null' # Placeholder + + # Handle case where input_file is missing (e.g., in _hocr_to_ocr_pdf) + 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.extra_attrs = extra_attrs + return instance + + +def get_options_and_plugins( + args=None, +) -> tuple['OCROptions', 'pluggy.PluginManager']: + """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 + options model. + + Args: + args: Command line arguments. If None, uses sys.argv. + + Returns: + Tuple of (OCROptions, PluginManager) + """ + import pluggy + from ocrmypdf._plugin_manager import get_plugin_manager + + # First pass: get plugins so we can register their options + pre_options, _unused = plugins_only_parser.parse_known_args(args=args) + plugin_manager = get_plugin_manager(pre_options.plugins) + + # Get parser and let plugins add their options + parser = get_parser() + plugin_manager.hook.initialize(plugin_manager=plugin_manager) # pylint: disable=no-member + plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member + + # Parse all arguments + namespace = parser.parse_args(args=args) + + # Convert to OCROptions + options = namespace_to_options(namespace) + + return options, plugin_manager