Source code for pockets.collections

# -*- coding: utf-8 -*-
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.

"""A pocket full of useful collection tools!"""

from __future__ import absolute_import, print_function
from collections import defaultdict
try:
    from collections.abc import Iterable, Mapping, Sized
except ImportError:
    from collections import Iterable, Mapping, Sized

from inspect import isclass

import six


try:
    from collections import OrderedDict
except ImportError:
    OrderedDict = dict


__all__ = [
    'groupify', 'keydefaultdict', 'is_listy', 'listify', 'mappify',
    'nesteddefaultdict', 'readable_join', 'uniquify']


[docs]def groupify(items, keys, val_key=None): """ Groups a list of items into nested OrderedDicts based on the given keys. Note: On Python 2.6 the return value will use regular dicts instead of OrderedDicts. >>> from __future__ import print_function >>> from json import dumps >>> >>> ex = lambda x: print(dumps(x, indent=2, sort_keys=True, default=repr)) >>> >>> class Reminder: ... def __init__(self, when, where, what): ... self.when = when ... self.where = where ... self.what = what ... def __repr__(self): ... return 'Reminder({0.when}, {0.where}, {0.what})'.format(self) ... >>> reminders = [ ... Reminder('Fri', 'Home', 'Eat cereal'), ... Reminder('Fri', 'Work', 'Feed Ivan'), ... Reminder('Sat', 'Home', 'Sleep in'), ... Reminder('Sat', 'Home', 'Play Zelda'), ... Reminder('Sun', 'Home', 'Sleep in'), ... Reminder('Sun', 'Work', 'Reset database')] >>> >>> ex(groupify(reminders, 'when')) { "Fri": [ "Reminder(Fri, Home, Eat cereal)", "Reminder(Fri, Work, Feed Ivan)" ], "Sat": [ "Reminder(Sat, Home, Sleep in)", "Reminder(Sat, Home, Play Zelda)" ], "Sun": [ "Reminder(Sun, Home, Sleep in)", "Reminder(Sun, Work, Reset database)" ] } >>> >>> ex(groupify(reminders, ['when', 'where'])) { "Fri": { "Home": [ "Reminder(Fri, Home, Eat cereal)" ], "Work": [ "Reminder(Fri, Work, Feed Ivan)" ] }, "Sat": { "Home": [ "Reminder(Sat, Home, Sleep in)", "Reminder(Sat, Home, Play Zelda)" ] }, "Sun": { "Home": [ "Reminder(Sun, Home, Sleep in)" ], "Work": [ "Reminder(Sun, Work, Reset database)" ] } } >>> >>> ex(groupify(reminders, ['when', 'where'], 'what')) { "Fri": { "Home": [ "Eat cereal" ], "Work": [ "Feed Ivan" ] }, "Sat": { "Home": [ "Sleep in", "Play Zelda" ] }, "Sun": { "Home": [ "Sleep in" ], "Work": [ "Reset database" ] } } >>> >>> ex(groupify(reminders, lambda r: '{0.when} - {0.where}'.format(r), 'what')) { "Fri - Home": [ "Eat cereal" ], "Fri - Work": [ "Feed Ivan" ], "Sat - Home": [ "Sleep in", "Play Zelda" ], "Sun - Home": [ "Sleep in" ], "Sun - Work": [ "Reset database" ] } Args: items (list): The list of items to arrange in groups. keys (str|callable|list): The key or keys that should be used to group `items`. If multiple keys are given, then each will correspond to an additional level of nesting in the order they are given. val_key (str|callable): A key or callable used to generate the leaf values in the nested OrderedDicts. If `val_key` is `None`, then the item itself is used. Defaults to `None`. Returns: OrderedDict: Nested OrderedDicts with `items` grouped by `keys`. """ # noqa: E501 if not keys: return items keys = listify(keys) last_key = keys[-1] is_callable = callable(val_key) groupified = OrderedDict() for item in items: current = groupified for key in keys: attr = key(item) if callable(key) else getattr(item, key) if attr not in current: current[attr] = [] if key is last_key else OrderedDict() current = current[attr] if val_key: value = val_key(item) if is_callable else getattr(item, val_key) else: value = item current.append(value) return groupified
[docs]class keydefaultdict(defaultdict): """ A defaultdict that passes the missed key to the factory function. >>> def echo_factory(missing_key): ... return missing_key ... >>> d = keydefaultdict(echo_factory) >>> d['Hello World'] 'Hello World' >>> d['Hello World'] = 'Goodbye' >>> d['Hello World'] 'Goodbye' """ def __missing__(self, key): if self.default_factory is None: raise KeyError(key) else: ret = self[key] = self.default_factory(key) return ret
[docs]def is_listy(x): """ Return True if `x` is "listy", i.e. a list-like object. "Listy" is defined as a sized iterable which is neither a map nor a string: >>> is_listy(['a', 'b']) True >>> is_listy(set()) True >>> is_listy(iter(['a', 'b'])) False >>> is_listy({'a': 'b'}) False >>> is_listy('a regular string') False Note: Iterables and generators fail the "listy" test because they are not sized. Args: x (any value): The object to test. Returns: bool: True if `x` is "listy", False otherwise. """ return isinstance(x, Sized) and \ isinstance(x, Iterable) and \ not isinstance(x, (Mapping, type(b''))) and \ not isinstance(x, six.string_types)
[docs]def listify(x, minlen=0, default=None, cls=None): """ Return a listified version of `x`. If `x` is a non-string iterable, it is wrapped in a list; otherwise a list is returned with `x` as its only element. >>> listify('a regular string') ['a regular string'] >>> listify(tuple(['a', 'b', 'c'])) ['a', 'b', 'c'] >>> listify({'a': 'A'}) [{'a': 'A'}] Note: Not guaranteed to return a copy of `x`. If `x` is already a list and `cls` is not specified, then `x` itself is returned. Args: x (any value): Value to listify. minlen (int): Minimum length of the returned list. If the returned list would be shorter than `minlen` it is padded with values from `default`. Defaults to 0. >>> listify([], minlen=0) [] >>> listify([], minlen=1) [None] >>> listify('item', minlen=3) ['item', None, None] default (any value): Value that should be used to pad the list if it would be shorter than `minlen`: >>> listify([], minlen=1, default='PADDING') ['PADDING'] >>> listify('item', minlen=3, default='PADDING') ['item', 'PADDING', 'PADDING'] cls (class or callable): Instead of wrapping `x` in a list, wrap it in an instance of `cls`. `cls` should accept an iterable object as its single parameter when called: >>> from collections import deque >>> listify(['a', 'b', 'c'], cls=deque) deque(['a', 'b', 'c']) Returns: list or `cls`: A listified version of `x`. """ if x is None: x = [] elif not isinstance(x, list): x = list(x) if is_listy(x) else [x] if minlen and len(x) < minlen: x.extend([default for i in range(minlen - len(x))]) if cls and not (isclass(cls) and issubclass(type(x), cls)): x = cls(x) return x
[docs]def mappify(x, default=True, cls=None): """ Return a mappified version of `x`. If `x` is a string, it becomes the only key of the returned dict. If `x` is a non-string iterable, the elements of `x` become keys in the returned dict. The values of the returned dict are set to `default`. If `x` is a map, it is returned directly. >>> mappify('a regular string') {'a regular string': True} >>> mappify(['a']) {'a': True} >>> mappify({'a': 'A'}) {'a': 'A'} Note: Not guaranteed to return a copy of `x`. If `x` is already a map and `cls` is not specified, then `x` itself is returned. Args: x (str, map, or iterable): Value to mappify. default (any value): Value used to fill out missing values of the returned dict. cls (class or callable): Instead of wrapping `x` in a dict, wrap it in an instance of `cls`. `cls` should accept a map object as its single parameter when called: >>> from collections import defaultdict >>> mappify('a', cls=lambda x: defaultdict(None, x)) defaultdict(None, {'a': True}) Returns: dict or `cls`: A mappified version of `x`. Raises: TypeError: If `x` is not a map, iterable, or string. """ if not isinstance(x, Mapping): if isinstance(x, six.string_types): x = {x: default} elif isinstance(x, Iterable): x = dict([(v, default) for v in x]) else: raise TypeError( 'Unable to mappify non-mappy {0}'.format(type(x)), x) if cls and not (isclass(cls) and issubclass(type(x), cls)): x = cls(x) return x
[docs]def nesteddefaultdict(): """ A defaultdict that returns nested defaultdicts as the default value. Each defaultdict returned as the default value will also return nested defaultdicts, and so on. >>> nested = nesteddefaultdict() >>> nested_child = nested['New Key 1'] >>> nested_child defaultdict(...) >>> nested_grandchild = nested_child['New Key 2'] >>> nested_grandchild defaultdict(...) """ return defaultdict(nesteddefaultdict)
[docs]def readable_join(xs, conjunction='and', sep=','): """ Accepts a list of strings and separates them with commas as grammatically appropriate with a conjunction before the final entry. Any input strings containing only whitespace will not be included in the result. >>> readable_join(['foo']) 'foo' >>> readable_join(['foo', 'bar']) 'foo and bar' >>> readable_join(['foo', 'bar', 'baz']) 'foo, bar, and baz' >>> readable_join(['foo', ' ', '', 'bar', '', ' ', 'baz']) 'foo, bar, and baz' >>> readable_join(['foo', 'bar', 'baz'], 'or') 'foo, bar, or baz' >>> readable_join(['foo', 'bar', 'baz'], 'but never') 'foo, bar, but never baz' """ xs = [s for s in map(lambda s: str(s).strip(), listify(xs)) if s] if len(xs) > 1: xs = list(xs) xs[-1] = conjunction + ' ' + xs[-1] return (sep + ' ' if len(xs) > 2 else ' ').join(xs)
[docs]def uniquify(x, key=lambda o: o, cls=None): """ Returns an order-preserved copy of `x` with duplicate items removed. >>> uniquify(['a', 'z', 'a', 'b', 'a', 'y', 'a', 'c', 'a', 'x']) ['a', 'z', 'b', 'y', 'c', 'x'] Args: x (Sequence): Sequence to uniquify. key (str or callable): Similar to `sorted`, specifies an attribute or function of one argument that is used to extract a comparison key from each list element: key=str.lower. By default, compares the elements directly. >>> strings = ['ASDF', 'asdf', 'ZXCV', 'zxcv'] >>> uniquify(strings, key=str.lower) ['ASDF', 'ZXCV'] cls (class or callable): Instead of wrapping `x` in a list, wrap it in an instance of `cls`. `cls` should accept an iterable object as its single parameter when called: >>> from collections import deque >>> listify(['a', 'b', 'c'], cls=deque) deque(['a', 'b', 'c']) Returns: list: An order-preserved copy of `x` with duplicate items removed. Raises: TypeError: If `x` is not "listy". """ if not is_listy(x): raise TypeError( 'Unable to uniquify non-listy {0}'.format(type(x)), x) seen = set() keys = [(key(o) if callable(key) else getattr(o, key), o) for o in x] x = [o for k, o in keys if k not in seen and not seen.add(k)] if cls and not (isclass(cls) and issubclass(type(x), cls)): x = cls(x) return x