texttable.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. #!/usr/bin/env python
  2. #
  3. # texttable - module for creating simple ASCII tables
  4. # Copyright (C) 2003-2011 Gerome Fournier <jef(at)foutaise.org>
  5. #
  6. # This library is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU Lesser General Public
  8. # License as published by the Free Software Foundation; either
  9. # version 2.1 of the License, or (at your option) any later version.
  10. #
  11. # This library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. # Lesser General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public
  17. # License along with this library; if not, write to the Free Software
  18. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  19. """module for creating simple ASCII tables
  20. Example:
  21. table = Texttable()
  22. table.set_cols_align(["l", "r", "c"])
  23. table.set_cols_valign(["t", "m", "b"])
  24. table.add_rows([ ["Name", "Age", "Nickname"],
  25. ["Mr\\nXavier\\nHuon", 32, "Xav'"],
  26. ["Mr\\nBaptiste\\nClement", 1, "Baby"] ])
  27. print(table.draw() + "\\n")
  28. table = Texttable()
  29. table.set_deco(Texttable.HEADER)
  30. table.set_cols_dtype(['t', # text
  31. 'f', # float (decimal)
  32. 'e', # float (exponent)
  33. 'i', # integer
  34. 'a']) # automatic
  35. table.set_cols_align(["l", "r", "r", "r", "l"])
  36. table.add_rows([["text", "float", "exp", "int", "auto"],
  37. ["abcd", "67", 654, 89, 128.001],
  38. ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023],
  39. ["lmn", 5e-78, 5e-78, 89.4, .000000000000128],
  40. ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]])
  41. print(table.draw())
  42. Result:
  43. +----------+-----+----------+
  44. | Name | Age | Nickname |
  45. +==========+=====+==========+
  46. | Mr | | |
  47. | Xavier | 32 | |
  48. | Huon | | Xav' |
  49. +----------+-----+----------+
  50. | Mr | | |
  51. | Baptiste | 1 | |
  52. | Clement | | Baby |
  53. +----------+-----+----------+
  54. text float exp int auto
  55. ===========================================
  56. abcd 67.000 6.540e+02 89 128.001
  57. efgh 67.543 6.540e-01 90 1.280e+22
  58. ijkl 0.000 5.000e-78 89 0.000
  59. mnop 0.023 5.000e+78 92 1.280e+22
  60. """
  61. from __future__ import division
  62. from __future__ import print_function
  63. from functools import reduce
  64. __all__ = ["Texttable", "ArraySizeError"]
  65. __author__ = 'Gerome Fournier <jef(at)foutaise.org>'
  66. __license__ = 'LGPL'
  67. __version__ = '0.8.1'
  68. __credits__ = """\
  69. Jeff Kowalczyk:
  70. - textwrap improved import
  71. - comment concerning header output
  72. Anonymous:
  73. - add_rows method, for adding rows in one go
  74. Sergey Simonenko:
  75. - redefined len() function to deal with non-ASCII characters
  76. Roger Lew:
  77. - columns datatype specifications
  78. Brian Peterson:
  79. - better handling of unicode errors
  80. """
  81. # Modified version of `texttable` for python3 support.
  82. import sys
  83. import string
  84. def len(iterable):
  85. """Redefining len here so it will be able to work with non-ASCII characters
  86. """
  87. if not isinstance(iterable, str):
  88. return iterable.__len__()
  89. try:
  90. return len(unicode(iterable, 'utf'))
  91. except:
  92. return iterable.__len__()
  93. class ArraySizeError(Exception):
  94. """Exception raised when specified rows don't fit the required size
  95. """
  96. def __init__(self, msg):
  97. self.msg = msg
  98. Exception.__init__(self, msg, '')
  99. def __str__(self):
  100. return self.msg
  101. class Texttable:
  102. BORDER = 1
  103. HEADER = 1 << 1
  104. HLINES = 1 << 2
  105. VLINES = 1 << 3
  106. def __init__(self, max_width=80):
  107. """Constructor
  108. - max_width is an integer, specifying the maximum width of the table
  109. - if set to 0, size is unlimited, therefore cells won't be wrapped
  110. """
  111. if max_width <= 0:
  112. max_width = False
  113. self._max_width = max_width
  114. self._precision = 3
  115. self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | \
  116. Texttable.HEADER
  117. self.set_chars(['-', '|', '+', '='])
  118. self.reset()
  119. def reset(self):
  120. """Reset the instance
  121. - reset rows and header
  122. """
  123. self._hline_string = None
  124. self._row_size = None
  125. self._header = []
  126. self._rows = []
  127. def set_chars(self, array):
  128. """Set the characters used to draw lines between rows and columns
  129. - the array should contain 4 fields:
  130. [horizontal, vertical, corner, header]
  131. - default is set to:
  132. ['-', '|', '+', '=']
  133. """
  134. if len(array) != 4:
  135. raise ArraySizeError("array should contain 4 characters")
  136. array = [ x[:1] for x in [ str(s) for s in array ] ]
  137. (self._char_horiz, self._char_vert,
  138. self._char_corner, self._char_header) = array
  139. def set_deco(self, deco):
  140. """Set the table decoration
  141. - 'deco' can be a combinaison of:
  142. Texttable.BORDER: Border around the table
  143. Texttable.HEADER: Horizontal line below the header
  144. Texttable.HLINES: Horizontal lines between rows
  145. Texttable.VLINES: Vertical lines between columns
  146. All of them are enabled by default
  147. - example:
  148. Texttable.BORDER | Texttable.HEADER
  149. """
  150. self._deco = deco
  151. def set_cols_align(self, array):
  152. """Set the desired columns alignment
  153. - the elements of the array should be either "l", "c" or "r":
  154. * "l": column flushed left
  155. * "c": column centered
  156. * "r": column flushed right
  157. """
  158. self._check_row_size(array)
  159. self._align = array
  160. def set_cols_valign(self, array):
  161. """Set the desired columns vertical alignment
  162. - the elements of the array should be either "t", "m" or "b":
  163. * "t": column aligned on the top of the cell
  164. * "m": column aligned on the middle of the cell
  165. * "b": column aligned on the bottom of the cell
  166. """
  167. self._check_row_size(array)
  168. self._valign = array
  169. def set_cols_dtype(self, array):
  170. """Set the desired columns datatype for the cols.
  171. - the elements of the array should be either "a", "t", "f", "e" or "i":
  172. * "a": automatic (try to use the most appropriate datatype)
  173. * "t": treat as text
  174. * "f": treat as float in decimal format
  175. * "e": treat as float in exponential format
  176. * "i": treat as int
  177. - by default, automatic datatyping is used for each column
  178. """
  179. self._check_row_size(array)
  180. self._dtype = array
  181. def set_cols_width(self, array):
  182. """Set the desired columns width
  183. - the elements of the array should be integers, specifying the
  184. width of each column. For example:
  185. [10, 20, 5]
  186. """
  187. self._check_row_size(array)
  188. try:
  189. array = map(int, array)
  190. if reduce(min, array) <= 0:
  191. raise ValueError
  192. except ValueError:
  193. sys.stderr.write("Wrong argument in column width specification\n")
  194. raise
  195. self._width = array
  196. def set_precision(self, width):
  197. """Set the desired precision for float/exponential formats
  198. - width must be an integer >= 0
  199. - default value is set to 3
  200. """
  201. if not type(width) is int or width < 0:
  202. raise ValueError('width must be an integer greater then 0')
  203. self._precision = width
  204. def header(self, array):
  205. """Specify the header of the table
  206. """
  207. self._check_row_size(array)
  208. self._header = map(str, array)
  209. def add_row(self, array):
  210. """Add a row in the rows stack
  211. - cells can contain newlines and tabs
  212. """
  213. self._check_row_size(array)
  214. if not hasattr(self, "_dtype"):
  215. self._dtype = ["a"] * self._row_size
  216. cells = []
  217. for i,x in enumerate(array):
  218. cells.append(self._str(i,x))
  219. self._rows.append(cells)
  220. def add_rows(self, rows, header=True):
  221. """Add several rows in the rows stack
  222. - The 'rows' argument can be either an iterator returning arrays,
  223. or a by-dimensional array
  224. - 'header' specifies if the first row should be used as the header
  225. of the table
  226. """
  227. # nb: don't use 'iter' on by-dimensional arrays, to get a
  228. # usable code for python 2.1
  229. if header:
  230. if hasattr(rows, '__iter__') and hasattr(rows, 'next'):
  231. self.header(rows.next())
  232. else:
  233. self.header(rows[0])
  234. rows = rows[1:]
  235. for row in rows:
  236. self.add_row(row)
  237. def draw(self):
  238. """Draw the table
  239. - the table is returned as a whole string
  240. """
  241. if not self._header and not self._rows:
  242. return
  243. self._compute_cols_width()
  244. self._check_align()
  245. out = ""
  246. if self._has_border():
  247. out += self._hline()
  248. if self._header:
  249. out += self._draw_line(self._header, isheader=True)
  250. if self._has_header():
  251. out += self._hline_header()
  252. length = 0
  253. for row in self._rows:
  254. length += 1
  255. out += self._draw_line(row)
  256. if self._has_hlines() and length < len(self._rows):
  257. out += self._hline()
  258. if self._has_border():
  259. out += self._hline()
  260. return out[:-1]
  261. def _str(self, i, x):
  262. """Handles string formatting of cell data
  263. i - index of the cell datatype in self._dtype
  264. x - cell data to format
  265. """
  266. try:
  267. f = float(x)
  268. except:
  269. return str(x)
  270. n = self._precision
  271. dtype = self._dtype[i]
  272. if dtype == 'i':
  273. return str(int(round(f)))
  274. elif dtype == 'f':
  275. return '%.*f' % (n, f)
  276. elif dtype == 'e':
  277. return '%.*e' % (n, f)
  278. elif dtype == 't':
  279. return str(x)
  280. else:
  281. if f - round(f) == 0:
  282. if abs(f) > 1e8:
  283. return '%.*e' % (n, f)
  284. else:
  285. return str(int(round(f)))
  286. else:
  287. if abs(f) > 1e8:
  288. return '%.*e' % (n, f)
  289. else:
  290. return '%.*f' % (n, f)
  291. def _check_row_size(self, array):
  292. """Check that the specified array fits the previous rows size
  293. """
  294. if not self._row_size:
  295. self._row_size = len(array)
  296. elif self._row_size != len(array):
  297. raise ArraySizeError("array should contain %d elements" % self._row_size)
  298. def _has_vlines(self):
  299. """Return a boolean, if vlines are required or not
  300. """
  301. return self._deco & Texttable.VLINES > 0
  302. def _has_hlines(self):
  303. """Return a boolean, if hlines are required or not
  304. """
  305. return self._deco & Texttable.HLINES > 0
  306. def _has_border(self):
  307. """Return a boolean, if border is required or not
  308. """
  309. return self._deco & Texttable.BORDER > 0
  310. def _has_header(self):
  311. """Return a boolean, if header line is required or not
  312. """
  313. return self._deco & Texttable.HEADER > 0
  314. def _hline_header(self):
  315. """Print header's horizontal line
  316. """
  317. return self._build_hline(True)
  318. def _hline(self):
  319. """Print an horizontal line
  320. """
  321. if not self._hline_string:
  322. self._hline_string = self._build_hline()
  323. return self._hline_string
  324. def _build_hline(self, is_header=False):
  325. """Return a string used to separated rows or separate header from
  326. rows
  327. """
  328. horiz = self._char_horiz
  329. if (is_header):
  330. horiz = self._char_header
  331. # compute cell separator
  332. s = "%s%s%s" % (horiz, [horiz, self._char_corner][self._has_vlines()],
  333. horiz)
  334. # build the line
  335. l = string.join([horiz * n for n in self._width], s)
  336. # add border if needed
  337. if self._has_border():
  338. l = "%s%s%s%s%s\n" % (self._char_corner, horiz, l, horiz,
  339. self._char_corner)
  340. else:
  341. l += "\n"
  342. return l
  343. def _len_cell(self, cell):
  344. """Return the width of the cell
  345. Special characters are taken into account to return the width of the
  346. cell, such like newlines and tabs
  347. """
  348. cell_lines = cell.split('\n')
  349. maxi = 0
  350. for line in cell_lines:
  351. length = 0
  352. parts = line.split('\t')
  353. for part, i in zip(parts, range(1, len(parts) + 1)):
  354. length = length + len(part)
  355. if i < len(parts):
  356. length = (length/8 + 1) * 8
  357. maxi = max(maxi, length)
  358. return maxi
  359. def _compute_cols_width(self):
  360. """Return an array with the width of each column
  361. If a specific width has been specified, exit. If the total of the
  362. columns width exceed the table desired width, another width will be
  363. computed to fit, and cells will be wrapped.
  364. """
  365. if hasattr(self, "_width"):
  366. return
  367. maxi = []
  368. if self._header:
  369. maxi = [ self._len_cell(x) for x in self._header ]
  370. for row in self._rows:
  371. for cell,i in zip(row, range(len(row))):
  372. try:
  373. maxi[i] = max(maxi[i], self._len_cell(cell))
  374. except (TypeError, IndexError):
  375. maxi.append(self._len_cell(cell))
  376. items = len(maxi)
  377. length = reduce(lambda x,y: x+y, maxi)
  378. if self._max_width and length + items * 3 + 1 > self._max_width:
  379. maxi = [(self._max_width - items * 3 -1) / items \
  380. for n in range(items)]
  381. self._width = maxi
  382. def _check_align(self):
  383. """Check if alignment has been specified, set default one if not
  384. """
  385. if not hasattr(self, "_align"):
  386. self._align = ["l"] * self._row_size
  387. if not hasattr(self, "_valign"):
  388. self._valign = ["t"] * self._row_size
  389. def _draw_line(self, line, isheader=False):
  390. """Draw a line
  391. Loop over a single cell length, over all the cells
  392. """
  393. line = self._splitit(line, isheader)
  394. space = " "
  395. out = ""
  396. for i in range(len(line[0])):
  397. if self._has_border():
  398. out += "%s " % self._char_vert
  399. length = 0
  400. for cell, width, align in zip(line, self._width, self._align):
  401. length += 1
  402. cell_line = cell[i]
  403. fill = width - len(cell_line)
  404. if isheader:
  405. align = "c"
  406. if align == "r":
  407. out += "%s " % (fill * space + cell_line)
  408. elif align == "c":
  409. out += "%s " % (fill/2 * space + cell_line \
  410. + (fill/2 + fill%2) * space)
  411. else:
  412. out += "%s " % (cell_line + fill * space)
  413. if length < len(line):
  414. out += "%s " % [space, self._char_vert][self._has_vlines()]
  415. out += "%s\n" % ['', self._char_vert][self._has_border()]
  416. return out
  417. def _splitit(self, line, isheader):
  418. """Split each element of line to fit the column width
  419. Each element is turned into a list, result of the wrapping of the
  420. string to the desired width
  421. """
  422. line_wrapped = []
  423. for cell, width in zip(line, self._width):
  424. array = []
  425. for c in cell.split('\n'):
  426. try:
  427. c = unicode(c, 'utf')
  428. except UnicodeDecodeError as strerror:
  429. sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (c, strerror))
  430. c = unicode(c, 'utf', 'replace')
  431. array.extend(textwrap.wrap(c, width))
  432. line_wrapped.append(array)
  433. max_cell_lines = reduce(max, map(len, line_wrapped))
  434. for cell, valign in zip(line_wrapped, self._valign):
  435. if isheader:
  436. valign = "t"
  437. if valign == "m":
  438. missing = max_cell_lines - len(cell)
  439. cell[:0] = [""] * (missing / 2)
  440. cell.extend([""] * (missing / 2 + missing % 2))
  441. elif valign == "b":
  442. cell[:0] = [""] * (max_cell_lines - len(cell))
  443. else:
  444. cell.extend([""] * (max_cell_lines - len(cell)))
  445. return line_wrapped
  446. if __name__ == '__main__':
  447. table = Texttable()
  448. table.set_cols_align(["l", "r", "c"])
  449. table.set_cols_valign(["t", "m", "b"])
  450. table.add_rows([ ["Name", "Age", "Nickname"],
  451. ["Mr\nXavier\nHuon", 32, "Xav'"],
  452. ["Mr\nBaptiste\nClement", 1, "Baby"] ])
  453. print(table.draw() + "\n")
  454. table = Texttable()
  455. table.set_deco(Texttable.HEADER)
  456. table.set_cols_dtype(['t', # text
  457. 'f', # float (decimal)
  458. 'e', # float (exponent)
  459. 'i', # integer
  460. 'a']) # automatic
  461. table.set_cols_align(["l", "r", "r", "r", "l"])
  462. table.add_rows([["text", "float", "exp", "int", "auto"],
  463. ["abcd", "67", 654, 89, 128.001],
  464. ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023],
  465. ["lmn", 5e-78, 5e-78, 89.4, .000000000000128],
  466. ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]])
  467. print(table.draw())