Source code for calcpy._op

"""Extension of Python operators and functions."""

import functools
import operator

from extepy import constantcreator as _constantcreator

from .matcher import _get_matcher
from ._arg import MISSING
from ._it import pairwise
from ._seq import count_unique


[docs] def arggetter(*keys, default=MISSING): """Return a callable object that fetches the argument(s) from its operand. Parameters: *keys (int | str): If it is an ``int`` (can be negative), it is the index of the positional argument to be fetched. If it is a ``str``, it is the name of keyword argument to be fetched. default (optional): Indicate how to return when an argument is not found. If not set, will raise an error when an argument is not found. Returns: callable: Raises: IndexError: Raises when the argument is not found and the default is not set. Examples: Get the first positional argument. >>> getter = arggetter(0) >>> getter("a", "b", "c", key="value") 'a' Get the positional arguments and keyword arguments. >>> getter = arggetter(-1, 0, 1, "key", default="default") >>> getter("a", "b", "c", key="value") ('c', 'a', 'b', 'value') Get the positional arguments and keyword arguments with default values. >>> getter = arggetter(-1, 0, 1, 5, "key", "other", default="default") >>> getter("a", "b", "c", key="value") ('c', 'a', 'b', 'default', 'value', 'default') It will return the key if the key is not found. >>> from calcpy.fun import skewer >>> lookup = {"a": 1, "b": 2} >>> g = skewer(arggetter(0, 0), lookup.get) # equivalent to lookup.get(x, x) >>> g("a") 1 >>> g("c") 'c' Raise an error if the index is out of range and the default is not set. >>> getter = arggetter(0, 1, 2) >>> getter("A", key="value") Traceback (most recent call last): ... IndexError: positional argument index is out of range """ def getter(*args, **kwargs): results = [] for key in keys: if isinstance(key, str): result = kwargs.get(key, default) else: count = len(args) if -count <= key < count: result = args[key] else: if default is MISSING: raise IndexError("positional argument index is out of range") else: result = default results.append(result) if len(results) == 1: # If only one result, return it directly return results[0] return tuple(results) return getter
def _get_attr(obj, attr, default): """Auxiliary function for attrgetter.""" for name in attr.split("."): if hasattr(obj, name): obj = getattr(obj, name) else: if default is MISSING: raise AttributeError(f"type object '{obj.__class__.__name__}' has no attribute '{attr}'") else: return default return obj
[docs] def attrgetter(*attrs, default=MISSING): """Return a callable object that fetches the given attribute(s) from its operand. Fully compatible with Python's built-in ``operator.attrgetter``, and furthermore support an optional default value when a named attr is not found. Parameters: *attrs (str): Attribute names. default (optional): Indicate how to return when a named attr is not found. If not set, will raise an error when an attribute is not found. Examples: Get attributes with default values. >>> from collections import namedtuple >>> name = namedtuple('Name', ['first', 'last']) >>> name.first = 'Zhiqing' >>> name.last = 'Xiao' >>> person = namedtuple('Person', ['name', 'city']) >>> person.name = name >>> person.city = 'Beijing' >>> g = attrgetter('name.first', 'name.middle', 'name.last', default='') >>> g(person) ('Zhiqing', '', 'Xiao') >>> g = attrgetter('name.first', 'city') >>> g(person) ('Zhiqing', 'Beijing') It will raise errors when the default is not set. >>> g = attrgetter('hello') >>> g(sum) Traceback (most recent call last): ... AttributeError: type object 'builtin_function_or_method' has no attribute 'hello' See also: https://docs.python.org/3/library/operator.html#operator.attrgetter """ if len(attrs) == 1: attr = attrs[0] def g(obj): return _get_attr(obj, attr, default) else: def g(obj): return tuple(_get_attr(obj, attr, default) for attr in attrs) return g
def _get_item(obj, item, default): """Auxiliary function for itemgetter.""" if isinstance(item, list): items = item else: items = [item] for i in items: try: obj = obj[i] except (IndexError, KeyError) as err: if default is MISSING: raise err else: return default return obj
[docs] def itemgetter(*items, default=MISSING): """Return a callable object that fetches the given item(s) from its operand. Fully compatible with Python's builtin ``operator.itemgetter``, and furthermore support multi-level item and an optional default value when a item is not found. Parameters: *items (str): Keys of items. default (optional): Indicate how to return when a named attr is not found. If not set, will raise an error when an item is not found. Examples: Get multiple items with default values. >>> person = {'name': {'first': 'Zhiqing', 'last': 'Xiao'}, 'city': 'Beijing'} >>> g = itemgetter(['name', 'first'], ['name', 'middle'], ['name', 'last'], 'city', default='') >>> g(person) ('Zhiqing', '', 'Xiao', 'Beijing') >>> itemgetter(4, default=0)([1, 2, 3]) 0 It will raise errors when the default is not set. >>> itemgetter(4)([1, 2, 3]) Traceback (most recent call last): ... IndexError: list index out of range See also: https://docs.python.org/3/library/operator.html#operator.itemgetter """ if len(items) == 1: item = items[0] def g(obj): return _get_item(obj, item, default) else: def g(obj): return tuple(_get_item(obj, item, default) for item in items) return g
[docs] def methodcaller(name, *args, **kwargs): """methodcaller Fully compatible with Python's built-in ``operator.methodcaller``. Support ``pandas`` accessor. Can be decorated by ``calcpy.fillerr`` when needed to resolve the case where method name is not found. Examples: >>> import pandas as pd >>> s = pd.Series(["a", "b"]) >>> methodcaller("str.upper")(s) 0 A 1 B dtype: object See also: https://docs.python.org/3/library/operator.html#operator.methodcaller """ def caller(obj, *args_, **kwargs_): return attrgetter(name)(obj)(*args, *args_, **kwargs, **kwargs_) return caller
[docs] class constantcreator(_constantcreator): """Callable that returns the same constant when it is called. Parameters: value : Constant value to be returned. copy : If ``True``, return a new copy of the constant value. Returns: callable: Callable object that returns ``value``, ignoring its parameters. Examples: Always return the string ``"value"``. >>> creator = constantcreator("value") >>> creator() 'value' Create a pd.DataFrame whose elements are all empty lists. >>> import pandas as pd >>> df = pd.DataFrame(index=range(3), columns=["A"]) >>> (df.map if hasattr(df, "map") else df.applymap)(constantcreator([])) A 0 [] 1 [] 2 [] Return a new copy when ``copy=True``. >>> import pandas as pd >>> df = pd.DataFrame(index=range(3), columns=["A"]) >>> constantcreator(df, copy=True)() is df False See also: https://extepy.github.io/builtin.html#extepy.constantcreator """ def __init__(self, value, /, copy=False): super().__init__(value, copy)
[docs] def all_(iterable, empty=True): """Return ``True`` of ``bool(x)`` is ``True`` for all ``x`` in the iterable. If the iterable is empty, return what ``empty`` specifies. Fully compatible with Python's built-in ``all()``. Parameters: iterable (iterable): empty : Value if ``iterable`` is empty. Returns: bool Examples: >>> all_([]) True >>> all_([False]) False >>> all_([True]) True >>> all_([True, False]) False >>> all_([True, True]) True See also: https://docs.python.org/3/library/functions.html#all """ if not iterable: return empty return all(iterable)
[docs] def any_(iterable, *, empty=False): """Return ``True`` if ``bool(x)`` is ``True`` for any ``x`` in the iterable. If the iterable is empty, return what ``empty`` specifies. Fully compatible with Python's built-in ``any()``. Parameters: iterable (iterable): empty : Value if ``iterable`` is empty. Returns: bool Examples: >>> any_([]) False >>> any_([False]) False >>> any_([True]) True >>> any_([True, False]) True >>> any_([True, True]) True See also: https://docs.python.org/3/library/functions.html#any """ if not iterable: return empty return any(iterable)
[docs] def never(iterable, *, empty=True): """Return ``True`` if ``bool(x)`` is ``False`` for all ``x`` in the iterable. If the iterable is empty, return what ``empty`` specifies. Parameters: iterable (iterable): empty : Value if ``iterable`` is empty. Returns: bool Examples: >>> never([]) True >>> never([False]) True >>> never([True]) False >>> never([True, False]) False >>> never([True, True]) False """ if not iterable: return True return not any(iterable)
[docs] def odd(iterable, *, empty=False): """Return ``True`` if an odd number of items in the iterable are ``True``. If the iterable is empty, return what ``empty`` specifies. Parameters: iterable (iterable): empty : Value if ``iterable`` is empty. Returns: bool Examples: >>> odd([]) False >>> odd([False]) False >>> odd([True]) True >>> odd([True, False]) True >>> odd([True, True]) False """ if not iterable: return empty return functools.reduce(operator.xor, iterable)
[docs] def and_(*args, empty=True): """Return ``True`` if all values are ``True``. Fully compatible with Python's built-in ``operator.and_``. Parameters: *args empty : Value if ``args`` have no values. Returns: bool: Examples: >>> and_() True >>> and_(True, True) True >>> and_(True, True, False) False See also: https://docs.python.org/3/library/operator.html#operator.and_ """ return all_(args, empty=empty)
[docs] def or_(*args, empty=False): """Return ``True`` if any values are ``True``. Fully compatible with Python's built-in ``operator.or_``. Parameters: *args empty : Value if ``args`` have no values. Returns: bool: Examples: >>> or_() False >>> or_(True, True) True >>> or_(True, True, False) True See also: https://docs.python.org/3/library/operator.html#operator.or_ """ return any_(args, empty=empty)
[docs] def xor(*args, empty=False): """Return ``True`` if any values are ``True``. Fully compatible with Python's built-in ``operator.xor``. Parameters: *args empty : Value if ``args`` have no values. Returns: bool: Examples: >>> xor() False >>> xor(True) True >>> xor(True, True, False) False See also: https://docs.python.org/3/library/operator.html#operator.xor """ return odd(args, empty=empty)
def _allpairwise_glet(glet_name): # glet is short for the collection of ["lt", "le", "gt", "ge"]. glet = getattr(operator, glet_name) def f(*args, key=None): if key is not None: args = (key(arg) for arg in args) iterable = (glet(*p) for p in pairwise(args)) return all(iterable) f.__name__ = glet_name f.__doc__ = f""" Return ``True`` when all arguments are {glet_name} the next argument. Fully compatible with Python's built-in ``operator.{glet_name}``. Parameters: *args key (callable): Returns: bool: Examples: >>> {glet_name}() True >>> {glet_name}(1) True >>> {glet_name}(1, 1) {glet(1, 1)} >>> {glet_name}(1, 2) {glet(1, 2)} >>> {glet_name}(2, 1) {glet(2, 1)} >>> {glet_name}(1, 1, 2) {glet(1, 1) and glet(1, 2)} >>> {glet_name}(1, 2, 3) {glet(1, 2) and glet(2, 3)} >>> {glet_name}(3, 2, 1) {glet(3, 2) and glet(2, 1)} See also: https://docs.python.org/3/library/operator.html#operator.{glet_name} """ return f lt = _allpairwise_glet("lt") le = _allpairwise_glet("le") gt = _allpairwise_glet("gt") ge = _allpairwise_glet("ge")
[docs] def eq(*args, key=None): """Check whether all parameters are the same. Fully compatible with Python's built-in ``operator.eq``. Parameters: *args key (callable) Returns: bool: Examples: >>> eq({"a": 1}, {"a": 1}, {"a": 1}) True >>> eq(1, 1, 2, 2) False See also: https://docs.python.org/3/library/operator.html#operator.eq """ distinct_count = count_unique(args, key=key) return distinct_count <= 1
[docs] def ne(*args, key=None): """Check whether all parameters are distinct. Fully compatible with Python's built-in ``operator.ne``. Parameters: *args key (callable) Returns: bool: Examples: >>> ne([1, 2], [1, 3], [2, 3]) True >>> ne([1, 2], [1, 3], [1, 3]) False See also: https://docs.python.org/3/library/operator.html#operator.ne """ original_count = len(args) distinct_count = count_unique(args, key=key) return original_count == distinct_count
[docs] def same(values, key=None): """Check whether all elements are the same. Parameters: values (iterable) key (callable) Returns: bool: Examples: >>> same([{"a": 1}, {"a": 1}, {"a": 1}]) True >>> same([1, 1, 2, 2]) False """ return eq(*values, key=key)
[docs] def distinct(values, *, key=None): """Check whether all elements are distinct. Parameters: values (iterable) key (callable) Returns: bool: Examples: >>> distinct([[1, 2], [1, 3], [2, 3]]) True >>> distinct([[1, 2], [1, 3], [1, 3]]) False """ return ne(*values, key=key)
def _concat(*args, matcher, assemble=True): results = [] for arg in args: results += matcher.disassemble(arg) if assemble: return matcher.assemble(results) return results
[docs] def concat(*args, key=None): """Concat multiple parameters. Parameters: *args key (callable) Examples: >>> concat([1, 2, 3], [], [4, 5], [5]) [1, 2, 3, 4, 5, 5] >>> concat((1, 2, 3), (), (4, 5), (5,)) (1, 2, 3, 4, 5, 5) >>> concat({1, 2, 3}, set(), {4, 5}, {5}) {1, 2, 3, 4, 5} >>> concat({1: 'a', 2: 'b'}, {}, {3: 'c', 4: 'd'}, {5: 'e'}) {1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e'} >>> import pandas as pd >>> s = pd.Series([0, 1]) >>> concat(s, s) 0 0 1 1 0 0 1 1 dtype: int64 """ if len(args) == 0: raise ValueError() matcher = _get_matcher(args[0], key=key) return _concat(*args, matcher=matcher)