feat: add PluginOptionRegistry for dynamic option models
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# SPDX-FileCopyrightText: 2024 James R. Barlow
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
"""Plugin option registry for dynamic model composition."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Type
|
||||
|
||||
from pydantic import BaseModel, create_model
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PluginOptionRegistry:
|
||||
"""Registry for plugin option models."""
|
||||
|
||||
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]) -> None:
|
||||
"""Register a plugin's option model.
|
||||
|
||||
Args:
|
||||
namespace: The namespace for the plugin options (e.g., 'tesseract')
|
||||
model_class: The Pydantic model class for the plugin options
|
||||
"""
|
||||
if namespace in self._option_models:
|
||||
log.warning(f"Plugin option namespace '{namespace}' already registered, overriding")
|
||||
|
||||
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__}")
|
||||
|
||||
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
|
||||
+15
-3
@@ -79,7 +79,7 @@ def setup_plugin_infrastructure(
|
||||
This function handles:
|
||||
1. Creating or validating the plugin manager
|
||||
2. Calling plugin initialization hooks
|
||||
3. Setting up any future plugin registries (Phase 2)
|
||||
3. Setting up plugin option registry
|
||||
|
||||
Args:
|
||||
plugins: List of plugin paths/names to load
|
||||
@@ -105,10 +105,22 @@ def setup_plugin_infrastructure(
|
||||
if not plugin_manager:
|
||||
plugin_manager = get_plugin_manager(plugins)
|
||||
|
||||
# Initialize plugins (this was missing in the API path)
|
||||
# Initialize plugins
|
||||
plugin_manager.hook.initialize(plugin_manager=plugin_manager) # pylint: disable=no-member
|
||||
|
||||
# Future: Initialize plugin option registry here (Phase 2)
|
||||
# Initialize plugin option registry
|
||||
from ocrmypdf._plugin_registry import PluginOptionRegistry
|
||||
registry = PluginOptionRegistry()
|
||||
|
||||
# Let plugins register their option models
|
||||
option_models = plugin_manager.hook.register_options() # pylint: disable=no-member
|
||||
for plugin_options in option_models:
|
||||
if plugin_options: # Skip None returns
|
||||
for namespace, model_class in plugin_options.items():
|
||||
registry.register_option_model(namespace, model_class)
|
||||
|
||||
# Store registry in plugin manager for later access
|
||||
plugin_manager._option_registry = registry
|
||||
|
||||
return plugin_manager
|
||||
|
||||
|
||||
@@ -29,6 +29,12 @@ class GhostscriptOptions(BaseModel):
|
||||
pdfa_image_compression: Annotated[str, Field(description="PDF/A image compression method")] = "auto"
|
||||
|
||||
|
||||
@hookimpl
|
||||
def register_options():
|
||||
"""Register Ghostscript option model."""
|
||||
return {'ghostscript': GhostscriptOptions}
|
||||
|
||||
|
||||
@hookimpl
|
||||
def add_options(parser):
|
||||
gs = parser.add_argument_group("Ghostscript", "Advanced control of Ghostscript")
|
||||
|
||||
@@ -33,6 +33,12 @@ class OptimizeOptions(BaseModel):
|
||||
jbig2_threshold: Annotated[float, Field(ge=0.4, le=0.9, description="JBIG2 symbol classification threshold")] = 0.85
|
||||
|
||||
|
||||
@hookimpl
|
||||
def register_options():
|
||||
"""Register optimization option model."""
|
||||
return {'optimize': OptimizeOptions}
|
||||
|
||||
|
||||
@hookimpl
|
||||
def add_options(parser):
|
||||
optimizing = parser.add_argument_group(
|
||||
|
||||
@@ -38,6 +38,12 @@ class TesseractOptions(BaseModel):
|
||||
downsample_above: Annotated[int, Field(ge=100, le=32767, description="Downsample images larger than this pixel size")] = 32767
|
||||
|
||||
|
||||
@hookimpl
|
||||
def register_options():
|
||||
"""Register Tesseract option model."""
|
||||
return {'tesseract': TesseractOptions}
|
||||
|
||||
|
||||
@hookimpl
|
||||
def add_options(parser):
|
||||
tess = parser.add_argument_group("Tesseract", "Advanced control of Tesseract OCR")
|
||||
|
||||
@@ -13,6 +13,7 @@ from pathlib import Path
|
||||
from typing import TYPE_CHECKING, NamedTuple
|
||||
|
||||
import pluggy
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ocrmypdf import Executor, PdfContext
|
||||
from ocrmypdf._options import OCROptions
|
||||
@@ -87,6 +88,28 @@ def add_options(parser: ArgumentParser) -> None:
|
||||
"""
|
||||
|
||||
|
||||
@hookspec
|
||||
def register_options() -> dict[str, type[BaseModel]]:
|
||||
"""Return plugin's option models keyed by namespace.
|
||||
|
||||
This hook allows plugins to register their option models with the
|
||||
plugin option registry. The returned dictionary should map namespace
|
||||
strings to Pydantic model classes.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping namespace strings to BaseModel classes
|
||||
|
||||
Example:
|
||||
@hookimpl
|
||||
def register_options():
|
||||
return {'tesseract': TesseractOptions}
|
||||
|
||||
Note:
|
||||
This hook will be called from the main process during plugin
|
||||
infrastructure setup, before child worker processes are forked.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec
|
||||
def check_options(options: OCROptions) -> None:
|
||||
"""Called to ask the plugin to check all of the options.
|
||||
|
||||
Reference in New Issue
Block a user