Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tabulate/__init__.py: 69%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

856 statements  

1"""Pretty-print tabular data.""" 

2 

3from collections import namedtuple 

4from collections.abc import Callable, Iterable, Sized 

5import dataclasses 

6from dataclasses import dataclass 

7from decimal import Decimal 

8from functools import partial, reduce 

9from html import escape as htmlescape 

10from importlib.metadata import PackageNotFoundError, version 

11import io 

12from itertools import chain, zip_longest as izip_longest 

13import math 

14import re 

15import textwrap 

16import warnings 

17 

18try: 

19 import wcwidth # optional wide-character (CJK) support 

20except ImportError: 

21 wcwidth = None 

22 

23try: 

24 __version__ = version("tabulate") # installed package 

25except PackageNotFoundError: 

26 try: 

27 from ._version import version as __version__ # editable / source checkout 

28 except ImportError: 

29 __version__ = "unknown" 

30 

31 

32__all__ = ["tabulate", "tabulate_formats", "simple_separated_format"] 

33 

34# minimum extra space in headers 

35MIN_PADDING = 2 

36 

37# Whether or not to preserve leading/trailing whitespace in data. 

38PRESERVE_WHITESPACE = False 

39 

40# TextWrapper breaks words longer than 'width'. 

41_BREAK_LONG_WORDS = True 

42# TextWrapper is breaking hyphenated words. 

43_BREAK_ON_HYPHENS = True 

44 

45 

46_DEFAULT_FLOATFMT = "g" 

47_DEFAULT_INTFMT = "" 

48_DEFAULT_MISSINGVAL = "" 

49# default align will be overwritten by "left", "center" or "decimal" 

50# depending on the formatter 

51_DEFAULT_ALIGN = "default" 

52 

53 

54# if True, enable wide-character (CJK) support 

55WIDE_CHARS_MODE = wcwidth is not None 

56 

57# Constant that can be used as part of passed rows to generate a separating line 

58# It is purposely an unprintable character, very unlikely to be used in a table 

59SEPARATING_LINE = "\001" 

60 

61Line = namedtuple("Line", ["begin", "hline", "sep", "end"]) 

62 

63 

64@dataclass 

65class DataRow: 

66 begin: str 

67 sep: str 

68 end: str 

69 escape_map: dict = None 

70 

71 

72# A table structure is supposed to be: 

73# 

74# --- lineabove --------- 

75# headerrow 

76# --- linebelowheader --- 

77# datarow 

78# --- linebetweenrows --- 

79# ... (more datarows) ... 

80# --- linebetweenrows --- 

81# last datarow 

82# --- linebelow --------- 

83# 

84# TableFormat's line* elements can be 

85# 

86# - either None, if the element is not used, 

87# - or a Line tuple, 

88# - or a function: [col_widths], [col_alignments] -> string. 

89# 

90# TableFormat's *row elements can be 

91# 

92# - either None, if the element is not used, 

93# - or a DataRow tuple, 

94# - or a function: [cell_values], [col_widths], [col_alignments] -> string. 

95# 

96# padding (an integer) is the amount of white space around data values. 

97# 

98# with_header_hide: 

99# 

100# - either None, to display all table elements unconditionally, 

101# - or a list of elements not to be displayed if the table has column headers. 

102# 

103TableFormat = namedtuple( 

104 "TableFormat", 

105 [ 

106 "lineabove", 

107 "linebelowheader", 

108 "linebetweenrows", 

109 "linebelow", 

110 "headerrow", 

111 "datarow", 

112 "padding", 

113 "with_header_hide", 

114 ], 

115) 

116 

117 

118def _is_file(f): 

119 return isinstance(f, io.IOBase) 

120 

121 

122def _is_separating_line_value(value): 

123 return type(value) is str and value.strip() == SEPARATING_LINE 

124 

125 

126def _is_separating_line(row): 

127 row_type = type(row) 

128 return (row_type is list or row_type is str) and ( 

129 (len(row) >= 1 and _is_separating_line_value(row[0])) 

130 or (len(row) >= 2 and _is_separating_line_value(row[1])) 

131 ) 

132 

133 

134def _pipe_segment_with_colons(align, colwidth): 

135 """Return a segment of a horizontal line with optional colons which 

136 indicate column's alignment (as in `pipe` output format).""" 

137 w = colwidth 

138 if align in ["right", "decimal"]: 

139 return ("-" * (w - 1)) + ":" 

140 elif align == "center": 

141 return ":" + ("-" * (w - 2)) + ":" 

142 elif align == "left": 

143 return ":" + ("-" * (w - 1)) 

144 else: 

145 return "-" * w 

146 

147 

148def _pipe_line_with_colons(colwidths, colaligns): 

149 """Return a horizontal line with optional colons to indicate column's 

150 alignment (as in `pipe` output format).""" 

151 if not colaligns: # e.g. printing an empty data frame (github issue #15) 

152 colaligns = [""] * len(colwidths) 

153 segments = "|".join(_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)) 

154 return f"|{segments}|" 

155 

156 

157def _grid_segment_with_colons(colwidth, align): 

158 """Return a segment of a horizontal line with optional colons which indicate 

159 column's alignment in a grid table.""" 

160 width = colwidth 

161 if align == "right": 

162 return ("=" * (width - 1)) + ":" 

163 elif align == "center": 

164 return ":" + ("=" * (width - 2)) + ":" 

165 elif align == "left": 

166 return ":" + ("=" * (width - 1)) 

167 else: 

168 return "=" * width 

169 

170 

171def _grid_line_with_colons(colwidths, colaligns): 

172 """Return a horizontal line with optional colons to indicate column's alignment 

173 in a grid table.""" 

174 if not colaligns: 

175 colaligns = [""] * len(colwidths) 

176 segments = "+".join(_grid_segment_with_colons(w, a) for a, w in zip(colaligns, colwidths)) 

177 return f"+{segments}+" 

178 

179 

180def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): 

181 alignment = { 

182 "left": "", 

183 "right": 'style="text-align: right;"| ', 

184 "center": 'style="text-align: center;"| ', 

185 "decimal": 'style="text-align: right;"| ', 

186 } 

187 # hard-coded padding _around_ align attribute and value together 

188 # rather than padding parameter which affects only the value 

189 values_with_attrs = [ 

190 " " + alignment.get(a, "") + c + " " for c, a in zip(cell_values, colaligns) 

191 ] 

192 colsep = separator * 2 

193 return (separator + colsep.join(values_with_attrs)).rstrip() 

194 

195 

196def _textile_row_with_attrs(cell_values, colwidths, colaligns): 

197 cell_values[0] += " " 

198 alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."} 

199 values = "|".join(alignment.get(a, "") + v for a, v in zip(colaligns, cell_values)) 

200 return f"|{values}|" 

201 

202 

203def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore): 

204 # this table header will be suppressed if there is a header row 

205 return "<table>\n<tbody>" 

206 

207 

208def _html_row_with_attrs(celltag, unsafe, cell_values, colwidths, colaligns): 

209 alignment = { 

210 "left": ' style="text-align: left;"', 

211 "right": ' style="text-align: right;"', 

212 "center": ' style="text-align: center;"', 

213 "decimal": ' style="text-align: right;"', 

214 } 

215 if unsafe: 

216 values_with_attrs = [ 

217 "<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ""), c) 

218 for c, a in zip(cell_values, colaligns) 

219 ] 

220 else: 

221 values_with_attrs = [ 

222 "<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ""), htmlescape(c)) 

223 for c, a in zip(cell_values, colaligns) 

224 ] 

225 rowhtml = "<tr>{}</tr>".format("".join(values_with_attrs).rstrip()) 

226 if celltag == "th": # it's a header row, create a new table header 

227 rowhtml = f"<table>\n<thead>\n{rowhtml}\n</thead>\n<tbody>" 

228 return rowhtml 

229 

230 

231def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, header=""): 

232 alignment = { 

233 "left": '<style="text-align: left;">', 

234 "right": '<style="text-align: right;">', 

235 "center": '<style="text-align: center;">', 

236 "decimal": '<style="text-align: right;">', 

237 } 

238 values_with_attrs = [ 

239 "{}{} {} ".format(celltag, alignment.get(a, ""), header + c + header) 

240 for c, a in zip(cell_values, colaligns) 

241 ] 

242 return "".join(values_with_attrs) + "||" 

243 

244 

245def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False, longtable=False): 

246 alignment = {"left": "l", "right": "r", "center": "c", "decimal": "r"} 

247 tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns]) 

248 return "\n".join( 

249 [ 

250 ("\\begin{tabular}{" if not longtable else "\\begin{longtable}{") 

251 + tabular_columns_fmt 

252 + "}", 

253 "\\toprule" if booktabs else "\\hline", 

254 ] 

255 ) 

256 

257 

258def _asciidoc_row(is_header, *args): 

259 """handle header and data rows for asciidoc format""" 

260 

261 def make_header_line(is_header, colwidths, colaligns): 

262 # generate the column specifiers 

263 

264 alignment = {"left": "<", "right": ">", "center": "^", "decimal": ">"} 

265 # use the column widths generated by tabulate for the asciidoc column width specifiers 

266 asciidoc_alignments = zip(colwidths, [alignment[colalign] for colalign in colaligns]) 

267 asciidoc_column_specifiers = [f"{align}{width:d}" for width, align in asciidoc_alignments] 

268 header_list = ['cols="' + (",".join(asciidoc_column_specifiers)) + '"'] 

269 

270 # generate the list of options (currently only "header") 

271 options_list = [] 

272 

273 if is_header: 

274 options_list.append("header") 

275 

276 if options_list: 

277 options_list = ",".join(options_list) 

278 header_list.append(f'options="{options_list}"') 

279 

280 # generate the list of entries in the table header field 

281 

282 line = "[{}]\n|====".format(",".join(header_list)) 

283 return line.rstrip() 

284 

285 if len(args) == 2: 

286 # two arguments are passed if called in the context of aboveline 

287 # print the table header with column widths and optional header tag 

288 line = make_header_line(False, *args) 

289 return line.rstrip() 

290 

291 elif len(args) == 3: 

292 # three arguments are passed if called in the context of dataline or headerline 

293 # print the table line and make the aboveline if it is a header 

294 

295 cell_values, colwidths, colaligns = args 

296 data_line = "|" + "|".join(cell_values) 

297 

298 if is_header: 

299 line = make_header_line(True, colwidths, colaligns) + "\n" + data_line 

300 return line.rstrip() 

301 else: 

302 return data_line.rstrip() 

303 

304 else: 

305 raise ValueError( 

306 "_asciidoc_row() requires two (colwidths, colaligns) " 

307 "or three (cell_values, colwidths, colaligns) arguments) " 

308 ) 

309 

310 

311LATEX_ESCAPE_RULES = { 

312 r"&": r"\&", 

313 r"%": r"\%", 

314 r"$": r"\$", 

315 r"#": r"\#", 

316 r"_": r"\_", 

317 r"^": r"\^{}", 

318 r"{": r"\{", 

319 r"}": r"\}", 

320 r"~": r"\textasciitilde{}", 

321 "\\": r"\textbackslash{}", 

322 r"<": r"\ensuremath{<}", 

323 r">": r"\ensuremath{>}", 

324} 

325 

326 

327_latex_row = DataRow("", "&", "\\\\", LATEX_ESCAPE_RULES) 

328 

329 

330GITHUB_ESCAPE_RULES = {r"|": r"\|"} 

331 

332 

333def _rst_escape_first_column(rows, headers): 

334 def escape_empty(val): 

335 if isinstance(val, (str, bytes)) and not val.strip(): 

336 return ".." 

337 else: 

338 return val 

339 

340 new_headers = list(headers) 

341 new_rows = [] 

342 if headers: 

343 new_headers[0] = escape_empty(headers[0]) 

344 for row in rows: 

345 new_row = list(row) 

346 if new_row: 

347 new_row[0] = escape_empty(row[0]) 

348 new_rows.append(new_row) 

349 return new_rows, new_headers 

350 

351 

