Repository: astanin/python-tabulate

Issue:
Proposed new tablefmt "fancy"
THANK YOU for an awesome library. I'm proposing a PR I'd like to write for you:

I like the existing tablefmt="fancy_grid". But the horizontal lines between each table row take up a lot of vertical space. How would you feel if I were to submit a PR adding a new table format, like "fancy_grid", but without the horizontal lines? eg:

The existing "fancy_grid":

```
>>> print(tabulate(table, headers, tablefmt="fancy_grid"))
╒════════╤═══════╕
│ item   │   qty │
╞════════╪═══════╡
│ spam   │    42 │
├────────┼───────┤
│ eggs   │   451 │
├────────┼───────┤
│ bacon  │     0 │
╘════════╧═══════╛
```

vs the proposed new format:

```
>>> print(tabulate(table, headers, tablefmt="fancy"))
╒════════╤═══════╕
│ item   │   qty │
╞════════╪═══════╡
│ spam   │    42 │
│ eggs   │   451 │
│ bacon  │     0 │
╘════════╧═══════╛
```

Any opinions about what the new table format should be called? I like "fancy", hence the existing "fancy_grid" format would be just like "fancy", but with extra **grid** lines between each table row.


Discussion:
kdeldycke: The only issue is that wide-characters tests do not pass on Python 2.7 on Windows.

So I can either:
* Drop the failing tests altogether
* Decide to drop Python 2.7 altogether

