From 98094b73ae7bd1b7662fcdaeb0e32ac197bea2c2 Mon Sep 17 00:00:00 2001 From: sg495 Date: Wed, 21 Feb 2024 18:38:22 +0000 Subject: [PATCH] More Inspector features. Introduced inspect_type function to replace can_validate in its TypeInspector-returning behaviour. --- docs/api/typing_validation.inspector.rst | 1 + docs/api/typing_validation.validation.rst | 5 +++ typing_validation/inspector.py | 55 ++++++++++++++++++++--- typing_validation/validation.py | 41 ++++++++++++----- 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/docs/api/typing_validation.inspector.rst b/docs/api/typing_validation.inspector.rst index 66215b8..f508058 100644 --- a/docs/api/typing_validation.inspector.rst +++ b/docs/api/typing_validation.inspector.rst @@ -9,6 +9,7 @@ TypeInspector .. autoclass:: typing_validation.inspector.TypeInspector :show-inheritance: :members: + :special-members: __repr__ UnsupportedType --------------- diff --git a/docs/api/typing_validation.validation.rst b/docs/api/typing_validation.validation.rst index 26d3008..619e02b 100644 --- a/docs/api/typing_validation.validation.rst +++ b/docs/api/typing_validation.validation.rst @@ -20,6 +20,11 @@ can_validate .. autofunction:: typing_validation.validation.can_validate +inspect_type +------------ + +.. autofunction:: typing_validation.validation.inspect_type + is_valid -------- diff --git a/typing_validation/inspector.py b/typing_validation/inspector.py index 298d21c..b869868 100644 --- a/typing_validation/inspector.py +++ b/typing_validation/inspector.py @@ -44,6 +44,11 @@ else: TypeConstructorArgs = typing.Tuple[str, Any] +if sys.version_info[1] >= 11: + from typing import Self +else: + from typing_extensions import Self + _typing_equiv = { list: typing.List, tuple: typing.Tuple, @@ -113,7 +118,7 @@ class TypeInspector: "_pending_generic_type_constr", ) - def __new__(cls) -> "TypeInspector": + def __new__(cls) -> Self: instance = super().__new__(cls) instance._recorded_constructors = [] instance._unsupported_types = [] @@ -134,6 +139,34 @@ def unsupported_types(self) -> typing.Tuple[Any, ...]: r"""The sequence of unsupported types encountered during validation.""" return tuple(self._unsupported_types) + @property + def type_structure(self) -> str: + """ + The structure of the recorded type: + + 1. The string spans multiple lines, with indentation levels matching + the nesting level of inner types. + 2. Any unsupported types encountered are wrapped using the generic type + :obj:`UnsupportedType`. + + """ + return "\n".join(self._repr()[0]) + + @property + def type_annotation(self) -> str: + """ + The type annotation for the recorded type. + Differs from the output of :attr:`type_structure` in the following ways: + + 1. The annotation is on a single line. + 2. Unsupported types are not wrapped. + + """ + return "".join( + line.strip() + for line in self._repr(mark_unsupported=False)[0] + ) + def _recorded_type(self, idx: int) -> typing.Tuple[Any, int]: # pylint: disable = too-many-return-statements, too-many-branches param: Any @@ -297,15 +330,23 @@ def __bool__(self) -> bool: return not self._unsupported_types def __repr__(self) -> str: - # addr = "0x"+f"{id(self):x}" - header = f"The following type can{'' if self else 'not'} be validated against:" - return header + "\n" + "\n".join(self._repr()[0]) + """ + Representation of the inspector, including the :attr:`type_structure`. + + :meta public: + """ + return ( + "TypeInspector instance for the following type:\n" + +self.type_structure + ) def _repr( - self, idx: int = 0, level: int = 0 + self, idx: int = 0, level: int = 0, + *, + mark_unsupported: bool = True ) -> typing.Tuple[typing.List[str], int]: # pylint: disable = too-many-return-statements, too-many-branches, too-many-statements, too-many-locals - basic_indent = " " + basic_indent = " " assert len(basic_indent) >= 2 indent = basic_indent * level next_indent = basic_indent * (level + 1) @@ -314,6 +355,8 @@ def _repr( lines: typing.List[str] tag, param = self._recorded_constructors[idx] if tag == "unsupported": + if not mark_unsupported: + return [indent+str(param)], idx return [ indent + "UnsupportedType[", indent + " " + str(param), diff --git a/typing_validation/validation.py b/typing_validation/validation.py index 293fe07..b051d90 100644 --- a/typing_validation/validation.py +++ b/typing_validation/validation.py @@ -904,29 +904,50 @@ def validate(val: Any, t: Any) -> Literal[True]: def can_validate(t: Any) -> TypeInspector: - r""" - Checks whether validation is supported for the given type ``t``: if not, :func:`validate` will raise :obj:`UnsupportedTypeError`. + """ + Checks whether validation is supported for the given type ``t``: if not, + :func:`validate` will raise :obj:`UnsupportedTypeError`. - The returned :class:`TypeInspector` instance can be used wherever a boolean is expected, and will indicate whether the type is supported or not: + .. warning:: + + The return type will be changed to :obj:`bool` in v1.3.0. + To obtain a :class:`TypeInspector` object, please use the newly + introduced :func:`inspect_type` instead. + + :param t: the type to be checked for validation support + :type t: :obj:`~typing.Any` + + """ + inspector = TypeInspector() + validate(inspector, t) + return inspector + + +def inspect_type(t: Any) -> TypeInspector: + r""" + Returns a :class:`TypeInspector` instance can be used wherever a boolean is + expected, and will indicate whether the type is supported or not: >>> from typing import * - >>> from typing_validation import can_validate - >>> res = can_validate(tuple[list[str], Union[int, float, Callable[[int], int]]]) + >>> from typing_validation import inspect_type + >>> res = inspect_type(tuple[list[str], Union[int, float, Callable[[int], int]]]) >>> bool(res) False - However, it also records (with minimal added cost) the full structure of the type as the latter was validated, which it then exposes via its + The instance also records (with minimal added cost) the full structure of + the type as the latter was validated, which it then exposes via its :attr:`TypeInspector.recorded_type` property: - >>> res = can_validate(tuple[list[Union[str, int]],...]) + >>> res = inspect_type(tuple[list[Union[str, int]],...]) >>> bool(res) True >>> res.recorded_type tuple[list[typing.Union[str, int]], ...] - Any unsupported subtype encountered during the validation is left in place, wrapped into an :class:`UnsupportedType`: + Any unsupported subtype encountered during the validation is left in place, + wrapped into an :class:`UnsupportedType`: - >>> can_validate(tuple[list[str], Union[int, float, Callable[[int], int]]]) + >>> inspect_type(tuple[list[str], Union[int, float, Callable[[int], int]]]) The following type cannot be validated against: tuple[ list[ @@ -945,7 +966,7 @@ def can_validate(t: Any) -> TypeInspector: :param t: the type to be checked for validation support :type t: :obj:`~typing.Any` - :raises AssertionError: if things go unexpectedly wrong with ``__args__`` for parametric types + """ inspector = TypeInspector() validate(inspector, t)