Completed
Push — develop ( e01780...be665b )
by Kale
01:10
created

_Regex.BOOLEAN_TRUE()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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