Source code for auxlib.type_coercion

# -*- coding: utf-8 -*-
"""Collection of functions to coerce conversion of types with an intelligent guess."""
from __future__ import absolute_import, division, print_function

from collections import Mapping
from itertools import chain
from re import IGNORECASE, compile

from .compat import NoneType, integer_types, isiterable, iteritems, string_types, text_type
from .decorators import memoize, memoizeproperty
from .exceptions import AuxlibError

__all__ = ["boolify", "typify", "maybecall", "listify", "numberify"]

BOOLISH_TRUE = ("true", "yes", "on", "y")
BOOLISH_FALSE = ("false", "off", "n", "no", "non", "none", "")
NULL_STRINGS = ("none", "~", "null", "\0")
BOOL_COERCEABLE_TYPES = integer_types + (bool, float, complex, list, set, dict, tuple)
NUMBER_TYPES = integer_types + (float, complex)
NUMBER_TYPES_SET = frozenset(NUMBER_TYPES)
STRING_TYPES_SET = frozenset(string_types)
BOOLNULL_TYPE_SET = frozenset([bool, NoneType])
BOOLSTRING_TYPES_SET = STRING_TYPES_SET | frozenset([bool])

NO_MATCH = object()


class TypeCoercionError(AuxlibError, ValueError):

    def __init__(self, value, msg, *args, **kwargs):
        self.value = value
        super(TypeCoercionError, self).__init__(msg, *args, **kwargs)


class _Regex(object):

    @memoizeproperty
    def BOOLEAN_TRUE(self):
        return compile(r'^true$|^yes$|^on$', IGNORECASE), True

    @memoizeproperty
    def BOOLEAN_FALSE(self):
        return compile(r'^false$|^no$|^off$', IGNORECASE), False

    @memoizeproperty
    def NONE(self):
        return compile(r'^none$|^null$', IGNORECASE), None

    @memoizeproperty
    def INT(self):
        return compile(r'^[-+]?\d+$'), int

    @memoizeproperty
    def BIN(self):
        return compile(r'^[-+]?0[bB][01]+$'), bin

    @memoizeproperty
    def OCT(self):
        return compile(r'^[-+]?0[oO][0-7]+$'), oct

    @memoizeproperty
    def HEX(self):
        return compile(r'^[-+]?0[xX][0-9a-fA-F]+$'), hex

    @memoizeproperty
    def FLOAT(self):
        return compile(r'^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$'), float

    @memoizeproperty
    def COMPLEX(self):
        return (compile(r'^(?:[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)?'  # maybe first float
                        r'[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?j$'),     # second float with j
                complex)

    @property
    def numbers(self):
        yield self.INT
        yield self.FLOAT
        yield self.BIN
        yield self.OCT
        yield self.HEX
        yield self.COMPLEX

    @property
    def boolean(self):
        yield self.BOOLEAN_TRUE
        yield self.BOOLEAN_FALSE

    @property
    def none(self):
        yield self.NONE

    def convert_number(self, value_string):
        return self._convert(value_string, (self.numbers, ))

    def convert(self, value_string):
        return self._convert(value_string, (self.boolean, self.none, self.numbers, ))

    def _convert(self, value_string, type_list):
        return next((typish(value_string) if callable(typish) else typish
                     for regex, typish in chain.from_iterable(type_list)
                     if regex.match(value_string)),
                    NO_MATCH)

_REGEX = _Regex()


