Source code for sloth.mmcif.validator

"""
SLOTH Validation

Validation exception classes, the :class:`ValidatorPlugin` that powers
per-category and cross-category checks, and :class:`CategoryValidator` (the
chainable wrapper).
"""

from enum import Enum, auto
from typing import Any, Callable, Dict, List, Tuple, Optional, TYPE_CHECKING

from .plugins import Plugin, PluginWrapper

if TYPE_CHECKING:
    from .models import Category


[docs] class ValidationSeverity(Enum): """Severity levels for validation errors.""" ERROR = auto() # Validation failures that should prevent processing WARNING = auto() # Issues that should be flagged but don't prevent processing INFO = auto() # Informational notices
[docs] class ValidationError(Exception): """Exception raised for validation errors."""
[docs] def __init__( self, message: str, path: str = "", severity: ValidationSeverity = ValidationSeverity.ERROR, ): """ Initialize validation error. Args: message: Error message path: Path where the error occurred (e.g., JSON path, category name) severity: Validation error severity """ self.message = message self.path = path self.severity = severity if path: super().__init__(f"{path}: {message}") else: super().__init__(message)
# --------------------------------------------------------------------------- # Validation plugin # ---------------------------------------------------------------------------
[docs] class ValidatorPlugin(Plugin): """Plugin for per-category validation with cross-checker support. Multiple validators can be registered for the same category β€” they will all run in registration order. """
[docs] def __init__(self): self._validators: Dict[str, List[Callable]] = {} self._cross_checkers: Dict[Tuple[str, str], List[Callable]] = {}
# -- registration helpers -----------------------------------------------
[docs] def register_validator( self, category_name: str, validator_function: Callable ) -> None: """Register a validator callable for a category name. Multiple validators for the same category are allowed. """ self._validators.setdefault(category_name, []).append(validator_function)
[docs] def register_cross_checker( self, category_pair: Tuple[str, str], cross_checker_function: Callable, ) -> None: """Register a cross-checker callable for a pair of category names.""" self._cross_checkers.setdefault(category_pair, []).append(cross_checker_function)
# -- lookup helpers -----------------------------------------------------
[docs] def get_validators(self, category_name: str) -> List[Callable]: """Return all validators for *category_name*.""" return self._validators.get(category_name, [])
[docs] def get_cross_checkers( self, category_pair: Tuple[str, str] ) -> List[Callable]: """Return all cross-checkers for *category_pair*.""" return self._cross_checkers.get(category_pair, [])
# -- Plugin interface ---------------------------------------------------
[docs] def create_wrapper(self, target) -> "CategoryValidator": return CategoryValidator(target, self)
[docs] def execute(self, target, *args, **kwargs) -> Any: results = [] for validator in self._validators.get(target.name, []): result = validator(target) if result is not None: results.append(result) return results or None
[docs] class CategoryValidator(PluginWrapper): """Chainable wrapper for category validation with cross-checking.""" _plugin: "ValidatorPlugin" def __call__(self) -> "CategoryValidator": """Execute the registered validator for this category.""" super().__call__() return self
[docs] def against(self, other_category: "Category") -> "CategoryValidator": """Execute cross-validation against *other_category*.""" for cross_checker in self._plugin.get_cross_checkers( (self._target.name, other_category.name) ): cross_checker(self._target, other_category) return self