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

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 

6 

7from pkg_resources import get_distribution 

8 

9 

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') 

15 

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 

26 

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 

34 

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,) 

43 

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) 

46 

47 

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 

52 

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) 

62 

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+) 

70 

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 

97 

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 

103 

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+) 

109 

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 

122 

123 except ImportError: 

124 try: 

125 from pytypes import is_of_type 

126 

127 def assert_is_of_type(field, value, typ): 

128 """ 

129 Type checker relying on `pytypes` (python 2+) 

130 

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) 

146 

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 

150 

151 def assert_is_of_type(field, value, typ): 

152 """ 

153 Neither `typeguard` nor `pytypes` are available on this platform. 

154 

155 This is a "light" implementation that basically resolves all `Union` and `TypeVar` into a flat list and 

156 then calls `isinstance`. 

157 

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) 

175 

176 return assert_is_of_type 

177 

178 

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() 

186 

187 

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 

193 

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) 

201 

202 except ImportError: 

203 pass