refactor: reorganize CLI and options initialization

Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
James R. Barlow
2025-12-21 12:21:48 -08:00
co-authored by aider
parent 08ee5690bc
commit 4f9c4c3e52
5 changed files with 74 additions and 59 deletions
+2 -6
View File
@@ -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)
+2 -9
View File
@@ -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
-25
View File
@@ -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
+2 -18
View File
@@ -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']
+68 -1
View File
@@ -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