Skip to content

Commit

Permalink
Migrated format_table and associated helpers from biocgenerics.
Browse files Browse the repository at this point in the history
These allow flexible pretty-printing of table-like structures. They
aren't really generics and so belong in here instead.
  • Loading branch information
LTLA committed Oct 31, 2023
1 parent 8ce88b6 commit cf62973
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/biocutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
from .is_list_of_type import is_list_of_type
from .normalize_subscript import normalize_subscript
from .print_truncated_list import print_truncated_list
from .print_wrapped_table import print_wrapped_table, create_floating_names, truncate_strings, print_type
161 changes: 161 additions & 0 deletions src/biocutils/print_wrapped_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
from typing import Sequence, List, Optional
from .subset import subset


def _get_max_width(col: List[str]):
width = 0
for y in col:
if len(y) > width:
width = len(y)
return width


def print_wrapped_table(columns: List[Sequence[str]], floating_names: Optional[Sequence[str]] = None, sep: str = " ", window: Optional[int] = None) -> str:
"""
Pretty-print a table with aligned and wrapped columns. All column contents
are padded so that they are right-justified. Wrapping is performed whenever
a new column would exceed the window width, in which case the entire column
(and all subsequent columns) are printed below the previous columns.
Args:
columns:
List of list of strings, where each inner list is the same length
and contains the visible contents of a column. Strings are
typically generated by calling `repr()` on data column values.
Callers are responsible for inserting ellipses, adding column type
information (e.g., with :py:meth:`~print_type`) or truncating long
strings (e.g., with :py:meth:`~truncate_strings`).
floating_names:
List of strings to be added to the left of the table. This is
printed repeatedly for each set of wrapped columns.
See also :py:meth:`~create_floating_names`.
sep:
Separator between columns.
window:
Size of the terminal window, in characters. We attempt to determine
this automatically, otherwise it is set to 150.
Returns:
String containing the pretty-printed table.
"""
if window is None:
import os

try:
window = os.get_terminal_size().columns
except:
window = 150

if len(columns) == 0:
raise ValueError("at least one column should be supplied in 'columns'")
n = len(columns[0])

floatwidth = 0
if floating_names is not None:
floatwidth = _get_max_width(floating_names)
new_floating_names = []
for y in floating_names:
new_floating_names.append(y.rjust(floatwidth))
floating_names = new_floating_names

output = ""

def reinitialize():
if floating_names is None:
return [""] * n
else:
return floating_names[:]

contents = reinitialize()
init = True
used = floatwidth

for col in columns:
width = _get_max_width(col)

if not init and used + width + len(sep) > window:
for line in contents:
output += line + "\n"
contents = reinitialize()
init = True
used = floatwidth

for i, y in enumerate(col):
if used > 0:
contents[i] += sep
contents[i] += y.rjust(width)
used += width + len(sep)
init = False

output += "\n".join(contents)
return output


def create_floating_names(names: Optional[List[str]], indices: Sequence[int]) -> List[str]:
"""
Create the floating names to use in :py:meth:`~print_wrapped_table`. If no
names are present, positional indices are used instead.
Args:
names:
List of row names, or None if no row names are available.
indices:
Integer indices for which to obtain the names.
Returns:
List of strings containing floating names.
"""
if names is not None:
return subset(names, indices)
else:
return ["[" + str(i) + "]" for i in indices]


def truncate_strings(values: List[str], width: int = 40) -> List[str]:
"""
Truncate long strings for printing in :py:meth:`~print_wrapped_table`.
Args:
values:
List of strings to be printed.
width:
Width beyond which to truncate the string.
Returns:
List containing truncated strings.
"""
replacement = values[:]
for i, y in enumerate(values):
if len(y) > width:
replacement[i] = y[: width - 3] + "..."
return replacement


def print_type(x) -> str:
"""
Print the type of an object, with some special behavior for certain classes
(e.g., to add the data type of NumPy arrays). This is intended for display
at the top of the columns of :py:meth:`~print_wrapped_table`.
Args:
x: Some object.
Return:
String containing the class of the object.
"""
cls = type(x).__name__

import sys
if "numpy" in sys.modules:
numpy = sys.modules["numpy"]
if isinstance(x, numpy.ndarray):
return cls + "[" + x.dtype.name + "]"

return cls
34 changes: 34 additions & 0 deletions tests/test_print_wrapped_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from biocutils import print_wrapped_table, create_floating_names, truncate_strings, print_type
import numpy as np


def test_print_wrapped_table():
contents = [
["asdasd", "1", "2", "3", "4"],
[""] + ["|"] * 4,
["asyudgausydga", "A", "B", "C", "D"],
]
print(print_wrapped_table(contents))
print(print_wrapped_table(contents, floating_names=["", "aarg", "boo", "ffoo", "stuff"]))
print(print_wrapped_table(contents, window=10))
print(
print_wrapped_table(
contents, window=10, floating_names=["", "AAAR", "BBBB", "XXX", "STUFF"]
)
)


def test_create_floating_names():
assert create_floating_names(None, [1,2,3,4]) == [ "[1]", "[2]", "[3]", "[4]" ]
assert create_floating_names(["A", "B", "C", "D", "E", "F"], [1,2,3,4]) == [ "B", "C", "D", "E" ]


def test_truncate_strings():
ref = ["A"*10, "B"*20, "C"*30]
assert truncate_strings(ref, width=25) == [ "A"*10, "B"*20, "C"*22 + "..." ]


def test_print_type():
assert print_type(np.array([1,2,3])) == "ndarray[int64]"
assert print_type(np.array([1,2.5,3.3])) == "ndarray[float64]"
assert print_type([1,2,3]) == "list"

0 comments on commit cf62973

Please sign in to comment.