352_table_formats = { 

353 "simple": TableFormat( 

354 lineabove=Line("", "-", " ", ""), 

355 linebelowheader=Line("", "-", " ", ""), 

356 linebetweenrows=None, 

357 linebelow=Line("", "-", " ", ""), 

358 headerrow=DataRow("", " ", ""), 

359 datarow=DataRow("", " ", ""), 

360 padding=0, 

361 with_header_hide=["lineabove", "linebelow"], 

362 ), 

363 "plain": TableFormat( 

364 lineabove=None, 

365 linebelowheader=None, 

366 linebetweenrows=None, 

367 linebelow=None, 

368 headerrow=DataRow("", " ", ""), 

369 datarow=DataRow("", " ", ""), 

370 padding=0, 

371 with_header_hide=None, 

372 ), 

373 "grid": TableFormat( 

374 lineabove=Line("+", "-", "+", "+"), 

375 linebelowheader=Line("+", "=", "+", "+"), 

376 linebetweenrows=Line("+", "-", "+", "+"), 

377 linebelow=Line("+", "-", "+", "+"), 

378 headerrow=DataRow("|", "|", "|"), 

379 datarow=DataRow("|", "|", "|"), 

380 padding=1, 

381 with_header_hide=None, 

382 ), 

383 "simple_grid": TableFormat( 

384 lineabove=Line("┌", "─", "┬", "┐"), 

385 linebelowheader=Line("├", "─", "┼", "┤"), 

386 linebetweenrows=Line("├", "─", "┼", "┤"), 

387 linebelow=Line("└", "─", "┴", "┘"), 

388 headerrow=DataRow("│", "│", "│"), 

389 datarow=DataRow("│", "│", "│"), 

390 padding=1, 

391 with_header_hide=None, 

392 ), 

393 "rounded_grid": TableFormat( 

394 lineabove=Line("╭", "─", "┬", "╮"), 

395 linebelowheader=Line("├", "─", "┼", "┤"), 

396 linebetweenrows=Line("├", "─", "┼", "┤"), 

397 linebelow=Line("╰", "─", "┴", "╯"), 

398 headerrow=DataRow("│", "│", "│"), 

399 datarow=DataRow("│", "│", "│"), 

400 padding=1, 

401 with_header_hide=None, 

402 ), 

403 "heavy_grid": TableFormat( 

404 lineabove=Line("┏", "━", "┳", "┓"), 

405 linebelowheader=Line("┣", "━", "╋", "┫"), 

406 linebetweenrows=Line("┣", "━", "╋", "┫"), 

407 linebelow=Line("┗", "━", "┻", "┛"), 

408 headerrow=DataRow("┃", "┃", "┃"), 

409 datarow=DataRow("┃", "┃", "┃"), 

410 padding=1, 

411 with_header_hide=None, 

412 ), 

413 "mixed_grid": TableFormat( 

414 lineabove=Line("┍", "━", "┯", "┑"), 

415 linebelowheader=Line("┝", "━", "┿", "┥"), 

416 linebetweenrows=Line("├", "─", "┼", "┤"), 

417 linebelow=Line("┕", "━", "┷", "┙"), 

418 headerrow=DataRow("│", "│", "│"), 

419 datarow=DataRow("│", "│", "│"), 

420 padding=1, 

421 with_header_hide=None, 

422 ), 

423 "double_grid": TableFormat( 

424 lineabove=Line("╔", "═", "╦", "╗"), 

425 linebelowheader=Line("╠", "═", "╬", "╣"), 

426 linebetweenrows=Line("╠", "═", "╬", "╣"), 

427 linebelow=Line("╚", "═", "╩", "╝"), 

428 headerrow=DataRow("║", "║", "║"), 

429 datarow=DataRow("║", "║", "║"), 

430 padding=1, 

431 with_header_hide=None, 

432 ), 

433 "fancy_grid": TableFormat( 

434 lineabove=Line("╒", "═", "╤", "╕"), 

435 linebelowheader=Line("╞", "═", "╪", "╡"), 

436 linebetweenrows=Line("├", "─", "┼", "┤"), 

437 linebelow=Line("╘", "═", "╧", "╛"), 

438 headerrow=DataRow("│", "│", "│"), 

439 datarow=DataRow("│", "│", "│"), 

440 padding=1, 

441 with_header_hide=None, 

442 ), 

443 "colon_grid": TableFormat( 

444 lineabove=Line("+", "-", "+", "+"), 

445 linebelowheader=_grid_line_with_colons, 

446 linebetweenrows=Line("+", "-", "+", "+"), 

447 linebelow=Line("+", "-", "+", "+"), 

448 headerrow=DataRow("|", "|", "|"), 

449 datarow=DataRow("|", "|", "|"), 

450 padding=1, 

451 with_header_hide=None, 

452 ), 

453 "outline": TableFormat( 

454 lineabove=Line("+", "-", "+", "+"), 

455 linebelowheader=Line("+", "=", "+", "+"), 

456 linebetweenrows=None, 

457 linebelow=Line("+", "-", "+", "+"), 

458 headerrow=DataRow("|", "|", "|"), 

459 datarow=DataRow("|", "|", "|"), 

460 padding=1, 

461 with_header_hide=None, 

462 ), 

463 "simple_outline": TableFormat( 

464 lineabove=Line("┌", "─", "┬", "┐"), 

465 linebelowheader=Line("├", "─", "┼", "┤"), 

466 linebetweenrows=None, 

467 linebelow=Line("└", "─", "┴", "┘"), 

468 headerrow=DataRow("│", "│", "│"), 

469 datarow=DataRow("│", "│", "│"), 

470 padding=1, 

471 with_header_hide=None, 

472 ), 

473 "rounded_outline": TableFormat( 

474 lineabove=Line("╭", "─", "┬", "╮"), 

475 linebelowheader=Line("├", "─", "┼", "┤"), 

476 linebetweenrows=None, 

477 linebelow=Line("╰", "─", "┴", "╯"), 

478 headerrow=DataRow("│", "│", "│"), 

479 datarow=DataRow("│", "│", "│"), 

480 padding=1, 

481 with_header_hide=None, 

482 ), 

483 "heavy_outline": TableFormat( 

484 lineabove=Line("┏", "━", "┳", "┓"), 

485 linebelowheader=Line("┣", "━", "╋", "┫"), 

486 linebetweenrows=None, 

487 linebelow=Line("┗", "━", "┻", "┛"), 

488 headerrow=DataRow("┃", "┃", "┃"), 

489 datarow=DataRow("┃", "┃", "┃"), 

490 padding=1, 

491 with_header_hide=None, 

492 ), 

493 "mixed_outline": TableFormat( 

494 lineabove=Line("┍", "━", "┯", "┑"), 

495 linebelowheader=Line("┝", "━", "┿", "┥"), 

496 linebetweenrows=None, 

497 linebelow=Line("┕", "━", "┷", "┙"), 

498 headerrow=DataRow("│", "│", "│"), 

499 datarow=DataRow("│", "│", "│"), 

500 padding=1, 

501 with_header_hide=None, 

502 ), 

503 "double_outline": TableFormat( 

504 lineabove=Line("╔", "═", "╦", "╗"), 

505 linebelowheader=Line("╠", "═", "╬", "╣"), 

506 linebetweenrows=None, 

507 linebelow=Line("╚", "═", "╩", "╝"), 

508 headerrow=DataRow("║", "║", "║"), 

509 datarow=DataRow("║", "║", "║"), 

510 padding=1, 

511 with_header_hide=None, 

512 ), 

513 "fancy_outline": TableFormat( 

514 lineabove=Line("╒", "═", "╤", "╕"), 

515 linebelowheader=Line("╞", "═", "╪", "╡"), 

516 linebetweenrows=None, 

517 linebelow=Line("╘", "═", "╧", "╛"), 

518 headerrow=DataRow("│", "│", "│"), 

519 datarow=DataRow("│", "│", "│"), 

520 padding=1, 

521 with_header_hide=None, 

522 ), 

523 "pipe": TableFormat( 

524 lineabove=_pipe_line_with_colons, 

525 linebelowheader=_pipe_line_with_colons, 

526 linebetweenrows=None, 

527 linebelow=None, 

528 headerrow=DataRow("|", "|", "|", GITHUB_ESCAPE_RULES), 

529 datarow=DataRow("|", "|", "|", GITHUB_ESCAPE_RULES), 

530 padding=1, 

531 with_header_hide=["lineabove"], 

532 ), 

533 "orgtbl": TableFormat( 

534 lineabove=None, 

535 linebelowheader=Line("|", "-", "+", "|"), 

536 linebetweenrows=None, 

537 linebelow=None, 

538 headerrow=DataRow("|", "|", "|"), 

539 datarow=DataRow("|", "|", "|"), 

540 padding=1, 

541 with_header_hide=None, 

542 ), 

543 "jira": TableFormat( 

544 lineabove=None, 

545 linebelowheader=None, 

546 linebetweenrows=None, 

547 linebelow=None, 

548 headerrow=DataRow("||", "||", "||"), 

549 datarow=DataRow("|", "|", "|"), 

550 padding=1, 

551 with_header_hide=None, 

552 ), 

553 "presto": TableFormat( 

554 lineabove=None, 

555 linebelowheader=Line("", "-", "+", ""), 

556 linebetweenrows=None, 

557 linebelow=None, 

558 headerrow=DataRow("", "|", ""), 

559 datarow=DataRow("", "|", ""), 

560 padding=1, 

561 with_header_hide=None, 

562 ), 

563 "pretty": TableFormat( 

564 lineabove=Line("+", "-", "+", "+"), 

565 linebelowheader=Line("+", "-", "+", "+"), 

566 linebetweenrows=None, 

567 linebelow=Line("+", "-", "+", "+"), 

568 headerrow=DataRow("|", "|", "|"), 

569 datarow=DataRow("|", "|", "|"), 

570 padding=1, 

571 with_header_hide=None, 

572 ), 

573 "psql": TableFormat( 

574 lineabove=Line("+", "-", "+", "+"), 

575 linebelowheader=Line("|", "-", "+", "|"), 

576 linebetweenrows=None, 

577 linebelow=Line("+", "-", "+", "+"), 

578 headerrow=DataRow("|", "|", "|"), 

579 datarow=DataRow("|", "|", "|"), 

580 padding=1, 

581 with_header_hide=None, 

582 ), 

583 "rst": TableFormat( 

584 lineabove=Line("", "=", " ", ""), 

585 linebelowheader=Line("", "=", " ", ""), 

586 linebetweenrows=None, 

587 linebelow=Line("", "=", " ", ""), 

588 headerrow=DataRow("", " ", ""), 

589 datarow=DataRow("", " ", ""), 

590 padding=0, 

591 with_header_hide=None, 

592 ), 

593 "mediawiki": TableFormat( 

594 lineabove=Line( 

595 '{| class="wikitable" style="text-align: left;"', 

596 "", 

597 "", 

598 "\n|+ <!-- caption -->\n|-", 

599 ), 

600 linebelowheader=Line("|-", "", "", ""), 

601 linebetweenrows=Line("|-", "", "", ""), 

602 linebelow=Line("|}", "", "", ""), 

603 headerrow=partial(_mediawiki_row_with_attrs, "!"), 

604 datarow=partial(_mediawiki_row_with_attrs, "|"), 

605 padding=0, 

606 with_header_hide=None, 

607 ), 

608 "moinmoin": TableFormat( 

609 lineabove=None, 

610 linebelowheader=None, 

611 linebetweenrows=None, 

612 linebelow=None, 

613 headerrow=partial(_moin_row_with_attrs, "||", header="'''"), 

614 datarow=partial(_moin_row_with_attrs, "||"), 

615 padding=1, 

616 with_header_hide=None, 

617 ), 

618 "html": TableFormat( 

619 lineabove=_html_begin_table_without_header, 

620 linebelowheader="", 

621 linebetweenrows=None, 

622 linebelow=Line("</tbody>\n</table>", "", "", ""), 

623 headerrow=partial(_html_row_with_attrs, "th", False), 

624 datarow=partial(_html_row_with_attrs, "td", False), 

625 padding=0, 

626 with_header_hide=["lineabove"], 

627 ), 

628 "unsafehtml": TableFormat( 

629 lineabove=_html_begin_table_without_header, 

630 linebelowheader="", 

631 linebetweenrows=None, 

632 linebelow=Line("</tbody>\n</table>", "", "", ""), 

633 headerrow=partial(_html_row_with_attrs, "th", True), 

634 datarow=partial(_html_row_with_attrs, "td", True), 

635 padding=0, 

636 with_header_hide=["lineabove"], 

637 ), 

638 "latex": TableFormat( 

639 lineabove=_latex_line_begin_tabular, 

640 linebelowheader=Line("\\hline", "", "", ""), 

641 linebetweenrows=None, 

642 linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), 

643 headerrow=_latex_row, 

644 datarow=_latex_row, 

645 padding=1, 

646 with_header_hide=None, 

647 ), 

648 "latex_raw": TableFormat( 

649 lineabove=_latex_line_begin_tabular, 

650 linebelowheader=Line("\\hline", "", "", ""), 

651 linebetweenrows=None, 

652 linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), 

653 headerrow=DataRow("", "&", "\\\\", {}), 

654 datarow=DataRow("", "&", "\\\\", {}), 

655 padding=1, 

656 with_header_hide=None, 

657 ), 

658 "latex_booktabs": TableFormat( 

659 lineabove=partial(_latex_line_begin_tabular, booktabs=True), 

660 linebelowheader=Line("\\midrule", "", "", ""), 

661 linebetweenrows=None, 

662 linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""), 

663 headerrow=_latex_row, 

664 datarow=_latex_row, 

665 padding=1, 

666 with_header_hide=None, 

667 ), 

668 "latex_longtable": TableFormat( 

669 lineabove=partial(_latex_line_begin_tabular, longtable=True), 

670 linebelowheader=Line("\\hline\n\\endhead", "", "", ""), 

671 linebetweenrows=None, 

672 linebelow=Line("\\hline\n\\end{longtable}", "", "", ""), 

673 headerrow=_latex_row, 

674 datarow=_latex_row, 

675 padding=1, 

676 with_header_hide=None, 

677 ), 

678 "tsv": TableFormat( 

679 lineabove=None, 

680 linebelowheader=None, 

681 linebetweenrows=None, 

682 linebelow=None, 

683 headerrow=DataRow("", "\t", ""), 

684 datarow=DataRow("", "\t", ""), 

685 padding=0, 

686 with_header_hide=None, 

687 ), 

688 "textile": TableFormat( 

689 lineabove=None, 

690 linebelowheader=None, 

691 linebetweenrows=None, 

692 linebelow=None, 

693 headerrow=DataRow("|_. ", "|_.", "|"), 

694 datarow=_textile_row_with_attrs, 

695 padding=1, 

696 with_header_hide=None, 

697 ), 

698 "asciidoc": TableFormat( 

699 lineabove=partial(_asciidoc_row, False), 

700 linebelowheader=None, 

701 linebetweenrows=None, 

702 linebelow=Line("|====", "", "", ""), 

703 headerrow=partial(_asciidoc_row, True), 

704 datarow=partial(_asciidoc_row, False), 

705 padding=1, 

706 with_header_hide=["lineabove"], 

707 ), 

708} 

709 

710# "github" is an alias for "pipe": both produce GitHub-flavored Markdown with 

711# alignment colons in the separator row. 

712_table_formats["github"] = _table_formats["pipe"] 

713 

714 

715tabulate_formats = sorted(_table_formats.keys()) 

716 

717# The table formats for which multiline cells will be folded into subsequent 

718# table rows. The key is the original format specified at the API. The value is 

719# the format that will be used to represent the original format. 

720multiline_formats = { 

721 "plain": "plain", 

722 "simple": "simple", 

723 "grid": "grid", 

724 "simple_grid": "simple_grid", 

725 "rounded_grid": "rounded_grid", 

726 "heavy_grid": "heavy_grid", 

727 "mixed_grid": "mixed_grid", 

728 "double_grid": "double_grid", 

729 "fancy_grid": "fancy_grid", 

730 "colon_grid": "colon_grid", 

731 "pipe": "pipe", 

732 "orgtbl": "orgtbl", 

733 "jira": "jira", 

734 "presto": "presto", 

735 "pretty": "pretty", 

736 "psql": "psql", 

737 "rst": "rst", 

738 "github": "github", 

739 "outline": "outline", 

740 "simple_outline": "simple_outline", 

741 "rounded_outline": "rounded_outline", 

742 "heavy_outline": "heavy_outline", 

743 "mixed_outline": "mixed_outline", 

744 "double_outline": "double_outline", 

745 "fancy_outline": "fancy_outline", 

746} 