[docs]def numberify(value): """ Examples: >>> [numberify(x) for x in ('1234', 1234, '0755', 0o0755, False, 0, '0', True, 1, '1')] [1234, 1234, 755, 493, 0, 0, 0, 1, 1, 1] >>> [numberify(x) for x in ('12.34', 12.34, 1.2+3.5j, '1.2+3.5j')] [12.34, 12.34, (1.2+3.5j), (1.2+3.5j)] """ if isinstance(value, bool): return int(value) if isinstance(value, NUMBER_TYPES): return value candidate = _REGEX.convert_number(value) if candidate is not NO_MATCH: return candidate raise TypeCoercionError(value, "Cannot convert {0} to a number.".format(value))
[docs]def boolify(value, nullable=False, return_string=False): """Convert a number, string, or sequence type into a pure boolean. Args: value (number, string, sequence): pretty much anything Returns: bool: boolean representation of the given value Examples: >>> [boolify(x) for x in ('yes', 'no')] [True, False] >>> [boolify(x) for x in (0.1, 0+0j, True, '0', '0.0', '0.1', '2')] [True, False, True, False, False, True, True] >>> [boolify(x) for x in ("true", "yes", "on", "y")] [True, True, True, True] >>> [boolify(x) for x in ("no", "non", "none", "off", "")] [False, False, False, False, False] >>> [boolify(x) for x in ([], set(), dict(), tuple())] [False, False, False, False] >>> [boolify(x) for x in ([1], set([False]), dict({'a': 1}), tuple([2]))] [True, True, True, True] """ # cast number types naturally if isinstance(value, BOOL_COERCEABLE_TYPES): return bool(value) # try to coerce string into number val = text_type(value).strip().lower().replace('.', '', 1) if val.isnumeric(): return bool(float(val)) elif val in BOOLISH_TRUE: return True elif nullable and val in NULL_STRINGS: return None elif val in BOOLISH_FALSE: return False else: # must be False try: return bool(complex(val)) except ValueError: if isinstance(value, string_types) and return_string: return value raise TypeCoercionError(value, "The value %r cannot be boolified." % value)
def boolify_truthy_string_ok(value): try: return boolify(value) except ValueError: assert isinstance(value, string_types), repr(value) return True @memoize
[docs]def typify(value, type_hint=None): """Take a primitive value, usually a string, and try to make a more relevant type out of it. An optional type_hint will try to coerce the value to that type. Args: value (Any): Usually a string, not a sequence type_hint (type or Tuple[type]): Examples: >>> typify('32') 32 >>> typify('32', float) 32.0 >>> typify('32.0') 32.0 >>> typify('32.0.0') '32.0.0' >>> [typify(x) for x in ('true', 'yes', 'on')] [True, True, True] >>> [typify(x) for x in ('no', 'FALSe', 'off')] [False, False, False] >>> [typify(x) for x in ('none', 'None', None)] [None, None, None] """ # value must be a string, or there at least needs to be a type hint if isinstance(value, string_types): value = value.strip() elif type_hint is None: # can't do anything because value isn't a string and there's no type hint return value # now we either have a stripped string, a type hint, or both # use the hint if it exists if type_hint is None: # no type hint, but we know value is a string, so try to match with the regex patterns return _regex_typify_string(value) else: return _typify_with_hint(value, type_hint)
def _typify_with_hint(value, type_hint): if isiterable(type_hint): return _typify_with_iterable_hint(value, type_hint) else: # coerce using the type hint, or use boolify for bool try: return boolify(value) if type_hint == bool else type_hint(value) except ValueError as e: # ValueError: invalid literal for int() with base 10: 'nope' raise TypeCoercionError(value, text_type(e)) def _typify_with_iterable_hint(value, type_hint): type_hint = frozenset(type_hint) if not (type_hint - NUMBER_TYPES_SET): return numberify(value) elif not (type_hint - STRING_TYPES_SET): return text_type(value) elif not (type_hint - BOOLNULL_TYPE_SET): return boolify(value, nullable=True) elif not (type_hint - BOOLSTRING_TYPES_SET): return boolify(value, return_string=True) else: raise NotImplementedError() def _regex_typify_string(string): candidate = _REGEX.convert(string) if candidate is not NO_MATCH: return candidate else: # nothing has caught so far; give up, and return the value that was given return string def typify_data_structure(value, type_hint=None): if isinstance(value, Mapping): return type(value)((k, typify(v, type_hint)) for k, v in iteritems(value)) elif isiterable(value): return type(value)(typify(v, type_hint) for v in value) else: return typify(value, type_hint)
[docs]def maybecall(value): return value() if callable(value) else value
[docs]def listify(val, return_type=tuple): """ Examples: >>> listify('abc', return_type=list) ['abc'] >>> listify(None) () >>> listify(False) (False,) >>> listify(('a', 'b', 'c'), return_type=list) ['a', 'b', 'c'] """ # TODO: flatlistify((1, 2, 3), 4, (5, 6, 7)) if val is None: return return_type() elif isiterable(val): return return_type(val) else: return return_type((val, ))