From a4ee513cd41cb191b84615759fa558136863e6a0 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 20 Dec 2025 16:37:50 -0800 Subject: [PATCH] refactor: clean up deprecated code and update plugin docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove outdated Phase comments from _options.py and cli.py - Remove unused methods from PluginOptionRegistry: - get_extended_options_model() - replaced by __getattr__ in OCROptions - map_legacy_options() - unused - validate_plugin_options() - unused - Update plugin documentation to document register_options hook - Add documentation for nested plugin option access pattern 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Opus 4.5 --- docs/plugins.md | 25 +++++- src/ocrmypdf/_options.py | 9 +-- src/ocrmypdf/_plugin_registry.py | 126 +++---------------------------- src/ocrmypdf/cli.py | 4 - 4 files changed, 35 insertions(+), 129 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 8a7e0155..982a8a8e 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -164,10 +164,29 @@ chaining operations. .. autofunction:: ocrmypdf.pluginspec.check_options ``` +### Plugin option models + +Plugins can define their own option models using Pydantic. This allows plugins to: + +- Define type-safe option structures with validation +- Add CLI arguments that map to their option model fields +- Access options via nested namespaces (e.g., `options.tesseract.timeout`) + +```{eval-rst} +.. autofunction:: ocrmypdf.pluginspec.register_options +``` + +Plugin options can be accessed in two ways: + +1. **Flat access** (backward compatible): `options.tesseract_timeout` +2. **Nested access**: `options.tesseract.timeout` + +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 -continue working due to duck-typing compatibility, but plugin developers should +**Plugin Interface Change**: Starting in OCRmyPDF v16.13.0, plugin hooks 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 accordingly. ::: diff --git a/src/ocrmypdf/_options.py b/src/ocrmypdf/_options.py index 1ccafc02..fcbd4f2e 100644 --- a/src/ocrmypdf/_options.py +++ b/src/ocrmypdf/_options.py @@ -153,10 +153,7 @@ class OCROptions(BaseModel): fast_web_view: float = 1.0 continue_on_soft_render_error: bool | None = None - # Plugin option namespaces (for backward compatibility, will be removed in Phase 5) - # These will be populated dynamically based on loaded plugins - - # Legacy tesseract options (for backward compatibility) + # Tesseract options - also accessible via options.tesseract. tesseract_config: list[str] = [] tesseract_pagesegmode: int | None = None tesseract_oem: int | None = None @@ -166,11 +163,11 @@ class OCROptions(BaseModel): tesseract_downsample_above: int = 32767 tesseract_downsample_large_images: bool | None = None - # Legacy ghostscript options (for backward compatibility) + # Ghostscript options - also accessible via options.ghostscript. pdfa_image_compression: str | None = None color_conversion_strategy: str = "LeaveColorUnchanged" - # Legacy jbig2 options (for backward compatibility) + # Optimize/JBIG2 options - also accessible via options.optimize. jbig2_lossy: bool | None = None jbig2_page_group_size: int | None = None jbig2_threshold: float = 0.85 diff --git a/src/ocrmypdf/_plugin_registry.py b/src/ocrmypdf/_plugin_registry.py index d7fd9eaf..8fa9d6e1 100644 --- a/src/ocrmypdf/_plugin_registry.py +++ b/src/ocrmypdf/_plugin_registry.py @@ -7,17 +7,22 @@ from __future__ import annotations import logging -from pydantic import BaseModel, create_model +from pydantic import BaseModel log = logging.getLogger(__name__) class PluginOptionRegistry: - """Registry for plugin option models.""" + """Registry for plugin option models. + + This registry collects option models from plugins during initialization. + 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). + """ def __init__(self): self._option_models: dict[str, type[BaseModel]] = {} - self._extended_model_cache: type[BaseModel] | None = None def register_option_model( self, namespace: str, model_class: type[BaseModel] @@ -34,123 +39,12 @@ class PluginOptionRegistry: ) self._option_models[namespace] = model_class - # Clear cache when new models are registered - self._extended_model_cache = None log.debug( - f"Registered plugin option model for namespace '{namespace}': {model_class.__name__}" + f"Registered plugin option model for namespace '{namespace}': " + f"{model_class.__name__}" ) def get_registered_models(self) -> dict[str, type[BaseModel]]: """Get all registered plugin option models.""" return self._option_models.copy() - - def get_extended_options_model( - self, base_model: type[BaseModel] - ) -> type[BaseModel]: - """Create an extended options model that includes all registered plugin options. - - Args: - base_model: The base OCROptions model to extend - - Returns: - A new model class that includes the base model fields plus all plugin option fields - """ - if self._extended_model_cache is not None: - return self._extended_model_cache - - # Start with base model fields - model_fields = {} - - # Add plugin option models as nested fields - for namespace, model_class in self._option_models.items(): - model_fields[namespace] = (model_class, model_class()) - - # Create the extended model - self._extended_model_cache = create_model( - 'ExtendedOCROptions', __base__=base_model, **model_fields - ) - - return self._extended_model_cache - - def clear_cache(self) -> None: - """Clear the extended model cache.""" - self._extended_model_cache = None - - def map_legacy_options(self, options: dict) -> dict: - """Map legacy flat options to nested plugin options. - - This method helps with backward compatibility by mapping flat option - names (like 'tesseract_timeout') to nested plugin option structures. - - Args: - options: Dictionary of flat options - - Returns: - Dictionary with both legacy flat options and nested plugin options - """ - result = options.copy() - - # Map tesseract options - if 'tesseract' in self._option_models: - tesseract_options = {} - tesseract_fields = self._option_models['tesseract'].model_fields.keys() - - for field in tesseract_fields: - legacy_key = f'tesseract_{field}' - if legacy_key in options: - tesseract_options[field] = options[legacy_key] - - if tesseract_options: - result['tesseract'] = tesseract_options - - # Map optimize options - if 'optimize' in self._option_models: - optimize_options = {} - optimize_fields = self._option_models['optimize'].model_fields.keys() - - for field in optimize_fields: - if field in options: - optimize_options[field] = options[field] - # Handle special case for optimize level - elif field == 'level' and 'optimize' in options: - optimize_options[field] = options['optimize'] - - if optimize_options: - result['optimize'] = optimize_options - - # Map ghostscript options - if 'ghostscript' in self._option_models: - ghostscript_options = {} - ghostscript_fields = self._option_models['ghostscript'].model_fields.keys() - - for field in ghostscript_fields: - if field in options: - ghostscript_options[field] = options[field] - - if ghostscript_options: - result['ghostscript'] = ghostscript_options - - return result - - def validate_plugin_options(self, options: dict) -> dict: - """Validate plugin options using their registered models. - - Args: - options: Dictionary containing plugin options - - Returns: - Dictionary with validated plugin options - - Raises: - ValidationError: If any plugin options are invalid - """ - validated = {} - - for namespace, model_class in self._option_models.items(): - if namespace in options: - # Validate using the plugin's model - plugin_options = model_class(**options[namespace]) - validated[namespace] = plugin_options.model_dump() - - return validated diff --git a/src/ocrmypdf/cli.py b/src/ocrmypdf/cli.py index 3ae6dc21..107430a7 100644 --- a/src/ocrmypdf/cli.py +++ b/src/ocrmypdf/cli.py @@ -462,10 +462,6 @@ 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 - # Handle backward compatibility for plugin options - # Map CLI arguments to the appropriate fields for now - # In Phase 2, this will be handled by plugin option models - instance = OCROptions(**known_fields) instance.extra_attrs = extra_attrs return instance