Completed
Push — develop ( 0ffb7c...6efd8f )
by Kale
01:05
created

typify()   D

Complexity

Conditions 9

Size

Total Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 9
dl 0
loc 53
rs 4.9852
c 3
b 1
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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