The later is I guess the way to go. Python 2 is long gone. Note that this was also pointed out in #149 .
kdeldycke: For the record, I added support for all these new formats in my standalone [`click-extra` package](https://github.com/kdeldycke/click-extra).

Patch:
diff --git a/README.md b/README.md
index 7378a76..37fbdbe 100644
--- a/README.md
+++ b/README.md
@@ -137,7 +137,15 @@ Supported table formats are:
 -   "simple"
 -   "github"
 -   "grid"
+-   "simple\_grid"
+-   "rounded\_grid"
+-   "double\_grid"
 -   "fancy\_grid"
+-   "outline"
+-   "simple\_outline"
+-   "rounded\_outline"
+-   "double\_outline"
+-   "fancy\_outline"
 -   "pipe"
 -   "orgtbl"
 -   "jira"
@@ -203,7 +211,47 @@ corresponds to the `pipe` format without alignment colons:
     | bacon  |     0 |
     +--------+-------+
 
-`fancy_grid` draws a grid using box-drawing characters:
+`simple_grid` draws a grid using single-line box-drawing characters:
+
+    >>> print(tabulate(table, headers, tablefmt="simple_grid"))
+    ┌────────┬───────┐
+    │ item   │   qty │
+    ├────────┼───────┤
+    │ spam   │    42 │
+    ├────────┼───────┤
+    │ eggs   │   451 │
+    ├────────┼───────┤
+    │ bacon  │     0 │
+    └────────┴───────┘
+
+`rounded_grid` draws a grid using single-line box-drawing characters with rounded corners:
+
+    >>> print(tabulate(table, headers, tablefmt="rounded_grid"))
+    ╭────────┬───────╮
+    │ item   │   qty │
+    ├────────┼───────┤
+    │ spam   │    42 │
+    ├────────┼───────┤
+    │ eggs   │   451 │
+    ├────────┼───────┤
+    │ bacon  │     0 │
+    ╰────────┴───────╯
+
+`double_grid` draws a grid using double-line box-drawing characters:
+
+    >>> print(tabulate(table, headers, tablefmt="double_grid"))
+    ╔════════╦═══════╗
+    ║ item   ║   qty ║
+    ╠════════╬═══════╣
+    ║ spam   ║    42 ║
+    ╠════════╬═══════╣
+    ║ eggs   ║   451 ║
+    ╠════════╬═══════╣
+    ║ bacon  ║     0 ║
+    ╚════════╩═══════╝
+
+`fancy_grid` draws a grid using a mix of single and
+    double-line box-drawing characters:
 
     >>> print(tabulate(table, headers, tablefmt="fancy_grid"))
     ╒════════╤═══════╕
@@ -216,6 +264,61 @@ corresponds to the `pipe` format without alignment colons:
     │ bacon  │     0 │
     ╘════════╧═══════╛
 
+`outline` is the same as the `grid` format but doesn't draw lines between rows:
+
+    >>> print(tabulate(table, headers, tablefmt="outline"))
+    +--------+-------+
+    | item   |   qty |
+    +========+=======+
+    | spam   |    42 |
+    | eggs   |   451 |
+    | bacon  |     0 |
+    +--------+-------+
+
+`simple_outline` is the same as the `simple_grid` format but doesn't draw lines between rows:
+
+    >>> print(tabulate(table, headers, tablefmt="simple_outline"))
+    ┌────────┬───────┐
+    │ item   │   qty │
+    ├────────┼───────┤
+    │ spam   │    42 │
+    │ eggs   │   451 │
+    │ bacon  │     0 │
+    └────────┴───────┘
+
+`rounded_outline` is the same as the `rounded_grid` format but doesn't draw lines between rows:
+
+    >>> print(tabulate(table, headers, tablefmt="rounded_outline"))
+    ╭────────┬───────╮
+    │ item   │   qty │
+    ├────────┼───────┤
+    │ spam   │    42 │
+    │ eggs   │   451 │
+    │ bacon  │     0 │
+    ╰────────┴───────╯
+
+`double_outline` is the same as the `double_grid` format but doesn't draw lines between rows:
+
+    >>> print(tabulate(table, headers, tablefmt="double_outline"))
+    ╔════════╦═══════╗
+    ║ item   ║   qty ║
+    ╠════════╬═══════╣
+    ║ spam   ║    42 ║
+    ║ eggs   ║   451 ║
+    ║ bacon  ║     0 ║
+    ╚════════╩═══════╝
+
+`fancy_outline` is the same as the `fancy_grid` format but doesn't draw lines between rows:
+
+    >>> print(tabulate(table, headers, tablefmt="fancy_outline"))
+    ╒════════╤═══════╕
+    │ item   │   qty │
+    ╞════════╪═══════╡
+    │ spam   │    42 │
+    │ eggs   │   451 │
+    │ bacon  │     0 │
+    ╘════════╧═══════╛
+
 `presto` is like tables formatted by Presto cli:
 
     >>> print(tabulate(table, headers, tablefmt="presto"))
diff --git a/tabulate.py b/tabulate.py
index a0b2dc2..f23a1a8 100644
--- a/tabulate.py
+++ b/tabulate.py
@@ -310,6 +310,36 @@ _table_formats = {
         padding=1,
         with_header_hide=None,
     ),
+    "simple_grid": TableFormat(
+        lineabove=Line("┌", "─", "┬", "┐"),
+        linebelowheader=Line("├", "─", "┼", "┤"),
+        linebetweenrows=Line("├", "─", "┼", "┤"),
+        linebelow=Line("└", "─", "┴", "┘"),
+        headerrow=DataRow("│", "│", "│"),
+        datarow=DataRow("│", "│", "│"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "rounded_grid": TableFormat(
+        lineabove=Line("╭", "─", "┬", "╮"),
+        linebelowheader=Line("├", "─", "┼", "┤"),
+        linebetweenrows=Line("├", "─", "┼", "┤"),
+        linebelow=Line("╰", "─", "┴", "╯"),
+        headerrow=DataRow("│", "│", "│"),
+        datarow=DataRow("│", "│", "│"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "double_grid": TableFormat(
+        lineabove=Line("╔", "═", "╦", "╗"),
+        linebelowheader=Line("╠", "═", "╬", "╣"),
+        linebetweenrows=Line("╠", "═", "╬", "╣"),
+        linebelow=Line("╚", "═", "╩", "╝"),
+        headerrow=DataRow("║", "║", "║"),
+        datarow=DataRow("║", "║", "║"),
+        padding=1,
+        with_header_hide=None,
+    ),
     "fancy_grid": TableFormat(
         lineabove=Line("╒", "═", "╤", "╕"),
         linebelowheader=Line("╞", "═", "╪", "╡"),
@@ -320,6 +350,46 @@ _table_formats = {
         padding=1,
         with_header_hide=None,
     ),
+    "outline": TableFormat(
+        lineabove=Line("+", "-", "+", "+"),
+        linebelowheader=Line("+", "=", "+", "+"),
+        linebetweenrows=None,
+        linebelow=Line("+", "-", "+", "+"),
+        headerrow=DataRow("|", "|", "|"),
+        datarow=DataRow("|", "|", "|"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "simple_outline": TableFormat(
+        lineabove=Line("┌", "─", "┬", "┐"),
+        linebelowheader=Line("├", "─", "┼", "┤"),
+        linebetweenrows=None,
+        linebelow=Line("└", "─", "┴", "┘"),
+        headerrow=DataRow("│", "│", "│"),
+        datarow=DataRow("│", "│", "│"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "rounded_outline": TableFormat(
+        lineabove=Line("╭", "─", "┬", "╮"),
+        linebelowheader=Line("├", "─", "┼", "┤"),
+        linebetweenrows=None,
+        linebelow=Line("╰", "─", "┴", "╯"),
+        headerrow=DataRow("│", "│", "│"),
+        datarow=DataRow("│", "│", "│"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "double_outline": TableFormat(
+        lineabove=Line("╔", "═", "╦", "╗"),
+        linebelowheader=Line("╠", "═", "╬", "╣"),
+        linebetweenrows=None,
+        linebelow=Line("╚", "═", "╩", "╝"),
+        headerrow=DataRow("║", "║", "║"),
+        datarow=DataRow("║", "║", "║"),
+        padding=1,
+        with_header_hide=None,
+    ),
     "fancy_outline": TableFormat(
         lineabove=Line("╒", "═", "╤", "╕"),
         linebelowheader=Line("╞", "═", "╪", "╡"),
@@ -537,6 +607,9 @@ multiline_formats = {
     "plain": "plain",
     "simple": "simple",
     "grid": "grid",
+    "simple_grid": "simple_grid",
+    "rounded_grid": "rounded_grid",
+    "double_grid": "double_grid",
     "fancy_grid": "fancy_grid",
     "pipe": "pipe",
     "orgtbl": "orgtbl",
@@ -1425,7 +1498,47 @@ def tabulate(
     | eggs | 451      |
     +------+----------+
 
-    "fancy_grid" draws a grid using box-drawing characters:
+    "simple_grid" draws a grid using single-line box-drawing
+    characters:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "simple_grid"))
+    ┌───────────┬───────────┐
+    │ strings   │   numbers │
+    ├───────────┼───────────┤
+    │ spam      │   41.9999 │
+    ├───────────┼───────────┤
+    │ eggs      │  451      │
+    └───────────┴───────────┘
+
+    "rounded_grid" draws a grid using single-line box-drawing
+    characters with rounded corners:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "rounded_grid"))
+    ╭───────────┬───────────╮
+    │ strings   │   numbers │
+    ├───────────┼───────────┤
+    │ spam      │   41.9999 │
+    ├───────────┼───────────┤
+    │ eggs      │  451      │
+    ╰───────────┴───────────╯
+
+    "double_grid" draws a grid using double-line box-drawing
+    characters:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "double_grid"))
+    ╔═══════════╦═══════════╗
+    ║ strings   ║   numbers ║
+    ╠═══════════╬═══════════╣
+    ║ spam      ║   41.9999 ║
+    ╠═══════════╬═══════════╣
+    ║ eggs      ║  451      ║
+    ╚═══════════╩═══════════╝
+
+    "fancy_grid" draws a grid using a mix of single and
+    double-line box-drawing characters:
 
     >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
     ...                ["strings", "numbers"], "fancy_grid"))
@@ -1437,6 +1550,67 @@ def tabulate(
     │ eggs      │  451      │
     ╘═══════════╧═══════════╛
 
+    "outline" is the same as the "grid" format but doesn't draw lines between rows:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "outline"))
+    +-----------+-----------+
+    | strings   |   numbers |
+    +===========+===========+
+    | spam      |   41.9999 |
+    | eggs      |  451      |
+    +-----------+-----------+
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="outline"))
+    +------+----------+
+    | spam |  41.9999 |
+    | eggs | 451      |
+    +------+----------+
+
+    "simple_outline" is the same as the "simple_grid" format but doesn't draw lines between rows:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "simple_outline"))
+    ┌───────────┬───────────┐
+    │ strings   │   numbers │
+    ├───────────┼───────────┤
+    │ spam      │   41.9999 │
+    │ eggs      │  451      │
+    └───────────┴───────────┘
+
+    "rounded_outline" is the same as the "rounded_grid" format but doesn't draw lines between rows:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "rounded_outline"))
+    ╭───────────┬───────────╮
+    │ strings   │   numbers │
+    ├───────────┼───────────┤
+    │ spam      │   41.9999 │
+    │ eggs      │  451      │
+    ╰───────────┴───────────╯
+
+    "double_outline" is the same as the "double_grid" format but doesn't draw lines between rows:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "double_outline"))
+    ╔═══════════╦═══════════╗
+    ║ strings   ║   numbers ║
+    ╠═══════════╬═══════════╣
+    ║ spam      ║   41.9999 ║
+    ║ eggs      ║  451      ║
+    ╚═══════════╩═══════════╝
+
+    "fancy_outline" is the same as the "fancy_grid" format but doesn't draw lines between rows:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "fancy_outline"))
+    ╒═══════════╤═══════════╕
+    │ strings   │   numbers │
+    ╞═══════════╪═══════════╡
+    │ spam      │   41.9999 │
+    │ eggs      │  451      │
+    ╘═══════════╧═══════════╛
+
     "pipe" is like tables in PHP Markdown Extra extension or Pandoc
     pipe_tables:
 


Test patch:
diff --git a/test/test_output.py b/test/test_output.py
index eed4489..ef27a91 100644
--- a/test/test_output.py
+++ b/test/test_output.py
@@ -478,6 +478,411 @@ def test_grid_multiline_with_empty_cells_headerless():
     assert_equal(expected, result)
 
 
+def test_simple_grid():
+    "Output: simple_grid with headers"
+    expected = "\n".join(
+        [
+            "┌───────────┬───────────┐",
+            "│ strings   │   numbers │",
+            "├───────────┼───────────┤",
+            "│ spam      │   41.9999 │",
+            "├───────────┼───────────┤",
+            "│ eggs      │  451      │",
+            "└───────────┴───────────┘",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="simple_grid")
+    assert_equal(expected, result)
+
+
+def test_simple_grid_wide_characters():
+    "Output: simple_grid with wide characters in headers"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_simple_grid_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "┌───────────┬──────────┐",
+            "│ strings   │     配列 │",
+            "├───────────┼──────────┤",
+            "│ spam      │  41.9999 │",
+            "├───────────┼──────────┤",
+            "│ eggs      │ 451      │",
+            "└───────────┴──────────┘",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="simple_grid")
+    assert_equal(expected, result)
+
+
+def test_simple_grid_headerless():
+    "Output: simple_grid without headers"
+    expected = "\n".join(
+        [
+            "┌──────┬──────────┐",
+            "│ spam │  41.9999 │",
+            "├──────┼──────────┤",
+            "│ eggs │ 451      │",
+            "└──────┴──────────┘",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="simple_grid")
+    assert_equal(expected, result)
+
+
+def test_simple_grid_multiline_headerless():
+    "Output: simple_grid 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, stralign="center", tablefmt="simple_grid")
+    assert_equal(expected, result)
+
+
+def test_simple_grid_multiline():
+    "Output: simple_grid 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="simple_grid")
+    assert_equal(expected, result)
+
+
+def test_simple_grid_multiline_with_empty_cells():
+    "Output: simple_grid 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="simple_grid")
+    assert_equal(expected, result)
+
+
+def test_simple_grid_multiline_with_empty_cells_headerless():
+    "Output: simple_grid 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="simple_grid")
+    assert_equal(expected, result)
+
+
+def test_rounded_grid():
+    "Output: rounded_grid with headers"
+    expected = "\n".join(
+        [
+            "╭───────────┬───────────╮",
+            "│ strings   │   numbers │",
+            "├───────────┼───────────┤",
+            "│ spam      │   41.9999 │",
+            "├───────────┼───────────┤",
+            "│ eggs      │  451      │",
+            "╰───────────┴───────────╯",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="rounded_grid")
+    assert_equal(expected, result)
+
+
+def test_rounded_grid_wide_characters():
+    "Output: rounded_grid with wide characters in headers"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_rounded_grid_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "╭───────────┬──────────╮",
+            "│ strings   │     配列 │",
+            "├───────────┼──────────┤",
+            "│ spam      │  41.9999 │",
+            "├───────────┼──────────┤",
+            "│ eggs      │ 451      │",
+            "╰───────────┴──────────╯",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="rounded_grid")
+    assert_equal(expected, result)
+
+
+def test_rounded_grid_headerless():
+    "Output: rounded_grid without headers"
+    expected = "\n".join(
+        [
+            "╭──────┬──────────╮",
+            "│ spam │  41.9999 │",
+            "├──────┼──────────┤",
+            "│ eggs │ 451      │",
+            "╰──────┴──────────╯",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="rounded_grid")
+    assert_equal(expected, result)
+
+
+def test_rounded_grid_multiline_headerless():
+    "Output: rounded_grid 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, stralign="center", tablefmt="rounded_grid")
+    assert_equal(expected, result)
+
+
+def test_rounded_grid_multiline():
+    "Output: rounded_grid 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="rounded_grid")
+    assert_equal(expected, result)
+
+
+def test_rounded_grid_multiline_with_empty_cells():
+    "Output: rounded_grid 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="rounded_grid")
+    assert_equal(expected, result)
+
+
+def test_rounded_grid_multiline_with_empty_cells_headerless():
+    "Output: rounded_grid 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="rounded_grid")
+    assert_equal(expected, result)
+
+
+def test_double_grid():
+    "Output: double_grid with headers"
+    expected = "\n".join(
+        [
+            "╔═══════════╦═══════════╗",
+            "║ strings   ║   numbers ║",
+            "╠═══════════╬═══════════╣",
+            "║ spam      ║   41.9999 ║",
+            "╠═══════════╬═══════════╣",
+            "║ eggs      ║  451      ║",
+            "╚═══════════╩═══════════╝",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="double_grid")
+    assert_equal(expected, result)
+
+
+def test_double_grid_wide_characters():
+    "Output: double_grid with wide characters in headers"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_double_grid_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "╔═══════════╦══════════╗",
+            "║ strings   ║     配列 ║",
+            "╠═══════════╬══════════╣",
+            "║ spam      ║  41.9999 ║",
+            "╠═══════════╬══════════╣",
+            "║ eggs      ║ 451      ║",
+            "╚═══════════╩══════════╝",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="double_grid")
+    assert_equal(expected, result)
+
+
+def test_double_grid_headerless():
+    "Output: double_grid without headers"
+    expected = "\n".join(
+        [
+            "╔══════╦══════════╗",
+            "║ spam ║  41.9999 ║",
+            "╠══════╬══════════╣",
+            "║ eggs ║ 451      ║",
+            "╚══════╩══════════╝",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="double_grid")
+    assert_equal(expected, result)
+
+
+def test_double_grid_multiline_headerless():
+    "Output: double_grid 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, stralign="center", tablefmt="double_grid")
+    assert_equal(expected, result)
+
+
+def test_double_grid_multiline():
+    "Output: double_grid 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="double_grid")
+    assert_equal(expected, result)
+
+
+def test_double_grid_multiline_with_empty_cells():
+    "Output: double_grid 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="double_grid")
+    assert_equal(expected, result)
+
+
+def test_double_grid_multiline_with_empty_cells_headerless():
+    "Output: double_grid 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="double_grid")
+    assert_equal(expected, result)
+
+
 def test_fancy_grid():
     "Output: fancy_grid with headers"
     expected = "\n".join(
@@ -495,6 +900,29 @@ def test_fancy_grid():
     assert_equal(expected, result)
 
 
+def test_fancy_grid_wide_characters():
+    "Output: fancy_grid with wide characters in headers"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_fancy_grid_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "╒═══════════╤══════════╕",
+            "│ strings   │     配列 │",
+            "╞═══════════╪══════════╡",
+            "│ spam      │  41.9999 │",
+            "├───────────┼──────────┤",
+            "│ eggs      │ 451      │",
+            "╘═══════════╧══════════╛",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="fancy_grid")
+    assert_equal(expected, result)
+
+
 def test_fancy_grid_headerless():
     "Output: fancy_grid without headers"
     expected = "\n".join(
@@ -590,6 +1018,266 @@ def test_fancy_grid_multiline_with_empty_cells_headerless():
     assert_equal(expected, result)
 
 
+def test_outline():
+    "Output: outline with headers"
+    expected = "\n".join(
+        [
+            "+-----------+-----------+",
+            "| strings   |   numbers |",
+            "+===========+===========+",
+            "| spam      |   41.9999 |",
+            "| eggs      |  451      |",
+            "+-----------+-----------+",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="outline")
+    assert_equal(expected, result)
+
+
+def test_outline_wide_characters():
+    "Output: outline with wide characters in headers"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_outline_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "+-----------+----------+",
+            "| strings   |     配列 |",
+            "+===========+==========+",
+            "| spam      |  41.9999 |",
+            "| eggs      | 451      |",
+            "+-----------+----------+",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="outline")
+    assert_equal(expected, result)
+
+
+def test_outline_headerless():
+    "Output: outline without headers"
+    expected = "\n".join(
+        [
+            "+------+----------+",
+            "| spam |  41.9999 |",
+            "| eggs | 451      |",
+            "+------+----------+",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="outline")
+    assert_equal(expected, result)
+
+
+def test_simple_outline():
+    "Output: simple_outline with headers"
+    expected = "\n".join(
+        [
+            "┌───────────┬───────────┐",
+            "│ strings   │   numbers │",
+            "├───────────┼───────────┤",
+            "│ spam      │   41.9999 │",
+            "│ eggs      │  451      │",
+            "└───────────┴───────────┘",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="simple_outline")
+    assert_equal(expected, result)
+
+
+def test_simple_outline_wide_characters():
+    "Output: simple_outline with wide characters in headers"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_simple_outline_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "┌───────────┬──────────┐",
+            "│ strings   │     配列 │",
+            "├───────────┼──────────┤",
+            "│ spam      │  41.9999 │",
+            "│ eggs      │ 451      │",
+            "└───────────┴──────────┘",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="simple_outline")
+    assert_equal(expected, result)
+
+
+def test_simple_outline_headerless():
+    "Output: simple_outline without headers"
+    expected = "\n".join(
+        [
+            "┌──────┬──────────┐",
+            "│ spam │  41.9999 │",
+            "│ eggs │ 451      │",
+            "└──────┴──────────┘",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="simple_outline")
+    assert_equal(expected, result)
+
+
+def test_rounded_outline():
+    "Output: rounded_outline with headers"
+    expected = "\n".join(
+        [
+            "╭───────────┬───────────╮",
+            "│ strings   │   numbers │",
+            "├───────────┼───────────┤",
+            "│ spam      │   41.9999 │",
+            "│ eggs      │  451      │",
+            "╰───────────┴───────────╯",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="rounded_outline")
+    assert_equal(expected, result)
+
+
+def test_rounded_outline_wide_characters():
+    "Output: rounded_outline with wide characters in headers"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_rounded_outline_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "╭───────────┬──────────╮",
+            "│ strings   │     配列 │",
+            "├───────────┼──────────┤",
+            "│ spam      │  41.9999 │",
+            "│ eggs      │ 451      │",
+            "╰───────────┴──────────╯",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="rounded_outline")
+    assert_equal(expected, result)
+
+
+def test_rounded_outline_headerless():
+    "Output: rounded_outline without headers"
+    expected = "\n".join(
+        [
+            "╭──────┬──────────╮",
+            "│ spam │  41.9999 │",
+            "│ eggs │ 451      │",
+            "╰──────┴──────────╯",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="rounded_outline")
+    assert_equal(expected, result)
+
+
+def test_double_outline():
+    "Output: double_outline with headers"
+    expected = "\n".join(
+        [
+            "╔═══════════╦═══════════╗",
+            "║ strings   ║   numbers ║",
+            "╠═══════════╬═══════════╣",
+            "║ spam      ║   41.9999 ║",
+            "║ eggs      ║  451      ║",
+            "╚═══════════╩═══════════╝",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="double_outline")
+    assert_equal(expected, result)
+
+
+def test_double_outline_wide_characters():
+    "Output: double_outline with wide characters in headers"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_double_outline_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "╔═══════════╦══════════╗",
+            "║ strings   ║     配列 ║",
+            "╠═══════════╬══════════╣",
+            "║ spam      ║  41.9999 ║",
+            "║ eggs      ║ 451      ║",
+            "╚═══════════╩══════════╝",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="double_outline")
+    assert_equal(expected, result)
+
+
+def test_double_outline_headerless():
+    "Output: double_outline without headers"
+    expected = "\n".join(
+        [
+            "╔══════╦══════════╗",
+            "║ spam ║  41.9999 ║",
+            "║ eggs ║ 451      ║",
+            "╚══════╩══════════╝",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="double_outline")
+    assert_equal(expected, result)
+
+
+def test_fancy_outline():
+    "Output: fancy_outline with headers"
+    expected = "\n".join(
+        [
+            "╒═══════════╤═══════════╕",
+            "│ strings   │   numbers │",
+            "╞═══════════╪═══════════╡",
+            "│ spam      │   41.9999 │",
+            "│ eggs      │  451      │",
+            "╘═══════════╧═══════════╛",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="fancy_outline")
+    assert_equal(expected, result)
+
+
+def test_fancy_outline_wide_characters():
+    "Output: fancy_outline with wide characters in headers"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_fancy_outline_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "╒═══════════╤══════════╕",
+            "│ strings   │     配列 │",
+            "╞═══════════╪══════════╡",
+            "│ spam      │  41.9999 │",
+            "│ eggs      │ 451      │",
+            "╘═══════════╧══════════╛",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="fancy_outline")
+    assert_equal(expected, result)
+
+
+def test_fancy_outline_headerless():
+    "Output: fancy_outline without headers"
+    expected = "\n".join(
+        [
+            "╒══════╤══════════╕",
+            "│ spam │  41.9999 │",
+            "│ eggs │ 451      │",
+            "╘══════╧══════════╛",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="fancy_outline")
+    assert_equal(expected, result)
+
+
 def test_pipe():
     "Output: pipe with headers"
     expected = "\n".join(