-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrated format_table and associated helpers from biocgenerics.
These allow flexible pretty-printing of table-like structures. They aren't really generics and so belong in here instead.
- Loading branch information
Showing
3 changed files
with
196 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |