boolify()   F
last analyzed

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