"""Utilities for working with collections."""
# Python Modules
from __future__ import annotations
import logging as log
from types import GeneratorType
from typing import Any, Iterable, Mapping, Optional
# 3rd Party Modules
# Project Modules
[docs]
log = log.getLogger(__name__)
[docs]
def is_iterable(obj: Any, consider_string_iterable: bool = False) -> bool:
"""
Tests if the object is iterable.
Parameters
----------
obj : any
An object.
consider_string_iterable : bool, default = False
Whether to consider strings iterable or not.
Returns
-------
bool
``True`` if ``obj`` is iterable.
"""
if isinstance(obj, str):
return consider_string_iterable
try:
iter(obj)
except TypeError:
return False
else:
return True
[docs]
def is_generator(obj: Any) -> bool:
"""
Checks if the object is a generator.
Parameters
----------
obj : any
An object.
Returns
-------
bool
``True`` if the object is a generator.
"""
return isinstance(obj, GeneratorType)
[docs]
def get_first_non_null_value(collection: Iterable) -> Optional[Any]:
"""Recursively try to get the first non-null value in a collection.
This method will recursively traverse the collection until it finds
a non-iterable value that is not ``None``.
Parameters
----------
collection : Iterable
The collection to retrieve the value from.
Returns
-------
Any, optional
The first non-null value in the series, if one exists, otherwise return
``None``.
"""
for value in collection:
if is_iterable(value) and not isinstance(value, str):
value = get_first_non_null_value(value)
if value is not None:
return value
return None
[docs]
def recursive_sort(obj: Any) -> Any:
"""
Attempts to sort an object recursively according to the following rules:
* If the object is a dictionary, it will be sorted by its keys and its
values will be sorted recursively.
* If the object is a list or tuple, it will be sorted by its values.
Typically, a value is a primitive, but lists, tuples, and dictionaries
are also valid. In these cases, the collections are compared after
sorting.
* Any other value is returned unchanged.
Parameters
----------
obj : Any
The input object
Returns
-------
Any
The returned object sorted.
"""
def _key_fn(e: Any) -> Any:
if isinstance(e, dict):
return sorted(e.items())
elif isinstance(e, (list, tuple)):
return sorted(e)
else:
return e
if isinstance(obj, dict):
return {k: recursive_sort(obj[k]) for k in sorted(obj)}
elif isinstance(obj, list):
values = sorted(obj, key=_key_fn)
return [recursive_sort(e) for e in values]
elif isinstance(obj, tuple):
values = sorted(obj, key=_key_fn)
return tuple([recursive_sort(e) for e in values])
elif is_iterable(obj) or is_generator(obj):
return list(obj)
else:
return obj
[docs]
def remove_none_from_dict(d: dict[str, Any]) -> dict[str, Any]:
"""Recursively remove ``None`` values from a (nested) dictionary."""
def _do_removal(obj: Any) -> Any:
if isinstance(obj, Mapping):
return {k: _do_removal(v) for k, v in obj.items() if v is not None}
elif isinstance(obj, list):
return [remove_none_from_dict(e) for e in obj]
elif isinstance(obj, set):
return {remove_none_from_dict(e) for e in obj}
else:
return obj
return _do_removal(d)