From e3bc3e103423734d39790f9222411a0108da5cfe Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Aug 2017 16:27:33 +0200 Subject: [PATCH] fixed output format with multiline header feature when the first row is empty --- src/crate/crash/tabulate.py | 460 +++++++++++++++++++------------- src/crate/crash/test_command.py | 116 ++++++++ 2 files changed, 397 insertions(+), 179 deletions(-) diff --git a/src/crate/crash/tabulate.py b/src/crate/crash/tabulate.py index a29b4813..61a8e528 100644 --- a/src/crate/crash/tabulate.py +++ b/src/crate/crash/tabulate.py @@ -42,6 +42,7 @@ def float_format(val): if python_version_tuple()[0] < "3": from itertools import izip_longest from functools import partial + _none_type = type(None) _int_type = int _long_type = long @@ -49,12 +50,14 @@ def float_format(val): _text_type = unicode _binary_type = str + def _is_file(f): return isinstance(f, file) else: from itertools import zip_longest as izip_longest from functools import reduce, partial + _none_type = type(None) _int_type = int _long_type = int @@ -63,9 +66,15 @@ def _is_file(f): _binary_type = bytes import io + + def _is_file(f): return isinstance(f, io.IOBase) +try: + import wcwidth # optional wide-character (CJK) support +except ImportError: + wcwidth = None __all__ = ["tabulate", "tabulate_formats", "simple_separated_format"] __version__ = "0.7.5" @@ -73,6 +82,8 @@ def _is_file(f): MIN_PADDING = 0 +# if True, enable wide-character (CJK) support +WIDE_CHARS_MODE = wcwidth is not None Line = namedtuple("Line", ["begin", "hline", "sep", "end"]) @@ -139,34 +150,35 @@ def _pipe_line_with_colons(colwidths, colaligns): def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): - alignment = { "left": '', - "right": 'align="right"| ', - "center": 'align="center"| ', - "decimal": 'align="right"| ' } + alignment = {"left": '', + "right": 'align="right"| ', + "center": 'align="center"| ', + "decimal": 'align="right"| '} # hard-coded padding _around_ align attribute and value together # rather than padding parameter which affects only the value values_with_attrs = [' ' + alignment.get(a, '') + c + ' ' for c, a in zip(cell_values, colaligns)] - colsep = separator*2 + colsep = separator * 2 return (separator + colsep.join(values_with_attrs)).rstrip() def _html_row_with_attrs(celltag, cell_values, colwidths, colaligns): - alignment = { "left": '', - "right": ' style="text-align: right;"', - "center": ' style="text-align: center;"', - "decimal": ' style="text-align: right;"' } + alignment = {"left": '', + "right": ' style="text-align: right;"', + "center": ' style="text-align: center;"', + "decimal": ' style="text-align: right;"'} values_with_attrs = ["<{0}{1}>{2}".format(celltag, alignment.get(a, ''), c) for c, a in zip(cell_values, colaligns)] return "" + "".join(values_with_attrs).rstrip() + "" def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False): - alignment = { "left": "l", "right": "r", "center": "c", "decimal": "r" } + alignment = {"left": "l", "right": "r", "center": "c", "decimal": "r"} tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns]) return "\n".join(["\\begin{tabular}{" + tabular_columns_fmt + "}", "\\toprule" if booktabs else "\hline"]) + LATEX_ESCAPE_RULES = {r"&": r"\&", r"%": r"\%", r"$": r"\$", r"#": r"\#", r"_": r"\_", r"^": r"\^{}", r"{": r"\{", r"}": r"\}", r"~": r"\textasciitilde{}", "\\": r"\textbackslash{}", @@ -176,119 +188,120 @@ def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False): def _latex_row(cell_values, colwidths, colaligns): def escape_char(c): return LATEX_ESCAPE_RULES.get(c, c) + escaped_values = ["".join(map(escape_char, cell)) for cell in cell_values] rowfmt = DataRow("", "&", "\\\\") return _build_simple_row(escaped_values, rowfmt) _table_formats = {"simple": - TableFormat(lineabove=Line("", "-", " ", ""), - linebelowheader=Line("", "-", " ", ""), - linebetweenrows=None, - linebelow=Line("", "-", " ", ""), - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, - with_header_hide=["lineabove", "linebelow"]), + TableFormat(lineabove=Line("", "-", " ", ""), + linebelowheader=Line("", "-", " ", ""), + linebetweenrows=None, + linebelow=Line("", "-", " ", ""), + headerrow=DataRow("", " ", ""), + datarow=DataRow("", " ", ""), + padding=0, + with_header_hide=["lineabove", "linebelow"]), "plain": - TableFormat(lineabove=None, linebelowheader=None, - linebetweenrows=None, linebelow=None, - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, with_header_hide=None), + TableFormat(lineabove=None, linebelowheader=None, + linebetweenrows=None, linebelow=None, + headerrow=DataRow("", " ", ""), + datarow=DataRow("", " ", ""), + padding=0, with_header_hide=None), "grid": - TableFormat(lineabove=Line("+", "-", "+", "+"), - linebelowheader=Line("+", "=", "+", "+"), - linebetweenrows=Line("+", "-", "+", "+"), - linebelow=Line("+", "-", "+", "+"), - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, with_header_hide=None), + TableFormat(lineabove=Line("+", "-", "+", "+"), + linebelowheader=Line("+", "=", "+", "+"), + linebetweenrows=Line("+", "-", "+", "+"), + linebelow=Line("+", "-", "+", "+"), + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, with_header_hide=None), "fancy_grid": - TableFormat(lineabove=Line("╒", "═", "╤", "╕"), - linebelowheader=Line("╞", "═", "╪", "╡"), - linebetweenrows=Line("├", "─", "┼", "┤"), - linebelow=Line("╘", "═", "╧", "╛"), - headerrow=DataRow("│", "│", "│"), - datarow=DataRow("│", "│", "│"), - padding=1, with_header_hide=None), + TableFormat(lineabove=Line("╒", "═", "╤", "╕"), + linebelowheader=Line("╞", "═", "╪", "╡"), + linebetweenrows=Line("├", "─", "┼", "┤"), + linebelow=Line("╘", "═", "╧", "╛"), + headerrow=DataRow("│", "│", "│"), + datarow=DataRow("│", "│", "│"), + padding=1, with_header_hide=None), "pipe": - TableFormat(lineabove=_pipe_line_with_colons, - linebelowheader=_pipe_line_with_colons, - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, - with_header_hide=["lineabove"]), + TableFormat(lineabove=_pipe_line_with_colons, + linebelowheader=_pipe_line_with_colons, + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=["lineabove"]), "orgtbl": - TableFormat(lineabove=None, - linebelowheader=Line("|", "-", "+", "|"), - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, with_header_hide=None), + TableFormat(lineabove=None, + linebelowheader=Line("|", "-", "+", "|"), + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, with_header_hide=None), "psql": - TableFormat(lineabove=Line("+", "-", "+", "+"), - linebelowheader=Line("|", "-", "+", "|"), - linebetweenrows=None, - linebelow=Line("+", "-", "+", "+"), - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, with_header_hide=None), + TableFormat(lineabove=Line("+", "-", "+", "+"), + linebelowheader=Line("|", "-", "+", "|"), + linebetweenrows=None, + linebelow=Line("+", "-", "+", "+"), + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, with_header_hide=None), "rst": - TableFormat(lineabove=Line("", "=", " ", ""), - linebelowheader=Line("", "=", " ", ""), - linebetweenrows=None, - linebelow=Line("", "=", " ", ""), - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, with_header_hide=None), + TableFormat(lineabove=Line("", "=", " ", ""), + linebelowheader=Line("", "=", " ", ""), + linebetweenrows=None, + linebelow=Line("", "=", " ", ""), + headerrow=DataRow("", " ", ""), + datarow=DataRow("", " ", ""), + padding=0, with_header_hide=None), "mediawiki": - TableFormat(lineabove=Line("{| class=\"wikitable\" style=\"text-align: left;\"", - "", "", "\n|+ \n|-"), - linebelowheader=Line("|-", "", "", ""), - linebetweenrows=Line("|-", "", "", ""), - linebelow=Line("|}", "", "", ""), - headerrow=partial(_mediawiki_row_with_attrs, "!"), - datarow=partial(_mediawiki_row_with_attrs, "|"), - padding=0, with_header_hide=None), + TableFormat(lineabove=Line("{| class=\"wikitable\" style=\"text-align: left;\"", + "", "", "\n|+ \n|-"), + linebelowheader=Line("|-", "", "", ""), + linebetweenrows=Line("|-", "", "", ""), + linebelow=Line("|}", "", "", ""), + headerrow=partial(_mediawiki_row_with_attrs, "!"), + datarow=partial(_mediawiki_row_with_attrs, "|"), + padding=0, with_header_hide=None), "html": - TableFormat(lineabove=Line("", "", "", ""), - linebelowheader=None, - linebetweenrows=None, - linebelow=Line("
", "", "", ""), - headerrow=partial(_html_row_with_attrs, "th"), - datarow=partial(_html_row_with_attrs, "td"), - padding=0, with_header_hide=None), + TableFormat(lineabove=Line("", "", "", ""), + linebelowheader=None, + linebetweenrows=None, + linebelow=Line("
", "", "", ""), + headerrow=partial(_html_row_with_attrs, "th"), + datarow=partial(_html_row_with_attrs, "td"), + padding=0, with_header_hide=None), "latex": - TableFormat(lineabove=_latex_line_begin_tabular, - linebelowheader=Line("\\hline", "", "", ""), - linebetweenrows=None, - linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), - headerrow=_latex_row, - datarow=_latex_row, - padding=1, with_header_hide=None), + TableFormat(lineabove=_latex_line_begin_tabular, + linebelowheader=Line("\\hline", "", "", ""), + linebetweenrows=None, + linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), + headerrow=_latex_row, + datarow=_latex_row, + padding=1, with_header_hide=None), "latex_booktabs": - TableFormat(lineabove=partial(_latex_line_begin_tabular, booktabs=True), - linebelowheader=Line("\\midrule", "", "", ""), - linebetweenrows=None, - linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""), - headerrow=_latex_row, - datarow=_latex_row, - padding=1, with_header_hide=None), + TableFormat(lineabove=partial(_latex_line_begin_tabular, booktabs=True), + linebelowheader=Line("\\midrule", "", "", ""), + linebetweenrows=None, + linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""), + headerrow=_latex_row, + datarow=_latex_row, + padding=1, with_header_hide=None), "tsv": - TableFormat(lineabove=None, linebelowheader=None, - linebetweenrows=None, linebelow=None, - headerrow=DataRow("", "\t", ""), - datarow=DataRow("", "\t", ""), - padding=0, with_header_hide=None)} - + TableFormat(lineabove=None, linebelowheader=None, + linebetweenrows=None, linebelow=None, + headerrow=DataRow("", "\t", ""), + datarow=DataRow("", "\t", ""), + padding=0, with_header_hide=None)} tabulate_formats = list(sorted(_table_formats.keys())) - +_multiline_codes = re.compile(r"\r|\n|\r\n") +_multiline_codes_bytes = re.compile(b"\r|\n|\r\n") _invisible_codes = re.compile(r"\x1b\[\d*m|\x1b\[\d*\;\d*\;\d*m") # ANSI color codes _invisible_codes_bytes = re.compile(b"\x1b\[\d*m|\x1b\[\d*\;\d*\;\d*m") # ANSI color codes @@ -334,10 +347,10 @@ def _isint(string, inttype=int): >>> _isint("123.45") False """ - return type(string) is inttype or\ - (isinstance(string, _binary_type) or isinstance(string, _text_type))\ - and\ - _isconvertible(inttype, string) + return type(string) is inttype or \ + (isinstance(string, _binary_type) or isinstance(string, _text_type)) \ + and \ + _isconvertible(inttype, string) def _type(string, has_invisible=True): @@ -357,7 +370,7 @@ def _type(string, has_invisible=True): """ if has_invisible and \ - (isinstance(string, _text_type) or isinstance(string, _binary_type)): + (isinstance(string, _text_type) or isinstance(string, _binary_type)): string = _strip_invisible(string) if string is None: @@ -410,10 +423,12 @@ def _padleft(width, s, has_invisible=True): True """ + def impl(val): iwidth = width + len(val) - len(_strip_invisible(val)) if has_invisible else width fmt = "{0:>%ds}" % iwidth return fmt.format(val) + num_lines = s.splitlines() return len(num_lines) > 1 and u'\n'.join(map(impl, num_lines)) or impl(s) @@ -425,10 +440,12 @@ def _padright(width, s, has_invisible=True): True """ + def impl(val): iwidth = width + len(val) - len(_strip_invisible(val)) if has_invisible else width fmt = "{0:<%ds}" % iwidth return fmt.format(val) + num_lines = s.splitlines() return len(num_lines) > 1 and u'\n'.join(map(impl, num_lines)) or impl(s) @@ -440,14 +457,20 @@ def _padboth(width, s, has_invisible=True): True """ + def impl(val): iwidth = width + len(val) - len(_strip_invisible(val)) if has_invisible else width fmt = "{0:^%ds}" % iwidth return fmt.format(val) + num_lines = s.splitlines() return len(num_lines) > 1 and u'\n'.join(map(impl, num_lines)) or impl(s) +def _padnone(ignore_width, s): + return s + + def _strip_invisible(s): "Remove invisible ANSI color codes." if isinstance(s, _text_type): @@ -455,6 +478,7 @@ def _strip_invisible(s): else: # a bytestring return re.sub(_invisible_codes_bytes, "", s) + def _max_line_width(s): """ Visible width of a potentially multinie content. @@ -467,6 +491,7 @@ def _max_line_width(s): return 0 return max(map(len, s.splitlines())) + def _visible_width(s): """Visible width of a printed string. ANSI color codes are removed. @@ -479,17 +504,33 @@ def _visible_width(s): else: return _max_line_width(_text_type(s)) +def _is_multiline(s): + if isinstance(s, _text_type): + return bool(re.search(_multiline_codes, s)) + else: # a bytestring + return bool(re.search(_multiline_codes_bytes, s)) -def _align_column(strings, alignment, minwidth=0, has_invisible=True): - """[string] -> [padded_string] - >>> list(map(str,_align_column(["12.345", "-1234.5", "1.23", "1234.5", "1e+234", "1.0e234"], "decimal"))) - [' 12.345 ', '-1234.5 ', ' 1.23 ', ' 1234.5 ', ' 1e+234 ', ' 1.0e234'] +def _multiline_width(multiline_s, line_width_fn=len): + return max(map(line_width_fn, re.split("[\r\n]", multiline_s))) - >>> list(map(str,_align_column(['123.4', '56.7890'], None))) - ['123.4', '56.7890'] - """ +def _choose_width_fn(has_invisible, enable_widechars, is_multiline): + """Return a function to calculate visible cell width.""" + if has_invisible: + line_width_fn = _visible_width + elif enable_widechars: # optional wide-character support if available + line_width_fn = wcwidth.wcswidth + else: + line_width_fn = len + if is_multiline: + width_fn = lambda s: _multiline_width(s, line_width_fn) + else: + width_fn = line_width_fn + return width_fn + + +def _align_column_choose_padfn(strings, alignment, has_invisible): if alignment == "right": strings = [s.strip() for s in strings] padfn = _padleft @@ -506,24 +547,61 @@ def _align_column(strings, alignment, minwidth=0, has_invisible=True): for s, decs in zip(strings, decimals)] padfn = _padleft elif not alignment: - return strings + padfn = _padnone else: strings = [s.strip() for s in strings] padfn = _padright + return strings, padfn - if has_invisible: - width_fn = _visible_width - else: - width_fn = _max_line_width - maxwidth = max(max(map(width_fn, strings)), minwidth) - padded_strings = [padfn(maxwidth, s, has_invisible) for s in strings] +def _align_column(strings, alignment, minwidth=0, + has_invisible=True, enable_widechars=False, is_multiline=False): + """[string] -> [padded_string] + + >>> list(map(str,_align_column(["12.345", "-1234.5", "1.23", "1234.5", "1e+234", "1.0e234"], "decimal"))) + [' 12.345 ', '-1234.5 ', ' 1.23 ', ' 1234.5 ', ' 1e+234 ', ' 1.0e234'] + + >>> list(map(str,_align_column(['123.4', '56.7890'], None))) + ['123.4', '56.7890'] + + """ + strings, padfn = _align_column_choose_padfn(strings, alignment, has_invisible) + width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline) + + s_widths = list(map(width_fn, strings)) + maxwidth = max(max(s_widths), minwidth) + # TODO: refactor column alignment in single-line and multiline modes + if is_multiline: + if not enable_widechars and not has_invisible: + padded_strings = [ + "\n".join([padfn(maxwidth, s) for s in ms.splitlines()]) + for ms in strings] + else: + # enable wide-character width corrections + s_lens = [max((len(s) for s in re.split("[\r\n]", ms))) for ms in strings] + visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)] + # wcswidth and _visible_width don't count invisible characters; + # padfn doesn't need to apply another correction + if strings[0] == '': + strings[0] = ' ' + padded_strings = ["\n".join([padfn(w, s) for s in (ms.splitlines() or ms)]) + for ms, w in zip(strings, visible_widths)] + else: # single-line cell values + if not enable_widechars and not has_invisible: + padded_strings = [padfn(maxwidth, s) for s in strings] + else: + # enable wide-character width corrections + s_lens = list(map(len, strings)) + visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)] + # wcswidth and _visible_width don't count invisible characters; + # padfn doesn't need to apply another correction + padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)] return padded_strings def _more_generic(type1, type2): - types = { _none_type: 0, int: 1, float: 2, _binary_type: 3, _text_type: 4 } - invtypes = { 4: _text_type, 3: _binary_type, 2: float, 1: int, 0: _none_type } + types = {_none_type: 0, int: 1, float: 2, _binary_type: 3, _text_type: 4} + invtypes = {4: _text_type, 3: _binary_type, 2: float, 1: int, 0: _none_type} moregeneric = max(types.get(type1, 4), types.get(type2, 4)) return invtypes[moregeneric] @@ -587,7 +665,14 @@ def _format(val, valtype, floatfmt, missingval="", has_invisible=True): return "{0}".format(val) -def _align_header(header, alignment, width): +def _align_header(header, alignment, width, visible_width, enable_widechars=False, is_multiline=False): + if is_multiline: + header_lines = re.split(_multiline_codes, header) + padded_lines = [_align_header(h, alignment, width, visible_width) for h in header_lines] + return "\n".join(padded_lines) + # else: not multiline + ninvisible = max(0, len(header) - visible_width) + width += ninvisible if alignment == "left": return _padright(width, header) elif alignment == "center": @@ -635,19 +720,19 @@ def _normalize_tabular_data(tabular_data, headers): keys = tabular_data.keys() vals = tabular_data.values # values matrix doesn't need to be transposed names = tabular_data.index - rows = [[v]+list(row) for v,row in zip(names, vals)] + rows = [[v] + list(row) for v, row in zip(names, vals)] else: raise ValueError("tabular data doesn't appear to be a dict or a DataFrame") if headers == "keys": - headers = list(map(_text_type,keys)) # headers should be strings + headers = list(map(_text_type, keys)) # headers should be strings else: # it's a usual an iterable of iterables, or a NumPy array rows = list(tabular_data) if (headers == "keys" and - hasattr(tabular_data, "dtype") and - getattr(tabular_data.dtype, "names")): + hasattr(tabular_data, "dtype") and + getattr(tabular_data.dtype, "names")): # numpy record array headers = tabular_data.dtype.names elif (headers == "keys" @@ -659,8 +744,8 @@ def _normalize_tabular_data(tabular_data, headers): elif (len(rows) > 0 and isinstance(rows[0], dict)): # dict or OrderedDict - uniq_keys = set() # implements hashed lookup - keys = [] # storage for set + uniq_keys = set() # implements hashed lookup + keys = [] # storage for set if headers == "firstrow": firstdict = rows[0] if len(rows) > 0 else {} keys.extend(firstdict.keys()) @@ -668,7 +753,7 @@ def _normalize_tabular_data(tabular_data, headers): rows = rows[1:] for row in rows: for k in row.keys(): - #Save unique items in input order + # Save unique items in input order if k not in uniq_keys: keys.append(k) uniq_keys.add(k) @@ -693,18 +778,18 @@ def _normalize_tabular_data(tabular_data, headers): # take headers from the first row if necessary if headers == "firstrow" and len(rows) > 0: - headers = list(map(_text_type, rows[0])) # headers should be strings + headers = list(map(_text_type, rows[0])) # headers should be strings rows = rows[1:] - headers = list(map(_text_type,headers)) - rows = list(map(list,rows)) + headers = list(map(_text_type, headers)) + rows = list(map(list, rows)) # pad with empty headers for initial columns if necessary if headers and len(rows) > 0: - nhs = len(headers) - ncols = len(rows[0]) - if nhs < ncols: - headers = [""]*(ncols - nhs) + headers + nhs = len(headers) + ncols = len(rows[0]) + if nhs < ncols: + headers = [""] * (ncols - nhs) + headers return rows, headers @@ -960,23 +1045,23 @@ def tabulate(tabular_data, headers=(), tablefmt="simple", # optimization: look for ANSI control codes once, # enable smart width functions only if a control code is found plain_text = '\n'.join(['\t'.join(map(_text_type, headers))] + \ - ['\t'.join(map(_text_type, row)) for row in list_of_lists]) + ['\t'.join(map(_text_type, row)) for row in list_of_lists]) + has_invisible = re.search(_invisible_codes, plain_text) - if has_invisible: - width_fn = _visible_width - else: - width_fn = _max_line_width + enable_widechars = wcwidth is not None and WIDE_CHARS_MODE + is_multiline = _is_multiline(plain_text) + width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline) # format rows and columns, convert numeric values to strings cols = list(zip(*list_of_lists)) coltypes = list(map(_column_type, cols)) cols = [[_format(v, ct, floatfmt, missingval, has_invisible) for v in c] - for c,ct in zip(cols, coltypes)] + for c, ct in zip(cols, coltypes)] # align columns - aligns = [numalign if ct in [int,float] else stralign for ct in coltypes] - minwidths = [width_fn(h) + MIN_PADDING for h in headers] if headers else [0]*len(cols) - cols = [_align_column(c, a, minw, has_invisible) + aligns = [numalign if ct in [int, float] else stralign for ct in coltypes] + minwidths = [width_fn(h) + MIN_PADDING for h in headers] if headers else [0] * len(cols) + cols = [_align_column(c, a, minw, has_invisible, enable_widechars, is_multiline) for c, a, minw in zip(cols, aligns, minwidths)] if headers: @@ -984,17 +1069,17 @@ def tabulate(tabular_data, headers=(), tablefmt="simple", t_cols = cols or [['']] * len(headers) t_aligns = aligns or [stralign] * len(headers) minwidths = [max(minw, width_fn(c[0])) for minw, c in zip(minwidths, t_cols)] - headers = [_align_header(h, a, minw) + headers = [_align_header(h, a, minw, width_fn(h), enable_widechars, is_multiline) for h, a, minw in zip(headers, t_aligns, minwidths)] rows = list(zip(*cols)) else: minwidths = [width_fn(c[0]) for c in cols] rows = list(zip(*cols)) - + if not isinstance(tablefmt, TableFormat): tablefmt = _table_formats.get(tablefmt, _table_formats["simple"]) - return _format_table(tablefmt, headers, rows, minwidths, aligns) + return _format_table(tablefmt, headers, rows, minwidths, aligns, is_multiline) def _build_simple_row(padded_cells, rowfmt): @@ -1020,65 +1105,82 @@ def _build_line(colwidths, colaligns, linefmt): if hasattr(linefmt, "__call__"): return linefmt(colwidths, colaligns) else: - begin, fill, sep, end = linefmt - cells = [fill*w for w in colwidths] + begin, fill, sep, end = linefmt + cells = [fill * w for w in colwidths] return _build_simple_row(cells, (begin, sep, end)) def _pad_row(cells, padding): if cells: pad = " " * padding - padded_cells = ['\n'.join([pad + sub_cell + pad for sub_cell in cell.splitlines()]) for cell in cells] + padded_cells = [pad + cell + pad for cell in cells] return padded_cells else: return cells -def _format_table(fmt, headers, rows, colwidths, colaligns): +def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt): + lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt)) + return lines + + +def _append_multiline_row(lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad): + colwidths = [w - 2 * pad for w in padded_widths] + cells_lines = [c.splitlines() for c in padded_multiline_cells] + nlines = max(map(len, cells_lines)) # number of lines in the row + # vertically pad cells where some lines are missing + cells_lines = [(cl + [' ' * w] * (nlines - len(cl))) for cl, w in zip(cells_lines, colwidths)] + lines_cells = [[cl[i] for cl in cells_lines] for i in range(nlines)] + for ln in lines_cells: + padded_ln = _pad_row(ln, 1) + _append_basic_row(lines, padded_ln, colwidths, colaligns, rowfmt) + return lines + + +def _append_line(lines, colwidths, colaligns, linefmt): + lines.append(_build_line(colwidths, colaligns, linefmt)) + return lines + + +def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline): """Produce a plain-text representation of the table.""" lines = [] hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else [] pad = fmt.padding headerrow = fmt.headerrow - padded_widths = [(w + 2*pad) for w in colwidths] - padded_headers = _pad_row(headers, pad) - padded_rows = [_pad_row(row, pad) for row in rows] + padded_widths = [(w + 2 * pad) for w in colwidths] + if is_multiline: + pad_row = lambda row, _: row # do it later, in _append_multiline_row + append_row = partial(_append_multiline_row, pad=pad) + else: + pad_row = _pad_row + append_row = _append_basic_row - def get_sub_row(row, idx): - new_row = [] - col_idx = 0 - for col in row: - subrows = col.splitlines() - new_row.append(idx < len(subrows) and subrows[idx] or " " * padded_widths[col_idx]) - col_idx += 1 - return new_row + padded_headers = pad_row(headers, pad) + padded_rows = [pad_row(row, pad) for row in rows] if fmt.lineabove and "lineabove" not in hidden: - lines.append(_build_line(padded_widths, colaligns, fmt.lineabove)) + _append_line(lines, padded_widths, colaligns, fmt.lineabove) if padded_headers: - lines.append(_build_row(padded_headers, padded_widths, colaligns, headerrow)) + append_row(lines, padded_headers, padded_widths, colaligns, headerrow) if fmt.linebelowheader and "linebelowheader" not in hidden: - lines.append(_build_line(padded_widths, colaligns, fmt.linebelowheader)) + _append_line(lines, padded_widths, colaligns, fmt.linebelowheader) if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: # initial rows with a line below for row in padded_rows[:-1]: - max_height = max(map(lambda x: len(x.splitlines()), row)) - for line in range(max_height): - lines.append(_build_row(get_sub_row(row, line), padded_widths, colaligns, fmt.datarow)) - lines.append(_build_line(padded_widths, colaligns, fmt.linebetweenrows)) + append_row(lines, row, padded_widths, colaligns, fmt.datarow) + _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) # the last row without a line below - lines.append(_build_row(padded_rows[-1], padded_widths, colaligns, fmt.datarow)) + append_row(lines, padded_rows[-1], padded_widths, colaligns, fmt.datarow) else: for row in padded_rows: - max_height = max(map(lambda x: len(x.splitlines()), row)) - for line in range(max_height): - lines.append(_build_row(get_sub_row(row, line), padded_widths, colaligns, fmt.datarow)) + append_row(lines, row, padded_widths, colaligns, fmt.datarow) if fmt.linebelow and "linebelow" not in hidden: - lines.append(_build_line(padded_widths, colaligns, fmt.linebelow)) + _append_line(lines, padded_widths, colaligns, fmt.linebelow) return "\n".join(lines) @@ -1111,8 +1213,8 @@ def _main(): usage = textwrap.dedent(_main.__doc__) try: opts, args = getopt.getopt(sys.argv[1:], - "h1o:s:F:f:", - ["help", "header", "output", "sep=", "float=", "format="]) + "h1o:s:F:f:", + ["help", "header", "output", "sep=", "float=", "format="]) except getopt.GetoptError as e: print(e) print(usage) diff --git a/src/crate/crash/test_command.py b/src/crate/crash/test_command.py index 1ea206d0..131ebe97 100644 --- a/src/crate/crash/test_command.py +++ b/src/crate/crash/test_command.py @@ -383,6 +383,23 @@ def test_tabulate_boolean_int_column(self): cmd.pprint(rows, cols=['x']) self.assertEqual(expected, output.getvalue()) + def test_multiline_header(self): + """ + Create another column with a non-string value and FALSE. + """ + rows = [[u'FALSE'], [1]] + expected = "\n".join(['+-------+', + '| x |', + '| y |', + '+-------+', + '| FALSE |', + '| 1 |', + '+-------+\n']) + cmd = CrateCmd() + with patch('sys.stdout', new_callable=StringIO) as output: + cmd.pprint(rows, cols=['x\ny']) + self.assertEqual(expected, output.getvalue()) + def test_multiline_row(self): """ Create ta column that holds rows with multiline text. @@ -401,6 +418,105 @@ def test_multiline_row(self): cmd.pprint(rows, cols=['show create table foo', 'a', 'b']) self.assertEqual(expected, output.getvalue()) + def test_tabulate_empty_line(self): + + self.maxDiff=None + rows = [u'Aldebaran', u'Star System'], [u'Berlin', u'City'], [u'Galactic Sector QQ7 Active J Gamma', u'Galaxy'], [u'', u'Planet'] + expected = "\n".join(['+------------------------------------+-------------+', + '| min(name) | kind |', + '+------------------------------------+-------------+', + '| Aldebaran | Star System |', + '| Berlin | City |', + '| Galactic Sector QQ7 Active J Gamma | Galaxy |', + '| | Planet |', + '+------------------------------------+-------------+\n']) + + cmd = CrateCmd() + with patch('sys.stdout', new_callable=StringIO) as output: + cmd.pprint(rows, cols=['min(name)', 'kind']) + #assert 0 + self.assertEqual(expected, output.getvalue()) + + def test_tabulate_empty_line_first_row(self): + + self.maxDiff=None + rows = [u'', u'Planet'], [u'Aldebaran', u'Star System'], [u'Berlin', u'City'], [u'Galactic Sector QQ7 Active J Gamma', u'Galaxy'] + expected = "\n".join(['+------------------------------------+-------------+', + '| min(name) | kind |', + '+------------------------------------+-------------+', + '| | Planet |', + '| Aldebaran | Star System |', + '| Berlin | City |', + '| Galactic Sector QQ7 Active J Gamma | Galaxy |', + '+------------------------------------+-------------+\n']) + + cmd = CrateCmd() + with patch('sys.stdout', new_callable=StringIO) as output: + cmd.pprint(rows, cols=['min(name)', 'kind']) + self.assertEqual(expected, output.getvalue()) + + def test_formating_1(self): + + self.maxDiff=None + rows = [u'', u''], [u'Aldebaran', u'Aldebaran'], [u'Algol', u'Algol'], [u'Allosimanius Syneca', u'Allosimanius - Syneca'], [u'Alpha Centauri', u'Alpha - Centauri'] + expected = "\n".join(['+---------------------+-----------------------+', + '| name | replaced |', + '+---------------------+-----------------------+', + '| | |', + '| Aldebaran | Aldebaran |', + '| Algol | Algol |', + '| Allosimanius Syneca | Allosimanius - Syneca |', + '| Alpha Centauri | Alpha - Centauri |', + '+---------------------+-----------------------+\n']) + + cmd = CrateCmd() + with patch('sys.stdout', new_callable=StringIO) as output: + cmd.pprint(rows, cols=['name', 'replaced']) + self.assertEqual(expected, output.getvalue()) + + def test_formating_2(self): + + self.maxDiff=None + rows = [u'Features and conformance views', u'FALSE', u'', u''], [u'Features and conformance views', u'TRUE', 1, u'SQL_FEATURES view'], [u'Features and conformance views', u'FALSE', 2, u'SQL_SIZING view'], [u'Features and conformance views', u'FALSE', 3, u'SQL_LANGUAGES view'] + expected = "\n".join(['+--------------------------------+--------------+----------------+--------------------+', + '| feature_name | is_supported | sub_feature_id | sub_feature_name |', + '+--------------------------------+--------------+----------------+--------------------+', + '| Features and conformance views | FALSE | | |', + '| Features and conformance views | TRUE | 1 | SQL_FEATURES view |', + '| Features and conformance views | FALSE | 2 | SQL_SIZING view |', + '| Features and conformance views | FALSE | 3 | SQL_LANGUAGES view |', + '+--------------------------------+--------------+----------------+--------------------+\n']) + + cmd = CrateCmd() + with patch('sys.stdout', new_callable=StringIO) as output: + cmd.pprint(rows, cols=['feature_name', 'is_supported', 'sub_feature_id', 'sub_feature_name']) + self.assertEqual(expected, output.getvalue()) + + def test_formating_1(self): + + self.maxDiff=None + rows = [u'', 1.0], [u'Aldebaran', 1.0], [u'Algol', 1.0], [u'Allosimanius Syneca', 1.0], [u'Alpha Centauri', 1.0], [u'Argabuthon', 1.0], [u'Arkintoofle Minor', 1.0], [u'Galactic Sector QQ7 Active J Gamma', 1.0], [u'North West Ripple', 1.0], [u'Outer Eastern Rim', 1.0], [u'NULL', 1.0] + expected = "\n".join(['+------------------------------------+--------+', + '| name | _score |', + '+------------------------------------+--------+', + '| | 1.0 |', + '| Aldebaran | 1.0 |', + '| Algol | 1.0 |', + '| Allosimanius Syneca | 1.0 |', + '| Alpha Centauri | 1.0 |', + '| Argabuthon | 1.0 |', + '| Arkintoofle Minor | 1.0 |', + '| Galactic Sector QQ7 Active J Gamma | 1.0 |', + '| North West Ripple | 1.0 |', + '| Outer Eastern Rim | 1.0 |', + '| NULL | 1.0 |', + '+------------------------------------+--------+\n']) + + cmd = CrateCmd() + with patch('sys.stdout', new_callable=StringIO) as output: + cmd.pprint(rows, cols=['name', '_score']) + self.assertEqual(expected, output.getvalue()) + def test_error_exit_code(self): """Test returns an error exit code""" stmt = u"select * from invalid sql statement"