Completed
Push — develop ( 3606a5...c5328e )
by Kale
01:06
created

typify()   D

Complexity

Conditions 8

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 8
dl 0
loc 46
rs 4.1666
1
"""Collection of functions to coerce conversion of types with an intelligent guess."""
2
import collections
3
import re
4
5
from ._vendor.five import (int_types as integer_types, string_t as string_types, items,
6
                           text_t as text_type)
7
from .decorators import memoize
8
9
10
__all__ = ["boolify", "typify", "maybecall", "listify"]
11
12
BOOLISH = ("true", "yes", "on", "y")
13
BOOLABLE_TYPES = integer_types + (bool, float, complex, list, set, dict, tuple)
14
15
16
def _generate_regex_type_map(func=None):
17
    RE_BOOLEAN_TRUE = re.compile(r'^true$|^yes$|^on$', re.IGNORECASE)
18
    RE_BOOLEAN_FALSE = re.compile(r'^false$|^no$|^off$', re.IGNORECASE)
19
    RE_INTEGER = re.compile(r'^[0-9]+$')
20
    RE_FLOAT = re.compile(r'^[0-9]+\.[0-9]+$')
21
    RE_NONE = re.compile(r'^None$', re.IGNORECASE)
22
23
    REGEX_TYPE_MAP = dict({RE_BOOLEAN_TRUE: True,
24
                           RE_BOOLEAN_FALSE: False,
25
                           RE_INTEGER: int,
26
                           RE_FLOAT: float,
27
                           RE_NONE: None, })
28
29
    func.REGEX_TYPE_MAP = REGEX_TYPE_MAP
30
    return REGEX_TYPE_MAP
31
32
33
def boolify(value):
34
    """Convert a number, string, or sequence type into a pure boolean.
35
36
    Args:
37
        value (number, string, sequence): pretty much anything
38
39
    Returns:
40
        bool: boolean representation of the given value
41
42
    Examples:
43
        >>> [boolify(x) for x in ('yes', 'no')]
44
        [True, False]
45
        >>> [boolify(x) for x in (0.1, 0+0j, True, '0', '0.0', '0.1', '2')]
46
        [True, False, True, False, False, True, True]
47
        >>> [boolify(x) for x in ("true", "yes", "on", "y")]
48
        [True, True, True, True]
49
        >>> [boolify(x) for x in ("no", "non", "none", "off")]
50
        [False, False, False, False]
51
        >>> [boolify(x) for x in ([], set(), dict(), tuple())]
52
        [False, False, False, False]
53
        >>> [boolify(x) for x in ([1], set([False]), dict({'a': 1}), tuple([2]))]
54
        [True, True, True, True]
55
    """
56
    # cast number types naturally
57
    if isinstance(value, BOOLABLE_TYPES):
58
        return bool(value)
59
    # try to coerce string into number
60
    val = text_type(value).strip().lower().replace('.', '', 1)
61
    if val.isnumeric():
62
        return bool(float(val))
63
    elif val in BOOLISH:  # now look for truthy strings
64
        return True
65
    else:  # must be False
66
        return False
67
68
69
@memoize
70
def typify(value, type_hint=None):
71
    """Take a primitive value, usually a string, and try to make a more relevant type out of it.
72
    An optional type_hint will try to coerce the value to that type.
73
74
    Args:
75
        value (str, number): Usually a string, not a sequence
76
        type_hint (type, optional):
77
78
    Examples:
79
        >>> typify('32')
80
        32
81
        >>> typify('32', float)
82
        32.0
83
        >>> typify('32.0')
84
        32.0
85
        >>> typify('32.0.0')
86
        '32.0.0'
87
        >>> [typify(x) for x in ('true', 'yes', 'on')]
88
        [True, True, True]
89
        >>> [typify(x) for x in ('no', 'FALSe', 'off')]
90
        [False, False, False]
91
        >>> [typify(x) for x in ('none', 'None', None)]
92
        [None, None, None]
93
94
    """
95
    # value must be a string, or there at least needs to be a type hint
96
    if isinstance(value, string_types):
97
        value = value.strip()
98
    elif type_hint is None:
99
        # can't do anything because value isn't a string and there' no type hint
100
        return value
101
102
    # now we either have a stripped string, a type hint, or both
103
    # use the hint if it exists
104
    if type_hint is not None:
105
        return boolify(value) if type_hint == bool else type_hint(value)
106
107
    # no type hint, so try to match with the regex patterns
108
    for regex, typish in items(getattr(typify, 'REGEX_TYPE_MAP', None)
109
                               or _generate_regex_type_map(typify)):
110
        if regex.match(value):
111
            return typish(value) if callable(typish) else typish
112
113
    # nothing has caught so far; give up, and return the value that was given
114
    return value
115
116
117
def maybecall(value):
118
    return value() if callable(value) else value
119
120
121
def listify(val):
122
    """
123
    Examples:
124
        >>> listify('abc')
125
        ['abc']
126
        >>> listify(None)
127
        []
128
        >>> listify(False)
129
        [False]
130
        >>> listify(('a', 'b', 'c'))
131
        ['a', 'b', 'c']
132
    """
133
    if val is None:
134
        return []
135
    elif isinstance(val, string_types):
136
        return [val]
137
    elif isinstance(val, collections.Iterable):
138
        return list(val)
139
    else:
140
        return [val]
141