Skip to content

Commit

Permalink
Column alignment
Browse files Browse the repository at this point in the history
  • Loading branch information
jwodder committed Jul 3, 2018
1 parent fff8013 commit 622c02a
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
v0.5.0 (in development)
-----------------------
- Added `align` and `align_fill` options

v0.4.0 (2018-07-01)
-------------------
- Added `left_padding` and `right_padding` options
Expand Down
11 changes: 11 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,17 @@ constructor or as attributes on a ``Txtble`` instance::
tbl = Txtble(data)
tbl.border = False

``align=()``
A sequence of alignment specifiers indicating how the contents of each
column, in order, should be horizontally aligned. The alignment specifiers
are ``'l'`` (left alignment), ``'c'`` (centered alignment), and ``'r'``
(right alignment). ``align`` may optionally be set to a single alignment
specifier to cause all columns to be aligned in that way.

``align_fill='l'``
If there are more columns than there are entries in ``align``, the extra
columns will have their alignment set to ``align_fill``.

``border=True``
Whether to draw a border around the edge of the table. ``border`` may
optionally be set to a ``BorderStyle`` instance to set the characters used
Expand Down
44 changes: 31 additions & 13 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,41 @@ Features to Add
- Borders & rules:
- Add an "hrule" class that can be passed in place of a data row to
indicate that a horizontal rule should be added. Its constructor should
take optional keyword arguments for setting the box-drawing characters to
use ('dash', 'joint', '`left_joint`', '`right_joint`', 'midjoint'?)
take an optional `border_style` argument.
- easy way to change just the column separator in non-rule rows?
- "doubling" ('|' → '||') specific vrules
- easy way to change the column separator in all rows?
- "doubling" ('|' → '||') specific vrules
- using different column vrules in the header (and/or header rule) than in
the data?
- drawing left & right border vrules but no top/bottom hrules
- drawing top & bottom border hrules but not left/right vrules
- drawing just a top or just a bottom border hrule

- Showing, wrapping, & aligning cells:
- separate parameters for padding on the right vs. left?
- Centering & right-aligning of specified columns
- Setting the text alignment of individual cells (e.g., having a centered
"—" cell in the middle of a bunch of left-aligned cells)
- Parameter for setting how to handle overly large fields: let them
- Cell alignment:
- Add an 'n' alignment option for aligning numeric values along a decimal
point
- Support also controlling the alignment of string values in the same
column
- Support configuring how numeric values should be aligned relative to
long string values in the same column
- Support setting column-specific alignments with `tbl.align[i] = 'c'` even
when `align` hasn't been previously set to a list
- setting the alignment of individual cells (e.g., having a centered "—"
cell in the middle of a bunch of left-aligned cells)
- configuring vertical alignment of cells when other cells in the same row
have more lines

- Cell widths and line-wrapping:
- parameters for setting minimum, maximum, and exact column widths
- parameter for setting how to handle overly large fields: let them
overflow, truncate them, or line-wrap them
- handling of lines that are still too long (e.g., due to containing a very
long word) after wrapping
- Parameters for setting minimum, maximum, and exact column widths

- Showing cells:
- setting a function to use for stringification of all non-string cell
values
- aligning numeric cells on a decimal point
- configuring vertical alignment of cells when other cells in the same row
have more lines
- customizing numeric formatting

- API:
- constructing a table from a sequence of dicts whose keys match the
Expand All @@ -39,6 +54,9 @@ Features to Add
- Allow `header_fill` and `row_fill` to be callables that are called
whenever a value from them is required?
- constructing from a CSV file?
- Add `Txtble` subclasses with default values configured for various common
types of text tables?
- cf. `tabulate`'s `tablefmt`

- Parameters for setting exact (and/or min/max?) number of columns
- adding filler cells to short rows until they reach a certain number of
Expand Down
182 changes: 182 additions & 0 deletions test/test_align.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import pytest
from txtble import Txtble
from test_data import HEADERS, DATA, TABLE

def test_align_lll():
tbl = Txtble(DATA, headers=HEADERS, align=['l', 'l', 'l'])
assert str(tbl) == TABLE

def test_align_ccc():
tbl = Txtble(DATA, headers=HEADERS, align=['c', 'c', 'c'])
assert str(tbl) == (
'+---------+----------+------------------+\n'
'| Month |Birthstone| Birth Flower |\n'
'+---------+----------+------------------+\n'
'| January | Garnet | Carnation |\n'
'|February | Amethyst | Violet |\n'
'| March |Aquamarine| Jonquil |\n'
'| April | Diamond | Sweetpea |\n'
'| May | Emerald |Lily Of The Valley|\n'
'| June | Pearl | Rose |\n'
'| July | Ruby | Larkspur |\n'
'| August | Peridot | Gladiolus |\n'
'|September| Sapphire | Aster |\n'
'| October | Opal | Calendula |\n'
'|November | Topaz | Chrysanthemum |\n'
'|December |Turquoise | Narcissus |\n'
'+---------+----------+------------------+'
)

