Source code for encomp.structures

"""Helpers for chunking and flattening common Python containers."""

from collections.abc import Iterable, Iterator, Sequence
from typing import Any, cast, overload

import numpy as np
import polars as pl

from .units import Quantity

__all__ = ["divide_chunks", "flatten"]


@overload
def divide_chunks[T](container: list[T], N: int) -> Iterator[list[T]]: ...


@overload
def divide_chunks[T](container: tuple[T, ...], N: int) -> Iterator[tuple[T, ...]]: ...


@overload
def divide_chunks[T](container: Sequence[T], N: int) -> Iterator[Sequence[T]]: ...


@overload
def divide_chunks(container: np.ndarray, N: int) -> Iterator[np.ndarray]: ...


[docs] def divide_chunks(container: Any, N: int) -> Any: """Yield slices of ``container`` with at most ``N`` elements each. Empty containers yield no chunks. ``N`` must be positive. The returned chunks preserve the container's slicing behavior, so lists produce lists, tuples produce tuples, and numpy arrays produce arrays. """ # validate eagerly: a generator body would defer these errors to the first # next() call, far from the call site if N < 1: raise ValueError(f"Chunk size must be at least 1, passed {N}") def _chunks() -> Iterator[Any]: for i in range(0, len(container), N): yield container[i : i + N] return _chunks()
[docs] def flatten(container: Iterable[Any], max_depth: int | None = None) -> Iterator[Any]: """Yield nested iterable contents as a flat stream. Strings, bytes, dictionaries, ``Quantity`` instances, numpy arrays and Polars objects are treated as atomic values rather than recursively iterated. """ def _flatten(items: Iterable[Any], depth: int) -> Iterator[Any]: if max_depth is not None and depth >= max_depth: yield items return for obj in items: # atomic despite being iterable: iterating a dict would silently drop its # values (keys only), and bytes would flatten into individual integers if isinstance(obj, (str, bytes, dict, Quantity, np.ndarray, pl.Series, pl.Expr)): yield obj continue if isinstance(obj, Iterable): yield from _flatten(cast("Iterable[Any]", obj), depth + 1) # pyrefly: ignore[redundant-cast] # cast required by pyright continue yield obj yield from _flatten(container, 0)