Add function check_validity for type and value validation

Implement check_validity

Create a new function check_validity(obj, type) that checks whether obj matches the expected value and type defined by type.

This function must support the additional metadata introduced by typing.Annotated and annotated_types as described in issue #984 (closed).

Important check_validity should not return anything. It must either complete silently (when valid) or raise one of the following exceptions:

  • TypeError when obj does not match the expected type
  • ValueError when obj matches type but violates the value constraint provided by type (via annotated_types).

check_validity will become a core validation mechanism in Pyxel, used across models, detectors, geometries, characteristics, ... .

This function could be implemented in a new Python file pyxel/util/checking.py.

Examples

Below are some examples that can be reused for unit testing.

More cases will be implemented later. The exceptions messages will also be improved later.

Please note that typing_extensions is used instead of typing to ensure compatibility with older Python versions.

>>> from typing_extensions import Annotated, Literal
>>> from annotated_types import Gt, Ge, Le, Interval

Valid inputs

>>> check_validity(3.0, float)        # Valid !
>>> check_validity(3, int)            # Valid !
>>> check_validity('Hello', str)      # Valid !
>>> check_validity(True, bool)        # Valid !
>>> check_validity((0, 0), tuple[int, int])  # Valid !
>>> check_validity('top_left', Literal['top_left,', top_right'])  # Valid !
>>> check_validity(4, int | None)     # Valid !
>>> check_validity(None, int | None)  # Valid !
>>> check_validity(3.14, float | Literal['auto'])   # Valid !
>>> check_validity('auto', float| Literal['auto'])  # Valid !

Valid inputs using typing.annotated and annotated_types

>>> check_validity(3.0, Annotated[float, Ge(0.0), Le(10.0)])  # Valid !
>>> check_validity((0, 0), tuple[Annotated[int, Ge(0)], Annotated[int, Ge(0)]]  # Valid !

Invalid types (raise a TypeError)

>>> check_validity(3.14, int)
TypeError("Expecting a 'int' value. Got a 'float'")

>>> check_validity(3.0, int)        # yes, for now this one is not valid. It may change in the future.
TypeError("Expecting a 'int' value. Got a 'float'")

>>> check_validity(3, float)        # Same here. Not valid but maybe in the future.
TypeError("Expecting a 'float' value. Got a 'int'")

>>> check_validity('3.14', float)
TypeError("Expecting a 'float' value. Got a 'str'

>>> check_validity(1, bool)
TypeError("Expecting a 'bool' value. Got a 'int'")

>>> check_validity([0, 0], tuple[int, int])
TypeError("Expecting a 'tuple[int, int]'. Got a 'list'")

>>> check_validity(3.14, int | None)
TypeError("Expecting a 'int' or 'None'. Got 3.14")

Valid types but invalid values (raise a ValueError)

>>> check_validity(3.0, Annotated[float, Ge(10.0), Le(20.)]
ValueError("Expecting >= 10.0 and <= 20.0. Got 3.0")

>>> check_validity(3, Annotated[int, Interval(ge=Ge(4), le=Le(16))]
ValueError("Expecting >= 4 and <= 16. Got 3")

>>> check_validity(0, Annotated[int, Gt(0)]
ValueError("Expecting > 0. Got 0")

>>> check_validity((0, 1, 2), tuple[int, int])
ValueError("Expecting tuple[int, int]. Got (0, 1, 2)")

>>> check_validity((0, -1), tuple[Annotated[int, Ge(0)], Annotated[int, Ge(0)]]
ValueError("Expecting >= 0. Got -1")  # Note: This message could be later improved

>>> check_validity('top', Literal['top_left,', top_right'])
ValueError("Expecting 'top_left' or 'top_right'. Got 'top'")

>>> check_validity(0.0, Annotated[float, Gt(0.0)] | None)
ValueError("Expecting > 0.0. Got 0.0")
Edited by Frederic Lemmel