From 5321d14ba13c34431f28454bb8f28cc3e5582c91 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Sun, 17 Nov 2019 19:20:00 +0100 Subject: [PATCH 01/17] version bump to 0.8.7 for future release --- setup.py | 2 +- tabulate.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c2ad593..fcdab9c 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ setup( name="tabulate", - version="0.8.6", + version="0.8.7", description="Pretty-print tabular data", long_description=LONG_DESCRIPTION, long_description_content_type="text/markdown", diff --git a/tabulate.py b/tabulate.py index 1595238..6d3643b 100755 --- a/tabulate.py +++ b/tabulate.py @@ -57,7 +57,7 @@ def _is_file(f): __all__ = ["tabulate", "tabulate_formats", "simple_separated_format"] -__version__ = "0.8.6" +__version__ = "0.8.7" # minimum extra space in headers From 3c9800c77eb60b5f62dbc392b891fbf10414f880 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Sun, 17 Nov 2019 20:20:34 +0100 Subject: [PATCH 02/17] fix #15: allow empty pipe tables with columns like empty dataframes --- tabulate.py | 2 ++ test/test_regression.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/tabulate.py b/tabulate.py index 6d3643b..d53ef7a 100755 --- a/tabulate.py +++ b/tabulate.py @@ -143,6 +143,8 @@ def _pipe_segment_with_colons(align, colwidth): def _pipe_line_with_colons(colwidths, colaligns): """Return a horizontal line with optional colons to indicate column's alignment (as in `pipe` output format).""" + if not colaligns: # e.g. printing an empty data frame (github issue #15) + colaligns = [""]*len(colwidths) segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)] return "|" + "|".join(segments) + "|" diff --git a/test/test_regression.py b/test/test_regression.py index f3da215..0906e9a 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -356,3 +356,17 @@ def test_ragged_rows(): expected = "\n".join(["- - - -", "1 2 3", "1 2", "1 2 3 4", "- - - -"]) result = tabulate(table) assert_equal(result, expected) + + +def test_empty_pipe_table_with_columns(): + "Regression: allow empty pipe tables with columns, like empty dataframes (github issue #15)" + table = [] + headers = ["Col1", "Col2"] + expected = "\n".join( + [ + "| Col1 | Col2 |", + "|--------|--------|", + ] + ) + result = tabulate(table, headers, tablefmt="pipe") + assert_equal(result, expected) From b0a4132a66900cfaebc23c59b11126c9eb327129 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Sun, 17 Nov 2019 20:26:28 +0100 Subject: [PATCH 03/17] reapply automatic code formatting / fix linting errors --- tabulate.py | 4 ++-- test/test_regression.py | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tabulate.py b/tabulate.py index d53ef7a..92164fb 100755 --- a/tabulate.py +++ b/tabulate.py @@ -143,8 +143,8 @@ def _pipe_segment_with_colons(align, colwidth): def _pipe_line_with_colons(colwidths, colaligns): """Return a horizontal line with optional colons to indicate column's alignment (as in `pipe` output format).""" - if not colaligns: # e.g. printing an empty data frame (github issue #15) - colaligns = [""]*len(colwidths) + if not colaligns: # e.g. printing an empty data frame (github issue #15) + colaligns = [""] * len(colwidths) segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)] return "|" + "|".join(segments) + "|" diff --git a/test/test_regression.py b/test/test_regression.py index 0906e9a..8cdfcb2 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -362,11 +362,6 @@ def test_empty_pipe_table_with_columns(): "Regression: allow empty pipe tables with columns, like empty dataframes (github issue #15)" table = [] headers = ["Col1", "Col2"] - expected = "\n".join( - [ - "| Col1 | Col2 |", - "|--------|--------|", - ] - ) + expected = "\n".join(["| Col1 | Col2 |", "|--------|--------|"]) result = tabulate(table, headers, tablefmt="pipe") assert_equal(result, expected) From e7daa576ff444f95c560b18ef0bb22b3b1b67b57 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Mon, 18 Nov 2019 22:42:13 +0100 Subject: [PATCH 04/17] fix #14: declare Python 3.8 support, enable python 3.8 CI tests --- .circleci/config.yml | 4 ++-- appveyor.yml | 2 ++ setup.py | 1 + tox.ini | 17 ++++++++++++++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7de92cb..8980735 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: docker: # specify the version you desire here # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` - - image: circleci/python:3.6.1 + - image: circleci/python:3.8 # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -50,7 +50,7 @@ jobs: name: run tests command: | . venv/bin/activate - tox -e py36-extra + tox -e py38-extra - store_artifacts: path: test-reports diff --git a/appveyor.yml b/appveyor.yml index 324b09a..ac38dcc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,10 +11,12 @@ environment: - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python37" + - PYTHON: "C:\\Python38" - PYTHON: "C:\\Python27-x64" - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" + - PYTHON: "C:\\Python38-x64" install: # We need wheel installed to build wheels diff --git a/setup.py b/setup.py index fcdab9c..4a4e8a5 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries", ], py_modules=["tabulate"], diff --git a/tox.ini b/tox.ini index c948bc5..26afadc 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ # for testing and it is disabled by default. [tox] -envlist = lint, py27, py35, py36, py37 +envlist = lint, py27, py35, py36, py37, py38 [testenv] commands = nosetests -v --with-doctest test/ tabulate.py @@ -78,6 +78,21 @@ deps = pandas wcwidth +[testenv:py38] +basepython = python3.8 +commands = nosetests -v --with-doctest -i 'py3test_*' test/ tabulate.py +deps = + nose + +[testenv:py38-extra] +basepython = python3.8 +commands = nosetests -v --with-doctest -i 'py3test_*' test/ tabulate.py +deps = + nose + numpy + pandas + wcwidth + [flake8] max-complexity = 22 max-line-length = 99 From 261693284d871f5223648e8ab611d1636e163883 Mon Sep 17 00:00:00 2001 From: Andrew Tjia Date: Tue, 26 Nov 2019 11:51:02 +0900 Subject: [PATCH 05/17] Fixes #20 Check that tablefmt is not a TableFormat object before checking if it is multiline. If it is a custom user-specified TableFormat, leave it alone. --- tabulate.py | 6 +++++- test/test_regression.py | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/tabulate.py b/tabulate.py index 92164fb..99b6118 100755 --- a/tabulate.py +++ b/tabulate.py @@ -1423,7 +1423,11 @@ def tabulate( has_invisible = re.search(_invisible_codes, plain_text) enable_widechars = wcwidth is not None and WIDE_CHARS_MODE - if tablefmt in multiline_formats and _is_multiline(plain_text): + if ( + not isinstance(tablefmt, TableFormat) + and tablefmt in multiline_formats + and _is_multiline(plain_text) + ): tablefmt = multiline_formats.get(tablefmt, tablefmt) is_multiline = True else: diff --git a/test/test_regression.py b/test/test_regression.py index 8cdfcb2..e79aad8 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -4,7 +4,7 @@ from __future__ import print_function from __future__ import unicode_literals -from tabulate import tabulate, _text_type, _long_type +from tabulate import tabulate, _text_type, _long_type, TableFormat, Line, DataRow from common import assert_equal, assert_in, SkipTest @@ -365,3 +365,21 @@ def test_empty_pipe_table_with_columns(): expected = "\n".join(["| Col1 | Col2 |", "|--------|--------|"]) result = tabulate(table, headers, tablefmt="pipe") assert_equal(result, expected) + + +def test_custom_tablefmt(): + "Regression: allow custom TableFormat that specifies with_header_hide (github issue #20)" + tablefmt = TableFormat( + lineabove=Line("", "-", " ", ""), + linebelowheader=Line("", "-", " ", ""), + linebetweenrows=None, + linebelow=Line("", "-", " ", ""), + headerrow=DataRow("", " ", ""), + datarow=DataRow("", " ", ""), + padding=0, + with_header_hide=["lineabove", "linebelow"], + ) + rows = [["foo", "bar"], ["baz", "qux"]] + expected = "\n".join(["A B", "--- ---", "foo bar", "baz qux"]) + result = tabulate(rows, headers=["A", "B"], tablefmt=tablefmt) + assert_equal(result, expected) From ae7e8b6f72d1a0d8f57de78b694cb0d43cade07d Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Tue, 3 Dec 2019 10:30:49 +0100 Subject: [PATCH 06/17] fix #19: document disable_numparse option --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 058d2eb..cecd55f 100644 --- a/README.md +++ b/README.md @@ -401,6 +401,16 @@ file: eggs 451 ---- ---- + +To disable this feature use `disable_numparse=True`. + + >>> print(tabulate.tabulate([["Ver1", "18.0"], ["Ver2","19.2"]], tablefmt="simple", disable_numparse=True)) + ---- ---- + Ver1 18.0 + Ver2 19.2 + ---- ---- + + ### Custom column alignment `tabulate` allows a custom column alignment to override the above. The From ba3c20364720421fccddb16bb6d9708fe89735b1 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Tue, 3 Dec 2019 15:13:04 -0600 Subject: [PATCH 07/17] Add `pretty` format to behave like PrettyTable This adds a new `pretty` default TableFormat to make it easy to convert legacy code using the PrettyTables library over to using tabulate. It does not provide 100% output compatibility, but it does address many of the differences between PrettyTable's output and the closest existing one of psql. This will help minimize the impact in unit tests and expected user output for projects to make the switch. Signed-off-by: Sean McGinnis --- README.md | 25 +++++++++++ tabulate.py | 24 +++++++++- test/test_output.py | 106 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cecd55f..afae114 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Supported table formats are: - "orgtbl" - "jira" - "presto" +- "pretty" - "psql" - "rst" - "mediawiki" @@ -221,6 +222,18 @@ corresponds to the `pipe` format without alignment colons: eggs | 451 bacon | 0 +`pretty` attempts to be close to the format emitted by the PrettyTables +library: + + >>> print(tabulate(table, headers, tablefmt="pretty")) + +-------+-----+ + | item | qty | + +-------+-----+ + | spam | 42 | + | eggs | 451 | + | bacon | 0 | + +-------+-----+ + `psql` is like tables formatted by Postgres' psql cli: >>> print(tabulate(table, headers, tablefmt="psql")) @@ -578,6 +591,18 @@ a multiline cell, and headers with a multiline cell: more | 42 spam | +`pretty` tables: + + >>> print(tabulate(table, headers, tablefmt="pretty")) + +------+-----+ + | item | qty | + | name | | + +------+-----+ + | eggs | 451 | + | more | 42 | + | spam | | + +------+-----+ + `psql` tables: >>> print(tabulate(table, headers, tablefmt="psql")) diff --git a/tabulate.py b/tabulate.py index 99b6118..0e15b27 100755 --- a/tabulate.py +++ b/tabulate.py @@ -354,6 +354,16 @@ def escape_empty(val): padding=1, with_header_hide=None, ), + "pretty": TableFormat( + lineabove=Line("+", "-", "+", "+"), + linebelowheader=Line("+", "-", "+", "+"), + linebetweenrows=None, + linebelow=Line("+", "-", "+", "+"), + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=None, + ), "psql": TableFormat( lineabove=Line("+", "-", "+", "+"), linebelowheader=Line("|", "-", "+", "|"), @@ -486,6 +496,7 @@ def escape_empty(val): "orgtbl": "orgtbl", "jira": "jira", "presto": "presto", + "pretty": "pretty", "psql": "psql", "rst": "rst", } @@ -1414,6 +1425,17 @@ def tabulate( if tablefmt == "rst": list_of_lists, headers = _rst_escape_first_column(list_of_lists, headers) + # PrettyTable formatting does not use any extra padding. + # Numbers are not parsed and are treated the same as strings for alignment. + # Check if pretty is the format being used and override the defaults so it + # does not impact other formats. + min_padding = MIN_PADDING + if tablefmt == "pretty": + min_padding = 0 + disable_numparse = True + numalign = "center" + stralign = "center" + # optimization: look for ANSI control codes once, # enable smart width functions only if a control code is found plain_text = "\t".join( @@ -1464,7 +1486,7 @@ def tabulate( for idx, align in enumerate(colalign): aligns[idx] = align minwidths = ( - [width_fn(h) + MIN_PADDING for h in headers] if headers else [0] * len(cols) + [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols) ) cols = [ _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline) diff --git a/test/test_output.py b/test/test_output.py index 1eb67c1..7122621 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -700,6 +700,112 @@ def test_psql_multiline_with_empty_cells_headerless(): assert_equal(expected, result) +def test_pretty(): + "Output: pretty with headers" + expected = "\n".join( + [ + "+---------+---------+", + "| strings | numbers |", + "+---------+---------+", + "| spam | 41.9999 |", + "| eggs | 451.0 |", + "+---------+---------+", + ] + ) + result = tabulate(_test_table, _test_table_headers, tablefmt="pretty") + assert_equal(expected, result) + + +def test_pretty_headerless(): + "Output: pretty without headers" + expected = "\n".join( + [ + "+------+---------+", + "| spam | 41.9999 |", + "| eggs | 451.0 |", + "+------+---------+", + ] + ) + result = tabulate(_test_table, tablefmt="pretty") + assert_equal(expected, result) + + +def test_pretty_multiline_headerless(): + "Output: pretty with multiline cells without headers" + table = [["foo bar\nbaz\nbau", "hello"], ["", "multiline\nworld"]] + expected = "\n".join( + [ + "+---------+-----------+", + "| foo bar | hello |", + "| baz | |", + "| bau | |", + "| | multiline |", + "| | world |", + "+---------+-----------+", + ] + ) + result = tabulate(table, tablefmt="pretty") + assert_equal(expected, result) + + +def test_pretty_multiline(): + "Output: pretty with multiline cells with headers" + table = [[2, "foo\nbar"]] + headers = ("more\nspam \x1b[31meggs\x1b[0m", "more spam\n& eggs") + expected = "\n".join( + [ + "+-----------+-----------+", + "| more | more spam |", + "| spam \x1b[31meggs\x1b[0m | & eggs |", + "+-----------+-----------+", + "| 2 | foo |", + "| | bar |", + "+-----------+-----------+", + ] + ) + result = tabulate(table, headers, tablefmt="pretty") + assert_equal(expected, result) + + +def test_pretty_multiline_with_empty_cells(): + "Output: pretty with multiline cells and empty cells with headers" + table = [ + ["hdr", "data", "fold"], + ["1", "", ""], + ["2", "very long data", "fold\nthis"], + ] + expected = "\n".join( + [ + "+-----+----------------+------+", + "| hdr | data | fold |", + "+-----+----------------+------+", + "| 1 | | |", + "| 2 | very long data | fold |", + "| | | this |", + "+-----+----------------+------+", + ] + ) + result = tabulate(table, headers="firstrow", tablefmt="pretty") + assert_equal(expected, result) + + +def test_pretty_multiline_with_empty_cells_headerless(): + "Output: pretty with multiline cells and empty cells without headers" + table = [["0", "", ""], ["1", "", ""], ["2", "very long data", "fold\nthis"]] + expected = "\n".join( + [ + "+---+----------------+------+", + "| 0 | | |", + "| 1 | | |", + "| 2 | very long data | fold |", + "| | | this |", + "+---+----------------+------+", + ] + ) + result = tabulate(table, tablefmt="pretty") + assert_equal(expected, result) + + def test_jira(): "Output: jira with headers" expected = "\n".join( From d73521927845c31a7b7640481dad68d503fd1e5f Mon Sep 17 00:00:00 2001 From: Wes Turner Date: Tue, 10 Dec 2019 09:20:56 -0500 Subject: [PATCH 08/17] BUG,SEC: html.escape(cell) to prevent XSS (#25) --- tabulate.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tabulate.py b/tabulate.py index 99b6118..d03fe65 100755 --- a/tabulate.py +++ b/tabulate.py @@ -55,6 +55,11 @@ def _is_file(f): except ImportError: wcwidth = None +try: + from html import escape as htmlescape +except ImportError: + from cgi import escape as htmlescape + __all__ = ["tabulate", "tabulate_formats", "simple_separated_format"] __version__ = "0.8.7" @@ -185,7 +190,8 @@ def _html_row_with_attrs(celltag, cell_values, colwidths, colaligns): "decimal": ' style="text-align: right;"', } values_with_attrs = [ - "<{0}{1}>{2}".format(celltag, alignment.get(a, ""), c) + "<{0}{1}>{2}".format(celltag, alignment.get(a, ""), + htmlescape(c)) for c, a in zip(cell_values, colaligns) ] rowhtml = "" + "".join(values_with_attrs).rstrip() + "" From f8b455b26bc36aa16006e02aebf22285c4d194d2 Mon Sep 17 00:00:00 2001 From: Wes Turner Date: Tue, 10 Dec 2019 09:21:24 -0500 Subject: [PATCH 09/17] PERF: allocate fewer strings --- tabulate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tabulate.py b/tabulate.py index d03fe65..bcfdec9 100755 --- a/tabulate.py +++ b/tabulate.py @@ -179,7 +179,7 @@ def _textile_row_with_attrs(cell_values, colwidths, colaligns): def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore): # this table header will be suppressed if there is a header row - return "\n".join(["", ""]) + return "
\n" def _html_row_with_attrs(celltag, cell_values, colwidths, colaligns): @@ -194,9 +194,9 @@ def _html_row_with_attrs(celltag, cell_values, colwidths, colaligns): htmlescape(c)) for c, a in zip(cell_values, colaligns) ] - rowhtml = "" + "".join(values_with_attrs).rstrip() + "" + rowhtml = "{}".format("".join(values_with_attrs).rstrip()) if celltag == "th": # it's a header row, create a new table header - rowhtml = "\n".join(["
", "", rowhtml, "", ""]) + rowhtml = "
\n\n{}\n\n".format(rowhtml) return rowhtml From 49686ac85e26c768cbf78559a950e20e1a0bedd5 Mon Sep 17 00:00:00 2001 From: Wes Turner Date: Tue, 10 Dec 2019 09:42:52 -0500 Subject: [PATCH 10/17] ENH: return a wrapped str w/ a _repr_html_ so that Jupyter displays the html (closes #25) --- README.md | 7 +++++-- tabulate.py | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cecd55f..e0201f8 100644 --- a/README.md +++ b/README.md @@ -319,7 +319,9 @@ MediaWiki-based sites: |<. eggs |>. 451 | |<. bacon |>. 0 | -`html` produces standard HTML markup: +`html` produces standard HTML markup as an html.escape'd str +with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML +and a .str method so that the raw HTML remains accessible: >>> print(tabulate(table, headers, tablefmt="html"))
@@ -714,4 +716,5 @@ naught101, John Vandenberg, Zack Dever, Christian Clauss, Benjamin Maier, Andy MacKinlay, Thomas Roten, Jue Wang, Joe King, Samuel Phan, Nick Satterly, Daniel Robbins, Dmitry B, Lars Butler, Andreas Maier, Dick Marinus, Sébastien Celles, Yago González, Andrew Gaul, Wim Glenn, -Jean Michel Rouly, Tim Gates, John Vandenberg, Sorin Sbarnea. +Jean Michel Rouly, Tim Gates, John Vandenberg, Sorin Sbarnea, +Wes Turner. diff --git a/tabulate.py b/tabulate.py index 99b6118..e7b0941 100755 --- a/tabulate.py +++ b/tabulate.py @@ -1342,7 +1342,9 @@ def tabulate( | eggs || align="right"| 451 |} - "html" produces HTML markup: + "html" produces HTML markup as an html.escape'd str + with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML + and a .str method so that the raw HTML remains accessible: >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], ... headers="firstrow", tablefmt="html")) @@ -1575,6 +1577,18 @@ def _append_line(lines, colwidths, colaligns, linefmt): return lines +class JupyterHTMLStr(str): + """Wrap the string with a _repr_html_ method so that Jupyter + displays the HTML table""" + + def _repr_html_(self): + return self + + def str(self): + """add a .str method so that the raw string is still accessible""" + return self + + def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline): """Produce a plain-text representation of the table.""" lines = [] @@ -1616,7 +1630,11 @@ def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline): _append_line(lines, padded_widths, colaligns, fmt.linebelow) if headers or rows: - return "\n".join(lines) + output = "\n".join(lines) + if fmt.lineabove == _html_begin_table_without_header: + return JupyterHTMLStr(output) + else: + return output else: # a completely empty table return "" From 396f8676a8270a67a50417bbc4d7539a888a1097 Mon Sep 17 00:00:00 2001 From: Wes Turner Date: Tue, 10 Dec 2019 16:53:35 -0500 Subject: [PATCH 11/17] TST: check that ._repr_html_()==.str, make .str a property --- README.md | 2 +- tabulate.py | 5 +++-- test/test_output.py | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e0201f8..f3847b5 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,7 @@ MediaWiki-based sites: `html` produces standard HTML markup as an html.escape'd str with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML -and a .str method so that the raw HTML remains accessible: +and a .str property so that the raw HTML remains accessible: >>> print(tabulate(table, headers, tablefmt="html"))
diff --git a/tabulate.py b/tabulate.py index e7b0941..fd835a8 100755 --- a/tabulate.py +++ b/tabulate.py @@ -1344,7 +1344,7 @@ def tabulate( "html" produces HTML markup as an html.escape'd str with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML - and a .str method so that the raw HTML remains accessible: + and a .str property so that the raw HTML remains accessible: >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], ... headers="firstrow", tablefmt="html")) @@ -1584,8 +1584,9 @@ class JupyterHTMLStr(str): def _repr_html_(self): return self + @property def str(self): - """add a .str method so that the raw string is still accessible""" + """add a .str property so that the raw string is still accessible""" return self diff --git a/test/test_output.py b/test/test_output.py index 1eb67c1..75f9003 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -914,6 +914,8 @@ def test_html(): ) result = tabulate(_test_table, _test_table_headers, tablefmt="html") assert_equal(expected, result) + assert hasattr(result, '_repr_html_') + assert result._repr_html_() == result.str def test_html_headerless(): @@ -930,6 +932,8 @@ def test_html_headerless(): ) result = tabulate(_test_table, tablefmt="html") assert_equal(expected, result) + assert hasattr(result, '_repr_html_') + assert result._repr_html_() == result.str def test_latex(): From 65a34648b914c0e4a368ef1bf44830d4258826c0 Mon Sep 17 00:00:00 2001 From: Wes Turner Date: Tue, 10 Dec 2019 17:15:14 -0500 Subject: [PATCH 12/17] TST: test_output/{test_html[_headerless]}: check that htmlescape escapes --- test/test_output.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/test/test_output.py b/test/test_output.py index 1eb67c1..1927a9c 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -897,22 +897,27 @@ def test_moinmoin_headerless(): assert_equal(expected, result) +_test_table_html_headers = ["", "<&numbers&>"] +_test_table_html = [["spam >", 41.9999], ["eggs &", 451.0]] +assert_equal.__self__.maxDiff = None + def test_html(): "Output: html with headers" expected = "\n".join( [ "
", "", - '', + '', "", "", - '', - '', + '', + '', "", "
strings numbers
<strings> <&numbers&>
spam 41.9999
eggs 451
spam > 41.9999
eggs & 451
", ] ) - result = tabulate(_test_table, _test_table_headers, tablefmt="html") + result = tabulate(_test_table_html, _test_table_html_headers, + tablefmt="html") assert_equal(expected, result) @@ -922,13 +927,13 @@ def test_html_headerless(): [ "", "", - '', - '', + '', + '', "", "
spam 41.9999
eggs451
spam > 41.9999
eggs &451
", ] ) - result = tabulate(_test_table, tablefmt="html") + result = tabulate(_test_table_html, tablefmt="html") assert_equal(expected, result) From 5e8393641f62edd552158576f8c1ce4cdfc877a7 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 19 Dec 2019 17:35:04 +0100 Subject: [PATCH 13/17] format code with black, suppress flake8 warnings in test_output --- tabulate.py | 3 +-- test/test_output.py | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tabulate.py b/tabulate.py index 03a98a4..5aa40d9 100755 --- a/tabulate.py +++ b/tabulate.py @@ -190,8 +190,7 @@ def _html_row_with_attrs(celltag, cell_values, colwidths, colaligns): "decimal": ' style="text-align: right;"', } values_with_attrs = [ - "<{0}{1}>{2}".format(celltag, alignment.get(a, ""), - htmlescape(c)) + "<{0}{1}>{2}".format(celltag, alignment.get(a, ""), htmlescape(c)) for c, a in zip(cell_values, colaligns) ] rowhtml = "{}".format("".join(values_with_attrs).rstrip()) diff --git a/test/test_output.py b/test/test_output.py index f963d60..9db7e05 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -901,13 +901,14 @@ def test_moinmoin_headerless(): _test_table_html = [["spam >", 41.9999], ["eggs &", 451.0]] assert_equal.__self__.maxDiff = None + def test_html(): "Output: html with headers" expected = "\n".join( [ "", "", - '', + '', # noqa "", "", '', @@ -916,10 +917,9 @@ def test_html(): "
<strings> <&numbers&>
<strings> <&numbers&>
spam > 41.9999
", ] ) - result = tabulate(_test_table_html, _test_table_html_headers, - tablefmt="html") + result = tabulate(_test_table_html, _test_table_html_headers, tablefmt="html") assert_equal(expected, result) - assert hasattr(result, '_repr_html_') + assert hasattr(result, "_repr_html_") assert result._repr_html_() == result.str @@ -937,7 +937,7 @@ def test_html_headerless(): ) result = tabulate(_test_table_html, tablefmt="html") assert_equal(expected, result) - assert hasattr(result, '_repr_html_') + assert hasattr(result, "_repr_html_") assert result._repr_html_() == result.str From d7b44d7bf42feca1f3add86273fba387a9139982 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 20 Dec 2019 09:49:48 +0000 Subject: [PATCH 14/17] Put absolute path of CHANGELOG --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3847b5..6dd734b 100644 --- a/README.md +++ b/README.md @@ -664,7 +664,7 @@ was run in Python 3.6.8 on Ubuntu 18.04 in WSL: Version history --------------- -The full version history can be found at the [changelog](./CHANGELOG). +The full version history can be found at the [changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG). How to contribute ----------------- From abf2ff5bb6f0c1b4e6f17e7e675df33d42bb484a Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 17 Jan 2020 19:57:11 +0000 Subject: [PATCH 15/17] update LICENSE years --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 97eab5d..81241ec 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2017 Sergey Astanin +Copyright (c) 2011-2020 Sergey Astanin and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the From ae37c207ef7eb5fc387e79cbd5f5e74dab3b0e2e Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Sun, 22 Mar 2020 17:50:07 +0100 Subject: [PATCH 16/17] pre-release: update the list of cotnributors, benchmark results, CHANGELOG --- CHANGELOG | 2 +- README.md | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9a6b32e..fcef4bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -- 0.8.7: Future release. +- 0.8.7: Bug fixes. New format: `pretty`. HTML escaping. - 0.8.6: Bug fixes. Stop supporting Python 3.3, 3.4. - 0.8.5: Fix broken Windows package. Minor documentation updates. - 0.8.4: Bug fixes. diff --git a/README.md b/README.md index 8761b5e..1823818 100644 --- a/README.md +++ b/README.md @@ -672,17 +672,17 @@ In the same time `tabulate` is comparable to other table pretty-printers. Given a 10x10 table (a list of lists) of mixed text and numeric data, `tabulate` appears to be slower than `asciitable`, and faster than `PrettyTable` and `texttable` The following mini-benchmark -was run in Python 3.6.8 on Ubuntu 18.04 in WSL: +was run in Python 3.8.1 in Windows 10 x64: - ========================== ========== =========== + =========================== ========== =========== Table formatter time, μs rel. time =========================== ========== =========== - csv to StringIO 21.4 1.0 - join with tabs and newlines 29.6 1.4 - asciitable (0.8.0) 506.8 23.7 - tabulate (0.8.6) 1079.9 50.5 - PrettyTable (0.7.2) 2032.0 95.0 - texttable (1.6.2) 3025.7 141.4 + csv to StringIO 12.4 1.0 + join with tabs and newlines 15.7 1.3 + asciitable (0.8.0) 208.3 16.7 + tabulate (0.8.7) 492.1 39.5 + PrettyTable (0.7.2) 945.5 76.0 + texttable (1.6.2) 1239.5 99.6 =========================== ========== =========== @@ -742,4 +742,5 @@ Maier, Andy MacKinlay, Thomas Roten, Jue Wang, Joe King, Samuel Phan, Nick Satterly, Daniel Robbins, Dmitry B, Lars Butler, Andreas Maier, Dick Marinus, Sébastien Celles, Yago González, Andrew Gaul, Wim Glenn, Jean Michel Rouly, Tim Gates, John Vandenberg, Sorin Sbarnea, -Wes Turner. +Wes Turner, Andrew Tija, Marco Gorelli, Sean McGinnis. + From e96293a3ef03a704be387a3f3a34b669860f7c6c Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Sun, 22 Mar 2020 17:55:34 +0100 Subject: [PATCH 17/17] implement #43 recommendation and publish also bdist_wheel --- HOWTOPUBLISH | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HOWTOPUBLISH b/HOWTOPUBLISH index 0167ba7..6730e8a 100644 --- a/HOWTOPUBLISH +++ b/HOWTOPUBLISH @@ -1,7 +1,7 @@ # update contributors and CHANGELOG in README python3 benchmark.py # then update README tox -e py33,py34,py36-extra -python3 setup.py sdist +python3 setup.py sdist bdist_wheel twine upload --repository-url https://test.pypi.org/legacy/ dist/* twine upload dist/* # tag version release