747 

748# TODO: Add multiline support for the remaining table formats: 

749# - mediawiki: Replace \n with <br> 

750# - moinmoin: TBD 

751# - html: Replace \n with <br> 

752# - latex*: Use "makecell" package: In header, replace X\nY with 

753# \thead{X\\Y} and in data row, replace X\nY with \makecell{X\\Y} 

754# - tsv: TBD 

755# - textile: Replace \n with <br/> (must be well-formed XML) 

756 

757_multiline_codes = re.compile(r"\r|\n|\r\n") 

758_multiline_codes_bytes = re.compile(b"\r|\n|\r\n") 

759 

760# Handle ANSI escape sequences for both control sequence introducer (CSI) and 

761# operating system command (OSC). Both of these begin with 0x1b (or octal 033), 

762# which will be shown below as ESC. 

763# 

764# CSI ANSI escape codes have the following format, defined in section 5.4 of ECMA-48: 

765# 

766# CSI: ESC followed by the '[' character (0x5b) 

767# Parameter Bytes: 0..n bytes in the range 0x30-0x3f 

768# Intermediate Bytes: 0..n bytes in the range 0x20-0x2f 

769# Final Byte: a single byte in the range 0x40-0x7e 

770# 

771# Also include the terminal hyperlink sequences as described here: 

772# https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda 

773# 

774# OSC 8 ; params ; uri ST display_text OSC 8 ;; ST 

775# 

776# Example: \x1b]8;;https://example.com\x5ctext to show\x1b]8;;\x5c 

777# 

778# Where: 

779# OSC: ESC followed by the ']' character (0x5d) 

780# params: 0..n optional key value pairs separated by ':' (e.g. foo=bar:baz=qux:abc=123) 

781# URI: the actual URI with protocol scheme (e.g. https://, file://, ftp://) 

782# ST: ESC followed by the '\' character (0x5c) 

783_esc = r"\x1b" 

784_csi = rf"{_esc}\[" 

785_osc = rf"{_esc}\]" 

786_st = rf"{_esc}\\" 

787 

788_ansi_escape_pat = rf""" 

789 ( 

790 # terminal colors, etc 

791 {_csi} # CSI 

792 [\x30-\x3f]* # parameter bytes 

793 [\x20-\x2f]* # intermediate bytes 

794 [\x40-\x7e] # final byte 

795 | 

796 # terminal hyperlinks 

797 {_osc}8; # OSC opening 

798 (\w+=\w+:?)* # key=value params list (submatch 2) 

799 ; # delimiter 

800 ([^{_esc}]+) # URI - anything but ESC (submatch 3) 

801 {_st} # ST 

802 ([^{_esc}]+) # link text - anything but ESC (submatch 4) 

803 {_osc}8;;{_st} # "closing" OSC sequence 

804 ) 

805""" 

806_ansi_codes = re.compile(_ansi_escape_pat, re.VERBOSE) 

807_ansi_codes_bytes = re.compile(_ansi_escape_pat.encode("utf8"), re.VERBOSE) 

808_ansi_color_reset_code = "\033[0m" 

809 

810_float_with_thousands_separators = re.compile( 

811 r"^(([+-]?[0-9]{1,3})(?:,([0-9]{3}))*)?(?(1)\.[0-9]*|\.[0-9]+)?$" 

812) 

813 

814 

815def simple_separated_format(separator): 

816 """Construct a simple TableFormat with columns separated by a separator. 

817 

818 >>> tsv = simple_separated_format("\\t") ; \ 

819 tabulate([["foo", 1], ["spam", 23]], tablefmt=tsv) == 'foo \\t 1\\nspam\\t23' 

820 True 

821 

822 """ 

823 return TableFormat( 

824 None, 

825 None, 

826 None, 

827 None, 

828 headerrow=DataRow("", separator, ""), 

829 datarow=DataRow("", separator, ""), 

830 padding=0, 

831 with_header_hide=None, 

832 ) 

833 

834 

835def _isnumber_with_thousands_separator(string): 

836 """ 

837 >>> _isnumber_with_thousands_separator(".") 

838 False 

839 >>> _isnumber_with_thousands_separator("1") 

840 True 

841 >>> _isnumber_with_thousands_separator("1.") 

842 True 

843 >>> _isnumber_with_thousands_separator(".1") 

844 True 

845 >>> _isnumber_with_thousands_separator("1000") 

846 False 

847 >>> _isnumber_with_thousands_separator("1,000") 

848 True 

849 >>> _isnumber_with_thousands_separator("1,0000") 

850 False 

851 >>> _isnumber_with_thousands_separator("1,000.1234") 

852 True 

853 >>> _isnumber_with_thousands_separator(b"1,000.1234") 

854 True 

855 >>> _isnumber_with_thousands_separator("+1,000.1234") 

856 True 

857 >>> _isnumber_with_thousands_separator("-1,000.1234") 

858 True 

859 """ 

860 try: 

861 string = string.decode() 

862 except (UnicodeDecodeError, AttributeError): 

863 pass 

864 

865 return bool(re.match(_float_with_thousands_separators, string)) 

866 

867 

868def _isconvertible(conv, string): 

869 try: 

870 conv(string) 

871 return True 

872 except (ValueError, TypeError): 

873 return False 

874 

875 

876def _isnumber(string): 

877 """Detects if something *could* be considered a numeric value, vs. just a string. 

878 

879 This promotes types convertible to both int and float to be considered 

880 a float. Note that, iff *all* values appear to be some form of numeric 

881 value such as eg. "1e2", they would be considered numbers! 

882 

883 The exception is things that appear to be numbers but overflow to 

884 +/-inf, eg. "1e23456"; we'll have to exclude them explicitly. 

885 

886 >>> _isnumber(123) 

887 True 

888 >>> _isnumber(123.45) 

889 True 

890 >>> _isnumber("123.45") 

891 True 

892 >>> _isnumber("123") 

893 True 

894 >>> _isnumber("spam") 

895 False 

896 >>> _isnumber("123e45") 

897 True 

898 >>> _isnumber("123e45678") # evaluates equal to 'inf', but ... isn't 

899 False 

900 >>> _isnumber("inf") 

901 True 

902 >>> from fractions import Fraction 

903 >>> _isnumber(Fraction(1,3)) 

904 True 

905 

906 """ 

907 return ( 

908 # fast path 

909 type(string) in (float, int) 

910 # covers 'NaN', +/- 'inf', and eg. '1e2', as well as any type 

911 # convertible to int/float. 

912 or ( 

913 _isconvertible(float, string) 

914 and ( 

915 # some other type convertible to float 

916 not isinstance(string, (str, bytes)) 

917 # or, a numeric string eg. "1e1...", "NaN", ..., but isn't 

918 # just an over/underflow 

919 or ( 

920 not (math.isinf(float(string)) or math.isnan(float(string))) 

921 or string.lower() in ["inf", "-inf", "nan"] 

922 ) 

923 ) 

924 ) 

925 ) 

926 

927 

928def _isint(string, inttype=int): 

929 """ 

930 >>> _isint("123") 

931 True 

932 >>> _isint("123.45") 

933 False 

934 """ 

935 return ( 

936 type(string) is inttype 

937 or ( 

938 (hasattr(string, "is_integer") or hasattr(string, "__array__")) 

939 and str(type(string)).startswith("<class 'numpy.int") 

940 ) # numpy.int64 and similar 

941 or ( 

942 isinstance(string, (bytes, str)) and _isconvertible(inttype, string) 

943 ) # integer as string 

944 ) 

945 

946 

947def _isbool(string): 

948 """ 

949 >>> _isbool(True) 

950 True 

951 >>> _isbool("False") 

952 True 

953 >>> _isbool(1) 

954 False 

955 """ 

956 return type(string) is bool or ( 

957 isinstance(string, (bytes, str)) and string in ("True", "False") 

958 ) 

959 

960 

961def _type(string, has_invisible=True, numparse=True): 

962 """The least generic type (type(None), int, float, str, unicode). 

963 

964 Treats empty string as missing for the purposes of type deduction, so as to not influence 

965 the type of an otherwise complete column; does *not* result in missingval replacement! 

966 

967 >>> _type(None) is type(None) 

968 True 

969 >>> _type("") is type(None) 

970 True 

971 >>> _type("foo") is type("") 

972 True 

973 >>> _type("1") is type(1) 

974 True 

975 >>> _type('\x1b[31m42\x1b[0m') is type(42) 

976 True 

977 >>> _type('\x1b[31m42\x1b[0m') is type(42) 

978 True 

979 

980 """ 

981 

982 if has_invisible and isinstance(string, (str, bytes)): 

983 string = _strip_ansi(string) 

984 

985 if string is None or (isinstance(string, (bytes, str)) and not string): 

986 return type(None) 

987 elif hasattr(string, "isoformat"): # datetime.datetime, date, and time 

988 return str 

989 elif _isbool(string): 

990 return bool 

991 elif numparse and ( 

992 _isint(string) 

993 or ( 

994 isinstance(string, str) 

995 and _isnumber_with_thousands_separator(string) 

996 and "." not in string 

997 ) 

998 ): 

999 return int 

1000 elif numparse and ( 

1001 _isnumber(string) 

1002 or (isinstance(string, str) and _isnumber_with_thousands_separator(string)) 

1003 ): 

1004 return float 

1005 elif isinstance(string, bytes): 

1006 return bytes 

1007 else: 

1008 return str 

1009 

1010 

1011def _afterpoint(string): 

1012 """Symbols after a decimal point, -1 if the string lacks the decimal point. 

1013 

1014 >>> _afterpoint("123.45") 

1015 2 

1016 >>> _afterpoint("1001") 

1017 -1 

1018 >>> _afterpoint("eggs") 

1019 -1 

1020 >>> _afterpoint("123e45") 

1021 2 

1022 >>> _afterpoint("123,456.78") 

1023 2 

1024 

1025 """ 

1026 if _isnumber(string) or _isnumber_with_thousands_separator(string): 

1027 if _isint(string): 

1028 return -1 

1029 else: 

1030 pos = string.rfind(".") 

1031 pos = string.lower().rfind("e") if pos < 0 else pos 

1032 if pos >= 0: 

1033 return len(string) - pos - 1 

1034 else: 

1035 return -1 # no point 

1036 else: 

1037 return -1 # not a number 

1038 

1039 

1040def _padleft(width, s): 

1041 """Flush right. 

1042 

1043 >>> _padleft(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430' 

1044 True 

1045 

1046 """ 

1047 fmt = f"{{0:>{width}s}}" 

1048 return fmt.format(s) 

1049 

1050 

1051def _padright(width, s): 

1052 """Flush left. 

1053 

1054 >>> _padright(6, '\u044f\u0439\u0446\u0430') == '\u044f\u0439\u0446\u0430 ' 

1055 True 

1056 

1057 """ 

1058 fmt = f"{{0:<{width}s}}" 

1059 return fmt.format(s) 

1060 

1061 

1062def _padboth(width, s): 

1063 """Center string. 

1064 

1065 >>> _padboth(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430 ' 

1066 True 

1067 

1068 """ 

1069 fmt = f"{{0:^{width}s}}" 

1070 return fmt.format(s) 

1071 

1072 

1073def _padnone(ignore_width, s): 

1074 return s 

1075 

1076 

1077def _strip_ansi(s): 

1078 r"""Remove ANSI escape sequences, both CSI (color codes, etc) and OSC hyperlinks. 

1079 

1080 CSI sequences are simply removed from the output, while OSC hyperlinks are replaced 

1081 with the link text. Note: it may be desirable to show the URI instead but this is not 

1082 supported. 

1083 

1084 >>> repr(_strip_ansi('\x1B]8;;https://example.com\x1B\\This is a link\x1B]8;;\x1B\\')) 

1085 "'This is a link'" 

1086 

1087 >>> repr(_strip_ansi('\x1b[31mred\x1b[0m text')) 

1088 "'red text'" 

1089 

1090 """ 

1091 if isinstance(s, str): 

1092 return _ansi_codes.sub(r"\4", s) 

1093 else: # a bytestring 

1094 return _ansi_codes_bytes.sub(r"\4", s) 

1095 

1096 

1097def _visible_width(s): 

1098 """Visible width of a printed string. ANSI color codes are removed. 

1099 

1100 >>> _visible_width('\x1b[31mhello\x1b[0m'), _visible_width("world") 

1101 (5, 5) 

1102 

1103 """ 

1104 # optional wide-character support 

1105 if wcwidth is not None and WIDE_CHARS_MODE: 

1106 # when already a string, it could contain terminal sequences, 

1107 # wcwidth >= 0.3.0 handles ANSI codes internally, 

1108 if hasattr(wcwidth, "width"): 

1109 return wcwidth.width(str(s)) 

1110 # while previous versions need them stripped first. 

1111 if isinstance(s, (str, bytes)): 

1112 return wcwidth.wcswidth(_strip_ansi(str(s))) 

1113 

1114 # Otherwise, coerce to string, guaranteed to be without any control codes, 

1115 # we can use wcswidth() directly. 

1116 return wcwidth.wcswidth(str(s)) 

1117 if isinstance(s, (str, bytes)): 

1118 return len(_strip_ansi(s)) 

1119 else: 

1120 return len(str(s)) 

1121 

1122 

1123def _is_multiline(s): 

1124 if isinstance(s, str): 

1125 return bool(re.search(_multiline_codes, s)) 

1126 else: # a bytestring 

1127 return bool(re.search(_multiline_codes_bytes, s)) 

1128 

1129 

1130def _multiline_width(multiline_s, line_width_fn=len): 

1131 """Visible width of a potentially multiline content.""" 

1132 return max(map(line_width_fn, re.split("[\r\n]", multiline_s))) 

1133 

1134 

1135def _choose_width_fn(has_invisible, enable_widechars, is_multiline): 

1136 """Return a function to calculate visible cell width.""" 

1137 if has_invisible: 

1138 line_width_fn = _visible_width 

1139 elif enable_widechars: # optional wide-character support if available 

1140 line_width_fn = wcwidth.wcswidth 

