Coverage for pyfields/typing_utils.py: 88%
54 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-11-06 16:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-11-06 16:35 +0000
1# Authors: Sylvain MARIE <sylvain.marie@se.com>
2# + All contributors to <https://github.com/smarie/python-pyfields>
3#
4# License: 3-clause BSD, <https://github.com/smarie/python-pyfields/blob/master/LICENSE>
5import sys
7from pkg_resources import get_distribution
10class FieldTypeError(TypeError): # FieldError
11 """
12 Error raised when the type of a field does not match expected type(s).
13 """
14 __slots__ = ('field', 'value', 'expected_types')
16 def __init__(self, field, value, expected_types):
17 self.field = field
18 self.value = value
19 # noinspection PyBroadException
20 try:
21 if len(expected_types) == 1: 21 ↛ 22line 21 didn't jump to line 22, because the condition on line 21 was never true
22 expected_types = expected_types[0]
23 except BaseException:
24 pass
25 self.expected_types = expected_types
27 def __str__(self):
28 # representing the object might fail, protect ourselves
29 # noinspection PyBroadException
30 try:
31 val_repr = repr(self.value)
32 except Exception as e:
33 val_repr = "<error while trying to represent value: %s>" % e
35 # detail error message
36 # noinspection PyBroadException
37 try:
38 # tuple or iterable of types ?
39 sub_msg = "Value type should be one of (%s)" % ', '.join(("%s" % _t for _t in self.expected_types))
40 except: # noqa E722
41 # single type
42 sub_msg = "Value should be of type %s" % (self.expected_types,)
44 return "Invalid value type provided for '%s'. %s. Instead, received a '%s': %s"\
45 % (self.field.qualname, sub_msg, self.value.__class__.__name__, val_repr)
48def _make_assert_is_of_type():
49 from packaging.version import parse as parse_version
50 try:
51 from typeguard import check_type as ct
53 # Note: only do this when we are sure that typeguard can be imported, otherwise this is slow
54 # see https://github.com/smarie/python-getversion/blob/ee495acf6cf06c5e860713edeee396206368e458/getversion/main.py#L84
55 typeguard_version = get_distribution("typeguard").version
56 if parse_version(typeguard_version) < parse_version("3.0.0"): 56 ↛ 57line 56 didn't jump to line 57, because the condition on line 56 was never true
57 check_type = ct
58 else:
59 # Name has disappeared from 3.0.0
60 def check_type(name, value, typ):
61 ct(value, typ)
63 try:
64 from typing import Union
65 except ImportError:
66 # (a) typing is not available, transform iterables of types into several calls
67 def assert_is_of_type(field, value, typ):
68 """
69 Type checker relying on `typeguard` (python 3.5+)
71 :param field:
72 :param value:
73 :param typ:
74 :return:
75 """
76 try:
77 # iterate on the types
78 t_gen = (t for t in typ)
79 except TypeError:
80 # not iterable : a single type
81 try:
82 check_type(field.qualname, value, typ)
83 except Exception as e:
84 # raise from
85 new_e = FieldTypeError(field, value, typ)
86 new_e.__cause__ = e
87 raise new_e
88 else:
89 # iterate and try them all
90 e = None
91 for _t in t_gen:
92 try:
93 check_type(field.qualname, value, typ)
94 return # success !!!!
95 except Exception as e1:
96 e = e1 # failed: lets try another one
98 # raise from
99 if e is not None:
100 new_e = FieldTypeError(field, value, typ)
101 new_e.__cause__ = e
102 raise new_e
104 else:
105 # (b) typing is available, use a Union
106 def assert_is_of_type(field, value, typ):
107 """
108 Type checker relying on `typeguard` (python 3.5+)
110 :param field:
111 :param value:
112 :param typ:
113 :return:
114 """
115 try:
116 check_type(field.qualname, value, Union[typ])
117 except Exception as e:
118 # raise from
119 new_e = FieldTypeError(field, value, typ)
120 new_e.__cause__ = e
121 raise new_e
123 except ImportError:
124 try:
125 from pytypes import is_of_type
127 def assert_is_of_type(field, value, typ):
128 """
129 Type checker relying on `pytypes` (python 2+)
131 :param field:
132 :param value:
133 :param typ:
134 :return:
135 """
136 try:
137 valid = is_of_type(value, typ)
138 except Exception as e:
139 # raise from
140 new_e = FieldTypeError(field, value, typ)
141 new_e.__cause__ = e
142 raise new_e
143 else:
144 if not valid:
145 raise FieldTypeError(field, value, typ)
147 except ImportError:
148 # from valid8.utils.typing_inspect import is_typevar, is_union_type, get_args
149 from valid8.utils.typing_tools import resolve_union_and_typevar
151 def assert_is_of_type(field, value, typ):
152 """
153 Neither `typeguard` nor `pytypes` are available on this platform.
155 This is a "light" implementation that basically resolves all `Union` and `TypeVar` into a flat list and
156 then calls `isinstance`.
158 :param field:
159 :param value:
160 :param typ:
161 :return:
162 """
163 types = resolve_union_and_typevar(typ)
164 try:
165 is_ok = isinstance(value, types)
166 except TypeError as e:
167 if e.args[0].startswith("Subscripted generics cannot"):
168 raise TypeError("Neither typeguard not pytypes is installed - therefore it is not possible to "
169 "validate subscripted typing structures such as %s" % types)
170 else:
171 raise
172 else:
173 if not is_ok:
174 raise FieldTypeError(field, value, typ)
176 return assert_is_of_type
179try: # very minimal way to check if typing it available, for runtime type checking
180 # noinspection PyUnresolvedReferences
181 from typing import Tuple # noqa
182except ImportError:
183 assert_is_of_type = None
184else:
185 assert_is_of_type = _make_assert_is_of_type()
188PY36 = sys.version_info >= (3, 6)
189get_type_hints = None
190if PY36: 190 ↛ exitline 190 didn't exit the module, because the condition on line 190 was never false
191 try:
192 from typing import get_type_hints as gth
194 def get_type_hints(obj, globalns=None, localns=None):
195 """
196 Fixed version of typing.get_type_hints to handle self forward references
197 """
198 if globalns is None and localns is None and isinstance(obj, type): 198 ↛ 200line 198 didn't jump to line 200, because the condition on line 198 was never false
199 localns = {obj.__name__: obj}
200 return gth(obj, globalns=globalns, localns=localns)
202 except ImportError:
203 pass