Source code for eradiate.validators

from __future__ import annotations

import typing as t
from numbers import Number

import attrs
import numpy as np
import pint
import xarray as xr

from .attrs import AUTO
from .units import PhysicalQuantity
from .units import unit_registry as ureg


[docs] def is_scalar(_, attribute, value): """ Validate iff value is of scalar type in the sense of Numpy. Raises ------ TypeError If the value is not scalar. """ if not np.isscalar(value): raise TypeError(f"'{attribute.name}' must be scalar, got {value}")
[docs] def is_number(_, attribute, value): """ Validate iff value is of a numeric type. Raises ------ TypeError If the value is not a :class:`Number`. """ if not isinstance(value, Number): raise TypeError( f"{attribute.name} must be a real number, " f"got {value} which is a {value.__class__}" )
[docs] def is_vector3(instance, attribute, value): """ Validate iff value can be converted to a (3,) Numpy array. Raises ------ TypeError If value cannot be converted to a (3,) :class:`numpy.ndarray`. """ return attrs.validators.deep_iterable( member_validator=is_number, iterable_validator=has_len(3) )(instance, attribute, value)
[docs] def is_positive(_, attribute, value): """ Validate iff value is a positive number. Raises ------ ValueError If the value is not positive or zero. """ if value < 0.0: raise ValueError(f"{attribute} must be positive or zero, got {value}")
[docs] def all_positive(_, attribute, value): """ Validate iff value is a vector with all its values positive. Raises ------ ValueError If not all values are positive. """ if isinstance(value, ureg.Quantity): value = value.magnitude if np.any(np.array(value) < 0): raise ValueError(f"{attribute} must be all positive or zero, got {value}")
[docs] def all_strictly_positive(_, attribute, value): """ Validate iff value is a vector with all its values strictly positive. Raises ------ ValueError If not all values are strictly positive. """ if isinstance(value, ureg.Quantity): value = value.magnitude if np.any(np.array(value) <= 0): raise ValueError(f"{attribute} must be all strictly positive, got {value}")
[docs] def path_exists(_, attribute, value): """ Validate iff initializer is called with a value defining a path to an existing location. Raises ------ FileNotFoundError If the value is not a :class:`pathlib.Path` which points to an existing location. """ if not value.exists(): raise FileNotFoundError( f"{attribute} points to '{str(value)}' (path does not exist)" )
[docs] def is_file(_, attribute, value): """ Validate iff initializer is called with a value defining a path to an existing file. Raises ------ FileNotFoundError If the value is not a :class:`pathlib.Path` which points to an existing file. """ if not value.is_file(): raise FileNotFoundError( f"{attribute.name} points to '{str(value)}' (not a file)" )
[docs] def is_dir(_, attribute, value): """ Validate iff initializer is called with a value defining a path to an existing directory. Raises ------ FileNotFoundError If the value is not a :class:`pathlib.Path` which points to an existing directory. """ if not value.is_dir(): raise FileNotFoundError( f"{attribute.name} points to '{str(value)}' (not a directory)" )
[docs] def has_len(size: int): """ Validate iff value is a vector with specified length. Parameters ---------- size : int Expected size of the validated value. Raises ------ ValueError If the value does not have the expected size. """ def f(_, attribute, value): if len(value) != size: raise ValueError( f"{attribute} must be have length {size}, " f"got {value} of length {len(value)}" ) return f
[docs] def has_quantity(quantity: PhysicalQuantity | str | None): """ Validate iff initializer is called with a value featuring a ``quantity`` field set to an expected value. Parameters ---------- quantity : :class:`.PhysicalQuantity` or str or None Expected quantity field. Raises ------ ValueError If the value's ``quantity`` field does not match the expected value. """ if quantity is not None: quantity = PhysicalQuantity(quantity) def f(_, attribute, value): if value.quantity != quantity: raise ValueError( f"incompatible quantity '{value.quantity}' " f"used to set field '{attribute.name}' " f"(allowed: '{quantity}')" ) return f
[docs] def on_quantity(wrapped_validator: t.Callable): """ Apply a validator to the magnitude of a quantity. Parameters ---------- wrapped_validator : callable The validator applied to the value's magnitude. """ def f(instance, attribute, value): if isinstance(value, ureg.Quantity): return wrapped_validator(instance, attribute, value.magnitude) else: return wrapped_validator(instance, attribute, value) return f
[docs] def auto_or(*wrapped_validators): """ Allow for an attribute to be set to :class:`.AUTO`. Parameters ---------- *wrapped_validators : callable Validators to be applied to values not equal to :class:`.AUTO`. """ def f(instance, attribute, value): if value is AUTO: return for validator in wrapped_validators: validator(instance, attribute, value) return f
[docs] def validate_absorption_data(instance, attribute, value): """Validate absorption data. Raises ------ TypeError If the value is not a dict with keys of type tuple and length 2, and values of type :class:`xarray.Dataset`. """ if isinstance(value, dict): for k, v in value.items(): if not isinstance(k, tuple) and list(map(type, k)) == [ pint.Quantity, pint.Quantity, ]: raise TypeError( f"{attribute.name} keys must be 2-tuple of pint.Quantity, " f"(got {type(k)})" ) if not isinstance(v, xr.Dataset): raise TypeError( f"{attribute.name} values must be xarray.Dataset, " f"(got {type(v)})" ) else: raise TypeError(f"{attribute.name} must be a dict, got {type(value)}")