1141 else: 

1142 line_width_fn = len 

1143 if is_multiline: 

1144 width_fn = lambda s: _multiline_width(s, line_width_fn) # noqa: E731 

1145 else: 

1146 width_fn = line_width_fn 

1147 return width_fn 

1148 

1149 

1150def _align_column_choose_padfn(strings, alignment, has_invisible, preserve_whitespace): 

1151 if alignment == "right": 

1152 if not preserve_whitespace: 

1153 strings = [s.strip() for s in strings] 

1154 padfn = _padleft 

1155 elif alignment == "center": 

1156 if not preserve_whitespace: 

1157 strings = [s.strip() for s in strings] 

1158 padfn = _padboth 

1159 elif alignment == "decimal": 

1160 if has_invisible: 

1161 decimals = [_afterpoint(_strip_ansi(s)) for s in strings] 

1162 else: 

1163 decimals = [_afterpoint(s) for s in strings] 

1164 maxdecimals = max(decimals) 

1165 strings = [s + (maxdecimals - decs) * " " for s, decs in zip(strings, decimals)] 

1166 padfn = _padleft 

1167 elif not alignment: 

1168 padfn = _padnone 

1169 else: 

1170 if not preserve_whitespace: 

1171 strings = [s.strip() for s in strings] 

1172 padfn = _padright 

1173 return strings, padfn 

1174 

1175 

1176def _align_column_choose_width_fn(has_invisible, enable_widechars, is_multiline): 

1177 if has_invisible: 

1178 line_width_fn = _visible_width 

1179 elif enable_widechars: # optional wide-character support if available 

1180 line_width_fn = wcwidth.wcswidth 

1181 else: 

1182 line_width_fn = len 

1183 if is_multiline: 

1184 width_fn = lambda s: _align_column_multiline_width( # noqa: E731 

1185 s, line_width_fn 

1186 ) 

1187 else: 

1188 width_fn = line_width_fn 

1189 return width_fn 

1190 

1191 

1192def _align_column_multiline_width(multiline_s, line_width_fn=len): 

1193 """Visible width of a potentially multiline content.""" 

1194 return list(map(line_width_fn, re.split("[\r\n]", multiline_s))) 

1195 

1196 

1197def _flat_list(nested_list): 

1198 ret = [] 

1199 for item in nested_list: 

1200 if isinstance(item, list): 

1201 ret.extend(item) 

1202 else: 

1203 ret.append(item) 

1204 return ret 

1205 

1206 

1207def _align_column( 

1208 strings, 

1209 alignment, 

1210 minwidth=0, 

1211 has_invisible=True, 

1212 enable_widechars=False, 

1213 is_multiline=False, 

1214 preserve_whitespace=False, 

1215): 

1216 """[string] -> [padded_string]""" 

1217 strings, padfn = _align_column_choose_padfn( 

1218 strings, alignment, has_invisible, preserve_whitespace 

1219 ) 

1220 width_fn = _align_column_choose_width_fn(has_invisible, enable_widechars, is_multiline) 

1221 

1222 s_widths = list(map(width_fn, strings)) 

1223 maxwidth = max(max(_flat_list(s_widths)), minwidth) 

1224 # TODO: refactor column alignment in single-line and multiline modes 

1225 if is_multiline: 

1226 if not enable_widechars and not has_invisible: 

1227 padded_strings = [ 

1228 "\n".join([padfn(maxwidth, s) for s in ms.splitlines()]) for ms in strings 

1229 ] 

1230 else: 

1231 # enable wide-character width corrections 

1232 s_lens = [[len(s) for s in re.split("[\r\n]", ms)] for ms in strings] 

1233 visible_widths = [ 

1234 [maxwidth - (w - ln) for w, ln in zip(mw, ml)] for mw, ml in zip(s_widths, s_lens) 

1235 ] 

1236 # wcswidth and _visible_width don't count invisible characters; 

1237 # padfn doesn't need to apply another correction 

1238 padded_strings = [ 

1239 "\n".join([padfn(w, s) for s, w in zip((ms.splitlines() or ms), mw)]) 

1240 for ms, mw in zip(strings, visible_widths) 

1241 ] 

1242 else: # single-line cell values 

1243 if not enable_widechars and not has_invisible: 

1244 padded_strings = [padfn(maxwidth, s) for s in strings] 

1245 else: 

1246 # enable wide-character width corrections 

1247 s_lens = list(map(len, strings)) 

1248 visible_widths = [maxwidth - (w - ln) for w, ln in zip(s_widths, s_lens)] 

1249 # wcswidth and _visible_width don't count invisible characters; 

1250 # padfn doesn't need to apply another correction 

1251 padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)] 

1252 return padded_strings 

1253 

1254 

1255def _more_generic(type1, type2): 

1256 types = { 

1257 type(None): 0, 

1258 bool: 1, 

1259 int: 2, 

1260 float: 3, 

1261 bytes: 4, 

1262 str: 5, 

1263 } 

1264 invtypes = { 

1265 5: str, 

1266 4: bytes, 

1267 3: float, 

1268 2: int, 

1269 1: bool, 

1270 0: type(None), 

1271 } 

1272 moregeneric = max(types.get(type1, 5), types.get(type2, 5)) 

1273 return invtypes[moregeneric] 

1274 

1275 

1276def _column_type(strings, has_invisible=True, numparse=True): 

1277 """The least generic type all column values are convertible to. 

1278 

1279 >>> _column_type([True, False]) is bool 

1280 True 

1281 >>> _column_type(["1", "2"]) is int 

1282 True 

1283 >>> _column_type(["1", "2.3"]) is float 

1284 True 

1285 >>> _column_type(["1", "2.3", "four"]) is str 

1286 True 

1287 >>> _column_type(["four", '\u043f\u044f\u0442\u044c']) is str 

1288 True 

1289 >>> _column_type([None, "brux"]) is str 

1290 True 

1291 >>> _column_type([1, 2, None]) is int 

1292 True 

1293 >>> import datetime as dt 

1294 >>> _column_type([dt.datetime(1991,2,19), dt.time(17,35)]) is str 

1295 True 

1296 

1297 """ 

1298 types = [_type(s, has_invisible, numparse) for s in strings] 

1299 return reduce(_more_generic, types, bool) 

1300 

1301 

1302def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True): 

1303 """Format a value according to its deduced type. Empty values are deemed valid for any type. 

1304 

1305 Unicode is supported: 

1306 

1307 >>> hrow = ['\u0431\u0443\u043a\u0432\u0430', '\u0446\u0438\u0444\u0440\u0430'] ; \ 

1308 tbl = [['\u0430\u0437', 2], ['\u0431\u0443\u043a\u0438', 4]] ; \ 

1309 good_result = '\\u0431\\u0443\\u043a\\u0432\\u0430 \\u0446\\u0438\\u0444\\u0440\\u0430\\n------- -------\\n\\u0430\\u0437 2\\n\\u0431\\u0443\\u043a\\u0438 4' ; \ 

1310 tabulate(tbl, headers=hrow) == good_result 

1311 True 

1312 

1313 """ 

1314 if val is None: 

1315 return missingval 

1316 if isinstance(val, (bytes, str)) and not val: 

1317 return "" 

1318 

1319 if valtype is str: 

1320 return f"{val}" 

1321 elif valtype is int: 

1322 if isinstance(val, str): 

1323 val_striped = val.encode("unicode_escape").decode("utf-8") 

1324 colored = re.search(r"(\\[xX]+[0-9a-fA-F]+\[\d+[mM]+)([0-9.]+)(\\.*)$", val_striped) 

1325 if colored: 

1326 total_groups = len(colored.groups()) 

1327 if total_groups == 3: 

1328 digits = colored.group(2) 

1329 if digits.isdigit(): 

1330 val_new = colored.group(1) + format(int(digits), intfmt) + colored.group(3) 

1331 val = val_new.encode("utf-8").decode("unicode_escape") 

1332 intfmt = "" 

1333 return format(val, intfmt) 

1334 elif valtype is bytes: 

1335 try: 

1336 return str(val, "ascii") 

1337 except (TypeError, UnicodeDecodeError): 

1338 return str(val) 

1339 elif valtype is float: 

1340 is_a_colored_number = has_invisible and isinstance(val, (str, bytes)) 

1341 if is_a_colored_number: 

1342 raw_val = _strip_ansi(val) 

1343 try: 

1344 formatted_val = format(float(raw_val), floatfmt) 

1345 except (ValueError, TypeError): 

1346 return f"{val}" 

1347 return val.replace(raw_val, formatted_val) 

1348 else: 

1349 if isinstance(val, str) and "," in val: 

1350 val = val.replace(",", "") # handle thousands-separators 

1351 if isinstance(val, Decimal): 

1352 return format(val, floatfmt) 

1353 try: 

1354 return format(float(val), floatfmt) 

1355 except (ValueError, TypeError): 

1356 return f"{val}" 

1357 else: 

1358 return f"{val}" 

1359 

1360 

1361def _align_header(header, alignment, width, visible_width, is_multiline=False, width_fn=None): 

1362 "Pad string header to width chars given known visible_width of the header." 

1363 if is_multiline: 

1364 header_lines = re.split(_multiline_codes, header) 

1365 padded_lines = [_align_header(h, alignment, width, width_fn(h)) for h in header_lines] 

1366 return "\n".join(padded_lines) 

1367 # else: not multiline 

1368 ninvisible = len(header) - visible_width 

1369 width += ninvisible 

1370 if alignment == "left": 

1371 return _padright(width, header) 

1372 elif alignment == "center": 

1373 return _padboth(width, header) 

1374 elif not alignment: 

1375 return f"{header}" 

1376 else: 

1377 return _padleft(width, header) 

1378 

1379 

1380def _remove_separating_lines(rows): 

1381 if isinstance(rows, list): 

1382 separating_lines = [] 

1383 sans_rows = [] 

1384 for index, row in enumerate(rows): 

1385 if _is_separating_line(row): 

1386 separating_lines.append(index) 

1387 else: 

1388 sans_rows.append(row) 

1389 return sans_rows, separating_lines 

1390 else: 

1391 return rows, None 

1392 

1393 

1394def _reinsert_separating_lines(rows, separating_lines): 

1395 if separating_lines: 

1396 for index in separating_lines: 

1397 rows.insert(index, SEPARATING_LINE) 

1398 

1399 

1400def _prepend_row_index(rows, index): 

1401 """Add a left-most index column.""" 

1402 if index is None or index is False: 

1403 return rows 

1404 if isinstance(index, Sized) and len(index) != len(rows): 

1405 raise ValueError( 

1406 "index must be as long as the number of data rows: " 

1407 f"len(index)={len(index)} len(rows)={len(rows)}" 

1408 ) 

1409 sans_rows, separating_lines = _remove_separating_lines(rows) 

1410 new_rows = [] 

1411 index_iter = iter(index) 

1412 for row in sans_rows: 

1413 index_v = next(index_iter) 

1414 new_rows.append([index_v] + list(row)) 

1415 rows = new_rows 

1416 _reinsert_separating_lines(rows, separating_lines) 

1417 return rows 

1418 

1419 

1420def _bool(val): 

1421 "A wrapper around standard bool() which doesn't throw on NumPy arrays" 

1422 try: 

1423 return bool(val) 

1424 except ValueError: # val is likely to be a numpy array with many elements 

1425 return False 

1426 

1427 

1428def _normalize_tabular_data(tabular_data, headers, showindex="default"): 

1429 """Transform a supported data type to a list of lists, and a list of headers, 

1430 with headers padding. 

1431 

1432 Supported tabular data types: 

1433 

1434 * list-of-lists or another iterable of iterables 

1435 

1436 * list of named tuples (usually used with headers="keys") 

1437 

1438 * list of dicts (usually used with headers="keys") 

1439 

1440 * list of OrderedDicts (usually used with headers="keys") 

1441 

1442 * list of dataclasses (usually used with headers="keys") 

1443 

1444 * 2D NumPy arrays 

1445 

1446 * NumPy record arrays (usually used with headers="keys") 

1447 

1448 * dict of iterables (usually used with headers="keys") 

1449 

1450 * pandas.DataFrame (usually used with headers="keys") 

1451 

1452 The first row can be used as headers if headers="firstrow", 

1453 column indices can be used as headers if headers="keys". 

1454 

1455 If showindex="default", show row indices of the pandas.DataFrame. 

1456 If showindex="always", show row indices for all types of data. 

1457 If showindex="never", don't show row indices for all types of data. 

1458 If showindex is an iterable, show its values as row indices. 

1459 

1460 """ 

1461 

1462 try: 

1463 bool(headers) 

1464 except ValueError: # numpy.ndarray, pandas.core.index.Index, ... 

1465 headers = list(headers) 

1466 

1467 err_msg = ( 

1468 "\n\nTo build a table python-tabulate requires two-dimensional data " 

1469 "like a list of lists or similar." 

1470 "\nDid you forget a pair of extra [] or ',' in ()?" 

1471 ) 

1472 index = None 

1473 if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"): 

1474 # dict-like and pandas.DataFrame? 

1475 if callable(tabular_data.values): 

1476 # likely a conventional dict 

1477 keys = tabular_data.keys() 

1478 try: 

1479 rows = list(izip_longest(*tabular_data.values())) # columns have to be transposed 

1480 except TypeError as e: # not iterable 

1481 raise TypeError(err_msg) from e 

1482 

1483 elif hasattr(tabular_data, "index"): 

1484 # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0) 

1485 keys = list(tabular_data) 

1486 if showindex in ["default", "always", True] and tabular_data.index.name is not None: 

1487 if isinstance(tabular_data.index.name, list): 

1488 keys[:0] = tabular_data.index.name 

1489 else: 

1490 keys[:0] = [tabular_data.index.name] 

1491 vals = tabular_data.values # values matrix doesn't need to be transposed 

1492 # for DataFrames add an index per default 

1493 index = list(tabular_data.index) 

1494 rows = [list(row) for row in vals] 

1495 else: 

1496 raise ValueError("tabular data doesn't appear to be a dict or a DataFrame") 

1497 

1498 if headers == "keys": 

1499 headers = list(map(str, keys)) # headers should be strings 

1500 

1501 else: # it's a usual iterable of iterables, or a NumPy array, or an iterable of dataclasses 

