Source code for aligntext

#
# MIT License
#
# Copyright (c) 2023-2025 nbiotcloud
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
"""
Text Align - Python Text Alignment.
"""

import types
from itertools import zip_longest as _zip_longest


[docs] def align( *rows, seps=None, sepfirst=None, seplast=None, alignments=None, rtrim=True, strip_empty_cols=False, header=None ): r""" Align `rows` using separators `seps` and align to `alignments`. Args: rows: Rows to be aligned. Keyword Args: seps (list, tuple): separators (see :any:`set_separators()`). sepfirst (str): prefix for every row. Empty by default. (see :any:`set_separators()`). seplast (str): suffix for every row. Empty by default. (see :any:`set_separators()`). alignments (list, tuple): alignments (see :any:`set_alignments()`). rtrim (bool): Remove whitespaces at the end of the line. strip_empty_cols (bool): Skip the column and the separator of empty columns. header: Header Row. >>> print(align([["a", "bb", "ccc"], ["zzz", "yyyy", "x"]], rtrim=True)) a bb ccc zzz yyyy x >>> print(align([["a", "bb", "ccc"], ["zzz", "yyyy", "x"]], header=('Col1', 'C2', 'Column3'), ... rtrim=True)) Col1 C2 Column3 ---- -- ------- a bb ccc zzz yyyy x >>> print(align(["a", "bb", "ccc"], ["zzz", "yyyy", "x"], seps=["|"], sepfirst="B", seplast="E", ... rtrim=True)) Ba |bb |cccE Bzzz|yyyy|x E >>> print(align(["a", "bb", "ccc"], ["zzz", "yyyy", "x"], alignments=(center, right), rtrim=True)) a bb ccc zzz yyyy x Generators are also allowed: >>> def myiter(num): ... for idx in range(num): ... yield idx >>> def myiteriter(rows, cols): ... for idx in range(rows): ... yield myiter(cols) >>> align(myiteriter(2, 3)) '0 1 2\n0 1 2' """ inst = Align(rtrim=rtrim, strip_empty_cols=strip_empty_cols) if header: header = tuple(header) inst.add_row(header) inst.add_row("-" * max(len(cellrow) for cellrow in cell.split("\n")) for cell in header) inst.add_rows(*rows) if seps is None: inst.set_separators(first=sepfirst, last=seplast) else: inst.set_separators(*seps, first=sepfirst, last=seplast) if alignments: inst.set_alignments(*alignments) return inst.get()
[docs] def iter_align(*rows, seps=None, sepfirst=None, seplast=None, alignments=None, rtrim=True, strip_empty_cols=False): r""" Iterate over aligned `rows` using separators `seps` and align to `alignments`. Args: rows: Rows to be aligned. Keyword Args: seps (list, tuple): separators (see :any:`set_separators()`). sepfirst (str): prefix for every row. Empty by default. (see :any:`set_separators()`). seplast (str): suffix for every row. Empty by default. (see :any:`set_separators()`). alignments (list, tuple): alignments (see :any:`set_alignments()`). rtrim (bool): Remove whitespaces at the end of the line. strip_empty_cols (bool): Skip the column and the separator of empty columns. >>> '\n'.join(iter_align([["a", "bb", "ccc"], ["zzz", "yyyy", "x"]])) 'a bb ccc\nzzz yyyy x' >>> '\n'.join(iter_align(["a", "bb", "ccc"], ["zzz", "yyyy", "x"], seps=["|"], sepfirst="B", seplast="E")) 'Ba |bb |cccE\nBzzz|yyyy|x E' >>> '\n'.join(iter_align(["a", "bb", "ccc"], ["zzz", "yyyy", "x"], alignments=(center, right))) ' a bb ccc\nzzz yyyy x' Generators are also allowed: >>> def myiter(num): ... for idx in range(num): ... yield idx >>> def myiteriter(rows, cols): ... for idx in range(rows): ... yield myiter(cols) >>> '\n' .join(iter_align(myiteriter(2, 3))) '0 1 2\n0 1 2' """ inst = Align(rtrim=rtrim, strip_empty_cols=strip_empty_cols) inst.add_rows(*rows) if seps is None: inst.set_separators(first=sepfirst, last=seplast) else: inst.set_separators(*seps, first=sepfirst, last=seplast) if alignments: inst.set_alignments(*alignments) return iter(inst)
[docs] class Align: """ Align table data. """ def __init__(self, rtrim=False, strip_empty_cols=False): r""" Align table data. Keyword Arguments: rtrim (bool): Remove whitespaces at the end of the line. strip_empty_cols (bool): Skip the column and the separator of empty columns. Example: >>> al = Align() Data is added via :any:`add_rows()` or via :any:`add_row()`: >>> al.add_rows([[1, 33, 1.001], ... ["foo", "barcelona", "tschustify"], ... ]) >>> al.add_row(None, "None") The aligned content is served by :any:`get()` or by iteration: >>> al.get() '1 33 1.001 \nfoo barcelona tschustify\n None ' >>> for row in al: ... row '1 33 1.001 ' 'foo barcelona tschustify' ' None ' Trailing whitespaces can be disabled via: >>> al.rtrim=True >>> for row in al: ... row '1 33 1.001' 'foo barcelona tschustify' ' None' The `len()` function will return the current filling level. >>> al.clear() >>> al.get() '' >>> len(al) 0 >>> al.add_row(1, 2, 3) >>> al.get() '1 2 3' >>> len(al) 1 Empty columns can be collapsed on request: >>> al = Align() >>> al.add_row("a", None, "cc") >>> al.add_row("ddd", "", "f") >>> for row in al: ... row 'a cc' 'ddd f ' >>> al.strip_empty_cols = True >>> for row in al: ... row 'a cc' 'ddd f ' Also multiline cells are supported: >>> al = Align() >>> al.add_row("a\nbb", "ccc") >>> al.add_row("ddd", "\nlonger") >>> for row in al: ... row 'a ccc ' 'bb ' 'ddd ' ' longer' """ super().__init__() self._separators = [] self._sepfirst = None self._seplast = None self.rtrim = rtrim self.strip_empty_cols = strip_empty_cols self._alignments = [left] self._rows = [] self._maxcols = 0
[docs] def add_rows(self, *rows): """ Add `rows`. Rows are accepted as `tuple`, `list` or positional arguments. The data is always extended to the width of the broadest row. >>> al = Align() >>> al.add_rows([[11, 12, 13], [14, 15, 16]]) # list of lists >>> al.add_rows(((21, 22, 23), (24, 25, 26))) # tuple of tuples >>> al.add_rows((31, 32, 33), [34, 35, 36]) # positional arguments >>> for row in al: ... row '11 12 13' '14 15 16' '21 22 23' '24 25 26' '31 32 33' '34 35 36' """ for row in _iter_items(rows): self._add_row(row)
[docs] def add_row(self, *cols): """ Add a row with `cols`. Column values are accepted as `tuple`, `list` or positional arguments. The data is always extended to the width of the broadest row. >>> al = Align() >>> al.add_row([1, 2, 3]) # list >>> al.add_row((4, 5, 6, 7)) # tuple >>> al.add_row(8, 9, 10) # positional arguments >>> for row in al: ... row '1 2 3 ' '4 5 6 7' '8 9 10 ' """ self._add_row(_iter_items(cols))
def _add_row(self, cols): normedcols = [self.norm(cell) for cell in cols] self._rows.append((True, normedcols)) self._maxcols = max(len(normedcols), self._maxcols)
[docs] def add_spacers(self, *spacers): """ Add unaligned `spacers`. >>> al = Align() >>> al.add_row(111, 12, 13) >>> al.add_spacers("---", None) >>> al.add_row(31, 321, 33) >>> al.add_spacers("===============") >>> al.add_row(21, 22, 231) >>> for row in al: ... row '111 12 13 ' '---' '' '31 321 33 ' '===============' '21 22 231' """ for spacer in spacers: self._add_spacer(spacer)
[docs] def add_spacer(self, spacer=None): """ Add unaligned `spacer`. >>> al = Align(rtrim=True) >>> al.add_row(111, 12, 13) >>> al.add_spacer() >>> al.add_row(31, 321, 33) >>> al.add_spacer("===============") >>> al.add_row(21, 22, 231) >>> for row in al: ... row '111 12 13' '' '31 321 33' '===============' '21 22 231' """ self._add_spacer(spacer)
def _add_spacer(self, spacer): if spacer is None: spacer = "" self._rows.append((False, spacer))
[docs] def clear(self): """Remove *all* added rows.""" self._rows = [] self._maxcols = 0
[docs] def get(self): """Return aligned data.""" return "\n".join(self._get())
[docs] def set_separators(self, *separators, first=None, last=None, rtrim=None): """ Set the column separators. Args: *separators (str): separators. Default is " ". Keyword Args: first (str): prefix for every row. Empty by default. last (str): suffix for every row. Empty by default. rtrim: Remove whitespaces at the end of the line. Do not change setting by default. If there are less separators specified than columns available, the last alignment is used for all remaining columns. If there are more separators specified than columns available, the dispensable ones are ignored. Example: >>> al = Align() >>> al.add_rows(["a", "bb", "ccc"], ["zz", "yyyy", "x"]) >>> al.set_separators("|", rtrim=True) >>> for row in al: ... row 'a |bb |ccc' 'zz|yyyy|x' >>> al.set_separators("|", "=", "&") >>> for row in al: ... row 'a |bb =ccc&' 'zz|yyyy=x &' >>> al.set_separators("|", first="> ", last=" <") >>> for row in al: ... row '> a |bb |ccc <' '> zz|yyyy|x <' >>> al.set_separators("|", "=", "&", last="END") >>> for row in al: ... row 'a |bb =cccEND' 'zz|yyyy=x END' """ self._separators = separators self._sepfirst = first self._seplast = last if rtrim is not None: self.rtrim = rtrim
[docs] def set_alignments(self, *alignments): """ Set the alignment for every column. All left aligned is the default. If there are less alignments specified than columns available, the last alignment is used for all remaining columns. If there are more alignments specified than columns available, the dispensable ones are ignored. Alignments shall be `left`, `center`, `right` or any function with the arguments `cell`, `width`, which returns an aligned string. Example: >>> al = Align() >>> al.add_rows([[1, 33, 1.001], ... ["foo", "barcelona", "tschustify"], ... ]) >>> al.set_alignments(center) >>> for row in al: ... row ' 1 33 1.001 ' 'foo barcelona tschustify' >>> al.set_alignments(center, right) >>> for row in al: ... row ' 1 33 1.001' 'foo barcelona tschustify' >>> al.set_alignments(center, right, left, right) >>> for row in al: ... row ' 1 33 1.001 ' 'foo barcelona tschustify' >>> al.set_alignments() >>> for row in al: ... row '1 33 1.001 ' 'foo barcelona tschustify' """ self._alignments = alignments
[docs] @staticmethod def norm(cell): """ Normalize function for any table `cell`. Return an empty string, when `cell` is None, otherwise convert `cell` to string. This static method can be overwritten to implement any other type behaviour. """ if cell is None: return "" return str(cell)
def __iter__(self): yield from self._get() def _get(self): # filter out all rows to be aligned rows = self._rows alignrows = [row for align, row in rows if align] widths = [ max(max(len(cellrow) for cellrow in cell.split("\n")) for cell in col) for col in _zip_longest(*alignrows, fillvalue="") ] sepfirst, separators = self._get_separators() alignments = self._get_alignments() collapse = self.strip_empty_cols for rowalign, row in rows: if rowalign: subrows = list(_zip_longest(*[cell.split("\n") for cell in row], fillvalue="")) for subrow in subrows: items = _zip_longest(alignments, subrow, widths, separators, fillvalue="") line = sepfirst + "".join( [alignment(cell, width) + sep for alignment, cell, width, sep in items if width or not collapse] ) if self.rtrim: line = line.rstrip() yield line else: line = row if self.rtrim: line = line.rstrip() yield line def _get_separators(self): sepfirst = self.norm(self._sepfirst) separators = [self.norm(sep) for sep in self._separators] if not separators: separators = [" "] seplast = self._seplast colslen = self._maxcols missing = colslen - len(separators) if missing > 0: separators = separators + separators[-1:] * (missing - 1) + [self.norm(seplast)] elif seplast is not None: separators = separators[: colslen - 1] + [self.norm(seplast)] else: separators = separators[:colslen] return sepfirst, separators def _get_alignments(self): alignments = self._alignments if not alignments: alignments = [left] colslen = self._maxcols missing = colslen - len(alignments) if missing > 0: return alignments + alignments[-1:] * missing return alignments[:colslen] def __len__(self): return len(self._rows)
[docs] def left(cell, width): """Justify `cell` to left with `width`.""" return cell.ljust(width)
[docs] def center(cell, width): """Center `cell` to `width`.""" return cell.center(width)
def _iter_items(args): """Iterate over items in `args`.""" if len(args) == 1 and isinstance(args[0], (tuple, list, types.GeneratorType)): items = args[0] else: items = args yield from items