Source code for xarray_validate.base

from __future__ import annotations

from abc import ABC, abstractmethod
from enum import Enum
from pathlib import Path
from typing import Any, List

import attrs


[docs] class ValidationMode(Enum): """ Validation behaviour mode. """ EAGER = "eager" #: Raise SchemaError on first validation failure (default behavior) LAZY = "lazy" #: Collect all validation errors and return them in ValidationResult
[docs] @attrs.define class ValidationResult: """ Result of schema validation with error location mapping. Parameters ---------- errors : list of tuple[str, SchemaError] List of (path, error) pairs mapping errors to tree locations. """ errors: list[tuple[str, SchemaError]] = attrs.field(factory=list) @property def has_errors(self): return bool(len(self.errors))
[docs] def add_error(self, path: str, error: SchemaError) -> None: """Add an error at the specified path.""" self.errors.append((path, error))
[docs] def get_error_summary(self) -> str: """Get a formatted summary of all validation errors.""" if not self.has_errors: return "Validation passed" lines = ["Validation failed with errors:"] for path, error in self.errors: lines.append(f" {path}: {error}") return "\n".join(lines)
[docs] @attrs.define class ValidationContext: """ Context for tracking validation state during schema tree traversal. Parameters ---------- path : list of str, optional Current validation path through the schema tree. mode : ValidationMode or str, default: :data:`ValidationMode.EAGER` Validation behavior mode (eager or lazy). Strings are converted to lowercase and passed to the :class:`ValidationMode` constructor. result : ValidationResult, optional Shared result object for collecting errors in lazy mode. """ path: list[str] = attrs.field(factory=list, converter=list) mode: ValidationMode = attrs.field( default=ValidationMode.EAGER, converter=lambda x: ValidationMode(x.lower() if isinstance(x, str) else x), ) result: ValidationResult = attrs.field(factory=ValidationResult)
[docs] def push(self, component: str) -> ValidationContext: """ Create a new context with an additional path component. Parameters ---------- component : str Path component to add (e.g., 'dtype', 'coords.x', 'data_vars.temperature') Returns ------- ValidationContext New context with extended path sharing the same mode and result. """ return ValidationContext( path=self.path + [component], mode=self.mode, result=self.result )
[docs] def get_path_string(self) -> str: """Get current path as dot-separated string.""" return ".".join(self.path) if self.path else "<root>"
[docs] def handle_error(self, error: SchemaError) -> None: """ Handle validation error based on mode. * In EAGER mode: raise the error immediately * In LAZY mode: collect error in result object Parameters ---------- error : SchemaError Validation error to handle. """ if self.mode == ValidationMode.EAGER: raise error else: # LAZY mode self.result.add_error(self.get_path_string(), error)
[docs] def get_errors(self) -> List[tuple[str, SchemaError]]: """Get all collected errors with their paths.""" return self.result.errors.copy()
@property def has_errors(self) -> bool: """Check if any errors have been collected.""" return self.result.has_errors
def raise_or_handle( error: SchemaError, context: ValidationContext | None = None, from_exc: Exception | None = None, ) -> None: """ Raise error or handle it via context if available. Parameters ---------- error : SchemaError The error to raise or handle. context : ValidationContext or None Validation context. If provided, error is handled via context. Otherwise, error is raised. from_exc : Exception or None Optional exception to chain from when raising. """ if context: context.handle_error(error) else: if from_exc is not None: raise error from from_exc else: raise error
[docs] class SchemaError(Exception): """Custom schema error."""
class BaseSchema(ABC): @abstractmethod def serialize(self): """ Serialize schema to basic Python types. """ pass @classmethod @abstractmethod def deserialize(cls, obj): """ Instantiate schema from basic Python types. """ pass @classmethod def from_yaml(cls, path: Path | str): """ Load schema from a YAML file. Parameters ---------- path : path-like Path to the YAML file containing the schema definition. Returns ------- Schema instance deserialized from the YAML file. Raises ------ ImportError If `ruamel.yaml <https://yaml.dev/doc/ruamel.yaml/>`__ is not installed. """ try: from ruamel.yaml import YAML except ImportError as e: raise ImportError( "Loading schemas from YAML files requires ruamel.yaml. " "Install it with:\n" " pip install xarray-validate[yaml]\n" "or:\n" " pip install ruamel-yaml" ) from e yaml = YAML(typ="safe") with open(path) as f: schema_dict = yaml.load(f) return cls.deserialize(schema_dict) @classmethod def convert(cls, value: Any): """ Attempt conversion of ``value`` to this schema type. """ if isinstance(value, cls): return value return cls.deserialize(value) @abstractmethod def validate(self, value: Any, context: ValidationContext | None = None) -> None: """ Validate object against this schema. Parameters ---------- value Object to validate. context : ValidationContext, optional Validation context for tracking tree traversal state. Returns ------- None Raises ------ SchemaError If validation fails. """ pass