1502 try: 

1503 rows = list(tabular_data) 

1504 except TypeError as e: # not iterable 

1505 raise TypeError(err_msg) from e 

1506 

1507 if headers == "keys" and not rows: 

1508 # an empty table (issue #81) 

1509 headers = [] 

1510 elif headers == "keys" and hasattr(tabular_data, "dtype") and tabular_data.dtype.names: 

1511 # numpy record array 

1512 headers = tabular_data.dtype.names 

1513 elif ( 

1514 headers == "keys" 

1515 and len(rows) > 0 

1516 and isinstance(rows[0], tuple) 

1517 and hasattr(rows[0], "_fields") 

1518 ): 

1519 # namedtuple 

1520 headers = list(map(str, rows[0]._fields)) 

1521 elif len(rows) > 0 and hasattr(rows[0], "keys") and hasattr(rows[0], "values"): 

1522 # dict-like object 

1523 uniq_keys = set() # implements hashed lookup 

1524 keys = [] # storage for set 

1525 if headers == "firstrow": 

1526 firstdict = rows[0] if len(rows) > 0 else {} 

1527 keys.extend(firstdict.keys()) 

1528 uniq_keys.update(keys) 

1529 rows = rows[1:] 

1530 for row in rows: 

1531 for k in row.keys(): 

1532 # Save unique items in input order 

1533 if k not in uniq_keys: 

1534 keys.append(k) 

1535 uniq_keys.add(k) 

1536 if headers == "keys": 

1537 headers = keys 

1538 elif isinstance(headers, dict): 

1539 # a dict of headers for a list of dicts 

1540 headers = [headers.get(k, k) for k in keys] 

1541 headers = list(map(str, headers)) 

1542 elif headers == "firstrow": 

1543 if len(rows) > 0: 

1544 headers = [firstdict.get(k, k) for k in keys] 

1545 headers = list(map(str, headers)) 

1546 else: 

1547 headers = [] 

1548 elif headers: 

1549 raise ValueError("headers for a list of dicts is not a dict or a keyword") 

1550 rows = [[row.get(k) for k in keys] for row in rows] 

1551 

1552 elif ( 

1553 headers == "keys" 

1554 and hasattr(tabular_data, "description") 

1555 and hasattr(tabular_data, "fetchone") 

1556 and hasattr(tabular_data, "rowcount") 

1557 ): 

1558 # Python Database API cursor object (PEP 0249) 

1559 # print tabulate(cursor, headers='keys') 

1560 headers = [column[0] for column in tabular_data.description] 

1561 

1562 elif dataclasses is not None and len(rows) > 0 and dataclasses.is_dataclass(rows[0]): 

1563 # Python's dataclass 

1564 field_names = [field.name for field in dataclasses.fields(rows[0])] 

1565 if headers == "keys": 

1566 headers = field_names 

1567 rows = [ 

1568 ([getattr(row, f) for f in field_names] if not _is_separating_line(row) else row) 

1569 for row in rows 

1570 ] 

1571 

1572 elif headers == "keys" and len(rows) > 0: 

1573 # keys are column indices 

1574 headers = list(map(str, range(len(rows[0])))) 

1575 

1576 # take headers from the first row if necessary 

1577 if headers == "firstrow" and len(rows) > 0: 

1578 if index is not None: 

1579 headers = [index[0]] + list(rows[0]) 

1580 index = index[1:] 

1581 else: 

1582 headers = rows[0] 

1583 headers = list(map(str, headers)) # headers should be strings 

1584 rows = rows[1:] 

1585 elif headers == "firstrow": 

1586 headers = [] 

1587 

1588 headers = list(map(str, headers)) 

1589 # rows = list(map(list, rows)) 

1590 rows = [r if _is_separating_line(r) else list(r) for r in rows] 

1591 

1592 # add or remove an index column 

1593 showindex_is_a_str = type(showindex) in [str, bytes] 

1594 if showindex_is_a_str and showindex == "default" and index is not None: 

1595 rows = _prepend_row_index(rows, index) 

1596 elif isinstance(showindex, Sized) and not showindex_is_a_str: 

1597 rows = _prepend_row_index(rows, list(showindex)) 

1598 elif isinstance(showindex, Iterable) and not showindex_is_a_str: 

1599 rows = _prepend_row_index(rows, showindex) 

1600 elif showindex == "always" or (_bool(showindex) and not showindex_is_a_str): 

1601 if index is None: 

1602 index = list(range(len(rows))) 

1603 rows = _prepend_row_index(rows, index) 

1604 elif showindex == "never" or (not _bool(showindex) and not showindex_is_a_str): 

1605 pass 

1606 

1607 # pad with empty headers for initial columns if necessary 

1608 headers_pad = 0 

1609 if headers and len(rows) > 0: 

1610 headers_pad = max(0, len(rows[0]) - len(headers)) 

1611 headers = [""] * headers_pad + headers 

1612 

1613 return rows, headers, headers_pad 

1614 

1615 

1616def _wrap_text_to_colwidths( 

1617 list_of_lists, 

1618 colwidths, 

1619 numparses=True, 

1620 missingval=_DEFAULT_MISSINGVAL, 

1621 break_long_words=_BREAK_LONG_WORDS, 

1622 break_on_hyphens=_BREAK_ON_HYPHENS, 

1623): 

1624 if len(list_of_lists): 

1625 num_cols = len(list_of_lists[0]) 

1626 else: 

1627 num_cols = 0 

1628 numparses = _expand_iterable(numparses, num_cols, True) 

1629 

1630 result = [] 

1631 

1632 for row in list_of_lists: 

1633 new_row = [] 

1634 for cell, width, numparse in zip(row, colwidths, numparses): 

1635 if _isnumber(cell) and numparse: 

1636 new_row.append(cell) 

1637 continue 

1638 

1639 if width is not None: 

1640 wrapper = _CustomTextWrap( 

1641 width=width, 

1642 break_long_words=break_long_words, 

1643 break_on_hyphens=break_on_hyphens, 

1644 ) 

1645 # Cast based on our internal type handling. Any future custom 

1646 # formatting of types (such as datetimes) may need to be more 

1647 # explicit than just `str` of the object. Also doesn't work for 

1648 # custom floatfmt/intfmt, nor with any missing/blank cells. 

1649 casted_cell = ( 

1650 missingval 

1651 if cell is None 

1652 else ( 

1653 str(cell) 

1654 if cell == "" or _isnumber(cell) 

1655 else str(_type(cell, numparse)(cell)) 

1656 ) 

1657 ) 

1658 wrapped = [ 

1659 "\n".join(wrapper.wrap(line)) 

1660 for line in casted_cell.splitlines() 

1661 if line.strip() != "" 

1662 ] 

1663 new_row.append("\n".join(wrapped)) 

1664 else: 

1665 new_row.append(cell) 

1666 result.append(new_row) 

1667 

1668 return result 

1669 

1670 

1671def _to_str(s, encoding="utf8", errors="ignore"): 

1672 """ 

1673 A type safe wrapper for converting a bytestring to str. This is essentially just 

1674 a wrapper around .decode() intended for use with things like map(), but with some 

1675 specific behavior: 

1676 

1677 1. if the given parameter is not a bytestring, it is returned unmodified 

1678 2. decode() is called for the given parameter and assumes utf8 encoding, but the 

1679 default error behavior is changed from 'strict' to 'ignore' 

1680 

1681 >>> repr(_to_str(b'foo')) 

1682 "'foo'" 

1683 

1684 >>> repr(_to_str('foo')) 

1685 "'foo'" 

1686 

1687 >>> repr(_to_str(42)) 

1688 "'42'" 

1689 

1690 """ 

1691 if isinstance(s, bytes): 

1692 return s.decode(encoding=encoding, errors=errors) 

1693 return str(s) 

1694 

1695 

1696def tabulate( 

1697 tabular_data, 

1698 headers=(), 

1699 tablefmt="simple", 

1700 floatfmt=_DEFAULT_FLOATFMT, 

1701 intfmt=_DEFAULT_INTFMT, 

1702 numalign=_DEFAULT_ALIGN, 

1703 stralign=_DEFAULT_ALIGN, 

1704 missingval=_DEFAULT_MISSINGVAL, 

1705 showindex="default", 

1706 disable_numparse=False, 

1707 colglobalalign=None, 

1708 colalign=None, 

1709 preserve_whitespace=False, 

1710 maxcolwidths=None, 

1711 headersglobalalign=None, 

1712 headersalign=None, 

1713 rowalign=None, 

1714 maxheadercolwidths=None, 

1715 break_long_words=_BREAK_LONG_WORDS, 

1716 break_on_hyphens=_BREAK_ON_HYPHENS, 

1717): 