def test_align_rrr():
tbl = Txtble(DATA, headers=HEADERS, align=['r', 'r', 'r'])
assert str(tbl) == (
'+---------+----------+------------------+\n'
'| Month|Birthstone| Birth Flower|\n'
'+---------+----------+------------------+\n'
'| January| Garnet| Carnation|\n'
'| February| Amethyst| Violet|\n'
'| March|Aquamarine| Jonquil|\n'
'| April| Diamond| Sweetpea|\n'
'| May| Emerald|Lily Of The Valley|\n'
'| June| Pearl| Rose|\n'
'| July| Ruby| Larkspur|\n'
'| August| Peridot| Gladiolus|\n'
'|September| Sapphire| Aster|\n'
'| October| Opal| Calendula|\n'
'| November| Topaz| Chrysanthemum|\n'
'| December| Turquoise| Narcissus|\n'
'+---------+----------+------------------+'
)

def test_align_rcl():
tbl = Txtble(DATA, headers=HEADERS, align=['r', 'c', 'l'])
assert str(tbl) == (
'+---------+----------+------------------+\n'
'| Month|Birthstone|Birth Flower |\n'
'+---------+----------+------------------+\n'
'| January| Garnet |Carnation |\n'
'| February| Amethyst |Violet |\n'
'| March|Aquamarine|Jonquil |\n'
'| April| Diamond |Sweetpea |\n'
'| May| Emerald |Lily Of The Valley|\n'
'| June| Pearl |Rose |\n'
'| July| Ruby |Larkspur |\n'
'| August| Peridot |Gladiolus |\n'
'|September| Sapphire |Aster |\n'
'| October| Opal |Calendula |\n'
'| November| Topaz |Chrysanthemum |\n'
'| December|Turquoise |Narcissus |\n'
'+---------+----------+------------------+'
)

def test_align_ccc_right_padding():
tbl = Txtble(DATA, headers=HEADERS, align=['c', 'c', 'c'], right_padding=2)
assert str(tbl) == (
'+-----------+------------+--------------------+\n'
'| Month |Birthstone | Birth Flower |\n'
'+-----------+------------+--------------------+\n'
'| January | Garnet | Carnation |\n'
'|February | Amethyst | Violet |\n'
'| March |Aquamarine | Jonquil |\n'
'| April | Diamond | Sweetpea |\n'
'| May | Emerald |Lily Of The Valley |\n'
'| June | Pearl | Rose |\n'
'| July | Ruby | Larkspur |\n'
'| August | Peridot | Gladiolus |\n'
'|September | Sapphire | Aster |\n'
'| October | Opal | Calendula |\n'
'|November | Topaz | Chrysanthemum |\n'
'|December |Turquoise | Narcissus |\n'
'+-----------+------------+--------------------+'
)

def test_align_extra_columns():
tbl = Txtble(DATA, headers=HEADERS, align=['c', 'c'])
assert str(tbl) == (
'+---------+----------+------------------+\n'
'| Month |Birthstone|Birth Flower |\n'
'+---------+----------+------------------+\n'
'| January | Garnet |Carnation |\n'
'|February | Amethyst |Violet |\n'
'| March |Aquamarine|Jonquil |\n'
'| April | Diamond |Sweetpea |\n'
'| May | Emerald |Lily Of The Valley|\n'
'| June | Pearl |Rose |\n'
'| July | Ruby |Larkspur |\n'
'| August | Peridot |Gladiolus |\n'
'|September| Sapphire |Aster |\n'
'| October | Opal |Calendula |\n'
'|November | Topaz |Chrysanthemum |\n'
'|December |Turquoise |Narcissus |\n'
'+---------+----------+------------------+'
)

def test_align_extra_columns_align_fill():
tbl = Txtble(DATA, headers=HEADERS, align=['c', 'c'], align_fill='r')
assert str(tbl) == (
'+---------+----------+------------------+\n'
'| Month |Birthstone| Birth Flower|\n'
'+---------+----------+------------------+\n'
'| January | Garnet | Carnation|\n'
'|February | Amethyst | Violet|\n'
'| March |Aquamarine| Jonquil|\n'
'| April | Diamond | Sweetpea|\n'
'| May | Emerald |Lily Of The Valley|\n'
'| June | Pearl | Rose|\n'
'| July | Ruby | Larkspur|\n'
'| August | Peridot | Gladiolus|\n'
'|September| Sapphire | Aster|\n'
'| October | Opal | Calendula|\n'
'|November | Topaz | Chrysanthemum|\n'
'|December |Turquoise | Narcissus|\n'
'+---------+----------+------------------+'
)

