From da6c201f243856aac3249043f5ca43a1d2f5515b Mon Sep 17 00:00:00 2001 From: Niru Maheswaranathan Date: Thu, 18 May 2017 23:51:59 -0700 Subject: [PATCH 1/2] working on support for variable widths in columns --- setup.py | 2 +- tableprint.py | 59 ++++++++++++++++++++++++++++------------- tests/test_functions.py | 2 +- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index bab43c2..5755109 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='0.6.1', + version='0.7.0', description='Pretty console printing of tabular data', long_description='''Formatted console printing of tabular data. diff --git a/tableprint.py b/tableprint.py index 5870e53..322b85b 100644 --- a/tableprint.py +++ b/tableprint.py @@ -20,7 +20,7 @@ import numpy as np __all__ = ('table', 'header', 'row', 'hr', 'top', 'bottom', 'banner', 'dataframe', 'humantime', 'styles') -__version__ = '0.5.4' +__version__ = '0.7.0' # set up table styles LineStyle = namedtuple('LineStyle', ('begin', 'hline', 'sep', 'end')) @@ -131,15 +131,22 @@ def header(headers, width=WIDTH, style=STYLE, add_hr=True): """ tablestyle = styles[style] + # parse width + if isinstance(width, int): + widths = [width] * len(headers) + else: + assert len(width) == len(headers), "Width and headers must have the same length" + widths = width + # string formatter - data = map(lambda x: ('{:^%d}' % (width + _ansi_len(x))).format(x), headers) + data = map(lambda x: ('{:^%d}' % (x[0] + _ansi_len(x[1]))).format(x[1]), zip(widths, headers)) # build the formatted str headerstr = _format_line(data, tablestyle.row) if add_hr: - upper = hr(len(headers), width, tablestyle.top) - lower = hr(len(headers), width, tablestyle.below_header) + upper = hr(len(headers), widths, tablestyle.top) + lower = hr(len(headers), widths, tablestyle.below_header) headerstr = '\n'.join([upper, headerstr, lower]) return headerstr @@ -169,6 +176,13 @@ def row(values, width=WIDTH, format_spec=FMT, style=STYLE): """ tablestyle = styles[style] + # parse width + if isinstance(width, int): + widths = [width] * len(values) + else: + assert len(width) == len(values), "Width and values must have the same length" + widths = width + assert isinstance(format_spec, string_types) | (type(format_spec) is list), \ "format_spec must be a string or list of strings" @@ -179,7 +193,7 @@ def row(values, width=WIDTH, format_spec=FMT, style=STYLE): def mapdata(val): # unpack - datum, prec = val + width, datum, prec = val if isinstance(datum, string_types): return ('{:>%i}' % (width + _ansi_len(datum))).format(datum) @@ -191,7 +205,7 @@ def row(values, width=WIDTH, format_spec=FMT, style=STYLE): raise ValueError('Elements in the values array must be strings, ints, or floats') # string formatter - data = map(mapdata, zip(values, format_spec)) + data = map(mapdata, zip(widths, values, format_spec)) # build the row string return _format_line(data, tablestyle.row) @@ -217,7 +231,14 @@ def hr(n, width=WIDTH, linestyle=LineStyle('', '─', '─', '')): rowstr : string A string consisting of the row border to print """ - hrstr = linestyle.sep.join([('{:%s^%i}' % (linestyle.hline, width)).format('')] * n) + + # parse width + if isinstance(width, int): + widths = [width] * n + else: + widths = width + + hrstr = linestyle.sep.join([('{:%s^%i}' % (linestyle.hline, width)).format('') for width in widths]) return linestyle.begin + hrstr + linestyle.end @@ -282,19 +303,19 @@ def humantime(t): raise ValueError("Input must be numeric") # weeks - if t >= 7*60*60*24: - weeks = np.floor(t / (7.*60.*60.*24.)) - timestr = "{:g} weeks, ".format(weeks) + humantime(t % (7*60*60*24)) + if t >= 7 * 60 * 60 * 24: + weeks = np.floor(t / (7 * 60 * 60 * 24)) + timestr = "{:g} weeks, ".format(weeks) + humantime(t % (7 * 60 * 60 * 24)) # days - elif t >= 60*60*24: - days = np.floor(t / (60.*60.*24.)) - timestr = "{:g} days, ".format(days) + humantime(t % (60*60*24)) + elif t >= 60 * 60 * 24: + days = np.floor(t / (60 * 60 * 24)) + timestr = "{:g} days, ".format(days) + humantime(t % (60 * 60 * 24)) # hours - elif t >= 60*60: - hours = np.floor(t / (60.*60.)) - timestr = "{:g} hours, ".format(hours) + humantime(t % (60*60)) + elif t >= 60 * 60: + hours = np.floor(t / (60 * 60)) + timestr = "{:g} hours, ".format(hours) + humantime(t % (60 * 60)) # minutes elif t >= 60: @@ -307,15 +328,15 @@ def humantime(t): # milliseconds elif t >= 1e-3: - timestr = "{:g} ms".format(t*1e3) + timestr = "{:g} ms".format(t * 1e3) # microseconds elif t >= 1e-6: - timestr = "{:g} \u03BCs".format(t*1e6) + timestr = "{:g} \u03BCs".format(t * 1e6) # nanoseconds or smaller else: - timestr = "{:g} ns".format(t*1e9) + timestr = "{:g} ns".format(t * 1e9) return timestr diff --git a/tests/test_functions.py b/tests/test_functions.py index f14755b..8eaaec1 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -22,5 +22,5 @@ def test_row(): assert row([1, 2, 3], width=3, style='clean') == ' 1 2 3 ' # invalid - with pytest.raises(ValueError) as context: + with pytest.raises(ValueError): row([{}]) From 34eea0fd38865b7d5c782eac7eb8b8de9055ee4a Mon Sep 17 00:00:00 2001 From: Niru Maheswaranathan Date: Thu, 25 May 2017 15:25:36 -0700 Subject: [PATCH 2/2] refactors parse_width into a separate function --- tableprint/printer.py | 42 +++++++++++++----------------------------- tableprint/utils.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/tableprint/printer.py b/tableprint/printer.py index 9ede5b5..a595b29 100644 --- a/tableprint/printer.py +++ b/tableprint/printer.py @@ -19,7 +19,7 @@ import numpy as np from six import string_types from .style import LineStyle, STYLES -from .utils import ansi_len, format_line +from .utils import ansi_len, format_line, parse_width __all__ = ('table', 'header', 'row', 'hrule', 'top', 'bottom', 'banner', 'dataframe') @@ -42,7 +42,7 @@ def table(data, headers=None, format_spec=FMT, width=WIDTH, style=STYLE, out=sys format_spec : string, optional Format specification for formatting numbers (Default: '5g') - width : int, optional + width : int or array_like, optional The width of each column in the table (Default: 11) style : string or tuple, optional @@ -53,17 +53,18 @@ def table(data, headers=None, format_spec=FMT, width=WIDTH, style=STYLE, out=sys """ 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, width, tablestyle.top)] \ - if headers is None else [header(headers, width, style)] + tablestr = [hrule(ncols, widths, tablestyle.top)] \ + if headers is None else [header(headers, widths, style)] # parse each row - tablestr += [row(d, width, format_spec, style) for d in data] + tablestr += [row(d, widths, format_spec, style) for d in data] # only add the final border if there was data in the table if len(data) > 0: - tablestr += [hrule(ncols, width, tablestyle.bottom)] + tablestr += [hrule(ncols, widths, tablestyle.bottom)] # print the table out.write('\n'.join(tablestr) + '\n') @@ -90,16 +91,10 @@ def header(headers, width=WIDTH, style=STYLE, add_hr=True): A string consisting of the full header row to print """ tablestyle = STYLES[style] - - # parse width - if isinstance(width, int): - widths = [width] * len(headers) - else: - assert len(width) == len(headers), "Width and headers must have the same length" - widths = width + widths = parse_width(width, len(headers)) # string formatter - data = map(lambda x: ('{:^%d}' % (x[0] + _ansi_len(x[1]))).format(x[1]), zip(widths, headers)) + data = map(lambda x: ('{:^%d}' % (x[0] + ansi_len(x[1]))).format(x[1]), zip(widths, headers)) # build the formatted str headerstr = format_line(data, tablestyle.row) @@ -135,13 +130,7 @@ def row(values, width=WIDTH, format_spec=FMT, style=STYLE): A string consisting of the full row of data to print """ tablestyle = STYLES[style] - - # parse width - if isinstance(width, int): - widths = [width] * len(values) - else: - assert len(width) == len(values), "Width and values must have the same length" - widths = width + widths = parse_width(width, len(values)) assert isinstance(format_spec, string_types) | isinstance(format_spec, list), \ "format_spec must be a string or list of strings" @@ -191,14 +180,9 @@ def hrule(n=1, width=WIDTH, linestyle=LineStyle('', '─', '─', '')): rowstr : string A string consisting of the row border to print """ - - # parse width - if isinstance(width, int): - widths = [width] * n - else: - widths = width - - hrstr = linestyle.sep.join([('{:%s^%i}' % (linestyle.hline, width)).format('') for width in widths]) + widths = parse_width(width, n) + hrstr = linestyle.sep.join([('{:%s^%i}' % (linestyle.hline, width)).format('') + for width in widths]) return linestyle.begin + hrstr + linestyle.end diff --git a/tableprint/utils.py b/tableprint/utils.py index 9dd4e1d..d37842a 100644 --- a/tableprint/utils.py +++ b/tableprint/utils.py @@ -74,3 +74,21 @@ def ansi_len(string): def format_line(data, linestyle): """Formats a list of elements using the given line style""" return linestyle.begin + linestyle.sep.join(data) + linestyle.end + + +def parse_width(width, n): + """Parses an int or array of widths + + Parameters + ---------- + width : int or array_like + n : int + """ + if isinstance(width, int): + widths = [width] * n + + else: + assert len(width) == n, "Widths and data do not match" + widths = width + + return widths