3
3
"""
4
4
import inspect
5
5
import sys
6
+ from enum import Enum
6
7
from functools import wraps
7
- from typing import Any , Callable , Dict , List , Optional , Type , TypeVar , Union , get_type_hints
8
+ from typing import Any , Callable , Dict , List , Optional , Type , TypeVar , Union , get_type_hints , Tuple
8
9
9
10
VER_3_7_AND_UP = sys .version_info [:3 ] >= (3 , 7 , 0 ) # PEP 560
10
11
19
20
TypeT = TypeVar ('TypeT' )
20
21
21
22
23
+ class ConverterTag (Enum ):
24
+ """When to apply a converter"""
25
+ OPTIONAL = 1
26
+ MANDATORY = 2
27
+
28
+
29
+ Converter = Callable [[Any ], Any ]
30
+ Converters = Dict [str , Converter ]
31
+ TaggedConverter = Tuple [Callable [[Any ], Any ], ConverterTag ]
32
+ TaggedConverters = Dict [str , TaggedConverter ]
33
+ OptionalTaggedConverters = Optional [TaggedConverters ]
34
+
35
+
36
+ def optional_converter (converter : Converter ) -> TaggedConverter :
37
+ """Create an optional converter"""
38
+ return converter , ConverterTag .OPTIONAL
39
+
40
+
41
+ def mandatory_converter (converter : Converter ) -> TaggedConverter :
42
+ """Create a mandatory converter"""
43
+ return converter , ConverterTag .MANDATORY
44
+
45
+
22
46
def type_checked_constructor (skip : bool = False ,
23
47
convert : bool = False ,
24
- converters : Optional [ Dict [ str , Callable [[ Any ], Any ]]] = None ) \
48
+ converters : OptionalTaggedConverters = None ) \
25
49
-> Callable [[Callable [..., TypeT ]], Callable [..., TypeT ]]:
26
50
"""Replaces the constructor of the given class (in-place)
27
51
with type-checked calls."""
@@ -57,7 +81,7 @@ def inner(first_arg: Any, *args: Any, **kwargs: Any) -> TypeT:
57
81
kwargs_dict ,
58
82
skip ,
59
83
convert ,
60
- converters )
84
+ _optional_converters_to_converters ( converters ) )
61
85
62
86
return inner
63
87
@@ -79,7 +103,7 @@ def inner(first_arg: Any, *args: Any, **kwargs: Any) -> TypeT:
79
103
80
104
def type_checked_call (skip : bool = False ,
81
105
convert : bool = False ,
82
- converters : Optional [ Dict [ str , Callable [[ Any ], Any ]]] = None ) \
106
+ converters : OptionalTaggedConverters = None ) \
83
107
-> Callable [[Callable [..., TypeT ]], Callable [..., TypeT ]]:
84
108
"""Wrap function with type checks."""
85
109
@@ -101,7 +125,7 @@ def wrapper(*args: Any, **kwargs: Any) -> TypeT:
101
125
kwargs_dict ,
102
126
skip ,
103
127
convert ,
104
- converters )
128
+ _optional_converters_to_converters ( converters ) )
105
129
106
130
setattr (wrapper , '__undictify_wrapped_func__' , func )
107
131
return wrapper
@@ -148,7 +172,7 @@ def _unpack_dict(func: WrappedOrFunc[TypeT], # pylint: disable=too-many-argumen
148
172
data : Dict [str , Any ],
149
173
skip_superfluous : bool ,
150
174
convert_types : bool ,
151
- converters : Optional [ Dict [ str , Callable [[ Any ], Any ]]] ) -> Any :
175
+ converters : TaggedConverters ) -> Any :
152
176
"""Constructs an object in a type-safe way from a dictionary."""
153
177
154
178
assert _is_dict (data ), 'Argument data needs to be a dictionary.'
@@ -191,11 +215,35 @@ def _unpack_dict(func: WrappedOrFunc[TypeT], # pylint: disable=too-many-argumen
191
215
return _unwrap_decorator_type (func )(** call_arguments )
192
216
193
217
218
+ def _optional_converters_to_converters (converters : OptionalTaggedConverters ) -> TaggedConverters :
219
+ """Convert None to empty dictionary"""
220
+ if not converters :
221
+ return {}
222
+ return converters
223
+
224
+
225
+ def _split_converters (converters : TaggedConverters ) \
226
+ -> Tuple [Converters , Converters ]:
227
+ """Partition into mandatory and optional."""
228
+ if not converters :
229
+ return {}, {}
230
+ mandatory : Converters = {}
231
+ optional : Converters = {}
232
+ for param_name , (converter , tag ) in converters .items ():
233
+ if tag == ConverterTag .MANDATORY :
234
+ mandatory [param_name ] = converter
235
+ elif tag == ConverterTag .OPTIONAL :
236
+ optional [param_name ] = converter
237
+ else :
238
+ assert False
239
+ return mandatory , optional
240
+
241
+
194
242
# pylint: disable=too-many-arguments,too-many-return-statements,too-many-branches
195
243
def _get_value (func : WrappedOrFunc [TypeT ],
196
244
value : Any , param_name : str ,
197
245
skip_superfluous : bool , convert_types : bool ,
198
- converters : Optional [ Dict [ str , Callable [[ Any ], Any ]]] ) -> Any :
246
+ converters : TaggedConverters ) -> Any :
199
247
"""Convert a single value into target type if possible."""
200
248
if _is_initvar_type (func ):
201
249
return value
@@ -218,13 +266,22 @@ def _get_value(func: WrappedOrFunc[TypeT],
218
266
raise TypeError (f'Parameter { param_name } of target function '
219
267
'is missing a type annotation.' )
220
268
221
- if Any not in allowed_types and param_name != 'self' :
222
- if not _isinstanceofone (value , allowed_types ):
223
- value_type = type (value )
224
- if converters and param_name in converters :
225
- result = converters [param_name ](value )
269
+ mandatory_converters , optional_converters = _split_converters (converters )
270
+
271
+ if param_name != 'self' :
272
+ value_type = type (value )
273
+ if mandatory_converters and param_name in mandatory_converters :
274
+ result = mandatory_converters [param_name ](value )
275
+ if not _isinstanceofone (result , allowed_types ):
276
+ raise TypeError (f'Mandatory custom conversion for { param_name } '
277
+ f'yields incorrect target type: '
278
+ f'{ _get_type_name (type (result ))} ' )
279
+ return result
280
+ if Any not in allowed_types and not _isinstanceofone (value , allowed_types ):
281
+ if optional_converters and param_name in optional_converters :
282
+ result = optional_converters [param_name ](value )
226
283
if not _isinstanceofone (result , allowed_types ):
227
- raise TypeError (f'Custom conversion for { param_name } '
284
+ raise TypeError (f'Optional custom conversion for { param_name } '
228
285
f'yields incorrect target type: '
229
286
f'{ _get_type_name (type (result ))} ' )
230
287
return result
@@ -269,7 +326,7 @@ def _string_to_bool(value: str) -> bool:
269
326
def _get_list_value (func : Callable [..., TypeT ], # pylint: disable=too-many-arguments
270
327
value : Any , log_name : str ,
271
328
skip_superfluous : bool , convert_types : bool ,
272
- converters : Optional [ Dict [ str , Callable [[ Any ], Any ]]] ) -> Any :
329
+ converters : TaggedConverters ) -> Any :
273
330
if not _is_list_type (func ) and \
274
331
not _is_optional_list_type (func ):
275
332
raise TypeError (f'No list expected for { log_name } ' )
@@ -285,7 +342,7 @@ def _get_list_value(func: Callable[..., TypeT], # pylint: disable=too-many-argu
285
342
286
343
def _get_dict_value (func : Callable [..., TypeT ], value : Any ,
287
344
skip_superfluous : bool , convert_types : bool ,
288
- converters : Optional [ Dict [ str , Callable [[ Any ], Any ]]] ) -> Any :
345
+ converters : TaggedConverters ) -> Any :
289
346
assert _is_dict (value )
290
347
if _is_optional_type (func ):
291
348
return _get_optional_type (func )(** value ) # type: ignore
0 commit comments