m2m模型翻译
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.

341 lines
14 KiB

6 months ago
  1. # Human friendly input/output in Python.
  2. #
  3. # Author: Peter Odding <peter@peterodding.com>
  4. # Last Change: February 16, 2020
  5. # URL: https://humanfriendly.readthedocs.io
  6. """
  7. Functions that render ASCII tables.
  8. Some generic notes about the table formatting functions in this module:
  9. - These functions were not written with performance in mind (*at all*) because
  10. they're intended to format tabular data to be presented on a terminal. If
  11. someone were to run into a performance problem using these functions, they'd
  12. be printing so much tabular data to the terminal that a human wouldn't be
  13. able to digest the tabular data anyway, so the point is moot :-).
  14. - These functions ignore ANSI escape sequences (at least the ones generated by
  15. the :mod:`~humanfriendly.terminal` module) in the calculation of columns
  16. widths. On reason for this is that column names are highlighted in color when
  17. connected to a terminal. It also means that you can use ANSI escape sequences
  18. to highlight certain column's values if you feel like it (for example to
  19. highlight deviations from the norm in an overview of calculated values).
  20. """
  21. # Standard library modules.
  22. import collections
  23. import re
  24. # Modules included in our package.
  25. from humanfriendly.compat import coerce_string
  26. from humanfriendly.terminal import (
  27. ansi_strip,
  28. ansi_width,
  29. ansi_wrap,
  30. terminal_supports_colors,
  31. find_terminal_size,
  32. HIGHLIGHT_COLOR,
  33. )
  34. # Public identifiers that require documentation.
  35. __all__ = (
  36. 'format_pretty_table',
  37. 'format_robust_table',
  38. 'format_rst_table',
  39. 'format_smart_table',
  40. )
  41. # Compiled regular expression pattern to recognize table columns containing
  42. # numeric data (integer and/or floating point numbers). Used to right-align the
  43. # contents of such columns.
  44. #
  45. # Pre-emptive snarky comment: This pattern doesn't match every possible
  46. # floating point number notation!?!1!1
  47. #
  48. # Response: I know, that's intentional. The use of this regular expression
  49. # pattern has a very high DWIM level and weird floating point notations do not
  50. # fall under the DWIM umbrella :-).
  51. NUMERIC_DATA_PATTERN = re.compile(r'^\d+(\.\d+)?$')
  52. def format_smart_table(data, column_names):
  53. """
  54. Render tabular data using the most appropriate representation.
  55. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
  56. containing the rows of the table, where each row is an
  57. iterable containing the columns of the table (strings).
  58. :param column_names: An iterable of column names (strings).
  59. :returns: The rendered table (a string).
  60. If you want an easy way to render tabular data on a terminal in a human
  61. friendly format then this function is for you! It works as follows:
  62. - If the input data doesn't contain any line breaks the function
  63. :func:`format_pretty_table()` is used to render a pretty table. If the
  64. resulting table fits in the terminal without wrapping the rendered pretty
  65. table is returned.
  66. - If the input data does contain line breaks or if a pretty table would
  67. wrap (given the width of the terminal) then the function
  68. :func:`format_robust_table()` is used to render a more robust table that
  69. can deal with data containing line breaks and long text.
  70. """
  71. # Normalize the input in case we fall back from a pretty table to a robust
  72. # table (in which case we'll definitely iterate the input more than once).
  73. data = [normalize_columns(r) for r in data]
  74. column_names = normalize_columns(column_names)
  75. # Make sure the input data doesn't contain any line breaks (because pretty
  76. # tables break horribly when a column's text contains a line break :-).
  77. if not any(any('\n' in c for c in r) for r in data):
  78. # Render a pretty table.
  79. pretty_table = format_pretty_table(data, column_names)
  80. # Check if the pretty table fits in the terminal.
  81. table_width = max(map(ansi_width, pretty_table.splitlines()))
  82. num_rows, num_columns = find_terminal_size()
  83. if table_width <= num_columns:
  84. # The pretty table fits in the terminal without wrapping!
  85. return pretty_table
  86. # Fall back to a robust table when a pretty table won't work.
  87. return format_robust_table(data, column_names)
  88. def format_pretty_table(data, column_names=None, horizontal_bar='-', vertical_bar='|'):
  89. """
  90. Render a table using characters like dashes and vertical bars to emulate borders.
  91. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
  92. containing the rows of the table, where each row is an
  93. iterable containing the columns of the table (strings).
  94. :param column_names: An iterable of column names (strings).
  95. :param horizontal_bar: The character used to represent a horizontal bar (a
  96. string).
  97. :param vertical_bar: The character used to represent a vertical bar (a
  98. string).
  99. :returns: The rendered table (a string).
  100. Here's an example:
  101. >>> from humanfriendly.tables import format_pretty_table
  102. >>> column_names = ['Version', 'Uploaded on', 'Downloads']
  103. >>> humanfriendly_releases = [
  104. ... ['1.23', '2015-05-25', '218'],
  105. ... ['1.23.1', '2015-05-26', '1354'],
  106. ... ['1.24', '2015-05-26', '223'],
  107. ... ['1.25', '2015-05-26', '4319'],
  108. ... ['1.25.1', '2015-06-02', '197'],
  109. ... ]
  110. >>> print(format_pretty_table(humanfriendly_releases, column_names))
  111. -------------------------------------
  112. | Version | Uploaded on | Downloads |
  113. -------------------------------------
  114. | 1.23 | 2015-05-25 | 218 |
  115. | 1.23.1 | 2015-05-26 | 1354 |
  116. | 1.24 | 2015-05-26 | 223 |
  117. | 1.25 | 2015-05-26 | 4319 |
  118. | 1.25.1 | 2015-06-02 | 197 |
  119. -------------------------------------
  120. Notes about the resulting table:
  121. - If a column contains numeric data (integer and/or floating point
  122. numbers) in all rows (ignoring column names of course) then the content
  123. of that column is right-aligned, as can be seen in the example above. The
  124. idea here is to make it easier to compare the numbers in different
  125. columns to each other.
  126. - The column names are highlighted in color so they stand out a bit more
  127. (see also :data:`.HIGHLIGHT_COLOR`). The following screen shot shows what
  128. that looks like (my terminals are always set to white text on a black
  129. background):
  130. .. image:: images/pretty-table.png
  131. """
  132. # Normalize the input because we'll have to iterate it more than once.
  133. data = [normalize_columns(r, expandtabs=True) for r in data]
  134. if column_names is not None:
  135. column_names = normalize_columns(column_names)
  136. if column_names:
  137. if terminal_supports_colors():
  138. column_names = [highlight_column_name(n) for n in column_names]
  139. data.insert(0, column_names)
  140. # Calculate the maximum width of each column.
  141. widths = collections.defaultdict(int)
  142. numeric_data = collections.defaultdict(list)
  143. for row_index, row in enumerate(data):
  144. for column_index, column in enumerate(row):
  145. widths[column_index] = max(widths[column_index], ansi_width(column))
  146. if not (column_names and row_index == 0):
  147. numeric_data[column_index].append(bool(NUMERIC_DATA_PATTERN.match(ansi_strip(column))))
  148. # Create a horizontal bar of dashes as a delimiter.
  149. line_delimiter = horizontal_bar * (sum(widths.values()) + len(widths) * 3 + 1)
  150. # Start the table with a vertical bar.
  151. lines = [line_delimiter]
  152. # Format the rows and columns.
  153. for row_index, row in enumerate(data):
  154. line = [vertical_bar]
  155. for column_index, column in enumerate(row):
  156. padding = ' ' * (widths[column_index] - ansi_width(column))
  157. if all(numeric_data[column_index]):
  158. line.append(' ' + padding + column + ' ')
  159. else:
  160. line.append(' ' + column + padding + ' ')
  161. line.append(vertical_bar)
  162. lines.append(u''.join(line))
  163. if column_names and row_index == 0:
  164. lines.append(line_delimiter)
  165. # End the table with a vertical bar.
  166. lines.append(line_delimiter)
  167. # Join the lines, returning a single string.
  168. return u'\n'.join(lines)
  169. def format_robust_table(data, column_names):
  170. """
  171. Render tabular data with one column per line (allowing columns with line breaks).
  172. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
  173. containing the rows of the table, where each row is an
  174. iterable containing the columns of the table (strings).
  175. :param column_names: An iterable of column names (strings).
  176. :returns: The rendered table (a string).
  177. Here's an example:
  178. >>> from humanfriendly.tables import format_robust_table
  179. >>> column_names = ['Version', 'Uploaded on', 'Downloads']
  180. >>> humanfriendly_releases = [
  181. ... ['1.23', '2015-05-25', '218'],
  182. ... ['1.23.1', '2015-05-26', '1354'],
  183. ... ['1.24', '2015-05-26', '223'],
  184. ... ['1.25', '2015-05-26', '4319'],
  185. ... ['1.25.1', '2015-06-02', '197'],
  186. ... ]
  187. >>> print(format_robust_table(humanfriendly_releases, column_names))
  188. -----------------------
  189. Version: 1.23
  190. Uploaded on: 2015-05-25
  191. Downloads: 218
  192. -----------------------
  193. Version: 1.23.1
  194. Uploaded on: 2015-05-26
  195. Downloads: 1354
  196. -----------------------
  197. Version: 1.24
  198. Uploaded on: 2015-05-26
  199. Downloads: 223
  200. -----------------------
  201. Version: 1.25
  202. Uploaded on: 2015-05-26
  203. Downloads: 4319
  204. -----------------------
  205. Version: 1.25.1
  206. Uploaded on: 2015-06-02
  207. Downloads: 197
  208. -----------------------
  209. The column names are highlighted in bold font and color so they stand out a
  210. bit more (see :data:`.HIGHLIGHT_COLOR`).
  211. """
  212. blocks = []
  213. column_names = ["%s:" % n for n in normalize_columns(column_names)]
  214. if terminal_supports_colors():
  215. column_names = [highlight_column_name(n) for n in column_names]
  216. # Convert each row into one or more `name: value' lines (one per column)
  217. # and group each `row of lines' into a block (i.e. rows become blocks).
  218. for row in data:
  219. lines = []
  220. for column_index, column_text in enumerate(normalize_columns(row)):
  221. stripped_column = column_text.strip()
  222. if '\n' not in stripped_column:
  223. # Columns without line breaks are formatted inline.
  224. lines.append("%s %s" % (column_names[column_index], stripped_column))
  225. else:
  226. # Columns with line breaks could very well contain indented
  227. # lines, so we'll put the column name on a separate line. This
  228. # way any indentation remains intact, and it's easier to
  229. # copy/paste the text.
  230. lines.append(column_names[column_index])
  231. lines.extend(column_text.rstrip().splitlines())
  232. blocks.append(lines)
  233. # Calculate the width of the row delimiter.
  234. num_rows, num_columns = find_terminal_size()
  235. longest_line = max(max(map(ansi_width, lines)) for lines in blocks)
  236. delimiter = u"\n%s\n" % ('-' * min(longest_line, num_columns))
  237. # Force a delimiter at the start and end of the table.
  238. blocks.insert(0, "")
  239. blocks.append("")
  240. # Embed the row delimiter between every two blocks.
  241. return delimiter.join(u"\n".join(b) for b in blocks).strip()
  242. def format_rst_table(data, column_names=None):
  243. """
  244. Render a table in reStructuredText_ format.
  245. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
  246. containing the rows of the table, where each row is an
  247. iterable containing the columns of the table (strings).
  248. :param column_names: An iterable of column names (strings).
  249. :returns: The rendered table (a string).
  250. Here's an example:
  251. >>> from humanfriendly.tables import format_rst_table
  252. >>> column_names = ['Version', 'Uploaded on', 'Downloads']
  253. >>> humanfriendly_releases = [
  254. ... ['1.23', '2015-05-25', '218'],
  255. ... ['1.23.1', '2015-05-26', '1354'],
  256. ... ['1.24', '2015-05-26', '223'],
  257. ... ['1.25', '2015-05-26', '4319'],
  258. ... ['1.25.1', '2015-06-02', '197'],
  259. ... ]
  260. >>> print(format_rst_table(humanfriendly_releases, column_names))
  261. ======= =========== =========
  262. Version Uploaded on Downloads
  263. ======= =========== =========
  264. 1.23 2015-05-25 218
  265. 1.23.1 2015-05-26 1354
  266. 1.24 2015-05-26 223
  267. 1.25 2015-05-26 4319
  268. 1.25.1 2015-06-02 197
  269. ======= =========== =========
  270. .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText
  271. """
  272. data = [normalize_columns(r) for r in data]
  273. if column_names:
  274. data.insert(0, normalize_columns(column_names))
  275. # Calculate the maximum width of each column.
  276. widths = collections.defaultdict(int)
  277. for row in data:
  278. for index, column in enumerate(row):
  279. widths[index] = max(widths[index], len(column))
  280. # Pad the columns using whitespace.
  281. for row in data:
  282. for index, column in enumerate(row):
  283. if index < (len(row) - 1):
  284. row[index] = column.ljust(widths[index])
  285. # Add table markers.
  286. delimiter = ['=' * w for i, w in sorted(widths.items())]
  287. if column_names:
  288. data.insert(1, delimiter)
  289. data.insert(0, delimiter)
  290. data.append(delimiter)
  291. # Join the lines and columns together.
  292. return '\n'.join(' '.join(r) for r in data)
  293. def normalize_columns(row, expandtabs=False):
  294. results = []
  295. for value in row:
  296. text = coerce_string(value)
  297. if expandtabs:
  298. text = text.expandtabs()
  299. results.append(text)
  300. return results
  301. def highlight_column_name(name):
  302. return ansi_wrap(name, bold=True, color=HIGHLIGHT_COLOR)