Source code for fnc.mappings

"""
Functions that operate on mappings.

A mapping includes dictionaries, lists, strings, ``collections.abc.Mapping`` and
``collections.abc.Sequence`` subclasses, and other mapping-like objects that either have an
``items()`` method, have ``keys()`` and ``__getitem__`` methods, or have an ``__iter__()`` method.
For functions that use :func:`get`, non-mapping object values can be selected from class attributes.
"""

from collections.abc import Mapping, Sequence

import fnc

from .helpers import UNSET, Sentinel, iterate


[docs] def at(paths, obj): """ Creates a ``tuple`` of elements from `obj` at the given `paths`. Examples: >>> at(['a', 'c'], {'a': 1, 'b': 2, 'c': 3, 'd': 4}) (1, 3) >>> at(['a', ['c', 'd', 'e']], {'a': 1, 'b': 2, 'c': {'d': {'e': 3}}}) (1, 3) >>> at(['a', 'c.d.e[0]'], {'a': 1, 'b': 2, 'c': {'d': {'e': [3]}}}) (1, 3) >>> at([0, 2], [1, 2, 3, 4]) (1, 3) Args: paths (Iterable): The object paths to pick. obj (Iterable): Iterable to pick from. Returns: tuple """ return tuple(get(path, obj) for path in paths)
[docs] def defaults(*objs): """ Create a ``dict`` extended with the key-values from the provided dictionaries such that keys are set once and not overridden by subsequent dictionaries. Examples: >>> obj = defaults({'a': 1}, {'b': 2}, {'c': 3, 'b': 5}, {'a': 4, 'c': 2}) >>> obj == {'a': 1, 'b': 2, 'c': 3} True Args: *objs (dict): Dictionary sources. Returns: dict """ return merge(*reversed(objs))
[docs] def get(path, obj, *, default=None): """ Get the `path` value at any depth of an object. If path doesn't exist, `default` is returned. Examples: >>> get('a.b.c', {}) is None True >>> get('a.b.c[1]', {'a': {'b': {'c': [1, 2, 3, 4]}}}) 2 >>> get('a.b.c.1', {'a': {'b': {'c': [1, 2, 3, 4]}}}) 2 >>> get('a.b.1.c[1]', {'a': {'b': [0, {'c': [1, 2]}]}}) 2 >>> get(['a', 'b', 1, 'c', 1], {'a': {'b': [0, {'c': [1, 2]}]}}) 2 >>> get('a.b.1.c.2', {'a': {'b': [0, {'c': [1, 2]}]}}, default=False) False Args: path (object): Path to test for. Can be a key value, list of keys, or a ``.`` delimited path-string. obj (Mapping): Object to process. default (mixed): Default value to return if path doesn't exist. Defaults to ``None``. Returns: object: Value of `obj` at path. """ if default is UNSET: # When NotSet given for default, then this method will raise if path is # not present in obj. sentinel = UNSET else: # When a returnable default is given, use a sentinel value to detect # when _get() returns a default value for a missing path so we can exit # early from the loop and not mistakenly iterate over the default. sentinel = Sentinel result = obj for key in fnc.aspath(path): result = _get(key, result, default=sentinel) if result is sentinel: result = default break return result
def _get(key, obj, *, default=UNSET): if isinstance(obj, dict): value = _get_dict(key, obj, default=default) elif not isinstance(obj, (Mapping, Sequence)) or isinstance(obj, tuple): value = _get_obj(key, obj, default=default) else: value = _get_item(key, obj, default=default) if value is UNSET: raise KeyError(f"Key {key!r} not found in {obj!r}") return value def _get_dict(key, obj, *, default=UNSET): value = obj.get(key, UNSET) if value is UNSET: value = default if not isinstance(key, int): try: value = obj.get(int(key), default) except Exception: pass return value def _get_item(key, obj, *, default=UNSET): try: return obj[key] except (KeyError, TypeError, IndexError): pass if not isinstance(key, int): try: return obj[int(key)] except (KeyError, TypeError, IndexError, ValueError): pass return default def _get_obj(key, obj, *, default=UNSET): value = _get_item(key, obj, default=UNSET) if value is UNSET: value = default try: value = getattr(obj, key) except AttributeError: pass return value
[docs] def has(path, obj): """ Return whether `path` exists in `obj`. Examples: >>> has(1, [1, 2, 3]) True >>> has('b', {'a': 1, 'b': 2}) True >>> has('c', {'a': 1, 'b': 2}) False >>> has('a.b[1].c[1]', {'a': {'b': [0, {'c': [1, 2]}]}}) True >>> has('a.b.1.c.2', {'a': {'b': [0, {'c': [1, 2]}]}}) False Args: path (object): Path to test for. Can be a key value, list of keys, or a ``.`` delimited path-string. obj (Iterable): Object to test. Returns: bool: Whether `obj` has `path`. """ try: get(path, obj, default=UNSET) return True except KeyError: return False
[docs] def invert(obj): """ Return a ``dict`` composed of the inverted keys and values of the given dictionary. Note: It's assumed that `obj` values are hashable as ``dict`` keys. Examples: >>> result = invert({'a': 1, 'b': 2, 'c': 3}) >>> result == {1: 'a', 2: 'b', 3: 'c'} True Args: obj (Mapping): Mapping to invert. Returns: dict: Inverted dictionary. """ return {value: key for key, value in iterate(obj)}
[docs] def mapkeys(iteratee, obj): """ Return a ``dict`` with keys from `obj` mapped with `iteratee` while containing the same values. Examples: >>> result = mapkeys(lambda k: k * 2, {'a': 1, 'b': 2, 'c': 3}) >>> result == {'aa': 1, 'bb': 2, 'cc': 3} True Args: iteratee (object): Iteratee applied to each key. obj (Mapping): Mapping to map. Returns: dict: Dictionary with mapped keys. """ iteratee = fnc.iteratee(iteratee) return {iteratee(key): value for key, value in iterate(obj)}
[docs] def mapvalues(iteratee, obj): """ Return a ``dict`` with values from `obj` mapped with `iteratee` while containing the same keys. Examples: >>> result = mapvalues(lambda v: v * 2, {'a': 1, 'b': 2, 'c': 3}) >>> result == {'a': 2, 'b': 4, 'c': 6} True >>> result = mapvalues({'d': 4}, {'a': 1, 'b': {'d': 4}, 'c': 3}) >>> result == {'a': False, 'b': True, 'c': False} True Args: iteratee (object): Iteratee applied to each key. obj (Mapping): Mapping to map. Returns: dict: Dictionary with mapped values. """ iteratee = fnc.iteratee(iteratee) return {key: iteratee(value) for key, value in iterate(obj)}
[docs] def merge(*objs): """ Create a ``dict`` merged with the key-values from the provided dictionaries such that each next dictionary extends the previous results. Examples: >>> item = merge({'a': 0}, {'b': 1}, {'b': 2, 'c': 3}, {'a': 1}) >>> item == {'a': 1, 'b': 2, 'c': 3} True Args: *objs (dict): Dictionary sources. Returns: dict """ result = {} for obj in objs: result.update(obj) return result
[docs] def omit(keys, obj): """ The opposite of :func:`pick`. This method creates an object composed of the property paths of `obj` that are not omitted. Examples: >>> omit(['a', 'c'], {'a': 1, 'b': 2, 'c': 3 }) == {'b': 2} True >>> omit([0, 3], ['a', 'b', 'c', 'd']) == {1: 'b', 2: 'c'} True Args: keys (Iterable): Keys to omit. obj (Iterable): Object to process. Returns: dict: Dictionary with `keys` omitted. """ return {key: value for key, value in iterate(obj) if key not in keys}
[docs] def pick(keys, obj): """ Create a ``dict`` composed of the picked `keys` from `obj`. Examples: >>> pick(['a', 'b'], {'a': 1, 'b': 2, 'c': 3}) == {'a': 1, 'b': 2} True >>> pick(['a', 'b'], {'b': 2}) == {'b': 2} True Args: keys (Iterable): Keys to omit. obj (Iterable): Object to process. Returns: dict: Dict containg picked properties. """ result = {} for key in keys: value = _get(key, obj, default=Sentinel) if value is not Sentinel: result[key] = value return result