Source code for qcodes.validators.validators

"""
Provides validators for different types of values. Validator validates if the
value belongs to the given type and is in the provided range.
"""
from __future__ import annotations

import math
import typing
from collections import abc
from collections.abc import Hashable
from typing import Any, Generic, Literal, Optional, TypeVar, Union, cast

import numpy as np

BIGSTRING = 1000000000
BIGINT = int(1e18)

numbertypes = Union[float, int, np.floating, np.integer]
shape_type = Union[int, typing.Callable[[], int]]
shape_tuple_type = Optional[tuple[shape_type, ...]]


[docs] def validate_all(*args: tuple[Validator[Any], Any], context: str = "") -> None: """ Takes a list of (validator, value) couplets and tests whether they are all valid, raising ValueError otherwise. Args: *args: Values to validate. context: keyword-only arg with a string to include in the error message giving the user context for the error. """ if context: context = "; " + context for i, (validator, value) in enumerate(args): validator.validate(value, "argument " + str(i) + context)
def range_str( min_val: float | np.floating[Any] | np.integer[Any] | None, max_val: float | np.floating[Any] | np.integer[Any] | None, name: str, ) -> str: """ Utility to represent ranges in Validator repr's. """ if max_val is not None: if min_val is not None: if max_val == min_val: return f" {name}={min_val}" else: return f" {min_val}<={name}<={max_val}" else: return f" {name}<={max_val}" elif min_val is not None: return f" {name}>={min_val}" else: return "" T = TypeVar("T")
[docs] class Validator(Generic[T]): """ Base class for all value validators each validator should implement: __init__: Here a private attribute, `_valid_values`, should be set. ``_valid_values`` must be a tuple of at least one valid value. If possible, it should include all valid values. The purpose of this attribute is to make it possible to find a valid value for a :class:`.Parameter`, given its validator. validate: Function of two args: value, context value is what you're testing. context is a string identifying the caller better. Raises an error (TypeError or ValueError) if the value fails. is_numeric: A boolean flag that marks if this a numeric type. The base class implements, valid_values: A property exposing ``_valid_values``, which is a tuple of examples of valid values. For very simple validators, like :class:`Bool` or :class:`Enum`, the tuple contains all valid values, but in general it just holds SOME valid values. These example values are intended to be useful when simulating instruments. Alternatively you may override ``_valid_values`` and provide your own implementation of getting valid values. """ _valid_values: tuple[T, ...] = () is_numeric = False # is this a numeric type (so it can be swept)?
[docs] def validate(self, value: T, context: str = "") -> None: raise NotImplementedError
@property def valid_values(self) -> tuple[T, ...]: return self._valid_values
[docs] class Anything(Validator[Any]): """Allow any value to pass.""" def __init__(self) -> None: self._valid_values = (0,)
[docs] def validate(self, value: Any, context: str = "") -> None: pass
# NOTE(giulioungaretti): why is_numeric? # it allows for set_step in parameter # TODO(giulioungaretti): possible refactor is_numeric = True def __repr__(self) -> str: return "<Anything>"
[docs] class Nothing(Validator[Any]): """ Allow no value to pass. """ def __init__(self, reason: str) -> None: if reason: self._reason = reason else: self._reason = "Nothing Validator"
[docs] def validate(self, value: Any, context: str = "") -> None: raise RuntimeError(f"{self.reason}; {context}")
def __repr__(self) -> str: return f"<Nothing({self.reason})>" @property def reason(self) -> str: return self._reason @reason.setter def reason(self, reason: str) -> None: self._reason = reason
[docs] class Bool(Validator[Union[bool, np.bool_]]): """ Requires a boolean. """ def __init__(self) -> None: self._valid_values = (True, False)
[docs] def validate(self, value: bool | np.bool_, context: str = "") -> None: """ Validates if bool else raises error. Args: value: Bool context: Context for validation. Raises: TypeError: IF not a boolean. """ if not isinstance(value, bool) and not isinstance(value, np.bool_): raise TypeError(f"{value!r} is not Boolean; {context}")
def __repr__(self) -> str: return "<Boolean>"
[docs] class Strings(Validator[str]): """ Requires a string optional parameters min_length and max_length limit the allowed length to min_length <= len(value) <= max_length Raises: TypeError: If min_length or max_length negative. Or max_length lower than min_length. """ def __init__(self, min_length: int = 0, max_length: int = BIGSTRING) -> None: if isinstance(min_length, int) and min_length >= 0: self._min_length = min_length else: raise TypeError("min_length must be a non-negative integer") if isinstance(max_length, int) and max_length >= max(min_length, 1): self._max_length = max_length else: raise TypeError( "max_length must be a positive integer no smaller than min_length" ) self._valid_values = ("." * min_length,)
[docs] def validate(self, value: str, context: str = "") -> None: """ Validates if string else raises error. Args: value: A string. context: Context for validation. Raises: TypeError: If not a string. ValueError: If length is not between min_length and max_length. """ if not isinstance(value, str): raise TypeError(f"{value!r} is not a string; {context}") vallen = len(value) if vallen < self._min_length or vallen > self._max_length: raise ValueError( f"{value!r} is invalid: length must be between " f"{self._min_length} and {self._max_length} inclusive; {context}" )
def __repr__(self) -> str: minv = self._min_length or None maxv = self._max_length if self._max_length < BIGSTRING else None return "<Strings{}>".format(range_str(minv, maxv, "len")) @property def min_length(self) -> int: return self._min_length @property def max_length(self) -> int: return self._max_length
[docs] class Numbers(Validator[numbertypes]): """ Requires a number of type int, float, numpy.integer or numpy.floating. Args: min_value: Minimal value allowed, default -inf. max_value: Maximal value allowed, default inf. Raises: TypeError: If min or max value not a number. Or if min_value is larger than the max_value. """ validtypes = (float, int, np.integer, np.floating) def __init__( self, min_value: numbertypes = -float("inf"), max_value: numbertypes = float("inf"), ) -> None: if isinstance(min_value, self.validtypes): self._min_value = min_value else: raise TypeError("min_value must be a number") valuesok = max_value > min_value if isinstance(max_value, self.validtypes) and valuesok: self._max_value = max_value else: raise TypeError("max_value must be a number bigger than min_value") self._valid_values = (min_value, max_value)
[docs] def validate(self, value: numbertypes, context: str = "") -> None: """ Validate if number else raises error. Args: value: A number. context: Context for validation. Raises: TypeError: If not int or float. ValueError: If number is not between the min and the max value. """ if not isinstance(value, self.validtypes): raise TypeError(f"{value!r} is not an int or float; {context}") if not (self._min_value <= value <= self._max_value): raise ValueError( f"{value!r} is invalid: must be between " f"{self._min_value} and {self._max_value} inclusive; {context}" )
is_numeric = True def __repr__(self) -> str: minv = self._min_value if math.isfinite(self._min_value) else None maxv = self._max_value if math.isfinite(self._max_value) else None return "<Numbers{}>".format(range_str(minv, maxv, "v")) @property def min_value(self) -> float: return float(self._min_value) @property def max_value(self) -> float: return float(self._max_value)
[docs] class Ints(Validator[Union[int, "np.integer[Any]", bool]]): """ Requires an integer. Optional parameters min_value and max_value, enforce min_value <= value <= max_value. Args: max_value: value must be <= max_value min_value: value must be >= min_value Raises: TypeError: If min_value and max_value is not an integer. Or min_value is larger than the min_value. """ validtypes = (int, np.integer) inttypes = Union[int, np.integer] def __init__( self, min_value: inttypes = -BIGINT, max_value: inttypes = BIGINT ) -> None: if isinstance(min_value, self.validtypes): self._min_value = int(min_value) else: raise TypeError("min_value must be an integer") if not isinstance(max_value, self.validtypes): raise TypeError("max_value must be an integer") if max_value > min_value: self._max_value = int(max_value) else: raise TypeError("max_value must be an integer bigger than min_value") self._valid_values = (min_value, max_value)
[docs] def validate(self, value: inttypes, context: str = "") -> None: """ Validates if int else raises error. Args: value: An integer. context: Context for validation. Raises: TypeError: If not an integer. ValueError: If not between min_value and max_value. """ if not isinstance(value, self.validtypes): raise TypeError(f"{value!r} is not an int; {context}") if not (self._min_value <= value <= self._max_value): raise ValueError( f"{value!r} is invalid: must be between " f"{self._min_value} and {self._max_value} inclusive; {context}" )
is_numeric = True def __repr__(self) -> str: minv = self._min_value if self._min_value > -BIGINT else None maxv = self._max_value if self._max_value < BIGINT else None return "<Ints{}>".format(range_str(minv, maxv, "v")) @property def min_value(self) -> int: return int(self._min_value) @property def max_value(self) -> int: return int(self._max_value)
[docs] class PermissiveInts(Ints): """ Requires an integer or a float close to an integer optional parameters min_value and max_value enforce min_value <= value <= max_value. Note that you probably always want to use this with a set_parser that converts the float repr to an actual int. """
[docs] def validate(self, value: numbertypes, context: str = "") -> None: """ Validates if int or close to int (remainder to the rounded value is less than 1e-5) else raises error. Args: value: Integer or close to integer. context: Context for validation. Raises: TypeError: If not an int or close to it. """ castvalue: int | np.integer[Any] if isinstance(value, (float, np.floating)): intrepr = int(np.round(value)) remainder = np.abs(value - intrepr) if remainder < 1e-05: castvalue = intrepr else: raise TypeError( f"{value!r} is not an int or close to an int; {context}" ) else: castvalue = value super().validate(castvalue, context=context)
[docs] class ComplexNumbers(Validator[Union[complex, "np.complexfloating[Any,Any]"]]): """ A validator for complex numbers. """ validtypes = (complex, np.complex128, np.complex64) def __init__(self) -> None: self._valid_values = ((1 + 1j),)
[docs] def validate( self, value: complex | np.complexfloating[Any, Any], context: str = "" ) -> None: """ Validates if complex number else raises error. Args: value: A complex number. context: Context for validation. Raises: TypeError: If not a complex number. """ # for some reason pyright does not think numpy complex # types as valid types here if not isinstance(value, self.validtypes): # pyright: ignore raise TypeError(f"{value!r} is not complex; {context}")
is_numeric = False # there is no meaningful way to sweep a complex number def __repr__(self) -> str: return "<Complex Number>"
[docs] class Enum(Validator[Hashable]): """ Requires one of a provided set of values. eg. Enum(val1, val2, val3) Raises: TypeError: If no value provided """ def __init__(self, *values: Hashable | None) -> None: if not len(values): raise TypeError("Enum needs at least one value") self._values = set(values) self._valid_values = tuple(values)
[docs] def validate(self, value: Hashable, context: str = "") -> None: try: if value not in self._values: raise ValueError(f"{value!r} is not in {self._values!r}; {context}") except TypeError as e: # in case of unhashable (mutable) type e.args = e.args + ( f"error looking for {value!r} in {self._values!r}; {context}", ) raise
def __repr__(self) -> str: return f"<Enum: {self._values!r}>" @property def values(self) -> set[Hashable]: return self._values.copy()
[docs] class OnOff(Validator[str]): """ Requires either the string 'on' or 'off'. """ def __init__(self) -> None: self._validator = Enum("on", "off") self._valid_values = cast(tuple[str, ...], self._validator._valid_values)
[docs] def validate(self, value: str, context: str = "") -> None: self._validator.validate(value, context)
[docs] class Multiples(Ints): """ A validator that checks if a value is an integer multiple of a fixed divisor. This class extends validators.Ints such that the value is also checked for being integer between an optional min_value and max_value. Furthermore this validator checks that the value is an integer multiple of an fixed, integer divisor. (i.e. value % divisor == 0) Args: divisor: the value need the be a multiple of this divisor max_value: value must be <= max_value min_value: value must be >= min_value """ def __init__(self, divisor: int = 1, **kwargs: Any) -> None: super().__init__(**kwargs) if not isinstance(divisor, int) or divisor <= 0: raise TypeError("divisor must be a positive integer") self._divisor = divisor self._valid_values = (divisor,)
[docs] def validate(self, value: int | np.integer[Any], context: str = "") -> None: """ Validates if the value is a integer multiple of divisor else raises error. Args: value: An integer. context: Context for validation. Raises: ValueError: If not a multiple of a divisor. """ super().validate(value=value, context=context) if not value % self._divisor == 0: raise ValueError( f"{value!r} is not a multiple of {self._divisor!r}; {context}" )
def __repr__(self) -> str: return super().__repr__()[:-1] + f", Multiples of {self._divisor}>" is_numeric = True @property def divisor(self) -> int: return self._divisor
[docs] class PermissiveMultiples(Validator[numbertypes]): """ A validator that checks whether a value is an integer multiple of a fixed divisor (to within some precision). If both value and divisor are integers, the (exact) Multiples validator is used. We also allow negative values, meaning that zero by construction is always a valid value. Args: divisor: The number that the validated value should be an integer multiple of. precision: The maximally allowed absolute error between the value and the nearest true multiple. Raises: ValueError: If divisor is zero. """ def __init__(self, divisor: numbertypes, precision: float = 1e-9) -> None: self._mulval: Multiples | None = None self._precision = precision self._numval = Numbers() self.divisor = divisor
[docs] def validate(self, value: numbertypes, context: str = "") -> None: """ Validate the given value. Note that this validator does not use context for anything. Raises: ValueError: If value is not the multiple of divisor. """ self._numval.validate(value) # if zero, it passes by definition if value == 0: return if self._mulval and isinstance(value, int): self._mulval.validate(abs(value)) else: # floating-point division cannot be trusted, so we try to # multiply our way out of the problem by constructing true # multiples in the relevant range and see if `value` is one # of them (within rounding errors) divs = int(np.divmod(value, self._divisor)[0]) true_vals = np.array([n * self._divisor for n in range(divs, divs + 2)]) abs_errs = [abs(tv - value) for tv in true_vals] if min(abs_errs) > self._precision: raise ValueError(f"{value} is not a multiple" + f" of {self._divisor}.")
def __repr__(self) -> str: repr_str = f"<PermissiveMultiples, Multiples of {self._divisor} to within {self._precision}>" return repr_str is_numeric = True @property def divisor(self) -> numbertypes: return self._divisor @divisor.setter def divisor(self, divisor: numbertypes) -> None: if divisor == 0: raise ValueError("Can not meaningfully check for multiples of zero.") if isinstance(divisor, int): self._mulval = Multiples(divisor=abs(divisor)) else: self._mulval = None self._valid_values = (divisor,) self._divisor = divisor @property def precision(self) -> float: return self._precision @precision.setter def precision(self, precision: float) -> None: self._precision = precision
[docs] class MultiType(Validator[Any]): """ Allow the combination of several different validators. By default, the resulting validator acts as a logical OR between the different validators. Pass combiner='AND' to require all validators to return True instead of atleast one returning True. Examples: 1. To allow numbers as well as "off": >>> MultiType(Numbers(), Enum("off")) or: >>> MultiType(Numbers(), Enum("off"), combiner='OR') 2. To require values that are divisible by 0.001 while >=0.002 and <=50000.0 >>> MultiType(PermissiveMultiples(divisor=1e-3), >>> Numbers(min_value=2e-3, max_value=5e4), >>> combiner='AND') Raises: TypeError: If no validators provided. Or if any of the provided argument is not a valid validator. Or if combiner is not in ['OR', 'AND']. """ def __init__( self, *validators: Validator[Any], combiner: Literal["OR", "AND"] = "OR" ) -> None: if not validators: raise TypeError("MultiType needs at least one Validator") if combiner not in ["OR", "AND"]: raise TypeError("MultiType combiner argument must be one of ['OR', 'AND']") for v in validators: if not isinstance(v, Validator): raise TypeError("each argument must be a Validator") if v.is_numeric: # if ANY of the contained validators is numeric, # the MultiType is considered numeric too. # this could cause problems if you want to sweep # from a non-numeric to a numeric value, so we # need to be careful about this in the sweep code self.is_numeric = True self._validators = tuple(validators) self._combiner: Literal["OR", "AND"] = combiner self._valid_values = tuple( vval for v in self._validators for vval in v._valid_values )
[docs] def validate(self, value: Any, context: str = "") -> None: args: list[str] = [] for v in self._validators: try: v.validate(value, context) if self._combiner == "OR": return except Exception as e: # collect the args from all validators so you can see why # each one that was tested failed args = args + list(e.args) if self._combiner == "AND": raise ValueError(*args) if self._combiner == "OR": raise ValueError(*args)
def __repr__(self) -> str: parts = (repr(v)[1:-1] for v in self._validators) return "<MultiType: {}>".format(", ".join(parts)) @property def combiner(self) -> Literal["OR", "AND"]: return self._combiner @property def validators(self) -> tuple[Validator[Any], ...]: return self._validators
[docs] class MultiTypeOr(MultiType): """ Allow the combination of several different validators. The resulting validator acts as a logical OR between the different validators. Example: To allow numbers as well as "off": >>> MultiTypeOr(Numbers(), Enum("off")) Raises: TypeError: If no validators provided. Or if any of the provided argument is not a valid validator. """ def __init__( self, *validators: Validator[Any], ) -> None: super().__init__(*validators, combiner="OR") def __repr__(self) -> str: parts = (repr(v)[1:-1] for v in self._validators) return "<MultiTypeOr: {}>".format(", ".join(parts))
[docs] class MultiTypeAnd(MultiType): """ Allow the combination of several different validators. The resulting validator acts as a logical AND between the different validators. Example: To require values that are divisible by 0.001 while >=0.002 and <=50000.0 >>> MultiType(PermissiveMultiples(divisor=1e-3), >>> Numbers(min_value=2e-3, max_value=5e4), >>> combiner='AND') Raises: TypeError: If no validators provided. Or if any of the provided argument is not a valid validator. """ def __init__( self, *validators: Validator[Any], ) -> None: super().__init__(*validators, combiner="AND") self._valid_values = () def __repr__(self) -> str: parts = (repr(v)[1:-1] for v in self._validators) return "<MultiTypeAnd: {}>".format(", ".join(parts))
[docs] class Arrays(Validator[np.ndarray]): """ Validator for numerical numpy arrays of numeric types (int, float, complex). By default it validates int and float arrays. Min and max validation is not supported for complex numbers. Args: min_value: Min value allowed, default None for which min value check is not performed max_value: Max value allowed, default None for which max value check is not performed shape: The shape of the array, tuple of either ints or Callables taking no arguments that return the size along that dim as an int. valid_types: Sequence of types that the validator should support. Should be a subset of the supported types, or None. If None, all real datatypes will validate. Raises: TypeError: If value of arrays are not supported. """ __real_types = (np.integer, np.floating) __supported_types = __real_types + (np.complexfloating,) def __init__( self, min_value: numbertypes | None = None, max_value: numbertypes | None = None, shape: abc.Sequence[shape_type] | None = None, valid_types: abc.Sequence[type] | None = None, ) -> None: if valid_types is not None: for mytype in valid_types: is_supported = any( np.issubdtype(mytype, supported_type) for supported_type in self.__supported_types ) if not is_supported: raise TypeError( f"Arrays validator only supports numeric " f"types: {mytype} is not supported." ) self.valid_types = valid_types else: self.valid_types = self.__real_types supports_complex = any( np.issubdtype(my_type, np.complexfloating) for my_type in self.valid_types ) limits_given = min_value is not None or max_value is not None min_real = any( np.issubdtype(type(min_value), real_type) for real_type in self.__real_types ) max_real = any( np.issubdtype(type(max_value), real_type) for real_type in self.__real_types ) if min_value is not None and not min_real: raise TypeError( f"min_value must be a real number. It is " f"{min_value} of type {type(min_value)}" ) if max_value is not None and not max_real: raise TypeError( f"max_value must be a real number. It is " f"{max_value} of type {type(max_value)}" ) if supports_complex and limits_given: raise TypeError( "Setting min_value or max_value is not supported for " "complex validators." ) min_value_is_valid_type = any( np.issubdtype(type(min_value), valid_type) for valid_type in self.valid_types ) max_value_is_valid_type = any( np.issubdtype(type(max_value), valid_type) for valid_type in self.valid_types ) if min_value_is_valid_type or min_value is None: self._min_value = min_value else: raise TypeError( f"min_value must be an instance of valid_types. " f"It is {min_value} of " f"type {type(min_value)}" ) if max_value_is_valid_type or max_value is None: self._max_value = max_value else: raise TypeError( f"max_value must be an instance of valid_types. " f"It is {max_value} of " f"type {type(max_value)}" ) if min_value is not None and max_value is not None: valuesok = max_value > min_value if not valuesok: raise TypeError("max_value must be bigger than min_value") if not isinstance(shape, abc.Sequence) and shape is not None: raise ValueError( f"Shape must be a sequence (List, Tuple ...) got a {type(shape)}" ) self._shape: shape_tuple_type = None if shape is not None: self._shape = tuple(shape) @property def valid_values(self) -> tuple[np.ndarray]: valid_type = self.valid_types[0] if valid_type == np.integer: valid_type = np.int32 if valid_type == np.floating: valid_type = np.float64 if valid_type == np.complexfloating: valid_type = np.complex128 if self.shape is None: return (np.array([self._min_value], dtype=valid_type),) else: val_arr: np.ndarray = np.empty(self.shape, dtype=valid_type) val_arr.fill(self._min_value) return (val_arr,) @property def shape_unevaluated(self) -> shape_tuple_type: return self._shape @property def shape(self) -> tuple[int, ...] | None: if self._shape is None: return None shape_array = [] for s in self._shape: if callable(s): shape_array.append(s()) else: shape_array.append(s) shape = tuple(shape_array) return shape
[docs] def validate(self, value: np.ndarray, context: str = "") -> None: if not isinstance(value, np.ndarray): raise TypeError(f"{value!r} is not a numpy array; {context}") if not any( np.issubdtype(value.dtype.type, valid_type) for valid_type in self.valid_types ): raise TypeError( f"type of {value} is not any of {self.valid_types}" f" it is {value.dtype}; {context}" ) if self.shape is not None: shape = self.shape if np.shape(value) != shape: raise ValueError( f"{value!r} does not have expected shape {shape}," f" it has shape {np.shape(value)}; {context}" ) # Only check if max is not inf as it can be expensive for large arrays if self._max_value != (float("inf")) and self._max_value is not None: if not (np.max(value) <= self._max_value): raise ValueError( f"{value!r} is invalid: all values must be between " f"{self._min_value} and {self._max_value} inclusive; {context}" ) # Only check if min is not -inf as it can be expensive for large arrays if self._min_value != (-float("inf")) and self._min_value is not None: if not (self._min_value <= np.min(value)): raise ValueError( f"{value!r} is invalid: all values must be between " f"{self._min_value} and {self._max_value} inclusive; {context}" )
is_numeric = True def __repr__(self) -> str: if self._min_value is None or not math.isfinite(self._min_value): minv = None else: minv = self._min_value if self._max_value is None or not math.isfinite(self._max_value): maxv = None else: maxv = self._max_value # we don't want the repr to execute any deferred shape argument # so we use shape_unevaluated return "<Arrays{}, shape: {}>".format( range_str(minv, maxv, "v"), self.shape_unevaluated ) @property def min_value(self) -> float | None: return float(self._min_value) if self._min_value is not None else None @property def max_value(self) -> float | None: return float(self._max_value) if self._max_value is not None else None
[docs] class Lists(Validator[list[T]]): """ Validator for lists Args: elt_validator: Used to validate the individual elements of the list. """ def __init__(self, elt_validator: Validator[T] = Anything()) -> None: self._elt_validator = elt_validator self._valid_values = ([vval for vval in elt_validator._valid_values],) def __repr__(self) -> str: msg = "<Lists : " msg += self._elt_validator.__repr__() + ">" return msg
[docs] def validate(self, value: list[T], context: str = "") -> None: """ Validate if list else raises error. Args: value: A list. context: Context for validation. Raises: TypeError: If not list. """ if not isinstance(value, list): raise TypeError(f"{value!r} is not a list; {context}") # Does not validate elements if not required to improve performance if not isinstance(self._elt_validator, Anything): for elt in value: self._elt_validator.validate(elt)
@property def elt_validator(self) -> Validator[Any]: return self._elt_validator
[docs] class Sequence(Validator[typing.Sequence[Any]]): """ Validator for Sequences. Args: elt_validator: Used to validate the individual elements of the :class:`Sequence`. length: Length of sequence. require_sorted: True or False. """ def __init__( self, elt_validator: Validator[Any] = Anything(), length: int | None = None, require_sorted: bool = False, ) -> None: self._elt_validator = elt_validator self._length = length self._require_sorted = require_sorted self._valid_values = ([vval for vval in elt_validator._valid_values],) def __repr__(self) -> str: msg = "<Sequence : " msg += f"len: {self._length} " msg += f"sorted: {self._require_sorted} " msg += self._elt_validator.__repr__() + ">" return msg
[docs] def validate(self, value: abc.Sequence[Any], context: str = "") -> None: """ Validates if sequence else raise typeerror. Args: value: A sequence. context: Context for validation. Raises: TypeError: If not a sequence. ValueError: If not of given length or if not sorted. """ if not isinstance(value, abc.Sequence): raise TypeError(f"{value!r} is not a sequence; {context}") if self._length and not len(value) == self._length: raise ValueError( f"{value!r} has not length {self._length} but {len(value)}" ) if self._require_sorted and tuple(sorted(value)) != tuple(value): raise ValueError(f"{value!r} is required to be sorted.") # Does not validate elements if not required to improve performance if not isinstance(self._elt_validator, Anything): for elt in value: self._elt_validator.validate(elt)
@property def elt_validator(self) -> Validator[Any]: return self._elt_validator @property def length(self) -> int | None: return self._length @property def require_sorted(self) -> bool: return self._require_sorted
[docs] class Callable(Validator[typing.Callable[..., Any]]): """ Validator for callables such as functions. """ def __init__(self) -> None: self._valid_values = (lambda: 0,)
[docs] def validate(self, value: abc.Callable[..., Any], context: str = "") -> None: """ Validates if callable else raise typeerror. Args: value: Value to validate. context: Context for validation. Raises: TypeError: If not a callable. """ if not callable(value): raise TypeError(f"{value!r} is not a callable; {context}")
def __repr__(self) -> str: return "<Callable>"
[docs] class Dict(Validator[dict[Hashable, Any]]): """ Validator for dictionaries. """ def __init__(self, allowed_keys: abc.Sequence[Hashable] | None = None) -> None: """ Validator for dictionary keys Args: allowed_keys: if set, all keys must be in allowed_keys """ self._allowed_keys = allowed_keys self._valid_values = ({0: 1},)
[docs] def validate(self, value: dict[Hashable, Any], context: str = "") -> None: """ Validates dictionary keys else raise typeerror. Args: value: Dictionary. context: Context for validation. Raises: TypeError: If not a dictionary. SyntaxError: If keys are not in allowed keys. """ if not isinstance(value, dict): raise TypeError(f"{value!r} is not a dictionary; {context}") allowed_keys = self._allowed_keys if allowed_keys is not None: forbidden_keys = [key for key in value if key not in allowed_keys] if forbidden_keys: raise SyntaxError( f"Dictionary keys {forbidden_keys} are not in allowed keys " f"{allowed_keys}" )
def __repr__(self) -> str: if self._allowed_keys is None: return "<Dict>" else: return f"<Dict {self._allowed_keys}>" @property def allowed_keys(self) -> abc.Sequence[Hashable] | None: return self._allowed_keys @allowed_keys.setter def allowed_keys(self, keys: abc.Sequence[Hashable] | None) -> None: self._allowed_keys = keys