Completed
Push — develop ( 40eca3...d2744c )
by Kale
01:05
created

boolify()   D

Complexity

Conditions 8

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

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