kalefranz/auxlib

View on GitHub
auxlib/collection.py

Summary

Maintainability
B
4 hrs
Test Coverage
# -*- coding: utf-8 -*-
"""Common collection classes."""
from __future__ import print_function, division, absolute_import
from functools import reduce
from collections import Mapping, Set

from .compat import isiterable, iteritems, odict, text_type


def make_immutable(value):
    # this function is recursive, and if nested data structures fold back on themselves,
    #   there will likely be recursion errors
    if isinstance(value, Mapping):
        if isinstance(value, frozenodict):
            return value
        return frozenodict((k, make_immutable(v)) for k, v in iteritems(value))
    elif isinstance(value, Set):
        if isinstance(value, frozenset):
            return value
        return frozenset(make_immutable(v) for v in value)
    elif isiterable(value):
        if isinstance(value, tuple):
            return value
        return tuple(make_immutable(v) for v in value)
    else:
        return value


# http://stackoverflow.com/a/14620633/2127762
class AttrDict(dict):
    """Sub-classes dict, and further allows attribute-like access to dictionary items.

    Examples:
        >>> d = AttrDict({'a': 1})
        >>> d.a, d['a'], d.get('a')
        (1, 1, 1)
        >>> d.b = 2
        >>> d.b, d['b']
        (2, 2)
    """
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self


class frozendict(Mapping):
    dict_cls = dict

    def __init__(self, *args, **kwargs):
        self._dict = self.dict_cls(*args, **kwargs)
        self._hash = None

    def __getitem__(self, key):
        return self._dict[key]

    def __contains__(self, key):
        return key in self._dict

    def copy(self, **add_or_replace):
        return self.__class__(self, **add_or_replace)

    def __iter__(self):
        return iter(self._dict)

    def __len__(self):
        return len(self._dict)

    def __repr__(self):
        return '<%s %r>' % (self.__class__.__name__, self._dict)

    def __hash__(self):
        if self._hash is None:
            h = 0
            for key, value in iteritems(self._dict):
                h ^= hash((key, value))
            self._hash = h
        return self._hash

    def __json__(self):
        return self._dict


class frozenodict(frozendict):
    dict_cls = odict


def first(seq, key=lambda x: bool(x), default=None, apply=lambda x: x):
    """Give the first value that satisfies the key test.

    Args:
        seq (iterable):
        key (callable): test for each element of iterable
        default: returned when all elements fail test
        apply (callable): applied to element before return, but not to default value

    Returns: first element in seq that passes key, mutated with optional apply

    Examples:
        >>> first([0, False, None, [], (), 42])
        42
        >>> first([0, False, None, [], ()]) is None
        True
        >>> first([0, False, None, [], ()], default='ohai')
        'ohai'
        >>> import re
        >>> m = first(re.match(regex, 'abc') for regex in ['b.*', 'a(.*)'])
        >>> m.group(1)
        'bc'

        The optional `key` argument specifies a one-argument predicate function
        like that used for `filter()`.  The `key` argument, if supplied, must be
        in keyword form.  For example:
        >>> first([1, 1, 3, 4, 5], key=lambda x: x % 2 == 0)
        4

    """
    return next((apply(x) for x in seq if key(x)), default() if callable(default) else default)


def firstitem(map, key=lambda k, v: bool(k), default=None, apply=lambda k, v: (k, v)):
    return next((apply(k, v) for k, v in map if key(k, v)), default)


def last(seq, key=lambda x: bool(x), default=None, apply=lambda x: x):
    return next((apply(x) for x in reversed(seq) if key(x)), default)


def call_each(seq):
    """Calls each element of sequence to invoke the side effect.

    Args:
        seq:

    Returns: None

    """
    try:
        reduce(lambda _, y: y(), seq)
    except TypeError as e:
        if text_type(e) != "reduce() of empty sequence with no initial value":
            raise