from collections import defaultdict
from itertools import zip_longest
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
def tree_map(
fn: Callable, tree: Any, *rest: Any, is_leaf: Optional[Callable] = None
) -> Any:
if is_leaf is not None and is_leaf(tree):
return fn(tree, *rest)
elif isinstance(tree, (list, tuple)):
TreeType = type(tree)
subtrees = (
tree_map(fn, child, *(r[i] for r in rest), is_leaf=is_leaf)
for i, child in enumerate(tree)
)
return TreeType(*subtrees) if hasattr(tree, "_fields") else TreeType(subtrees)
elif isinstance(tree, dict):
return {
k: tree_map(fn, child, *(r[k] for r in rest), is_leaf=is_leaf)
for k, child in tree.items()
}
else:
return fn(tree, *rest)
def tree_map_with_path(
fn: Callable,
tree: Any,
*rest: Any,
is_leaf: Optional[Callable] = None,
path: Optional[Any] = None,
) -> Any:
if is_leaf is not None and is_leaf(tree):
return fn(path, tree, *rest)
elif isinstance(tree, (list, tuple)):
prefix = f"{path}." if path else ""
TreeType = type(tree)
return TreeType(
tree_map_with_path(
fn, child, *(r[i] for r in rest), is_leaf=is_leaf, path=f"{prefix}{i}"
)
for i, child in enumerate(tree)
)
elif isinstance(tree, dict):
prefix = f"{path}." if path else ""
return {
k: tree_map_with_path(
fn, child, *(r[k] for r in rest), is_leaf=is_leaf, path=f"{prefix}{k}"
)
for k, child in tree.items()
}
else:
return fn(path, tree, *rest)
def tree_flatten(
tree: Any,
prefix: str = "",
is_leaf: Optional[Callable] = None,
destination: Optional[Union[List[Tuple[str, Any]], Dict[str, Any]]] = None,
) -> Union[List[Tuple[str, Any]], Dict[str, Any]]:
if destination is None:
destination = []
if isinstance(destination, list):
_add_to_destination = destination.extend
elif isinstance(destination, dict):
_add_to_destination = destination.update
else:
raise ValueError("Destination should be either a list or a dictionary or None")
if is_leaf is not None and is_leaf(tree):
_add_to_destination([(prefix[1:], tree)])
return destination
if isinstance(tree, (list, tuple)):
for i, item in enumerate(tree):
tree_flatten(item, f"{prefix}.{i}", is_leaf, destination)
return destination
if isinstance(tree, dict):
for key, value in tree.items():
tree_flatten(value, f"{prefix}.{key}", is_leaf, destination)
return destination
_add_to_destination([(prefix[1:], tree)])
return destination
def tree_unflatten(tree: Union[List[Tuple[str, Any]], Dict[str, Any]]) -> Any:
items = tree.items() if isinstance(tree, dict) else tree
if len(items) == 1:
key, value = next(iter(items))
if key == "":
return value
children = defaultdict(list)
for key, value in items:
current_idx, *next_idx = key.split(".", maxsplit=1)
next_idx = "" if not next_idx else next_idx[0]
children[current_idx].append((next_idx, value))
try:
keys = sorted((int(idx), idx) for idx in children.keys())
l = []
for i, k in keys:
l.extend([{} for _ in range(i - len(l))])
l.append(tree_unflatten(children[k]))
return l
except ValueError:
return {k: tree_unflatten(v) for k, v in children.items()}
def tree_reduce(fn, tree, initializer=None, is_leaf=None):
if is_leaf is not None and is_leaf(tree):
return tree if initializer is None else fn(initializer, tree)
accumulator = initializer
if isinstance(tree, (list, tuple)):
for item in tree:
accumulator = tree_reduce(fn, item, accumulator, is_leaf)
elif isinstance(tree, dict):
for item in tree.values():
accumulator = tree_reduce(fn, item, accumulator, is_leaf)
else:
return tree if accumulator is None else fn(accumulator, tree)
return accumulator
def tree_merge(tree_a, tree_b, merge_fn=None):
if isinstance(tree_a, (dict, list, tuple)) and len(tree_a) == 0:
tree_a = None
if isinstance(tree_b, (dict, list, tuple)) and len(tree_b) == 0:
tree_b = None
if tree_a is None and tree_b is not None:
return tree_b
if tree_a is not None and tree_b is None:
return tree_a
if isinstance(tree_a, (list, tuple)) and isinstance(tree_b, (list, tuple)):
TreeType = type(tree_a)
return TreeType(
tree_merge(a, b, merge_fn) for a, b in zip_longest(tree_a, tree_b)
)
elif isinstance(tree_a, dict) and isinstance(tree_b, dict):
return {
k: tree_merge(tree_a.get(k, None), tree_b.get(k, None), merge_fn)
for k in set(tree_a.keys()) | set(tree_b.keys())
}
else:
if merge_fn is None:
raise ValueError(
(
"Trees contain elements at the same locations but no merge "
"function was provided"
)
)
return merge_fn(tree_a, tree_b)