|
1
|
|
|
# -*- coding: utf-8 -*- |
|
2
|
|
|
"""Collection of functions to coerce conversion of types with an intelligent guess.""" |
|
3
|
|
|
from __future__ import absolute_import, division, print_function |
|
4
|
|
|
|
|
5
|
|
|
from collections import Mapping |
|
6
|
|
|
from itertools import chain |
|
7
|
|
|
from re import IGNORECASE, compile |
|
8
|
|
|
|
|
9
|
|
|
from .compat import NoneType, integer_types, isiterable, iteritems, string_types, text_type |
|
10
|
|
|
from .decorators import memoize, memoizeproperty |
|
11
|
|
|
from .exceptions import AuxlibError |
|
12
|
|
|
|
|
13
|
|
|
__all__ = ["boolify", "typify", "maybecall", "listify", "numberify"] |
|
14
|
|
|
|
|
15
|
|
|
BOOLISH_TRUE = ("true", "yes", "on", "y") |
|
16
|
|
|
BOOLISH_FALSE = ("false", "off", "n", "no", "non", "none", "") |
|
17
|
|
|
NULL_STRINGS = ("none", "~", "null", "\0") |
|
18
|
|
|
BOOL_COERCEABLE_TYPES = integer_types + (bool, float, complex, list, set, dict, tuple) |
|
19
|
|
|
NUMBER_TYPES = integer_types + (float, complex) |
|
20
|
|
|
NUMBER_TYPES_SET = frozenset(NUMBER_TYPES) |
|
21
|
|
|
STRING_TYPES_SET = frozenset(string_types) |
|
22
|
|
|
BOOLNULL_TYPE_SET = frozenset([bool, NoneType]) |
|
23
|
|
|
BOOLSTRING_TYPES_SET = STRING_TYPES_SET | frozenset([bool]) |
|
24
|
|
|
|
|
25
|
|
|
NO_MATCH = object() |
|
26
|
|
|
|
|
27
|
|
|
|
|
28
|
|
|
class TypeCoercionError(AuxlibError, ValueError): |
|
29
|
|
|
|
|
30
|
|
|
def __init__(self, value, msg, *args, **kwargs): |
|
31
|
|
|
self.value = value |
|
32
|
|
|
super(TypeCoercionError, self).__init__(msg, *args, **kwargs) |
|
33
|
|
|
|
|
34
|
|
|
|
|
35
|
|
|
class _Regex(object): |
|
36
|
|
|
|
|
37
|
|
|
@memoizeproperty |
|
38
|
|
|
def BOOLEAN_TRUE(self): |
|
39
|
|
|
return compile(r'^true$|^yes$|^on$', IGNORECASE), True |
|
40
|
|
|
|
|
41
|
|
|
@memoizeproperty |
|
42
|
|
|
def BOOLEAN_FALSE(self): |
|
43
|
|
|
return compile(r'^false$|^no$|^off$', IGNORECASE), False |
|
44
|
|
|
|
|
45
|
|
|
@memoizeproperty |
|
46
|
|
|
def NONE(self): |
|
47
|
|
|
return compile(r'^none$|^null$', IGNORECASE), None |
|
48
|
|
|
|
|
49
|
|
|
@memoizeproperty |
|
50
|
|
|
def INT(self): |
|
51
|
|
|
return compile(r'^[-+]?\d+$'), int |
|
52
|
|
|
|
|
53
|
|
|
@memoizeproperty |
|
54
|
|
|
def BIN(self): |
|
55
|
|
|
return compile(r'^[-+]?0[bB][01]+$'), bin |
|
56
|
|
|
|
|
57
|
|
|
@memoizeproperty |
|
58
|
|
|
def OCT(self): |
|
59
|
|
|
return compile(r'^[-+]?0[oO][0-7]+$'), oct |
|
60
|
|
|
|
|
61
|
|
|
@memoizeproperty |
|
62
|
|
|
def HEX(self): |
|
63
|
|
|
return compile(r'^[-+]?0[xX][0-9a-fA-F]+$'), hex |
|
64
|
|
|
|
|
65
|
|
|
@memoizeproperty |
|
66
|
|
|
def FLOAT(self): |
|
67
|
|
|
return compile(r'^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$'), float |
|
68
|
|
|
|
|
69
|
|
|
@memoizeproperty |
|
70
|
|
|
def COMPLEX(self): |
|
71
|
|
|
return (compile(r'^(?:[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)?' # maybe first float |
|
72
|
|
|
r'[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?j$'), # second float with j |
|
73
|
|
|
complex) |
|
74
|
|
|
|
|
75
|
|
|
@property |
|
76
|
|
|
def numbers(self): |
|
77
|
|
|
yield self.INT |
|
78
|
|
|
yield self.FLOAT |
|
79
|
|
|
yield self.BIN |
|
80
|
|
|
yield self.OCT |
|
81
|
|
|
yield self.HEX |
|
82
|
|
|
yield self.COMPLEX |
|
83
|
|
|
|
|
84
|
|
|
@property |
|
85
|
|
|
def boolean(self): |
|
86
|
|
|
yield self.BOOLEAN_TRUE |
|
87
|
|
|
yield self.BOOLEAN_FALSE |
|
88
|
|
|
|
|
89
|
|
|
@property |
|
90
|
|
|
def none(self): |
|
91
|
|
|
yield self.NONE |
|
92
|
|
|
|
|
93
|
|
|
def convert_number(self, value_string): |
|
94
|
|
|
return self._convert(value_string, (self.numbers, )) |
|
95
|
|
|
|
|
96
|
|
|
def convert(self, value_string): |
|
97
|
|
|
return self._convert(value_string, (self.boolean, self.none, self.numbers, )) |
|
98
|
|
|
|
|
99
|
|
|
def _convert(self, value_string, type_list): |
|
100
|
|
|
return next((typish(value_string) if callable(typish) else typish |
|
101
|
|
|
for regex, typish in chain.from_iterable(type_list) |
|
102
|
|
|
if regex.match(value_string)), |
|
103
|
|
|
NO_MATCH) |
|
104
|
|
|
|
|
105
|
|
|
_REGEX = _Regex() |
|
106
|
|
|
|
|
107
|
|
|
|
|
108
|
|
|
def numberify(value): |
|
109
|
|
|
""" |
|
110
|
|
|
|
|
111
|
|
|
Examples: |
|
112
|
|
|
>>> [numberify(x) for x in ('1234', 1234, '0755', 0o0755, False, 0, '0', True, 1, '1')] |
|
113
|
|
|
[1234, 1234, 755, 493, 0, 0, 0, 1, 1, 1] |
|
114
|
|
|
>>> [numberify(x) for x in ('12.34', 12.34, 1.2+3.5j, '1.2+3.5j')] |
|
115
|
|
|
[12.34, 12.34, (1.2+3.5j), (1.2+3.5j)] |
|
116
|
|
|
|
|
117
|
|
|
""" |
|
118
|
|
|
if isinstance(value, bool): |
|
119
|
|
|
return int(value) |
|
120
|
|
|
if isinstance(value, NUMBER_TYPES): |
|
121
|
|
|
return value |
|
122
|
|
|
candidate = _REGEX.convert_number(value) |
|
123
|
|
|
if candidate is not NO_MATCH: |
|
124
|
|
|
return candidate |
|
125
|
|
|
raise TypeCoercionError(value, "Cannot convert {0} to a number.".format(value)) |
|
126
|
|
|
|
|
127
|
|
|
|
|
128
|
|
|
def boolify(value, nullable=False, return_string=False): |
|
129
|
|
|
"""Convert a number, string, or sequence type into a pure boolean. |
|
130
|
|
|
|
|
131
|
|
|
Args: |
|
132
|
|
|
value (number, string, sequence): pretty much anything |
|
133
|
|
|
|
|
134
|
|
|
Returns: |
|
135
|
|
|
bool: boolean representation of the given value |
|
136
|
|
|
|
|
137
|
|
|
Examples: |
|
138
|
|
|
>>> [boolify(x) for x in ('yes', 'no')] |
|
139
|
|
|
[True, False] |
|
140
|
|
|
>>> [boolify(x) for x in (0.1, 0+0j, True, '0', '0.0', '0.1', '2')] |
|
141
|
|
|
[True, False, True, False, False, True, True] |
|
142
|
|
|
>>> [boolify(x) for x in ("true", "yes", "on", "y")] |
|
143
|
|
|
[True, True, True, True] |
|
144
|
|
|
>>> [boolify(x) for x in ("no", "non", "none", "off", "")] |
|
145
|
|
|
[False, False, False, False, False] |
|
146
|
|
|
>>> [boolify(x) for x in ([], set(), dict(), tuple())] |
|
147
|
|
|
[False, False, False, False] |
|
148
|
|
|
>>> [boolify(x) for x in ([1], set([False]), dict({'a': 1}), tuple([2]))] |
|
149
|
|
|
[True, True, True, True] |
|
150
|
|
|
|
|
151
|
|
|
""" |
|
152
|
|
|
# cast number types naturally |
|
153
|
|
|
if isinstance(value, BOOL_COERCEABLE_TYPES): |
|
154
|
|
|
return bool(value) |
|
155
|
|
|
# try to coerce string into number |
|
156
|
|
|
val = text_type(value).strip().lower().replace('.', '', 1) |
|
157
|
|
|
if val.isnumeric(): |
|
158
|
|
|
return bool(float(val)) |
|
159
|
|
|
elif val in BOOLISH_TRUE: |
|
160
|
|
|
return True |
|
161
|
|
|
elif nullable and val in NULL_STRINGS: |
|
162
|
|
|
return None |
|
163
|
|
|
elif val in BOOLISH_FALSE: |
|
164
|
|
|
return False |
|
165
|
|
|
else: # must be False |
|
166
|
|
|
try: |
|
167
|
|
|
return bool(complex(val)) |
|
168
|
|
|
except ValueError: |
|
169
|
|
|
if isinstance(value, string_types) and return_string: |
|
170
|
|
|
return value |
|
171
|
|
|
raise TypeCoercionError(value, "The value %r cannot be boolified." % value) |
|
172
|
|
|
|
|
173
|
|
|
|
|
174
|
|
|
def boolify_truthy_string_ok(value): |
|
175
|
|
|
try: |
|
176
|
|
|
return boolify(value) |
|
177
|
|
|
except ValueError: |
|
178
|
|
|
assert isinstance(value, string_types), repr(value) |
|
179
|
|
|
return True |
|
180
|
|
|
|
|
181
|
|
|
|
|
182
|
|
|
@memoize |
|
183
|
|
|
def typify(value, type_hint=None): |
|
184
|
|
|
"""Take a primitive value, usually a string, and try to make a more relevant type out of it. |
|
185
|
|
|
An optional type_hint will try to coerce the value to that type. |
|
186
|
|
|
|
|
187
|
|
|
Args: |
|
188
|
|
|
value (Any): Usually a string, not a sequence |
|
189
|
|
|
type_hint (type or Tuple[type]): |
|
190
|
|
|
|
|
191
|
|
|
Examples: |
|
192
|
|
|
>>> typify('32') |
|
193
|
|
|
32 |
|
194
|
|
|
>>> typify('32', float) |
|
195
|
|
|
32.0 |
|
196
|
|
|
>>> typify('32.0') |
|
197
|
|
|
32.0 |
|
198
|
|
|
>>> typify('32.0.0') |
|
199
|
|
|
'32.0.0' |
|
200
|
|
|
>>> [typify(x) for x in ('true', 'yes', 'on')] |
|
201
|
|
|
[True, True, True] |
|
202
|
|
|
>>> [typify(x) for x in ('no', 'FALSe', 'off')] |
|
203
|
|
|
[False, False, False] |
|
204
|
|
|
>>> [typify(x) for x in ('none', 'None', None)] |
|
205
|
|
|
[None, None, None] |
|
206
|
|
|
|
|
207
|
|
|
""" |
|
208
|
|
|
# value must be a string, or there at least needs to be a type hint |
|
209
|
|
|
if isinstance(value, string_types): |
|
210
|
|
|
value = value.strip() |
|
211
|
|
|
elif type_hint is None: |
|
212
|
|
|
# can't do anything because value isn't a string and there's no type hint |
|
213
|
|
|
return value |
|
214
|
|
|
|
|
215
|
|
|
# now we either have a stripped string, a type hint, or both |
|
216
|
|
|
# use the hint if it exists |
|
217
|
|
|
if type_hint is None: |
|
218
|
|
|
# no type hint, but we know value is a string, so try to match with the regex patterns |
|
219
|
|
|
return _regex_typify_string(value) |
|
220
|
|
|
else: |
|
221
|
|
|
return _typify_with_hint(value, type_hint) |
|
222
|
|
|
|
|
223
|
|
|
|
|
224
|
|
|
def _typify_with_hint(value, type_hint): |
|
225
|
|
|
if isiterable(type_hint): |
|
226
|
|
|
return _typify_with_iterable_hint(value, type_hint) |
|
227
|
|
|
else: |
|
228
|
|
|
# coerce using the type hint, or use boolify for bool |
|
229
|
|
|
try: |
|
230
|
|
|
return boolify(value) if type_hint == bool else type_hint(value) |
|
231
|
|
|
except ValueError as e: |
|
232
|
|
|
# ValueError: invalid literal for int() with base 10: 'nope' |
|
233
|
|
|
raise TypeCoercionError(value, text_type(e)) |
|
234
|
|
|
|
|
235
|
|
|
|
|
236
|
|
|
def _typify_with_iterable_hint(value, type_hint): |
|
237
|
|
|
type_hint = frozenset(type_hint) |
|
238
|
|
|
if not (type_hint - NUMBER_TYPES_SET): |
|
239
|
|
|
return numberify(value) |
|
240
|
|
|
elif not (type_hint - STRING_TYPES_SET): |
|
241
|
|
|
return text_type(value) |
|
242
|
|
|
elif not (type_hint - BOOLNULL_TYPE_SET): |
|
243
|
|
|
return boolify(value, nullable=True) |
|
244
|
|
|
elif not (type_hint - BOOLSTRING_TYPES_SET): |
|
245
|
|
|
return boolify(value, return_string=True) |
|
246
|
|
|
else: |
|
247
|
|
|
raise NotImplementedError() |
|
248
|
|
|
|
|
249
|
|
|
|
|
250
|
|
|
def _regex_typify_string(string): |
|
251
|
|
|
candidate = _REGEX.convert(string) |
|
252
|
|
|
if candidate is not NO_MATCH: |
|
253
|
|
|
return candidate |
|
254
|
|
|
else: |
|
255
|
|
|
# nothing has caught so far; give up, and return the value that was given |
|
256
|
|
|
return string |
|
257
|
|
|
|
|
258
|
|
|
|
|
259
|
|
|
def typify_data_structure(value, type_hint=None): |
|
260
|
|
|
if isinstance(value, Mapping): |
|
261
|
|
|
return type(value)((k, typify(v, type_hint)) for k, v in iteritems(value)) |
|
262
|
|
|
elif isiterable(value): |
|
263
|
|
|
return type(value)(typify(v, type_hint) for v in value) |
|
264
|
|
|
else: |
|
265
|
|
|
return typify(value, type_hint) |
|
266
|
|
|
|
|
267
|
|
|
|
|
268
|
|
|
def maybecall(value): |
|
269
|
|
|
return value() if callable(value) else value |
|
270
|
|
|
|
|
271
|
|
|
|
|
272
|
|
|
def listify(val, return_type=tuple): |
|
273
|
|
|
""" |
|
274
|
|
|
Examples: |
|
275
|
|
|
>>> listify('abc', return_type=list) |
|
276
|
|
|
['abc'] |
|
277
|
|
|
>>> listify(None) |
|
278
|
|
|
() |
|
279
|
|
|
>>> listify(False) |
|
280
|
|
|
(False,) |
|
281
|
|
|
>>> listify(('a', 'b', 'c'), return_type=list) |
|
282
|
|
|
['a', 'b', 'c'] |
|
283
|
|
|
""" |
|
284
|
|
|
# TODO: flatlistify((1, 2, 3), 4, (5, 6, 7)) |
|
285
|
|
|
if val is None: |
|
286
|
|
|
return return_type() |
|
287
|
|
|
elif isiterable(val): |
|
288
|
|
|
return return_type(val) |
|
289
|
|
|
else: |
|
290
|
|
|
return return_type((val, )) |
|
291
|
|
|
|