Source code for tollan.utils.fmt
"""
Formatting utilities for YAML, masks, and bitmasks.
Includes pretty-printing for YAML, numpy masks, and Flag-based bitmasks.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
import numpy as np
import pandas as pd
import pyaml
from .yaml import add_numpy_scalar_representers
if TYPE_CHECKING:
from enum import Flag
import numpy.typing as npt
__all__ = [
"BitmaskStats",
"bitmask_stats",
"pformat_bitmask",
"pformat_fancy_index",
"pformat_mask",
"pformat_yaml",
]
add_numpy_scalar_representers(pyaml.PYAMLDumper)
pyaml.add_representer(None, lambda s, d: s.represent_str(str(d))) # type: ignore[arg-type]
[docs]
def pformat_yaml(obj: Any) -> str:
"""
Pretty-format an object as a YAML string.
If the object has a __wrapped__ attribute, formats the wrapped object instead.
Example
-------
>>> data = {'a': 1, 'b': [2, 3]}
>>> print(pformat_yaml(data))
a: 1
b:
- 2
- 3
"""
if hasattr(obj, "__wrapped__"):
# unwrap if has wrapped interface
obj = obj.__wrapped__
return f"\n{pyaml.dump(obj)}"
[docs]
def pformat_fancy_index(
arg: slice | npt.ArrayLike | list[slice],
) -> str:
"""
Pretty-format a numpy fancy index, slice, or mask.
Examples
--------
>>> pformat_fancy_index(slice(1, 10, 2))
'[1:10:2]'
>>> pformat_fancy_index(slice(None, 5))
'[:5]'
>>> import numpy as np
>>> mask = np.array([True, False, True, True])
>>> pformat_fancy_index(mask)
'<mask 3/4>'
>>> pformat_fancy_index([slice(0, 2), slice(3, 5)])
'[[0:2], [3:5]]'
"""
if isinstance(arg, slice):
start = "" if arg.start is None else arg.start
stop = "" if arg.stop is None else arg.stop
result = f"[{start}:{stop}{{}}]"
if arg.step is None or arg.step == 1:
result = result.format("")
else:
result = result.format(f":{arg.step}")
return result
if isinstance(arg, np.ndarray):
return f"<mask {np.sum(arg)}/{arg.size}>"
if isinstance(arg, list):
s = ", ".join(pformat_fancy_index(a) for a in arg)
return f"[{s}]"
return str(arg)
def _pformat_mask(g: int, n: int, p: float) -> str:
return f"{g}/{n} ({p:.2%})"
[docs]
def pformat_mask(mask: npt.NDArray[np.bool_]) -> str:
"""
Pretty-format a boolean mask as 'selected/total (percentage)'.
Example
-------
>>> import numpy as np
>>> mask = np.array([True, False, True, True, False])
>>> pformat_mask(mask)
'3/5 (60.00%)'
"""
g = mask.sum()
n = mask.size
p = g / n
return _pformat_mask(g, n, p)
[docs]
def bitmask_stats(bm_cls: type[Flag], bitmask: npt.NDArray) -> pd.DataFrame:
"""
Compute statistics for each flag in a bitmask.
Returns a DataFrame with columns: name, selected, total, frac, summary.
"""
records = []
for name, value in bm_cls.__members__.items():
m = (bitmask & value.value) > 0
g = m.sum()
n = m.size
# Avoid division by zero warning when n=0
p = g / n if n > 0 else float("nan")
records.append(
{
"name": name,
"selected": g,
"total": n,
"frac": p,
"summary": _pformat_mask(g, n, p),
},
)
return pd.DataFrame.from_records(records)
[docs]
@dataclass
class BitmaskStats:
"""
Compute and format statistics for bitmask flags.
Use .pformat() for a summary table.
Example
-------
>>> from enum import Flag, auto
>>> import numpy as np
>>> class Status(Flag):
... OK = auto()
... WARNING = auto()
... ERROR = auto()
>>> bitmask = np.array([1, 3, 5, 7])
>>> stats = BitmaskStats(Status, bitmask)
>>> print(stats.pformat())
name summary
OK 4/4 (100.00%)
WARNING 2/4 (50.00%)
ERROR 2/4 (50.00%)
"""
bm_cls: type[Flag]
bitmask: npt.NDArray
def __post_init__(self) -> None:
"""Compute statistics after initialization."""
self._stats = bitmask_stats(self.bm_cls, self.bitmask)
@property
def stats(self) -> pd.DataFrame:
"""Get the statistics table.
Returns
-------
DataFrame
Table with columns: name, selected, total, frac, summary
"""
return self._stats
[docs]
def pformat(self) -> str:
"""Format statistics as string table.
Returns
-------
str
Pretty-formatted table showing name and summary columns
"""
return self.stats.to_string(columns=("name", "summary"), index=False)
[docs]
def pformat_bitmask(bm_cls: type[Flag], bitmask: npt.NDArray) -> str:
"""
Pretty-format bitmask statistics as a summary table.
Example
-------
>>> from enum import Flag, auto
>>> import numpy as np
>>> class Status(Flag):
... OK = auto()
... WARNING = auto()
... ERROR = auto()
>>> bitmask = np.array([1, 3, 5, 7])
>>> print(pformat_bitmask(Status, bitmask))
name summary
OK 4/4 (100.00%)
WARNING 2/4 (50.00%)
ERROR 2/4 (50.00%)
"""
return BitmaskStats(bm_cls, bitmask).pformat()