1718 """Format a fixed width table for pretty printing. 

1719 

1720 >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]])) 

1721 --- --------- 

1722 1 2.34 

1723 -56 8.999 

1724 2 10001 

1725 --- --------- 

1726 

1727 The first required argument (`tabular_data`) can be a 

1728 list-of-lists (or another iterable of iterables), a list of named 

1729 tuples, a dictionary of iterables, an iterable of dictionaries, 

1730 an iterable of dataclasses, a two-dimensional NumPy array, 

1731 NumPy record array, or a Pandas' dataframe. 

1732 

1733 

1734 Table headers 

1735 ------------- 

1736 

1737 To print nice column headers, supply the second argument (`headers`): 

1738 

1739 - `headers` can be an explicit list of column headers 

1740 - if `headers="firstrow"`, then the first row of data is used 

1741 - if `headers="keys"`, then dictionary keys or column indices are used 

1742 

1743 Otherwise a headerless table is produced. 

1744 

1745 If the number of headers is less than the number of columns, they 

1746 are supposed to be names of the last columns. This is consistent 

1747 with the plain-text format of R and Pandas' dataframes. 

1748 

1749 >>> print(tabulate([["sex","age"],["Alice","F",24],["Bob","M",19]], 

1750 ... headers="firstrow")) 

1751 sex age 

1752 ----- ----- ----- 

1753 Alice F 24 

1754 Bob M 19 

1755 

1756 By default, pandas.DataFrame data have an additional column called 

1757 row index. To add a similar column to all other types of data, 

1758 use `showindex="always"` or `showindex=True`. To suppress row indices 

1759 for all types of data, pass `showindex="never" or `showindex=False`. 

1760 To add a custom row index column, pass `showindex=some_iterable`. 

1761 

1762 >>> print(tabulate([["F",24],["M",19]], showindex="always")) 

1763 - - -- 

1764 0 F 24 

1765 1 M 19 

1766 - - -- 

1767 

1768 

1769 Column and Headers alignment 

1770 ---------------------------- 

1771 

1772 `tabulate` tries to detect column types automatically, and aligns 

1773 the values properly. By default it aligns decimal points of the 

1774 numbers (or flushes integer numbers to the right), and flushes 

1775 everything else to the left. Possible column alignments 

1776 (`numalign`, `stralign`) are: "right", "center", "left", "decimal" 

1777 (only for `numalign`), and None (to disable alignment). 

1778 

1779 `colglobalalign` allows for global alignment of columns, before any 

1780 specific override from `colalign`. Possible values are: None 

1781 (defaults according to coltype), "right", "center", "decimal", 

1782 "left". 

1783 `colalign` allows for column-wise override starting from left-most 

1784 column. Possible values are: "global" (no override), "right", 

1785 "center", "decimal", "left". 

1786 `headersglobalalign` allows for global headers alignment, before any 

1787 specific override from `headersalign`. Possible values are: None 

1788 (follow columns alignment), "right", "center", "left". 

1789 `headersalign` allows for header-wise override starting from left-most 

1790 given header. Possible values are: "global" (no override), "same" 

1791 (follow column alignment), "right", "center", "left". 

1792 

1793 Note on intended behaviour: If there is no `tabular_data`, any column 

1794 alignment argument is ignored. Hence, in this case, header 

1795 alignment cannot be inferred from column alignment. 

1796 

1797 Table formats 

1798 ------------- 

1799 

1800 `intfmt` is a format specification used for columns which 

1801 contain numeric data without a decimal point. This can also be 

1802 a list or tuple of format strings, one per column. 

1803 

1804 `floatfmt` is a format specification used for columns which 

1805 contain numeric data with a decimal point. This can also be 

1806 a list or tuple of format strings, one per column. 

1807 

1808 `None` values are replaced with a `missingval` string (like 

1809 `floatfmt`, this can also be a list of values for different 

1810 columns): 

1811 

1812 >>> print(tabulate([["spam", 1, None], 

1813 ... ["eggs", 42, 3.14], 

1814 ... ["other", None, 2.7]], missingval="?")) 

1815 ----- -- ---- 

1816 spam 1 ? 

1817 eggs 42 3.14 

1818 other ? 2.7 

1819 ----- -- ---- 

1820 

1821 Various plain-text table formats (`tablefmt`) are supported: 

1822 'plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', 'mediawiki', 

1823 'latex', 'latex_raw', 'latex_booktabs', 'latex_longtable' and tsv. 

1824 Variable `tabulate_formats`contains the list of currently supported formats. 

1825 

1826 "plain" format doesn't use any pseudographics to draw tables, 

1827 it separates columns with a double space: 

1828 

1829 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1830 ... ["strings", "numbers"], "plain")) 

1831 strings numbers 

1832 spam 41.9999 

1833 eggs 451 

1834 

1835 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="plain")) 

1836 spam 41.9999 

1837 eggs 451 

1838 

1839 "simple" format is like Pandoc simple_tables: 

1840 

1841 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1842 ... ["strings", "numbers"], "simple")) 

1843 strings numbers 

1844 --------- --------- 

1845 spam 41.9999 

1846 eggs 451 

1847 

1848 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="simple")) 

1849 ---- -------- 

1850 spam 41.9999 

1851 eggs 451 

1852 ---- -------- 

1853 

1854 "grid" is similar to tables produced by Emacs table.el package or 

1855 Pandoc grid_tables: 

1856 

1857 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1858 ... ["strings", "numbers"], "grid")) 

1859 +-----------+-----------+ 

1860 | strings | numbers | 

1861 +===========+===========+ 

1862 | spam | 41.9999 | 

1863 +-----------+-----------+ 

1864 | eggs | 451 | 

1865 +-----------+-----------+ 

1866 

1867 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="grid")) 

1868 +------+----------+ 

1869 | spam | 41.9999 | 

1870 +------+----------+ 

1871 | eggs | 451 | 

1872 +------+----------+ 

1873 

1874 "simple_grid" draws a grid using single-line box-drawing 

1875 characters: 

1876 

1877 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1878 ... ["strings", "numbers"], "simple_grid")) 

1879 ┌───────────┬───────────┐ 

1880 │ strings │ numbers │ 

1881 ├───────────┼───────────┤ 

1882 │ spam │ 41.9999 │ 

1883 ├───────────┼───────────┤ 

1884 │ eggs │ 451 │ 

1885 └───────────┴───────────┘ 

1886 

1887 "rounded_grid" draws a grid using single-line box-drawing 

1888 characters with rounded corners: 

1889 

1890 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1891 ... ["strings", "numbers"], "rounded_grid")) 

1892 ╭───────────┬───────────╮ 

1893 │ strings │ numbers │ 

1894 ├───────────┼───────────┤ 

1895 │ spam │ 41.9999 │ 

1896 ├───────────┼───────────┤ 

1897 │ eggs │ 451 │ 

1898 ╰───────────┴───────────╯ 

1899 

1900 "heavy_grid" draws a grid using bold (thick) single-line box-drawing 

1901 characters: 

1902 

1903 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1904 ... ["strings", "numbers"], "heavy_grid")) 

1905 ┏━━━━━━━━━━━┳━━━━━━━━━━━┓ 

1906 ┃ strings ┃ numbers ┃ 

1907 ┣━━━━━━━━━━━╋━━━━━━━━━━━┫ 

1908 ┃ spam ┃ 41.9999 ┃ 

1909 ┣━━━━━━━━━━━╋━━━━━━━━━━━┫ 

1910 ┃ eggs ┃ 451 ┃ 

1911 ┗━━━━━━━━━━━┻━━━━━━━━━━━┛ 

1912 

1913 "mixed_grid" draws a grid using a mix of light (thin) and heavy (thick) lines 

1914 box-drawing characters: 

1915 

1916 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1917 ... ["strings", "numbers"], "mixed_grid")) 

1918 ┍━━━━━━━━━━━┯━━━━━━━━━━━┑ 

1919 │ strings │ numbers │ 

1920 ┝━━━━━━━━━━━┿━━━━━━━━━━━┥ 

1921 │ spam │ 41.9999 │ 

1922 ├───────────┼───────────┤ 

1923 │ eggs │ 451 │ 

1924 ┕━━━━━━━━━━━┷━━━━━━━━━━━┙ 

1925 

1926 "double_grid" draws a grid using double-line box-drawing 

1927 characters: 

1928 

1929 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1930 ... ["strings", "numbers"], "double_grid")) 

1931 ╔═══════════╦═══════════╗ 

1932 ║ strings ║ numbers ║ 

1933 ╠═══════════╬═══════════╣ 

1934 ║ spam ║ 41.9999 ║ 

1935 ╠═══════════╬═══════════╣ 

1936 ║ eggs ║ 451 ║ 

1937 ╚═══════════╩═══════════╝ 

1938 

1939 "fancy_grid" draws a grid using a mix of single and 

1940 double-line box-drawing characters: 

1941 

1942 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1943 ... ["strings", "numbers"], "fancy_grid")) 

1944 ╒═══════════╤═══════════╕ 

1945 │ strings │ numbers │ 

1946 ╞═══════════╪═══════════╡ 

1947 │ spam │ 41.9999 │ 

1948 ├───────────┼───────────┤ 

1949 │ eggs │ 451 │ 

1950 ╘═══════════╧═══════════╛ 

1951 

1952 "colon_grid" is similar to "grid" but uses colons only to define 

1953 columnwise content alignment, without whitespace padding, 

1954 similar to the alignment specification of Pandoc `grid_tables`: 

1955 

1956 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1957 ... ["strings", "numbers"], "colon_grid")) 

1958 +-----------+-----------+ 

1959 | strings | numbers | 

1960 +:==========+:==========+ 

1961 | spam | 41.9999 | 

1962 +-----------+-----------+ 

1963 | eggs | 451 | 

1964 +-----------+-----------+ 

1965 

1966 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1967 ... ["strings", "numbers"], "colon_grid", 

1968 ... colalign=["right", "left"])) 

1969 +-----------+-----------+ 

1970 | strings | numbers | 

1971 +==========:+:==========+ 

1972 | spam | 41.9999 | 

1973 +-----------+-----------+ 

1974 | eggs | 451 | 

1975 +-----------+-----------+ 

1976 

1977 "outline" is the same as the "grid" format but doesn't draw lines between rows: 

1978 

1979 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1980 ... ["strings", "numbers"], "outline")) 

1981 +-----------+-----------+ 

1982 | strings | numbers | 

1983 +===========+===========+ 

1984 | spam | 41.9999 | 

1985 | eggs | 451 | 

1986 +-----------+-----------+ 

1987 

1988 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="outline")) 

1989 +------+----------+ 

1990 | spam | 41.9999 | 

1991 | eggs | 451 | 

1992 +------+----------+ 

1993 

1994 "simple_outline" is the same as the "simple_grid" format but doesn't draw lines between rows: 

1995 

1996 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

1997 ... ["strings", "numbers"], "simple_outline")) 

1998 ┌───────────┬───────────┐ 

1999 │ strings │ numbers │ 

2000 ├───────────┼───────────┤ 

2001 │ spam │ 41.9999 │ 

2002 │ eggs │ 451 │ 

2003 └───────────┴───────────┘ 

2004 

2005 "rounded_outline" is the same as the "rounded_grid" format but doesn't draw lines between rows: 

2006 

2007 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

2008 ... ["strings", "numbers"], "rounded_outline")) 

2009 ╭───────────┬───────────╮ 

2010 │ strings │ numbers │ 

2011 ├───────────┼───────────┤ 

2012 │ spam │ 41.9999 │ 

2013 │ eggs │ 451 │ 

2014 ╰───────────┴───────────╯ 

2015 

2016 "heavy_outline" is the same as the "heavy_grid" format but doesn't draw lines between rows: 

2017 

2018 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

2019 ... ["strings", "numbers"], "heavy_outline")) 

2020 ┏━━━━━━━━━━━┳━━━━━━━━━━━┓ 

2021 ┃ strings ┃ numbers ┃ 

2022 ┣━━━━━━━━━━━╋━━━━━━━━━━━┫ 

2023 ┃ spam ┃ 41.9999 ┃ 

2024 ┃ eggs ┃ 451 ┃ 

2025 ┗━━━━━━━━━━━┻━━━━━━━━━━━┛ 

2026 

2027 "mixed_outline" is the same as the "mixed_grid" format but doesn't draw lines between rows: 

2028 

2029 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

2030 ... ["strings", "numbers"], "mixed_outline")) 

2031 ┍━━━━━━━━━━━┯━━━━━━━━━━━┑ 

2032 │ strings │ numbers │ 

2033 ┝━━━━━━━━━━━┿━━━━━━━━━━━┥ 

2034 │ spam │ 41.9999 │ 

2035 │ eggs │ 451 │ 

2036 ┕━━━━━━━━━━━┷━━━━━━━━━━━┙ 

2037 

2038 "double_outline" is the same as the "double_grid" format but doesn't draw lines between rows: 

2039 

2040 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

2041 ... ["strings", "numbers"], "double_outline")) 

2042 ╔═══════════╦═══════════╗ 

2043 ║ strings ║ numbers ║ 

2044 ╠═══════════╬═══════════╣ 

2045 ║ spam ║ 41.9999 ║ 

2046 ║ eggs ║ 451 ║ 

2047 ╚═══════════╩═══════════╝ 

2048 

2049 "fancy_outline" is the same as the "fancy_grid" format but doesn't draw lines between rows: 

2050 

2051 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

2052 ... ["strings", "numbers"], "fancy_outline")) 

2053 ╒═══════════╤═══════════╕ 

2054 │ strings │ numbers │ 

2055 ╞═══════════╪═══════════╡ 

2056 │ spam │ 41.9999 │ 

2057 │ eggs │ 451 │ 

2058 ╘═══════════╧═══════════╛ 

2059 

2060 "pipe" is like tables in PHP Markdown Extra extension or Pandoc 

2061 pipe_tables: 

2062 

2063 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

2064 ... ["strings", "numbers"], "pipe")) 

2065 | strings | numbers | 

2066 |:----------|----------:| 

2067 | spam | 41.9999 | 

2068 | eggs | 451 | 

2069 

2070 "presto" is like tables produce by the Presto CLI: 

2071 

2072 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

2073 ... ["strings", "numbers"], "presto")) 

2074 strings | numbers 

2075 -----------+----------- 

2076 spam | 41.9999 

2077 eggs | 451 

2078 

2079 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="pipe")) 

2080 |:-----|---------:| 

2081 | spam | 41.9999 | 

2082 | eggs | 451 | 

2083 

2084 "orgtbl" is like tables in Emacs org-mode and orgtbl-mode. They 

2085 are slightly different from "pipe" format by not using colons to 

2086 define column alignment, and using a "+" sign to indicate line 

2087 intersections: 

2088 

2089 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

2090 ... ["strings", "numbers"], "orgtbl")) 

2091 | strings | numbers | 

2092 |-----------+-----------| 

2093 | spam | 41.9999 | 

2094 | eggs | 451 | 

2095 

2096 

2097 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="orgtbl")) 

2098 | spam | 41.9999 | 

2099 | eggs | 451 | 

2100 

2101 "rst" is like a simple table format from reStructuredText; please 

2102 note that reStructuredText accepts also "grid" tables: 

2103 

2104 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], 

2105 ... ["strings", "numbers"], "rst")) 

2106 ========= ========= 

2107 strings numbers 

2108 ========= ========= 

2109 spam 41.9999 

2110 eggs 451 

2111 ========= ========= 

2112 

2113 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="rst")) 

2114 ==== ======== 

2115 spam 41.9999 

2116 eggs 451 

2117 ==== ======== 

2118 

2119 "mediawiki" produces a table markup used in Wikipedia and on other 

2120 MediaWiki-based sites: 

2121 

2122 >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], 

2123 ... headers="firstrow", tablefmt="mediawiki")) 

2124 {| class="wikitable" style="text-align: left;" 

2125 |+ <!-- caption --> 

2126 |- 

2127 ! strings !! style="text-align: right;"| numbers 

2128 |- 

2129 | spam || style="text-align: right;"| 41.9999 

2130 |- 

2131 | eggs || style="text-align: right;"| 451 

2132 |} 

2133 

2134 "html" produces HTML markup as an html.escape'd str 

2135 with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML 

2136 and a .str property so that the raw HTML remains accessible 

2137 the unsafehtml table format can be used if an unescaped HTML format is required: 

2138 

2139 >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], 

2140 ... headers="firstrow", tablefmt="html")) 

2141 <table> 

2142 <thead> 

2143 <tr><th style="text-align: left;">strings </th><th style="text-align: right;"> numbers</th></tr> 

2144 </thead> 

2145 <tbody> 

2146 <tr><td style="text-align: left;">spam </td><td style="text-align: right;"> 41.9999</td></tr> 

2147 <tr><td style="text-align: left;">eggs </td><td style="text-align: right;"> 451 </td></tr> 

2148 </tbody> 

2149 </table> 

2150 

2151 "latex" produces a tabular environment of LaTeX document markup: 

2152 

2153 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex")) 

2154 \\begin{tabular}{lr} 

2155 \\hline 

2156 spam & 41.9999 \\\\ 

2157 eggs & 451 \\\\ 

2158 \\hline 

2159 \\end{tabular} 

2160 

2161 "latex_raw" is similar to "latex", but doesn't escape special characters, 

2162 such as backslash and underscore, so LaTeX commands may embedded into 

2163 cells' values: 

2164 

2165 >>> print(tabulate([["spam$_9$", 41.9999], ["\\\\emph{eggs}", "451.0"]], tablefmt="latex_raw")) 

2166 \\begin{tabular}{lr} 

2167 \\hline 

2168 spam$_9$ & 41.9999 \\\\ 

2169 \\emph{eggs} & 451 \\\\ 

2170 \\hline 

2171 \\end{tabular} 

2172 

2173 "latex_booktabs" produces a tabular environment of LaTeX document markup 

2174 using the booktabs.sty package: 

2175 

2176 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_booktabs")) 

2177 \\begin{tabular}{lr} 

2178 \\toprule 

2179 spam & 41.9999 \\\\ 

2180 eggs & 451 \\\\ 

2181 \\bottomrule 

2182 \\end{tabular} 

2183 

2184 "latex_longtable" produces a tabular environment that can stretch along 

2185 multiple pages, using the longtable package for LaTeX. 

2186 

2187 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_longtable")) 

2188 \\begin{longtable}{lr} 

2189 \\hline 

2190 spam & 41.9999 \\\\ 

2191 eggs & 451 \\\\ 

2192 \\hline 

2193 \\end{longtable} 

2194 

2195 

2196 Number parsing 

2197 -------------- 

2198 By default, anything which can be parsed as a number is a number. 

2199 This ensures numbers represented as strings are aligned properly. 

2200 This can lead to weird results for particular strings such as 

2201 specific git SHAs e.g. "42992e1" will be parsed into the number 

2202 429920 and aligned as such. 

2203 

2204 To completely disable number parsing (and alignment), use 

2205 `disable_numparse=True`. For more fine grained control, a list column 

2206 indices is used to disable number parsing only on those columns 

2207 e.g. `disable_numparse=[0, 2]` would disable number parsing only on the 

2208 first and third columns. 

2209 

2210 Column Widths and Auto Line Wrapping 

2211 ------------------------------------ 

2212 Tabulate will, by default, set the width of each column to the length of the 

2213 longest element in that column. However, in situations where fields are expected 

2214 to reasonably be too long to look good as a single line, tabulate can help automate 

2215 word wrapping long fields for you. Use the parameter `maxcolwidths` to provide a 

2216 list of maximal column widths: 

2217 

2218 >>> print(tabulate( \ 

2219 [('1', 'John Smith', \ 

2220 'This is a rather long description that might look better if it is wrapped a bit')], \ 

2221 headers=("Issue Id", "Author", "Description"), \ 

2222 maxcolwidths=[None, None, 30], \ 

2223 tablefmt="grid" \ 

2224 )) 

2225 +------------+------------+-------------------------------+ 

2226 | Issue Id | Author | Description | 

2227 +============+============+===============================+ 

2228 | 1 | John Smith | This is a rather long | 

2229 | | | description that might look | 

2230 | | | better if it is wrapped a bit | 

2231 +------------+------------+-------------------------------+ 

2232 

2233 Header column width can be specified in a similar way using `maxheadercolwidths`. 

2234 

2235 """ 

