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:
-
TypeErrorwhenobjdoes not match the expectedtype -
ValueErrorwhenobjmatchestypebut violates the value constraint provided bytype(viaannotated_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")