def test_align_extra_aligns():
tbl = Txtble(DATA, headers=HEADERS, align=['r', 'c', 'l', 'c', 'r'])
assert str(tbl) == (
'+---------+----------+------------------+\n'
'| Month|Birthstone|Birth Flower |\n'
'+---------+----------+------------------+\n'
'| January| Garnet |Carnation |\n'
'| February| Amethyst |Violet |\n'
'| March|Aquamarine|Jonquil |\n'
'| April| Diamond |Sweetpea |\n'
'| May| Emerald |Lily Of The Valley|\n'
'| June| Pearl |Rose |\n'
'| July| Ruby |Larkspur |\n'
'| August| Peridot |Gladiolus |\n'
'|September| Sapphire |Aster |\n'
'| October| Opal |Calendula |\n'
'| November| Topaz |Chrysanthemum |\n'
'| December|Turquoise |Narcissus |\n'
'+---------+----------+------------------+'
)

@pytest.mark.parametrize('align', ['q', 'L', 'left', None, '<'])
def test_bad_align(align):
tbl = Txtble(DATA, headers=HEADERS, align=['r', 'c', align])
with pytest.raises(ValueError):
str(tbl)

@pytest.mark.parametrize('align_fill', [None, 'l'])
def test_align_all_c(align_fill):
tbl = Txtble(DATA, headers=HEADERS, align='c', align_fill=align_fill)
assert str(tbl) == (
'+---------+----------+------------------+\n'
'| Month |Birthstone| Birth Flower |\n'
'+---------+----------+------------------+\n'
'| January | Garnet | Carnation |\n'
'|February | Amethyst | Violet |\n'
'| March |Aquamarine| Jonquil |\n'
'| April | Diamond | Sweetpea |\n'
'| May | Emerald |Lily Of The Valley|\n'
'| June | Pearl | Rose |\n'
'| July | Ruby | Larkspur |\n'
'| August | Peridot | Gladiolus |\n'
'|September| Sapphire | Aster |\n'
'| October | Opal | Calendula |\n'
'|November | Topaz | Chrysanthemum |\n'
'|December |Turquoise | Narcissus |\n'
'+---------+----------+------------------+'
)
35 changes: 27 additions & 8 deletions txtble.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
Visit <https://github.com/jwodder/txtble> for more information.
"""

__version__ = '0.4.0'
__version__ = '0.5.0.dev1'
__author__ = 'John Thorvald Wodder II'
__author_email__ = 'txtble@varonathe.org'
__license__ = 'MIT'
Expand Down Expand Up @@ -83,6 +83,8 @@ def bot_rule(self, widths, capped, sep_cols):
@attr.s
class Txtble(object):
data = attr.ib(default=(), converter=lambda d: list(map(list, d)))
align = attr.ib(default=())
align_fill = attr.ib(default='l')
border = attr.ib(default=True)
border_style = attr.ib(default=ASCII_BORDERS)
column_border = attr.ib(default=True)
Expand Down Expand Up @@ -193,10 +195,16 @@ def show(self):
else:
row_border = self.border_style

if isinstance(self.align, string_types):
align = [self.align] * columns
else:
align = to_len(list(self.align), columns, self.align_fill)

def showrow(row):
return join_cells(
row,
widths,
align = align,
col_sep = column_border.vline if column_border else '',
border = border.vline if border else '',
rstrip = self.rstrip,
Expand Down Expand Up @@ -237,21 +245,21 @@ def __init__(self, tbl, value):
raise IndeterminateWidthError(line)
self.width = max(map(wcswidth, self.lines))

def box(self, width, height):
def box(self, width, height, align):
if width == 0:
lines = list(self.lines)
else:
lines = [line + ' ' * (width-wcswidth(line)) for line in self.lines]
lines = [afill(line, width, align) for line in self.lines]
return to_len(lines, height, ' ' * width)


def join_cells(cells, widths, col_sep='|', border='|', rstrip=True,
left_padding='', right_padding=''):
assert 0 < len(cells) == len(widths)
def join_cells(cells, widths, align, col_sep, border, rstrip, left_padding,
right_padding):
assert 0 < len(cells) == len(widths) == len(align)
height = max(len(c.lines) for c in cells)
boxes = [c.box(w, height) for (c,w) in zip(cells, widths)]
boxes = [c.box(w, height, a) for (c,w,a) in zip(cells, widths, align)]
if not border and rstrip:
boxes[-1] = cells[-1].box(0, height)
boxes[-1] = cells[-1].box(0, height, align[-1])
return [
border + left_padding
+ (right_padding + col_sep + left_padding).join(line_bits)
Expand Down Expand Up @@ -322,6 +330,17 @@ def mkpadding(s):
raise ValueError('padding cannot contain newlines')
return padding

def afill(s, width, align):
spaces = width - wcswidth(s)
if align == 'l':
return s + ' ' * spaces
elif align == 'c':
return ' ' * (spaces // 2) + s + ' ' * ((spaces+1) // 2)
elif align == 'r':
return ' ' * spaces + s
else:
raise ValueError('{!r}: invalid alignment specifier'.format(align))


class IndeterminateWidthError(ValueError):
"""
Expand Down

0 comments on commit 622c02a

Please sign in to comment.