2236 

2237 if tabular_data is None: 

2238 tabular_data = [] 

2239 

2240 list_of_lists, headers, headers_pad = _normalize_tabular_data( 

2241 tabular_data, headers, showindex=showindex 

2242 ) 

2243 list_of_lists, separating_lines = _remove_separating_lines(list_of_lists) 

2244 

2245 if maxcolwidths is not None: 

2246 if type(maxcolwidths) is tuple: # Check if tuple, convert to list if so 

2247 maxcolwidths = list(maxcolwidths) 

2248 if len(list_of_lists): 

2249 num_cols = len(list_of_lists[0]) 

2250 else: 

2251 num_cols = 0 

2252 if isinstance(maxcolwidths, int): # Expand scalar for all columns 

2253 maxcolwidths = _expand_iterable(maxcolwidths, num_cols, maxcolwidths) 

2254 else: # Ignore col width for any 'trailing' columns 

2255 maxcolwidths = _expand_iterable(maxcolwidths, num_cols, None) 

2256 

2257 numparses = _expand_numparse(disable_numparse, num_cols) 

2258 list_of_lists = _wrap_text_to_colwidths( 

2259 list_of_lists, 

2260 maxcolwidths, 

2261 numparses=numparses, 

2262 missingval=missingval, 

2263 break_long_words=break_long_words, 

2264 break_on_hyphens=break_on_hyphens, 

2265 ) 

2266 

2267 if maxheadercolwidths is not None: 

2268 num_cols = len(list_of_lists[0]) if list_of_lists else len(headers) 

2269 if isinstance(maxheadercolwidths, int): # Expand scalar for all columns 

2270 maxheadercolwidths = _expand_iterable(maxheadercolwidths, num_cols, maxheadercolwidths) 

2271 else: # Ignore col width for any 'trailing' columns 

2272 maxheadercolwidths = _expand_iterable(maxheadercolwidths, num_cols, None) 

2273 

2274 numparses = _expand_numparse(disable_numparse, num_cols) 

2275 headers = _wrap_text_to_colwidths( 

2276 [headers], 

2277 maxheadercolwidths, 

2278 numparses=numparses, 

2279 missingval=missingval, 

2280 break_long_words=break_long_words, 

2281 break_on_hyphens=break_on_hyphens, 

2282 )[0] 

2283 

2284 # empty values in the first column of RST tables should be escaped (issue #82) 

2285 # "" should be escaped as "\\ " or ".." 

2286 if tablefmt == "rst": 

2287 list_of_lists, headers = _rst_escape_first_column(list_of_lists, headers) 

2288 

2289 # PrettyTable formatting does not use any extra padding. 

2290 # Numbers are not parsed and are treated the same as strings for alignment. 

2291 # Check if pretty is the format being used and override the defaults so it 

2292 # does not impact other formats. 

2293 min_padding = MIN_PADDING 

2294 if tablefmt == "pretty": 

2295 min_padding = 0 

2296 disable_numparse = True 

2297 numalign = "center" if numalign == _DEFAULT_ALIGN else numalign 

2298 stralign = "center" if stralign == _DEFAULT_ALIGN else stralign 

2299 else: 

2300 numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign 

2301 stralign = "left" if stralign == _DEFAULT_ALIGN else stralign 

2302 

2303 # 'colon_grid' uses colons in the line beneath the header to represent a column's 

2304 # alignment instead of literally aligning the text differently. Hence, 

2305 # left alignment of the data in the text output is enforced. 

2306 if tablefmt == "colon_grid": 

2307 colglobalalign = "left" 

2308 headersglobalalign = "left" 

2309 

2310 # optimization: look for ANSI control codes once, 

2311 # enable smart width functions only if a control code is found 

2312 # 

2313 # convert the headers and rows into a single, tab-delimited string ensuring 

2314 # that any bytestrings are decoded safely (i.e. errors ignored) 

2315 plain_text = "\t".join( 

2316 chain( 

2317 # headers 

2318 map(_to_str, headers), 

2319 # rows: chain the rows together into a single iterable after mapping 

2320 # the bytestring conversino to each cell value 

2321 chain.from_iterable(map(_to_str, row) for row in list_of_lists), 

2322 ) 

2323 ) 

2324 

2325 has_invisible = _ansi_codes.search(plain_text) is not None 

2326 

2327 enable_widechars = wcwidth is not None and WIDE_CHARS_MODE 

2328 if ( 

2329 not isinstance(tablefmt, TableFormat) 

2330 and tablefmt in multiline_formats 

2331 and _is_multiline(plain_text) 

2332 ): 

2333 tablefmt = multiline_formats.get(tablefmt, tablefmt) 

2334 is_multiline = True 

2335 else: 

2336 is_multiline = False 

2337 width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline) 

2338 

2339 # format rows and columns, convert numeric values to strings 

2340 cols = list(izip_longest(*list_of_lists)) 

2341 numparses = _expand_numparse(disable_numparse, len(cols)) 

2342 coltypes = [_column_type(col, numparse=np) for col, np in zip(cols, numparses)] 

2343 if isinstance(floatfmt, str): # old version 

2344 float_formats = len(cols) * [floatfmt] # just duplicate the string to use in each column 

2345 else: # if floatfmt is list, tuple etc we have one per column 

2346 float_formats = list(floatfmt) 

2347 if len(float_formats) < len(cols): 

2348 float_formats.extend((len(cols) - len(float_formats)) * [_DEFAULT_FLOATFMT]) 

2349 if isinstance(intfmt, str): # old version 

2350 int_formats = len(cols) * [intfmt] # just duplicate the string to use in each column 

2351 else: # if intfmt is list, tuple etc we have one per column 

2352 int_formats = list(intfmt) 

2353 if len(int_formats) < len(cols): 

2354 int_formats.extend((len(cols) - len(int_formats)) * [_DEFAULT_INTFMT]) 

2355 if isinstance(missingval, str): 

2356 missing_vals = len(cols) * [missingval] 

2357 else: 

2358 missing_vals = list(missingval) 

2359 if len(missing_vals) < len(cols): 

2360 missing_vals.extend((len(cols) - len(missing_vals)) * [_DEFAULT_MISSINGVAL]) 

2361 cols = [ 

2362 [_format(v, ct, fl_fmt, int_fmt, miss_v, has_invisible) for v in c] 

2363 for c, ct, fl_fmt, int_fmt, miss_v in zip( 

2364 cols, coltypes, float_formats, int_formats, missing_vals 

2365 ) 

2366 ] 

2367 

2368 # align columns 

2369 # first set global alignment 

2370 if colglobalalign is not None: # if global alignment provided 

2371 aligns = [colglobalalign] * len(cols) 

2372 else: # default 

2373 aligns = [numalign if ct in [int, float] else stralign for ct in coltypes] 

2374 # then specific alignments 

2375 if colalign is not None: 

2376 assert isinstance(colalign, Iterable) 

2377 if isinstance(colalign, str): 

2378 warnings.warn( 

2379 f"As a string, `colalign` is interpreted as {list(colalign)}. " 

2380 f'Did you mean `colglobalalign = "{colalign}"` or `colalign = ("{colalign}",)`?', 

2381 stacklevel=2, 

2382 ) 

2383 for idx, align in enumerate(colalign): 

2384 if not idx < len(aligns): 

2385 break 

2386 elif align != "global": 

2387 aligns[idx] = align 

2388 minwidths = [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols) 

2389 aligns_copy = aligns.copy() 

2390 # Reset alignments in copy of alignments list to "left" for 'colon_grid' format, 

2391 # which enforces left alignment in the text output of the data. 

2392 if tablefmt == "colon_grid": 

2393 aligns_copy = ["left"] * len(cols) 

2394 cols = [ 

2395 _align_column( 

2396 c, 

2397 a, 

2398 minw, 

2399 has_invisible, 

2400 enable_widechars, 

2401 is_multiline, 

2402 preserve_whitespace, 

2403 ) 

2404 for c, a, minw in zip(cols, aligns_copy, minwidths) 

2405 ] 

2406 

2407 aligns_headers = None 

2408 if headers: 

2409 # align headers and add headers 

2410 t_cols = cols or [[""]] * len(headers) 

2411 # first set global alignment 

2412 if headersglobalalign is not None: # if global alignment provided 

2413 aligns_headers = [headersglobalalign] * len(t_cols) 

2414 else: # default 

2415 aligns_headers = aligns or [stralign] * len(headers) 

2416 # then specific header alignments 

2417 if headersalign is not None: 

2418 assert isinstance(headersalign, Iterable) 

2419 if isinstance(headersalign, str): 

2420 warnings.warn( 

2421 f"As a string, `headersalign` is interpreted as {list(headersalign)}. " 

2422 f'Did you mean `headersglobalalign = "{headersalign}"` ' 

2423 f'or `headersalign = ("{headersalign}",)`?', 

2424 stacklevel=2, 

2425 ) 

2426 for idx, align in enumerate(headersalign): 

2427 hidx = headers_pad + idx 

2428 if not hidx < len(aligns_headers): 

2429 break 

2430 elif align == "same" and hidx < len(aligns): # same as column align 

2431 aligns_headers[hidx] = aligns[hidx] 

2432 elif align != "global": 

2433 aligns_headers[hidx] = align 

2434 minwidths = [ 

2435 max(minw, max(width_fn(cl) for cl in c)) for minw, c in zip(minwidths, t_cols) 

2436 ] 

2437 headers = [ 

2438 _align_header(h, a, minw, width_fn(h), is_multiline, width_fn) 

2439 for h, a, minw in zip(headers, aligns_headers, minwidths) 

2440 ] 

2441 rows = list(zip(*cols)) 

2442 else: 

2443 minwidths = [max(width_fn(cl) for cl in c) for c in cols] 

2444 rows = list(zip(*cols)) 

2445 

2446 if not isinstance(tablefmt, TableFormat): 

2447 tablefmt = _table_formats.get(tablefmt, _table_formats["simple"]) 

2448 

2449 ra_default = rowalign if isinstance(rowalign, str) else None 

2450 rowaligns = _expand_iterable(rowalign, len(rows), ra_default) 

2451 _reinsert_separating_lines(rows, separating_lines) 

2452 

2453 return _format_table( 

2454 tablefmt, 

2455 headers, 

2456 aligns_headers, 

2457 rows, 

2458 minwidths, 

2459 aligns, 

2460 is_multiline, 

2461 rowaligns=rowaligns, 

2462 ) 

2463 

2464 

2465def _expand_numparse(disable_numparse, column_count): 

2466 """ 

2467 Return a list of bools of length `column_count` which indicates whether 

2468 number parsing should be used on each column. 

2469 If `disable_numparse` is a list of indices, each of those indices are False, 

2470 and everything else is True. 

2471 If `disable_numparse` is a bool, then the returned list is all the same. 

2472 """ 

2473 if isinstance(disable_numparse, Iterable): 

2474 numparses = [True] * column_count 

2475 for index in disable_numparse: 

2476 numparses[index] = False 

2477 return numparses 

2478 else: 

2479 return [not disable_numparse] * column_count 

2480 

2481 

2482def _expand_iterable(original, num_desired, default): 

2483 """ 

2484 Expands the `original` argument to return a return a list of 

2485 length `num_desired`. If `original` is shorter than `num_desired`, it will 

2486 be padded with the value in `default`. 

2487 If `original` is not a list to begin with (i.e. scalar value) a list of 

2488 length `num_desired` completely populated with `default will be returned 

2489 """ 

2490 if isinstance(original, Iterable) and not isinstance(original, str): 

2491 return original + [default] * (num_desired - len(original)) 

2492 else: 

2493 return [default] * num_desired 

2494 

2495 

2496def _pad_row(cells, padding): 

2497 if cells: 

2498 if cells == SEPARATING_LINE: 

2499 return SEPARATING_LINE 

2500 pad = " " * padding 

2501 padded_cells = [pad + cell + pad for cell in cells] 

2502 return padded_cells 

2503 else: 

2504 return cells 

2505 

2506 

2507def _build_simple_row(padded_cells: list[list], rowfmt: DataRow) -> str: 

2508 "Format row according to DataRow format without padding." 

2509 begin = rowfmt.begin 

2510 sep = rowfmt.sep 

2511 end = rowfmt.end 

2512 escape_map: dict = rowfmt.escape_map 

2513 

2514 if escape_map: 

2515 

2516 def escape_char(c): 

2517 return escape_map.get(c, c) 

2518 

2519 escaped_cells = ["".join(map(escape_char, cell)) for cell in padded_cells] 

2520 else: 

2521 escaped_cells = padded_cells 

2522 

2523 return (begin + sep.join(escaped_cells) + end).rstrip() 

2524 

2525 

2526def _build_row( 

2527 padded_cells: list[list], 

2528 colwidths: list[int], 

2529 colaligns: list[str], 

2530 rowfmt: DataRow | Callable, 

2531) -> str: 

2532 "Return a string which represents a row of data cells." 

2533 if not rowfmt: 

2534 return None 

2535 if callable(rowfmt): 

2536 return rowfmt(padded_cells, colwidths, colaligns) 

2537 else: 

2538 return _build_simple_row(padded_cells, rowfmt) 

2539 

2540 

2541def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt, rowalign=None): 

2542 # NOTE: rowalign is ignored and exists for api compatibility with _append_multiline_row 

2543 lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt)) 

2544 return lines 

2545 

2546 

2547def _align_cell_veritically(text_lines, num_lines, column_width, row_alignment): 

2548 delta_lines = num_lines - len(text_lines) 

2549 blank = [" " * column_width] 

2550 if row_alignment == "bottom": 

2551 return blank * delta_lines + text_lines 

