"""Pretty-print tabular data.""" from collections import namedtuple from collections.abc import Iterable, Sized from html import escape as htmlescape from itertools import chain, zip_longest as izip_longest from functools import reduce, partial import io import re import math import textwrap import dataclasses try: import wcwidth # optional wide-character (CJK) support except ImportError: wcwidth = None def _is_file(f): return isinstance(f, io.IOBase) __all__ = ["tabulate", "tabulate_formats", "simple_separated_format"] try: from .version import version as __version__ # noqa: F401 except ImportError: pass # running __init__.py as a script, AppVeyor pytests # minimum extra space in headers MIN_PADDING = 2 # Whether or not to preserve leading/trailing whitespace in data. PRESERVE_WHITESPACE = False _DEFAULT_FLOATFMT = "g" _DEFAULT_INTFMT = "" _DEFAULT_MISSINGVAL = "" # default align will be overwritten by "left", "center" or "decimal" # depending on the formatter _DEFAULT_ALIGN = "default" # if True, enable wide-character (CJK) support WIDE_CHARS_MODE = wcwidth is not None # Constant that can be used as part of passed rows to generate a separating line # It is purposely an unprintable character, very unlikely to be used in a table SEPARATING_LINE = "\001" Line = namedtuple("Line", ["begin", "hline", "sep", "end"]) DataRow = namedtuple("DataRow", ["begin", "sep", "end"]) # A table structure is supposed to be: # # --- lineabove --------- # headerrow # --- linebelowheader --- # datarow # --- linebetweenrows --- # ... (more datarows) ... # --- linebetweenrows --- # last datarow # --- linebelow --------- # # TableFormat's line* elements can be # # - either None, if the element is not used, # - or a Line tuple, # - or a function: [col_widths], [col_alignments] -> string. # # TableFormat's *row elements can be # # - either None, if the element is not used, # - or a DataRow tuple, # - or a function: [cell_values], [col_widths], [col_alignments] -> string. # # padding (an integer) is the amount of white space around data values. # # with_header_hide: # # - either None, to display all table elements unconditionally, # - or a list of elements not to be displayed if the table has column headers. # TableFormat = namedtuple( "TableFormat", [ "lineabove", "linebelowheader", "linebetweenrows", "linebelow", "headerrow", "datarow", "padding", "with_header_hide", ], ) def _is_separating_line(row): row_type = type(row) is_sl = (row_type == list or row_type == str) and ( (len(row) >= 1 and row[0] == SEPARATING_LINE) or (len(row) >= 2 and row[1] == SEPARATING_LINE) ) return is_sl def _pipe_segment_with_colons(align, colwidth): """Return a segment of a horizontal line with optional colons which indicate column's alignment (as in `pipe` output format).""" w = colwidth if align in ["right", "decimal"]: return ("-" * (w - 1)) + ":" elif align == "center": return ":" + ("-" * (w - 2)) + ":" elif align == "left": return ":" + ("-" * (w - 1)) else: return "-" * w def _pipe_line_with_colons(colwidths, colaligns): """Return a horizontal line with optional colons to indicate column's alignment (as in `pipe` output format).""" if not colaligns: # e.g. printing an empty data frame (github issue #15) colaligns = [""] * len(colwidths) segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)] return "|" + "|".join(segments) + "|" def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): 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 return (separator + colsep.join(values_with_attrs)).rstrip() def _textile_row_with_attrs(cell_values, colwidths, colaligns): cell_values[0] += " " alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."} values = (alignment.get(a, "") + v for a, v in zip(colaligns, cell_values)) return "|" + "|".join(values) + "|" def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore): # this table header will be suppressed if there is a header row return "