Source code for valguard.core

"""Define result types for use in assessment result manipulations.

Results are represented by subclasses of `TypedValue`, such as `IntValue` or `StrValue`.
Each subclass corresponds to a standard Python type, for example, `int` for `IntValue`
and `str` for `StrValue`. A `ValidationError` is raised at construction time if the
stored value does not have the correct type. All values are immutable once created.

A key design feature is that arguments are not coerced: `FloatValue(12)` will fail
because `12` is an integer. Instead, use `FloatValue(12.0)`. This prevents unexpected
coercions in processing pipelines.

Use `isinstance` to determine the type of a value. For example, if
`isinstance(x, IntValue)` is true, then `x.value` is guaranteed to be an int.

The `__str__` method returns a user-friendly string representation of the value.
For `FloatValue`, the output is formatted to two decimal places.

The name `Value` is provided as a convenient alias for `TypedValue[Any]`.
"""

import math
from abc import ABC

from .exceptions import ImplicitConversionError, TypeMismatchError, ValidationError

# -------------------------------------------------------------------------------------
#   Value and Typed Value
# -------------------------------------------------------------------------------------


[docs] class TypedValue[T](ABC): """Validate and store a value of a specific Python type. Subclasses must define `_type`, indicating the exact type of value accepted. For example, an `IntValue` subclass would define `_type = int`. For type-checked access, use the `as_*` properties, like `as_int` and `as_bool`. """ _type: type[T] # Subclass to define __slots__ = ("_value",) def __init__(self, value: T) -> None: if type(value) is not self._type: msg = ( f"Invalid value: {value!r} (type {type(value).__name__}), " f"expected type {self._type.__name__}" ) raise ValidationError(msg) self._value = value
[docs] def __str__(self) -> str: """Return a string representation suitable for writing to a results file.""" return str(self.value)
[docs] def __repr__(self) -> str: """Return a detailed representation for error messages.""" return f"{self.__class__.__name__}({self.value!r})"
def __eq__(self, other: object) -> bool: if not isinstance(other, TypedValue): return NotImplemented return type(self) is type(other) and self._value == other._value def __hash__(self) -> int: return hash((self.__class__, self._value, self._type)) def __bool__(self) -> bool: raise ImplicitConversionError def __int__(self) -> int: raise ImplicitConversionError def __float__(self) -> float: raise ImplicitConversionError @property def value(self) -> T: return self._value @property def as_int(self) -> int: raise TypeMismatchError @property def as_float(self) -> float: raise TypeMismatchError @property def as_bool(self) -> bool: raise TypeMismatchError @property def as_str(self) -> str: raise TypeMismatchError @property def to_float(self) -> float: """Convert value to float if applicable.""" raise TypeMismatchError
# ------------------------------------------------------------------------------------- # Numeric Values # -------------------------------------------------------------------------------------
[docs] class _NumericValue[T: float](TypedValue[T], ABC): """Base class for numeric values that support conversion to float via `to_float`.""" __slots__ = () @property def to_float(self) -> float: return float(self.value)
[docs] class IntValue(_NumericValue[int]): """Integer value.""" __slots__ = () _type = int @property def as_int(self) -> int: return self.value
[docs] class FloatValue(_NumericValue[float]): """Float value. Formatted to two decimal places for display.""" __slots__ = () _type = float def __init__(self, value: float) -> None: super().__init__(value) if not math.isfinite(self.value): msg = f"Invalid value: expected finite float, got {self.value}" raise ValidationError(msg) def __str__(self) -> str: """Return a two-decimal-place string representation (for display/output).""" return f"{self.value:.2f}" def __repr__(self) -> str: """Return full-precision representation for debugging.""" return f"{self.__class__.__name__}({self.value})" @property def as_float(self) -> float: return self.value
# ------------------------------------------------------------------------------------- # Boolean Value # -------------------------------------------------------------------------------------
[docs] class BoolValue(TypedValue[bool]): """Boolean value.""" __slots__ = () _type = bool @property def as_bool(self) -> bool: return self.value
# ------------------------------------------------------------------------------------- # String Value # -------------------------------------------------------------------------------------
[docs] class StrValue(TypedValue[str]): """String value.""" __slots__ = () _type = str @property def as_str(self) -> str: return self.value