Source code for xotl.tools.values

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#

"""Some generic coercers (or checkers) for value types.

This module coercion function are not related in any way to deprecated old
python feature, are similar to a combination of object mold/check:

- *Mold* - Fit values to expected conventions.

- *Check* - These functions must return `nil` [#pyni]_ special value to
  specify that expected fit is not possible.

.. [#pyni] We don't use Python classic `NotImplemented` special value in order
           to obtain False if the value is not coerced (`nil`).

A custom coercer could be created with closures, for an example see
`create_int_range_coerce`:func:.

This module uses `Unset` value to define absent -not being specified-
arguments.

Also contains sub-modules to obtain, convert and check values of common types.

.. versionadded:: 1.7.0

"""

import re
from abc import ABCMeta

from xotl.tools.deprecation import deprecate_linked
from xotl.tools.fp.prove import vouch
from xotl.tools.future.functools import lwraps
from xotl.tools.symbols import Unset, boolean

deprecate_linked(check="xotl.tools.values")
del deprecate_linked


_coercer_decorator = lwraps(__coercer__=True)  # FIX: refactor


[docs]class logical(boolean): """Represent Common Lisp two special values `t` and `nil`. Include redefinition of `__call__`:meth: to check values with special semantic: - When called as ``t(arg)``, check if `arg` is not `nil` returning a logical true: the same argument if `arg` is nil or a true boolean value, else return `t`. That means that `False` or `0` are valid true values for Common Lisp but not for Python. - When called as ``nil(arg)``, check if `arg` is `nil` returning `t` or `nil` if not. Constructor could receive a valid name ('nil' or 't') or any other ``boolean`` instance. """ __slots__ = () _valid = {"nil": False, "t": True} def __new__(cls, arg): from xotl.tools.symbols import Invalid, boolean name = ("t" if arg else "nil") if isinstance(arg, boolean) else arg value = cls._valid.get(name, Invalid) if value is not Invalid: return super().__new__(cls, name, value) else: msg = 'retrieving invalid logical instance "{}"' raise TypeError(msg.format(arg)) def __call__(self, arg): if self: # self is t return arg if arg or arg is nil else self else: # self is nil return t if arg is self else self
nil, t = logical("nil"), logical("t")
[docs]class MetaCoercer(ABCMeta): r"""Meta-class for `coercer`:class:. This meta-class allows that several objects are considered valid instances of `coercer`:class:\ : - Functions decorated with `coercer`:class: (used with its decorator facet). - Instances of any sub-class of `custom`:class:. - Instances of `coercer`:class: itself. See the class declaration (`coercer`:class:) for more information. """ def __instancecheck__(self, instance): return getattr(instance, "__coercer__", False) or super().__instancecheck__(instance)
[docs]class coercer(metaclass=MetaCoercer): """Special coercer class. This class has several facets: - Pure type-checkers when a type or tuple of types are received as argument. See `istype`:class: for more information. - Return equivalent coercer from some special values: * Any true value -> identity_coerce * Any false or empty value -> void_coerce - A decorator for functions; when a function is given, decorate it to become a coercer. The mark itself is not enough, functions intended to be coercers must fulfills the protocol (not to produce exception and return `nil` on fails). For example:: >>> @coercer ... def age_coerce(arg): ... res = int_coerce(arg) ... return res if t(res) and 0 < arg <= 120 else nil # TODO: Change next, don't use isinstance >>> isinstance(age_coerce, coercer) True """ __slots__ = () __coercer__ = True def __new__(cls, source): from types import FunctionType as function from xotl.tools.symbols import boolean if source == 1 and isinstance(source, boolean): return identity_coerce elif source is None or (source == 0 and isinstance(source, boolean)): return void_coerce elif isinstance(source, coercer): # TODO: don't use isinstance return source elif isinstance(source, (function, staticmethod, classmethod)): return _coercer_decorator(source) else: inner = types_tuple_coerce(source) return istype(inner) if inner else nil
[docs]def coercer_name(arg, join=None): """Get the name of a coercer. :param arg: Coercer to get the name. Also processes collections (tuple, list, or set) of coercers. Any other value is considered invalid and raises an exception. :param join: When a collection is used; if this argument is None a collection of names is returned, if not None then is used to join the items in a resulting string. For example:: >>> coercer_name((int_coerce, float_coerce)) ('int', 'float') >>> coercer_name((int_coerce, float_coerce), join='-') 'int-float' To obtain pretty-print tuples, use something like:: >>> coercer_name((int_coerce, float_coerce), ... join=lambda arg: '(%s)' % ', '.join(arg)) This function not only works with coercers, all objects that fulfill needed protocol to get names will also be valid. """ # TODO: Maybe this function must be moved to `xotl.tools.names` if isinstance(arg, (tuple, list, set)): res = type(arg)(coercer_name(c) for c in arg) if isinstance(join, str): join = join.join return str(join(res)) if join else res else: try: res = arg.__name__ except Exception: res = str(arg) suffix = str("_coerce") if res.endswith(suffix): res = res[: -len(suffix)] return res
[docs]@coercer def identity_coerce(arg): "Leaves unchanged the passed argument `arg`." return arg
[docs]@coercer def void_coerce(arg): """Always `nil`.""" return nil
[docs]@coercer def type_coerce(arg): """Check if `arg` is a valid type.""" return arg if isinstance(arg, type) else nil
[docs]@coercer def types_tuple_coerce(arg): """Check if `arg` is valid for `isinstance` or `issubclass` 2nd argument. Type checkers are any class, a type or tuple of types. For example:: >>> types_tuple_coerce(object) == (object,) True >>> types_tuple_coerce((int, float)) == (int, float) true >>> types_tuple_coerce('not-a-type') is nil True See `type_coerce` for more information. """ if t(type_coerce(arg)): return (arg,) elif isinstance(arg, tuple) and all(t(type_coerce(tp)) for tp in arg): return arg else: return nil
[docs]@coercer def callable_coerce(arg): """Check if `arg` is a callable object.""" return arg if callable(arg) else nil
[docs]@coercer def file_coerce(arg): """Check if `arg` is a file-like object.""" from io import IOBase METHODS = ("close", "write", "read") ok = isinstance(arg, IOBase) or all(hasattr(arg, a) for a in METHODS) return arg if ok else nil
[docs]@coercer def float_coerce(arg): """Check if `arg` is a valid float. Other types are checked (string, int, complex). """ if isinstance(arg, float): return arg elif isinstance(arg, int): return float(arg) elif isinstance(arg, (str, bytes)): try: return float(arg) except ValueError: return nil elif isinstance(arg, complex): return arg.real if arg.imag == 0 else nil else: return nil
[docs]@coercer def int_coerce(arg): """Check if `arg` is a valid integer. Other types are checked (string, float, complex). """ if isinstance(arg, int): return arg else: arg = float_coerce(arg) if t(arg): res = int(arg) return res if arg - res == 0 else nil else: return nil
[docs]@coercer def number_coerce(arg): """Check if `arg` is a valid number (integer or float). Types that are checked (string, int, float, complex). """ if isinstance(arg, int): return arg else: f = float_coerce(arg) if t(f): i = int(f) return i if f - i == 0 else f else: return nil
[docs]@coercer def positive_int_coerce(arg): """Check if `arg` is a valid positive integer.""" res = int_coerce(arg) return res if res is nil or res >= 0 else nil
[docs]def create_int_range_coerce(min, max): """Create a coercer to check integers between a range.""" min, max = vouch(int_coerce, min), vouch(int_coerce, max) if min < max: @coercer def inner(arg): 'Check if `arg` is a valid integer between "{}" and "{}".' arg = int_coerce(arg) if t(arg) and min <= arg <= max: return arg else: return nil inner.__name__ = str("int_between_{}_and_{}_coerce".format(min, max)) inner.__doc__ = inner.__doc__.format(min, max) return inner else: msg = '"{}" must be less than or equal "{}"' raise ValueError(msg.format(min, max))
# Identifiers and strings # TODO: In Py3k "ña" is a valid identifier and this regex won't allow it _IDENTIFIER_REGEX = re.compile(r"(?i)^[_a-z][\w]*$") # XXX: 'eight' pending.
[docs]@coercer def identifier_coerce(arg): """Check if `arg` is a valid Python identifier. .. note:: Only Python 2's version of valid identifier. This means that some Python 3 valid identifiers are not considered valid. This helps to keep things working the same in Python 2 and 3. """ ok = isinstance(arg, str) and _IDENTIFIER_REGEX.match(arg) return str(arg) if ok else nil
_FULL_IDENTIFIER_REGEX = re.compile(r"(?i)^[_a-z][\w]*([.][_a-z][\w]*)*$")
[docs]@coercer def full_identifier_coerce(arg): """Check if `arg` is a valid dotted Python identifier. See `identifier_coerce`:func: for what "validity" means. """ ok = isinstance(arg, str) and _FULL_IDENTIFIER_REGEX.match(arg) return str(arg) if ok else nil
[docs]@coercer def names_coerce(arg): """Check `arg` as a tuple of valid object names (identifiers). If only one string is given, is returned as the only member of the resulting tuple. """ arg = (arg,) if isinstance(arg, str) else tuple(arg) return iterable(identifier_coerce)(arg)
# == Iterators ==
[docs]def create_unique_member_coerce(coerce, container): """Useful to wrap member coercers when coercing containers. See `iterable`:class: and `mapping`:class:. Resulting coercer check that a member must be unique (not repeated) after it's coerced. For example:: >>> from xotl.tools.values import (mapping, create_unique_member_coerce, ... int_coerce, float_coerce) >>> sample = {'1': 1, 2.0: '3', 1.0 + 0j: '4.1'} >>> dc = mapping(int_coerce, float_coerce) >>> dc(dict(sample)) {1: 1.0, 2: 3.0} >>> dc = mapping(create_unique_member_coerce(int_coerce), float_coerce) >>> dc(dict(sample)) nil """ coerce = vouch(coercer, coerce) @coercer def inner(arg): """Check a member with "{}" coercer and warrant that is unique.""" # assert arg in container res = coerce(arg) if t(res) and hash(res) != hash(arg) and res in container: res = nil return res cname = coercer_name(coerce) inner.__name__ = str("unique_member_{}_coerce".format(cname)) inner.__doc__ = inner.__doc__.format(cname) return inner
[docs]@coercer def sized_coerce(arg): """Return a valid sized iterable from `arg`. If `arg` is iterable but not sized, is converted to a list. For example:: >>> sized_coerce(i for i in range(1, 10, 2)) [1, 3, 5, 7, 9] >>> s = {1, 2, 3} >>> sized_coerce(s) is s True """ from collections.abc import Iterable, Sized if isinstance(arg, Iterable): return arg if isinstance(arg, Sized) else list(arg) else: return nil
[docs]@coercer.register class custom: """Base class for any custom coercer. The field `inner` stores an internal data used for the custom coercer; could be a callable, an inner coercer, or a tuple of inner checkers if more than one is needed, ... The field `scope` stores the exit (not regular) condition: the value that fails or -if needed- a tuple with (exit-value, exit-coercer) or (error-value, error). The exit condition is not always a failure, for example in `some`:class: it is the one that is valid among other inner coercers. To understand better this think on (AND, OR) operators a chain of ANDs exits with the first failure and a chains of ORs exits with the first success. All custom coercers are callable (must redefine `__call__`:meth:) receiving one argument that must be coerced. For example:: >>> def foobar(*args): ... coerce = pargs(int_coerce) ... return coerce(args) This class has two protected fields (`_str_join` and `_repr_join`) that are used to call `coercer_name`:func: in `__str__`:meth: and `__repr__`:meth: special methods. """ __slots__ = ("inner", "scope") _str_join = "_" _repr_join = ", " def __init__(self, *args, **kwargs): # This constructor is a placeholder for those custom coercers that can # return an instance of a different type in the `__new__`:meth:. self.scope = Unset def __str__(self): name = coercer_name(self.inner, join=self._str_join) cls_name = type(self).__name__ return str("{}_{}_coerce".format(name, cls_name)) def __repr__(self): name = coercer_name(self.inner, join=self._repr_join) cls_name = type(self).__name__ return str("{}({})".format(cls_name, name)) def __call__(self, arg): return nil
[docs] @classmethod def flatten(cls, obj, avoid=Unset): """Flatten a coercer set. :param obj: Could be a coercer representing other inner coercers, or a tuple or list containing coercers. """ aux = obj.inner if isinstance(obj, cls) else obj if isinstance(aux, (tuple, list)): if not types_tuple_coerce(aux): res = (i for l in map(cls.flatten, aux) for i in l) else: res = (coercer(aux),) else: res = (aux,) if avoid is not Unset: res = (i for i in res if i is not avoid) return tuple(res)
[docs]class istype(custom): """Pure type-checker. It's constructed from an argument valid for `types_tuple_coerce`:func: coercer. For example:: >>> int_coerce = istype(int) >>> int_coerce(1) 1 >>> int_coerce('1') nil >>> number_coerce = istype((int, float, complex)) >>> number_coerce(1.25) 1.25 >>> number_coerce('1.25') nil """ __slots__ = () def __new__(cls, types): if types: self = super().__new__(cls) self.inner = vouch(types_tuple_coerce, types) return self else: return void_coerce def __call__(self, arg): return arg if isinstance(arg, self.inner) else nil
[docs]class typecast(istype): """A type-caster. It's constructed from an argument valid for `types_tuple_coerce`:func: coercer. Similar to `istype`:class: but try to convert the value if needed. For example:: >>> int_cast = typecast(int) >>> int_cast('1') 1 >>> int_cast('1x') nil """ __slots__ = () def __call__(self, arg): res = super().__call__(arg) i = 0 while not t(res) and i < len(self.inner): try: tp = self.inner[i] res = tp(arg) self.scope = tp except Exception: i += 1 return res
[docs]class safe(custom): """Uses a function (or callable) in a safe way. Receives a coercer that expects only one argument and returns another value. If the returned value is a ``boolean`` (maybe the coercer is a predicate), it's converted to a ``logical`` instance. The wrapped coercer is called in a safe way (inside try/except); if an exception is raised the coercer returns ``nil`` and the error is saved in the instance attribute ``scope``. """ __slots__ = () def __init__(self, func): super().__init__() self.inner = vouch(coercer, func) def __call__(self, arg): try: from xotl.tools.symbol import boolean res = self.inner(arg) return logical(res) if isinstance(res, boolean) else res except Exception as error: self.scope = (arg, error) return nil
[docs]class compose(custom): """Returns the composition of several inner `coercers`. ``compose(f1, ... fn)`` is equivalent to f1(...(fn(arg))...)``. If no coercer is given return `identity_coerce`:func:. Could be considered an "AND" operator with some light differences because the nature of coercers: ordering the coercers is important when some can modify (adapt) original values. If no value results in `coercers`, a default coercer could be given as a keyword argument; `identity_coerce` is assumed if missing. """ __slots__ = () def __new__(cls, *coercers, **kwds): inner = cls.flatten(coercers, avoid=identity_coerce) count = len(inner) if count > 1: self = super().__new__(cls) self.inner = inner return self elif count == 1: return inner[0] else: res = kwds.pop("default", identity_coerce) if not kwds: return res else: msg = '`compose` got unexpected keyword argument(s) "{}"' raise TypeError(msg.format(set(kwds))) def __call__(self, arg): coercers = self.inner i = 0 res = arg ok = True while ok and i < len(coercers): coerce = coercers[i] aux = coerce(res) if t(aux): i += 1 else: ok = False self.scope = (res, coerce) res = aux return res
[docs]class some(custom): """Represent OR composition of several inner `coercers`. ``compose(f1, ... fn)`` is equivalent to f1(arg) or f2(arg) ... fn(arg)`` in the sense "the first not `nil`". If no coercer is given return `void_coerce`:func:. """ __slots__ = () def __new__(cls, *coercers): inner = cls.flatten(coercers, avoid=void_coerce) if len(inner) > 1: self = super().__new__(cls) self.inner = inner return self elif len(inner) == 1: return inner[0] else: return void_coerce def __call__(self, arg): coercers = self.inner i = 0 res = nil while res is nil and i < len(coercers): coercer = coercers[i] value = coercer(arg) if t(value): res = value self.scope = coercer else: i += 1 return res
[docs]class combo(custom): """Represent a zip composition of several inner `coercers`. An instance of this class is constructed from a sequence of coercers and the its purpose is coerce a sequence of values. Return a sequence\ [#type]_ where each item contains the i-th element from applying the i-th coercer to the i-th value from argument sequence:: coercers -> (coercer-1, coercer-2, ... ) values -> (value-1, value-2, ... ) combo(coercers)(values) -> (coercer-1(value-1), coercer-2(value-2), ...) If any value is coerced invalid, the function returns `nil` and the combo's instance variable `scope` receives the duple ``(failed-value, failed-coercer)``. The returned sequence is truncated in length to the length of the shortest sequence (coercers or arguments). If no coercer is given, all sequences are coerced as empty. .. [#type] The returned sequence is of the same type as the argument sequence if possible. """ __slots__ = () def __init__(self, *coercers): super().__init__() coercers = pargs(coercer)(coercers) self.inner = tuple(vouch(coercer, c) for c in coercers) def __call__(self, arg): from collections.abc import Iterable if isinstance(arg, Iterable): coercers = self.inner items = iter(arg) i = 0 res = [] ok = True while t(res) and ok and i < len(coercers): item = next(items, Unset) if item is not Unset: coerce = coercers[i] value = coerce(item) if t(value): res.append(value) i += 1 else: res = nil self.scope = (item, coerce) else: ok = False if t(res): try: res = type(arg)(res) except Exception: pass else: res = nil return res
[docs]class pargs(custom): r"""Create a inner coercer that check variable argument passing. Created coercer closure must always receives an argument that is an valid iterable with all members coerced properly with the argument of this outer creator function. If the inner closure argument has only a member and this one is not properly coerced but it's an iterabled with all members that coerced well, this member will be the assumed iterable instead the original argument. In the following example:: >>> from xotl.tools.values import (iterable, int_coerce) >>> def foobar(*args): ... coerce = iterable(int_coerce) ... return coerce(args) >>> args = (1, 2.0, '3.0') >>> foobar(*args) (1, 2, 3) >>> foobar(args) nil An example using `pargs`:class:\ :: >>> from xotl.tools.values import (pargs, int_coerce) >>> def foobar(*args): ... # Below, "coercer" receives the returned "inner" ... coerce = pargs(int_coerce) ... return coerce(args) >>> args = (1, 2.0, '3.0') >>> foobar(*args) (1, 2, 3) >>> foobar(args) (1, 2, 3) The second form is an example of the real utility of this coercer closure: if by error a sequence is passed as it to a function that expect a variable number of argument, this coercer fixes it. Instance variable `scope` stores the last processed invalid argument. When executed, usually `arg` is a tuple received by a function as ``*args`` form. When executed, returns a tuple, or the same type of source iterable argument if possible. See `xotl.tools.params`:mod: for a more specialized and full function arguments conformer. See `combo`:class: for a combined coercer that validate each member with a separate member coercer. """ __slots__ = () def __init__(self, arg_coerce): super().__init__() self.inner = vouch(coercer, arg_coerce) def __call__(self, arg): from collections.abc import Iterable coerce = self.inner if isinstance(arg, Iterable): arg = tuple(arg) if len(arg) == 1: item = arg[0] aux = coerce(item) if t(aux): res = (aux,) elif isinstance(item, Iterable): res = Unset arg = tuple(item) else: self.scope = item res = nil else: res = Unset if res is Unset: res = arg i = 0 while t(res) and i < len(res): item = res[i] new = coerce(item) if t(new): if new is not item: if isinstance(res, tuple): res = list(res) res[i] = new i += 1 else: self.scope = item res = nil if t(res): res = tuple(res) else: res = nil return res
[docs]class iterable(custom): """Create a inner coercer that coerces an `iterable` member a member. See constructor for more information. Return a list, or the same type of source iterable argument if possible. For example:: >>> from xotl.tools.values import (iterable, int_coerce, ... create_unique_member_coerce) >>> sample = {'1', 1, '1.0'} >>> sc = iterable(int_coerce) >>> sc(set(sample)) == {1} True See `mapping`:class: for more details of this problem. The equivalent safe example is:: >>> member_coerce = create_unique_member_coerce(int_coerce, sample) >>> sc = iterable(member_coerce) >>> sc(set(sample)) nil when executed coerces `arg` (an iterable) member a member using `member_coercer`. If any member coercion fails, the full execution also fails. There are three types of results when an instance is executed: (1) iterables that are coerced without modifications, (2) the modified ones but conserving its type, and (3) those that are returned in a list. """ __slots__ = () def __init__(self, member_coerce, outer_coerce=True): """Constructor for `iterable`:class: coercers. :param member_coerce: A coerce to check each iterable member. :param outer_coerce: A coerce to check the type of the entire iterable. Normally a type or tuple of types subclases of ``collections.Iterable``. """ super().__init__() member_coerce = vouch(coercer, member_coerce) outer_coerce = compose(coercer(outer_coerce), sized_coerce) self.inner = (member_coerce, outer_coerce) def __call__(self, arg): from collections.abc import MutableSequence, Sequence, Set member_coerce, outer_coerce = self.inner modified = False aux = outer_coerce(arg) if t(aux): arg = aux if isinstance(arg, Sequence): res = arg retyped = False mutable = isinstance(arg, MutableSequence) else: res = list(arg) retyped = mutable = True i = 0 while t(res) and i < len(res): item = res[i] new = member_coerce(item) if t(new): if new is not item: if not mutable: res = list(res) retyped = mutable = True res[i] = new modified = True i += 1 else: self.scope = item res = nil if t(res): if isinstance(arg, Set) and not modified: res = arg elif retyped: try: res = type(arg)(res) except Exception: pass else: self.scope = arg res = nil return res
[docs]class mapping(custom): """Create a coercer to check dictionaries. Receives two coercers, one for keys and one for values. For example:: >>> from xotl.tools.values import (mapping, int_coerce, float_coerce, ... create_unique_member_coerce) >>> sample = {'1': 1, 2.0: '3', 1.0 + 0j: '4.1'} >>> dc = mapping(int_coerce, float_coerce) >>> dc(dict(sample)) == {1: 1.0, 2: 3.0} True When coercing containers it's probable that members become repeated after coercing them. This could be not desirable (mainly in sets and dictionaries). In those cases use `create_unique_member_coerce`:func: to wrap member coercer. For example:: >>> key_coerce = create_unique_member_coerce(int_coerce, sample) >>> dc = mapping(key_coerce, float_coerce) >>> dc(dict(sample)) nil Above problem is because it's the same integer (same hash) coerced versions of ``'1'`` and ``1.0+0j``. This problem of objects of different types that have the same hash is a problem to use a example as below:: >>> {1: int, 1.0: float, 1+0j: complex} == {1: complex} True """ __slots__ = () _str_join = _repr_join = ":" def __new__(cls, key_coercer=Unset, value_coercer=Unset): """Constructor for `mapping`:class: coercers. :param key_coercer: A coerce to check each one of the mapping keys. :param value_coercer: A coerce to check each one of corresponding mapping values. """ from collections.abc import Mapping if key_coercer is value_coercer is Unset: return coercer(Mapping) else: self = super().__new__(cls) key_coercer = vouch(coercer, key_coercer or True) value_coercer = vouch(coercer, value_coercer or True) self.inner = (key_coercer, value_coercer) return self def __call__(self, arg): from collections.abc import Mapping, MutableMapping if isinstance(arg, Mapping): key_coercer, value_coercer = self.inner res = arg retyped = False mutable = isinstance(arg, MutableMapping) keys = list(res) i = 0 while t(res) and i < len(keys): key = keys[i] value = res[key] new_key = key_coercer(key) if t(new_key): new_value = value_coercer(value) if t(new_value): if new_key is not key or new_value is not value: if not mutable: res = dict(res) retyped = mutable = True if key is not new_key: del res[key] res[new_key] = new_value i += 1 else: self.scope = ({key: value}, value_coercer) res = nil else: self.scope = ({key: value}, key_coercer) res = nil if t(res) and retyped: try: res = type(arg)(res) except Exception: pass else: self.scope = () res = nil return res
del re, ABCMeta, lwraps