Completed
Push — develop ( ce1198...1cbae8 )
by Kale
9s
created

boolify()   F

Complexity

Conditions 10

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 10
c 3
b 0
f 0
dl 0
loc 44
rs 3.1304

How to fix   Complexity   

Complexity

Complex classes like boolify() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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