Repository: nirum/tableprint

Issue:
Feature request: Horizontal alignment
This is a simple, lightweight, and awesome package and exactly what I needed to print some database data in my CLI app.  Any chance you could add two parameters for horizontal alignment that takes a string for the alignment of the cell data, such as "left", "center", or "right"?  Something like header_align and row_align??  As it is right now I have to pad all the strings to be the same length as the longest one otherwise the table is difficult to read.

Thanks so much!

Discussion:

Patch:
diff --git a/Makefile b/Makefile
index 6ad5c19..6fa34eb 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ upload:
 	twine upload dist/*
 
 test2:
-	python2 /Users/nirum/anaconda/bin/nosetests --logging-level=INFO
+	python2 -m nose --logging-level=INFO
 
 test:
 	nosetests -v --with-coverage --cover-package=tableprint --logging-level=INFO
diff --git a/tableprint/metadata.py b/tableprint/metadata.py
index 2b1f034..c67d4f8 100644
--- a/tableprint/metadata.py
+++ b/tableprint/metadata.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 # Version info
-__version__ = '0.8.0'
+__version__ = '0.8.1'
 __license__ = 'MIT'
 
 # Project description(s)
diff --git a/tableprint/printer.py b/tableprint/printer.py
index eb2b4bf..45f85b8 100644
--- a/tableprint/printer.py
+++ b/tableprint/printer.py
@@ -22,13 +22,16 @@ from .utils import ansi_len, format_line, parse_width
 
 __all__ = ('table', 'header', 'row', 'hrule', 'top', 'bottom', 'banner', 'dataframe', 'TableContext')
 
+# Defaults
 STYLE = 'round'
 WIDTH = 11
 FMT = '5g'
+ALIGN = 'right'
+ALIGNMENTS = {"left": "<", "right": ">", "center": "^"}
 
 
 class TableContext:
-    def __init__(self, headers, width=WIDTH, style=STYLE, add_hr=True, out=sys.stdout):
+    def __init__(self, headers, width=WIDTH, align=ALIGN, style=STYLE, add_hr=True, out=sys.stdout):
         """Context manager for table printing
 
         Parameters
@@ -39,6 +42,9 @@ class TableContext:
         width : int or array_like, optional
             The width of each column in the table (Default: 11)
 
+        align : string
+            The alignment to use ('left', 'center', or 'right'). (Default: 'right')
+
         style : string or tuple, optional
             A formatting style. (Default: 'round')
 
@@ -52,9 +58,9 @@ class TableContext:
                     t.row(np.random.randn(3))
         """
         self.out = out
-        self.config = {'width': width, 'style': style}
+        self.config = {'width': width, 'style': style, 'align': align}
         self.headers = header(headers, add_hr=add_hr, **self.config)
-        self.bottom = bottom(len(headers), **self.config)
+        self.bottom = bottom(len(headers), width=width, style=style)
 
     def __call__(self, data):
         self.out.write(row(data, **self.config) + '\n')
@@ -70,7 +76,7 @@ class TableContext:
         self.out.flush()
 
 
-def table(data, headers=None, format_spec=FMT, width=WIDTH, style=STYLE, out=sys.stdout):
+def table(data, headers=None, format_spec=FMT, width=WIDTH, align=ALIGN, style=STYLE, out=sys.stdout):
     """Print a table with the given data
 
     Parameters
@@ -87,22 +93,26 @@ def table(data, headers=None, format_spec=FMT, width=WIDTH, style=STYLE, out=sys
     width : int or array_like, optional
         The width of each column in the table (Default: 11)
 
+    align : string
+        The alignment to use ('left', 'center', or 'right'). (Default: 'right')
+
     style : string or tuple, optional
         A formatting style. (Default: 'fancy_grid')
 
     out : writer, optional
         A file handle or object that has write() and flush() methods (Default: sys.stdout)
     """
+    # Number of columns in the table.
     ncols = len(data[0]) if headers is None else len(headers)
     tablestyle = STYLES[style]
     widths = parse_width(width, ncols)
 
     # Initialize with a hr or the header
     tablestr = [hrule(ncols, widths, tablestyle.top)] \
-        if headers is None else [header(headers, widths, style)]
+        if headers is None else [header(headers, width=widths, align=align, style=style)]
 
     # parse each row
-    tablestr += [row(d, widths, format_spec, style) for d in data]
+    tablestr += [row(d, widths, format_spec, align, style) for d in data]
 
     # only add the final border if there was data in the table
     if len(data) > 0:
@@ -113,7 +123,7 @@ def table(data, headers=None, format_spec=FMT, width=WIDTH, style=STYLE, out=sys
     out.flush()
 
 
-def header(headers, width=WIDTH, style=STYLE, add_hr=True):
+def header(headers, width=WIDTH, align=ALIGN, style=STYLE, add_hr=True):
     """Returns a formatted row of column header strings
 
     Parameters
@@ -134,9 +144,10 @@ def header(headers, width=WIDTH, style=STYLE, add_hr=True):
     """
     tablestyle = STYLES[style]
     widths = parse_width(width, len(headers))
+    alignment = ALIGNMENTS[align]
 
     # string formatter
-    data = map(lambda x: ('{:^%d}' % (x[0] + ansi_len(x[1]))).format(x[1]), zip(widths, headers))
+    data = map(lambda x: ('{:%s%d}' % (alignment, x[0] + ansi_len(x[1]))).format(x[1]), zip(widths, headers))
 
     # build the formatted str
     headerstr = format_line(data, tablestyle.row)
@@ -149,7 +160,7 @@ def header(headers, width=WIDTH, style=STYLE, add_hr=True):
     return headerstr
 
 
-def row(values, width=WIDTH, format_spec=FMT, style=STYLE):
+def row(values, width=WIDTH, format_spec=FMT, align=ALIGN, style=STYLE):
     """Returns a formatted row of data
 
     Parameters
@@ -163,6 +174,9 @@ def row(values, width=WIDTH, format_spec=FMT, style=STYLE):
     format_spec : string
         The precision format string used to format numbers in the values array (Default: '5g')
 
+    align : string
+        The alignment to use ('left', 'center', or 'right'). (Default: 'right')
+
     style : namedtuple, optional
         A line formatting style
 
@@ -187,10 +201,10 @@ def row(values, width=WIDTH, format_spec=FMT, style=STYLE):
         width, datum, prec = val
 
         if isinstance(datum, string_types):
-            return ('{:>%i}' % (width + ansi_len(datum))).format(datum)
+            return ('{:%s%i}' % (ALIGNMENTS[align], width + ansi_len(datum))).format(datum)
 
         elif isinstance(datum, Number):
-            return ('{:>%i.%s}' % (width, prec)).format(datum)
+            return ('{:%s%i.%s}' % (ALIGNMENTS[align], width, prec)).format(datum)
 
         else:
             raise ValueError('Elements in the values array must be strings, ints, or floats')
@@ -255,7 +269,7 @@ def banner(message, width=30, style='banner', out=sys.stdout):
     out : writer
         An object that has write() and flush() methods (Default: sys.stdout)
     """
-    out.write(header([message], max(width, len(message)), style) + '\n')
+    out.write(header([message], width=max(width, len(message)), style=style) + '\n')
     out.flush()
 
 


Test patch:
diff --git a/tests/test_io.py b/tests/test_io.py
index c866986..d07a075 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -11,18 +11,18 @@ def test_context():
     with TableContext('ABC', style='round', width=5, out=output) as t:
         t([1, 2, 3])
         t([4, 5, 6])
-    assert output.getvalue() == '╭───────┬───────┬───────╮\n│   A   │   B   │   C   │\n├───────┼───────┼───────┤\n│     1 │     2 │     3 │\n│     4 │     5 │     6 │\n╰───────┴───────┴───────╯\n'
+    assert output.getvalue() == '╭───────┬───────┬───────╮\n│     A │     B │     C │\n├───────┼───────┼───────┤\n│     1 │     2 │     3 │\n│     4 │     5 │     6 │\n╰───────┴───────┴───────╯\n'
 
 
 def test_table():
     """Tests the table function"""
     output = StringIO()
     table([[1, 2, 3], [4, 5, 6]], 'ABC', style='round', width=5, out=output)
-    assert output.getvalue() == '╭───────┬───────┬───────╮\n│   A   │   B   │   C   │\n├───────┼───────┼───────┤\n│     1 │     2 │     3 │\n│     4 │     5 │     6 │\n╰───────┴───────┴───────╯\n'
+    assert output.getvalue() == '╭───────┬───────┬───────╮\n│     A │     B │     C │\n├───────┼───────┼───────┤\n│     1 │     2 │     3 │\n│     4 │     5 │     6 │\n╰───────┴───────┴───────╯\n'
 
     output = StringIO()
     table(["bar"], "foo", style='grid', width=3, out=output)
-    assert output.getvalue() == '+---+---+---+\n| f | o | o |\n+---+---+---+\n|  b|  a|  r|\n+---+---+---+\n'
+    assert output.getvalue() == '+---+---+---+\n|  f|  o|  o|\n+---+---+---+\n|  b|  a|  r|\n+---+---+---+\n'
 
 
 def test_frame():
@@ -30,7 +30,7 @@ def test_frame():
     df = pd.DataFrame({'a': [1,], 'b': [2,], 'c': [3,]})
     output = StringIO()
     dataframe(df, width=4, style='fancy_grid', out=output)
-    assert output.getvalue() == '╒════╤════╤════╕\n│ a  │ b  │ c  │\n╞════╪════╪════╡\n│   1│   2│   3│\n╘════╧════╧════╛\n'
+    assert output.getvalue() == '╒════╤════╤════╕\n│   a│   b│   c│\n╞════╪════╪════╡\n│   1│   2│   3│\n╘════╧════╧════╛\n'
 
 
 def test_banner():