You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

100 lines
3.6 KiB

4 years ago
  1. # coding: utf8
  2. from __future__ import unicode_literals, print_function
  3. from .util import to_string, zip_longest, basestring_
  4. ALIGN_MAP = {"l": "<", "r": ">", "c": "^"}
  5. def table(
  6. data,
  7. header=None,
  8. footer=None,
  9. divider=False,
  10. widths="auto",
  11. max_col=30,
  12. spacing=3,
  13. aligns=None,
  14. multiline=False,
  15. ):
  16. """Format tabular data.
  17. data (iterable / dict): The data to render. Either a list of lists (one per
  18. row) or a dict for two-column tables.
  19. header (iterable): The header columns.
  20. footer (iterable): The footer columns.
  21. divider (bool): Show a divider line between header/footer and body.
  22. widths (iterable or 'auto'): Column widths in order. If "auto", widths
  23. will be calculated automatically based on the largest value.
  24. max_col (int): Maximum column width.
  25. spacing (int): Spacing between columns, in spaces.
  26. aligns (iterable / unicode): Column alignments in order. 'l' (left,
  27. default), 'r' (right) or 'c' (center). If a string, value is used
  28. for all columns.
  29. multiline (bool): If a cell value is a list of a tuple, render it on
  30. multiple lines, with one value per line.
  31. RETURNS (unicode): The formatted table.
  32. """
  33. if isinstance(data, dict):
  34. data = list(data.items())
  35. if multiline:
  36. zipped_data = []
  37. for i, item in enumerate(data):
  38. vals = [v if isinstance(v, (list, tuple)) else [v] for v in item]
  39. zipped_data.extend(list(zip_longest(*vals, fillvalue="")))
  40. if i < len(data) - 1:
  41. zipped_data.append(["" for i in item])
  42. data = zipped_data
  43. if widths == "auto":
  44. widths = _get_max_widths(data, header, footer, max_col)
  45. settings = {"widths": widths, "spacing": spacing, "aligns": aligns}
  46. divider_row = row(["-" * width for width in widths], **settings)
  47. rows = []
  48. if header:
  49. rows.append(row(header, **settings))
  50. if divider:
  51. rows.append(divider_row)
  52. for i, item in enumerate(data):
  53. rows.append(row(item, **settings))
  54. if footer:
  55. if divider:
  56. rows.append(divider_row)
  57. rows.append(row(footer, **settings))
  58. return "\n{}\n".format("\n".join(rows))
  59. def row(data, widths="auto", spacing=3, aligns=None):
  60. """Format data as a table row.
  61. data (iterable): The individual columns to format.
  62. widths (iterable, int or 'auto'): Column widths, either one integer for all
  63. columns or an iterable of values. If "auto", widths will be calculated
  64. automatically based on the largest value.
  65. spacing (int): Spacing between columns, in spaces.
  66. aligns (iterable / unicode): Column alignments in order. 'l' (left,
  67. default), 'r' (right) or 'c' (center). If a string, value is used
  68. for all columns.
  69. RETURNS (unicode): The formatted row.
  70. """
  71. cols = []
  72. if isinstance(aligns, basestring_): # single align value
  73. aligns = [aligns for _ in data]
  74. if not hasattr(widths, "__iter__"): # single number
  75. widths = [widths for _ in range(len(data))]
  76. for i, col in enumerate(data):
  77. align = ALIGN_MAP.get(aligns[i] if aligns and i < len(aligns) else "l")
  78. col_width = len(col) if widths == "auto" else widths[i]
  79. tpl = "{:%s%d}" % (align, col_width)
  80. cols.append(tpl.format(to_string(col)))
  81. return (" " * spacing).join(cols)
  82. def _get_max_widths(data, header, footer, max_col):
  83. all_data = list(data)
  84. if header:
  85. all_data.append(header)
  86. if footer:
  87. all_data.append(footer)
  88. widths = [[len(to_string(col)) for col in item] for item in all_data]
  89. return [min(max(w), max_col) for w in list(zip(*widths))]