2552 elif row_alignment == "center": 

2553 top_delta = delta_lines // 2 

2554 bottom_delta = delta_lines - top_delta 

2555 return top_delta * blank + text_lines + bottom_delta * blank 

2556 else: 

2557 return text_lines + blank * delta_lines 

2558 

2559 

2560def _append_multiline_row( 

2561 lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad, rowalign=None 

2562): 

2563 colwidths = [w - 2 * pad for w in padded_widths] 

2564 cells_lines = [c.splitlines() for c in padded_multiline_cells] 

2565 nlines = max(map(len, cells_lines)) # number of lines in the row 

2566 # vertically pad cells where some lines are missing 

2567 # cells_lines = [ 

2568 # (cl + [" " * w] * (nlines - len(cl))) for cl, w in zip(cells_lines, colwidths) 

2569 # ] 

2570 

2571 cells_lines = [ 

2572 _align_cell_veritically(cl, nlines, w, rowalign) for cl, w in zip(cells_lines, colwidths) 

2573 ] 

2574 lines_cells = [[cl[i] for cl in cells_lines] for i in range(nlines)] 

2575 for ln in lines_cells: 

2576 padded_ln = _pad_row(ln, pad) 

2577 _append_basic_row(lines, padded_ln, colwidths, colaligns, rowfmt) 

2578 return lines 

2579 

2580 

2581def _build_line(colwidths, colaligns, linefmt): 

2582 "Return a string which represents a horizontal line." 

2583 if not linefmt: 

2584 return None 

2585 if callable(linefmt): 

2586 return linefmt(colwidths, colaligns) 

2587 else: 

2588 begin, fill, sep, end = linefmt 

2589 cells = [fill * w for w in colwidths] 

2590 rowfmt = DataRow(begin, sep, end) 

2591 return _build_simple_row(cells, rowfmt) 

2592 

2593 

2594def _append_line(lines, colwidths, colaligns, linefmt): 

2595 lines.append(_build_line(colwidths, colaligns, linefmt)) 

2596 return lines 

2597 

2598 

2599class JupyterHTMLStr(str): 

2600 """Wrap the string with a _repr_html_ method so that Jupyter 

2601 displays the HTML table""" 

2602 

2603 def _repr_html_(self): 

2604 return self 

2605 

2606 @property 

2607 def str(self): 

2608 """add a .str property so that the raw string is still accessible""" 

2609 return self 

2610 

2611 

2612def _format_table( 

2613 fmt, headers, headersaligns, rows, colwidths, colaligns, is_multiline, rowaligns 

2614): 

2615 """Produce a plain-text representation of the table.""" 

2616 lines = [] 

2617 hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else [] 

2618 pad = fmt.padding 

2619 headerrow = fmt.headerrow 

2620 

2621 padded_widths = [(w + 2 * pad) for w in colwidths] 

2622 if is_multiline: 

2623 pad_row = lambda row, _: row # noqa: E731 # do it later, in _append_multiline_row 

2624 append_row = partial(_append_multiline_row, pad=pad) 

2625 else: 

2626 pad_row = _pad_row 

2627 append_row = _append_basic_row 

2628 

2629 padded_headers = pad_row(headers, pad) 

2630 

2631 if fmt.lineabove and "lineabove" not in hidden: 

2632 _append_line(lines, padded_widths, colaligns, fmt.lineabove) 

2633 

2634 if padded_headers: 

2635 append_row(lines, padded_headers, padded_widths, headersaligns, headerrow) 

2636 if fmt.linebelowheader and "linebelowheader" not in hidden: 

2637 _append_line(lines, padded_widths, colaligns, fmt.linebelowheader) 

2638 

2639 if rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: 

2640 # initial rows with a line below 

2641 for row, ralign in zip(rows[:-1], rowaligns): 

2642 if row != SEPARATING_LINE: 

2643 append_row( 

2644 lines, 

2645 pad_row(row, pad), 

2646 padded_widths, 

2647 colaligns, 

2648 fmt.datarow, 

2649 rowalign=ralign, 

2650 ) 

2651 _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) 

2652 # the last row without a line below 

2653 append_row( 

2654 lines, 

2655 pad_row(rows[-1], pad), 

2656 padded_widths, 

2657 colaligns, 

2658 fmt.datarow, 

2659 rowalign=rowaligns[-1], 

2660 ) 

2661 else: 

2662 separating_line = ( 

2663 fmt.linebetweenrows 

2664 or fmt.linebelowheader 

2665 or fmt.linebelow 

2666 or fmt.lineabove 

2667 or Line("", "", "", "") 

2668 ) 

2669 for row in rows: 

2670 # test to see if either the 1st column or the 2nd column (account for showindex) has 

2671 # the SEPARATING_LINE flag 

2672 if _is_separating_line(row): 

2673 _append_line(lines, padded_widths, colaligns, separating_line) 

2674 else: 

2675 append_row(lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow) 

2676 

2677 if fmt.linebelow and "linebelow" not in hidden: 

2678 _append_line(lines, padded_widths, colaligns, fmt.linebelow) 

2679 

2680 if headers or rows: 

2681 output = "\n".join(lines) 

2682 if fmt.lineabove == _html_begin_table_without_header: 

2683 return JupyterHTMLStr(output) 

2684 else: 

2685 return output 

2686 else: # a completely empty table 

2687 return "" 

2688 

2689 

2690class _CustomTextWrap(textwrap.TextWrapper): 

2691 """A custom implementation of CPython's textwrap.TextWrapper. This supports 

2692 both wide characters (Korea, Japanese, Chinese) - including mixed string. 

2693 For the most part, the `_handle_long_word` and `_wrap_chunks` functions were 

2694 copy pasted out of the CPython baseline, and updated with our custom length 

2695 and line appending logic. 

2696 """ 

2697 

2698 def __init__(self, *args, **kwargs): 

2699 self._active_codes = [] 

2700 self.max_lines = None # For python2 compatibility 

2701 textwrap.TextWrapper.__init__(self, *args, **kwargs) 

2702 

2703 @staticmethod 

2704 def _len(item): 

2705 """Custom len that gets console column width for wide 

2706 and non-wide characters as well as ignores color codes""" 

2707 stripped = _strip_ansi(item) 

2708 if wcwidth: 

2709 return wcwidth.wcswidth(stripped) 

2710 else: 

2711 return len(stripped) 

2712 

2713 def _update_lines(self, lines, new_line): 

2714 """Adds a new line to the list of lines the text is being wrapped into 

2715 This function will also track any ANSI color codes in this string as well 

2716 as add any colors from previous lines order to preserve the same formatting 

2717 as a single unwrapped string. 

2718 """ 

2719 code_matches = list(_ansi_codes.finditer(new_line)) 

2720 color_codes = [code.string[code.span()[0] : code.span()[1]] for code in code_matches] 

2721 

2722 # Add color codes from earlier in the unwrapped line, and then track any new ones we add. 

2723 new_line = "".join(self._active_codes) + new_line 

2724 

2725 for code in color_codes: 

2726 if code != _ansi_color_reset_code: 

2727 self._active_codes.append(code) 

2728 else: # A single reset code resets everything 

2729 self._active_codes = [] 

2730 

2731 # Always ensure each line is color terminated if any colors are 

2732 # still active, otherwise colors will bleed into other cells on the console 

2733 if len(self._active_codes) > 0: 

2734 new_line = new_line + _ansi_color_reset_code 

2735 

2736 lines.append(new_line) 

2737 

2738 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): 

2739 """_handle_long_word(chunks : [string], 

2740 cur_line : [string], 

2741 cur_len : int, width : int) 

2742 Handle a chunk of text (most likely a word, not whitespace) that 

2743 is too long to fit in any line. 

2744 """ 

2745 # Figure out when indent is larger than the specified width, and make 

2746 # sure at least one character is stripped off on every pass 

2747 if width < 1: 

2748 space_left = 1 

2749 else: 

2750 space_left = width - cur_len 

2751 

2752 # If we're allowed to break long words, then do so: put as much 

2753 # of the next chunk onto the current line as will fit. 

2754 if self.break_long_words and space_left > 0: 

2755 # Tabulate Custom: Build the string up piece-by-piece in order to 

2756 # take each charcter's width into account 

2757 chunk = reversed_chunks[-1] 

2758 i = 1 

2759 # Only count printable characters, so strip_ansi first, index later. 

2760 stripped_chunk = _strip_ansi(chunk) 

2761 while i <= len(stripped_chunk) and self._len(stripped_chunk[:i]) <= space_left: 

2762 i = i + 1 

2763 # Always consume at least one character so _wrap_chunks makes 

2764 # progress even when the first character is wider than space_left 

2765 # (e.g. a 2-column CJK char in a 1-column-wide slot). 

2766 i = max(i, 2) 

2767 # Consider escape codes when breaking words up 

2768 total_escape_len = 0 

2769 last_group = 0 

2770 if _ansi_codes.search(chunk) is not None: 

2771 for group, _, _, _ in _ansi_codes.findall(chunk): 

2772 escape_len = len(group) 

2773 if group in chunk[last_group : i + total_escape_len + escape_len - 1]: 

2774 total_escape_len += escape_len 

2775 found = _ansi_codes.search(chunk[last_group:]) 

2776 last_group += found.end() 

2777 cur_line.append(chunk[: i + total_escape_len - 1]) 

2778 reversed_chunks[-1] = chunk[i + total_escape_len - 1 :] 

2779 

2780 # Otherwise, we have to preserve the long word intact. Only add 

2781 # it to the current line if there's nothing already there -- 

2782 # that minimizes how much we violate the width constraint. 

2783 elif not cur_line: 

2784 cur_line.append(reversed_chunks.pop()) 

2785 

2786 # If we're not allowed to break long words, and there's already 

2787 # text on the current line, do nothing. Next time through the 

2788 # main loop of _wrap_chunks(), we'll wind up here again, but 

2789 # cur_len will be zero, so the next line will be entirely 

2790 # devoted to the long word that we can't handle right now. 

2791 

2792 def _wrap_chunks(self, chunks): 

2793 """_wrap_chunks(chunks : [string]) -> [string] 

2794 Wrap a sequence of text chunks and return a list of lines of 

2795 length 'self.width' or less. (If 'break_long_words' is false, 

2796 some lines may be longer than this.) Chunks correspond roughly 

2797 to words and the whitespace between them: each chunk is 

2798 indivisible (modulo 'break_long_words'), but a line break can 

2799 come between any two chunks. Chunks should not have internal 

2800 whitespace; ie. a chunk is either all whitespace or a "word". 

2801 Whitespace chunks will be removed from the beginning and end of 

2802 lines, but apart from that whitespace is preserved. 

2803 """ 

2804 lines = [] 

2805 if self.width <= 0: 

2806 raise ValueError(f"invalid width {self.width!r} (must be > 0)") 

2807 if self.max_lines is not None: 

2808 if self.max_lines > 1: 

2809 indent = self.subsequent_indent 

2810 else: 

2811 indent = self.initial_indent 

2812 if self._len(indent) + self._len(self.placeholder.lstrip()) > self.width: 

2813 raise ValueError("placeholder too large for max width") 

2814 

2815 # Arrange in reverse order so items can be efficiently popped 

2816 # from a stack of chucks. 

2817 chunks.reverse() 

2818 

2819 while chunks: 

2820 # Start the list of chunks that will make up the current line. 

2821 # cur_len is just the length of all the chunks in cur_line. 

2822 cur_line = [] 

2823 cur_len = 0 

2824 

2825 # Figure out which static string will prefix this line. 

2826 if lines: 

2827 indent = self.subsequent_indent 

2828 else: 

2829 indent = self.initial_indent 

2830 

2831 # Maximum width for this line. 

2832 width = self.width - self._len(indent) 

2833 

2834 # First chunk on line is whitespace -- drop it, unless this 

2835 # is the very beginning of the text (ie. no lines started yet). 

2836 if self.drop_whitespace and chunks[-1].strip() == "" and lines: 

2837 del chunks[-1] 

2838 

2839 while chunks: 

2840 chunk_len = self._len(chunks[-1]) 

2841 

2842 # Can at least squeeze this chunk onto the current line. 

2843 if cur_len + chunk_len <= width: 

2844 cur_line.append(chunks.pop()) 

2845 cur_len += chunk_len 

2846 

2847 # Nope, this line is full. 

2848 else: 

2849 break 

2850 

2851 # The current line is full, and the next chunk is too big to 

2852 # fit on *any* line (not just this one). 

2853 if chunks and self._len(chunks[-1]) > width: 

2854 self._handle_long_word(chunks, cur_line, cur_len, width) 

2855 cur_len = sum(map(self._len, cur_line)) 

2856 

2857 # If the last chunk on this line is all whitespace, drop it. 

2858 if self.drop_whitespace and cur_line and cur_line[-1].strip() == "": 

2859 cur_len -= self._len(cur_line[-1]) 

2860 del cur_line[-1] 

2861 

2862 if cur_line: 

2863 if ( 

2864 self.max_lines is None 

2865 or len(lines) + 1 < self.max_lines 

2866 or ( 

2867 not chunks 

2868 or self.drop_whitespace 

2869 and len(chunks) == 1 

2870 and not chunks[0].strip() 

2871 ) 

2872 and cur_len <= width 

2873 ): 

2874 # Convert current line back to a string and store it in 

2875 # list of all lines (return value). 

2876 self._update_lines(lines, indent + "".join(cur_line)) 

2877 else: 

2878 while cur_line: 

2879 if cur_line[-1].strip() and cur_len + self._len(self.placeholder) <= width: 

2880 cur_line.append(self.placeholder) 

2881 self._update_lines(lines, indent + "".join(cur_line)) 

2882 break 

2883 cur_len -= self._len(cur_line[-1]) 

2884 del cur_line[-1] 

2885 else: 

2886 if lines: 

2887 prev_line = lines[-1].rstrip() 

2888 if self._len(prev_line) + self._len(self.placeholder) <= self.width: 

2889 lines[-1] = prev_line + self.placeholder 

2890 break 

2891 self._update_lines(lines, indent + self.placeholder.lstrip()) 

2892 break 

2893 

2894 return lines 

2895 

2896 

2897if __name__ == "__main__": 

2898 from .cli import _main 

2899 

2900 _main()