terminaltables3 4.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- terminaltables3-4.0.0/LICENSE +21 -0
- terminaltables3-4.0.0/PKG-INFO +103 -0
- terminaltables3-4.0.0/README.md +65 -0
- terminaltables3-4.0.0/pyproject.toml +91 -0
- terminaltables3-4.0.0/terminaltables3/__init__.py +17 -0
- terminaltables3-4.0.0/terminaltables3/ascii_table.py +63 -0
- terminaltables3-4.0.0/terminaltables3/base_table.py +236 -0
- terminaltables3-4.0.0/terminaltables3/build.py +174 -0
- terminaltables3-4.0.0/terminaltables3/github_table.py +73 -0
- terminaltables3-4.0.0/terminaltables3/other_tables.py +173 -0
- terminaltables3-4.0.0/terminaltables3/terminal_io.py +102 -0
- terminaltables3-4.0.0/terminaltables3/width_and_alignment.py +196 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Robpol86
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: terminaltables3
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: Generate simple tables in terminals from a nested list of strings. Fork of terminaltables.
|
|
5
|
+
Home-page: https://github.com/matthewdeanmartin/terminaltables3
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: Shell,Bash,ANSI,ASCII,terminal,tables
|
|
8
|
+
Author: Robpol86
|
|
9
|
+
Author-email: robpol86@gmail.com
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Environment :: MacOS X
|
|
14
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Operating System :: POSIX
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
29
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
30
|
+
Classifier: Topic :: Terminals
|
|
31
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
32
|
+
Project-URL: Bug Tracker, https://github.com/matthewdeanmartin/terminaltables3/issues
|
|
33
|
+
Project-URL: Change Log, https://github.com/matthewdeanmartin/terminaltables3/blob/master/CHANGELOG.md
|
|
34
|
+
Project-URL: Documentation, https://github.com/matthewdeanmartin/terminaltables3
|
|
35
|
+
Project-URL: Repository, https://github.com/matthewdeanmartin/terminaltables3
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# What is it
|
|
39
|
+
|
|
40
|
+
Easily draw tables in terminal/console applications from a list of lists of strings. Supports multi-line rows.
|
|
41
|
+
|
|
42
|
+
Tested on Python 3.8+
|
|
43
|
+
|
|
44
|
+
**This is a fork of the terminaltables project. Which is archived and unmaintained. This library is in a new namespace
|
|
45
|
+
but should otherwise be a drop in replacement. Maintaining goals consist of maintaining ecosystem compatibility, type
|
|
46
|
+
annotations and responding to community pull requests.**
|
|
47
|
+
|
|
48
|
+
To Upgrade
|
|
49
|
+
==========
|
|
50
|
+
Replace all instances of `terminaltables` with `terminaltables3` in your code. If other libraries depend on `terminaltables`
|
|
51
|
+
in your venv they will not conflict because it is a new namespace.
|
|
52
|
+
|
|
53
|
+
As of right now, the documentation as the robpol86 version.
|
|
54
|
+
|
|
55
|
+
📖 Full documentation: https://robpol86.github.io/terminaltables
|
|
56
|
+
|
|
57
|
+
Quickstart
|
|
58
|
+
==========
|
|
59
|
+
|
|
60
|
+
Install:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install terminaltables3
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Usage:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from terminaltables3 import AsciiTable
|
|
70
|
+
|
|
71
|
+
table_data = [
|
|
72
|
+
["Heading1", "Heading2"],
|
|
73
|
+
["row1 column1", "row1 column2"],
|
|
74
|
+
["row2 column1", "row2 column2"],
|
|
75
|
+
["row3 column1", "row3 column2"],
|
|
76
|
+
]
|
|
77
|
+
table = AsciiTable(table_data)
|
|
78
|
+
print
|
|
79
|
+
table.table
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
+--------------+--------------+
|
|
84
|
+
| Heading1 | Heading2 |
|
|
85
|
+
+--------------+--------------+
|
|
86
|
+
| row1 column1 | row1 column2 |
|
|
87
|
+
| row2 column1 | row2 column2 |
|
|
88
|
+
| row3 column1 | row3 column2 |
|
|
89
|
+
+--------------+--------------+
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Example Implementations
|
|
93
|
+
=======================
|
|
94
|
+

|
|
95
|
+
|
|
96
|
+
Source code for examples:
|
|
97
|
+
|
|
98
|
+
- [example1.py](https://github.com/matthewdeanmartin/terminaltables/blob/master/example1.py)
|
|
99
|
+
- [example2.py](https://github.com/matthewdeanmartin/terminaltables/blob/master/example2.py)
|
|
100
|
+
- [example3.py](https://github.com/matthewdeanmartin/terminaltables/blob/master/example3.py)
|
|
101
|
+
|
|
102
|
+
[Change Log](https://github.com/matthewdeanmartin/terminaltables/blob/master/CHANGELOG.md)
|
|
103
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# What is it
|
|
2
|
+
|
|
3
|
+
Easily draw tables in terminal/console applications from a list of lists of strings. Supports multi-line rows.
|
|
4
|
+
|
|
5
|
+
Tested on Python 3.8+
|
|
6
|
+
|
|
7
|
+
**This is a fork of the terminaltables project. Which is archived and unmaintained. This library is in a new namespace
|
|
8
|
+
but should otherwise be a drop in replacement. Maintaining goals consist of maintaining ecosystem compatibility, type
|
|
9
|
+
annotations and responding to community pull requests.**
|
|
10
|
+
|
|
11
|
+
To Upgrade
|
|
12
|
+
==========
|
|
13
|
+
Replace all instances of `terminaltables` with `terminaltables3` in your code. If other libraries depend on `terminaltables`
|
|
14
|
+
in your venv they will not conflict because it is a new namespace.
|
|
15
|
+
|
|
16
|
+
As of right now, the documentation as the robpol86 version.
|
|
17
|
+
|
|
18
|
+
📖 Full documentation: https://robpol86.github.io/terminaltables
|
|
19
|
+
|
|
20
|
+
Quickstart
|
|
21
|
+
==========
|
|
22
|
+
|
|
23
|
+
Install:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install terminaltables3
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from terminaltables3 import AsciiTable
|
|
33
|
+
|
|
34
|
+
table_data = [
|
|
35
|
+
["Heading1", "Heading2"],
|
|
36
|
+
["row1 column1", "row1 column2"],
|
|
37
|
+
["row2 column1", "row2 column2"],
|
|
38
|
+
["row3 column1", "row3 column2"],
|
|
39
|
+
]
|
|
40
|
+
table = AsciiTable(table_data)
|
|
41
|
+
print
|
|
42
|
+
table.table
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
+--------------+--------------+
|
|
47
|
+
| Heading1 | Heading2 |
|
|
48
|
+
+--------------+--------------+
|
|
49
|
+
| row1 column1 | row1 column2 |
|
|
50
|
+
| row2 column1 | row2 column2 |
|
|
51
|
+
| row3 column1 | row3 column2 |
|
|
52
|
+
+--------------+--------------+
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example Implementations
|
|
56
|
+
=======================
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
Source code for examples:
|
|
60
|
+
|
|
61
|
+
- [example1.py](https://github.com/matthewdeanmartin/terminaltables/blob/master/example1.py)
|
|
62
|
+
- [example2.py](https://github.com/matthewdeanmartin/terminaltables/blob/master/example2.py)
|
|
63
|
+
- [example3.py](https://github.com/matthewdeanmartin/terminaltables/blob/master/example3.py)
|
|
64
|
+
|
|
65
|
+
[Change Log](https://github.com/matthewdeanmartin/terminaltables/blob/master/CHANGELOG.md)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "terminaltables3"
|
|
3
|
+
version = "4.0.0"
|
|
4
|
+
description = "Generate simple tables in terminals from a nested list of strings. Fork of terminaltables."
|
|
5
|
+
authors = [
|
|
6
|
+
"Robpol86 <robpol86@gmail.com>",
|
|
7
|
+
"Matthew Martin <matthewdeanmartin@gmail.com>"
|
|
8
|
+
]
|
|
9
|
+
keywords = ["Shell","Bash","ANSI","ASCII","terminal","tables"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Environment :: MacOS X",
|
|
14
|
+
"Environment :: Win32 (MS Windows)",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: MacOS :: MacOS X",
|
|
18
|
+
"Operating System :: Microsoft :: Windows",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Operating System :: POSIX",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
"Topic :: Terminals",
|
|
30
|
+
"Topic :: Text Processing :: Markup",
|
|
31
|
+
]
|
|
32
|
+
packages = [
|
|
33
|
+
{ include = "terminaltables3" },
|
|
34
|
+
]
|
|
35
|
+
include = [
|
|
36
|
+
"terminaltables3/**/*.py",
|
|
37
|
+
]
|
|
38
|
+
exclude = [
|
|
39
|
+
]
|
|
40
|
+
license = "MIT"
|
|
41
|
+
readme = "README.md"
|
|
42
|
+
repository = "https://github.com/matthewdeanmartin/terminaltables3"
|
|
43
|
+
homepage = "https://github.com/matthewdeanmartin/terminaltables3"
|
|
44
|
+
documentation = "https://github.com/matthewdeanmartin/terminaltables3"
|
|
45
|
+
|
|
46
|
+
[tool.poetry.urls]
|
|
47
|
+
"Bug Tracker" = "https://github.com/matthewdeanmartin/terminaltables3/issues"
|
|
48
|
+
"Change Log" = "https://github.com/matthewdeanmartin/terminaltables3/blob/master/CHANGELOG.md"
|
|
49
|
+
|
|
50
|
+
[tool.poetry.scripts]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
[tool.poetry.dependencies]
|
|
54
|
+
# per vermin's estimation
|
|
55
|
+
python = ">=3.8"
|
|
56
|
+
|
|
57
|
+
[tool.poetry.dev-dependencies]
|
|
58
|
+
# complementary deps
|
|
59
|
+
colorama=">=0.3.7"
|
|
60
|
+
colorclass=">=2.2.0"
|
|
61
|
+
termcolor = "*"
|
|
62
|
+
|
|
63
|
+
# build deps
|
|
64
|
+
pytest = ">=8.2.1"
|
|
65
|
+
pytest-cov = ">=5.0.0"
|
|
66
|
+
vermin = ">=1.6.0"
|
|
67
|
+
tox = ">=4.15.0"
|
|
68
|
+
isort = ">=5.13.2"
|
|
69
|
+
bandit = ">=1.7.8"
|
|
70
|
+
black = ">=24.4.2"
|
|
71
|
+
pylint = ">=3.2.2"
|
|
72
|
+
ruff = ">=0.4.5"
|
|
73
|
+
mypy = ">=1.10.0"
|
|
74
|
+
pillow = ">=10.3.0"
|
|
75
|
+
pre-commit = ">=2"
|
|
76
|
+
|
|
77
|
+
# pytest = ">=7.4.3"
|
|
78
|
+
pytest-xdist =">=3.5.0"
|
|
79
|
+
pytest-randomly=">=3.15.0"
|
|
80
|
+
pytest-sugar =">=0.9.7"
|
|
81
|
+
|
|
82
|
+
# pytest-snapshot = "*"
|
|
83
|
+
# pytest-asyncio = "*"
|
|
84
|
+
pytest-network= "*"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# check-wheel-contents = "*" # requires <4
|
|
88
|
+
|
|
89
|
+
[build-system]
|
|
90
|
+
requires = ["poetry-core>=1.0.0"]
|
|
91
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Generate simple tables in terminals from a nested list of strings.
|
|
2
|
+
|
|
3
|
+
Use SingleTable or DoubleTable instead of AsciiTable for box-drawing characters.
|
|
4
|
+
|
|
5
|
+
https://github.com/Robpol86/terminaltables3
|
|
6
|
+
https://pypi.python.org/pypi/terminaltables3
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from terminaltables3.ascii_table import AsciiTable # noqa
|
|
10
|
+
from terminaltables3.github_table import GithubFlavoredMarkdownTable # noqa
|
|
11
|
+
from terminaltables3.other_tables import DoubleTable # noqa
|
|
12
|
+
from terminaltables3.other_tables import PorcelainTable # noqa
|
|
13
|
+
from terminaltables3.other_tables import SingleTable # noqa
|
|
14
|
+
|
|
15
|
+
__author__ = "@Robpol86"
|
|
16
|
+
__license__ = "MIT"
|
|
17
|
+
__version__ = "3.1.0"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""AsciiTable is the main table class. To be inherited by other tables. Define convenience methods here."""
|
|
2
|
+
|
|
3
|
+
from terminaltables3.base_table import BaseTable
|
|
4
|
+
from terminaltables3.terminal_io import terminal_size
|
|
5
|
+
from terminaltables3.width_and_alignment import (
|
|
6
|
+
column_max_width,
|
|
7
|
+
max_dimensions,
|
|
8
|
+
table_width,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsciiTable(BaseTable):
|
|
13
|
+
"""Draw a table using regular ASCII characters, such as ``+``, ``|``, and ``-``.
|
|
14
|
+
|
|
15
|
+
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
|
|
16
|
+
:ivar str title: Optional title to show within the top border of the table.
|
|
17
|
+
:ivar bool inner_column_border: Separates columns.
|
|
18
|
+
:ivar bool inner_footing_row_border: Show a border before the last row.
|
|
19
|
+
:ivar bool inner_heading_row_border: Show a border after the first row.
|
|
20
|
+
:ivar bool inner_row_border: Show a border in between every row.
|
|
21
|
+
:ivar bool outer_border: Show the top, left, right, and bottom border.
|
|
22
|
+
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
|
|
23
|
+
:ivar int padding_left: Number of spaces to pad on the left side of every cell.
|
|
24
|
+
:ivar int padding_right: Number of spaces to pad on the right side of every cell.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def column_max_width(self, column_number: int) -> int:
|
|
28
|
+
"""Return the maximum width of a column based on the current terminal width.
|
|
29
|
+
|
|
30
|
+
:param int column_number: The column number to query.
|
|
31
|
+
|
|
32
|
+
:return: The max width of the column.
|
|
33
|
+
:rtype: int
|
|
34
|
+
"""
|
|
35
|
+
inner_widths = max_dimensions(self.table_data)[0]
|
|
36
|
+
outer_border = 2 if self.outer_border else 0
|
|
37
|
+
inner_border = 1 if self.inner_column_border else 0
|
|
38
|
+
padding = self.padding_left + self.padding_right
|
|
39
|
+
return column_max_width(
|
|
40
|
+
inner_widths, column_number, outer_border, inner_border, padding
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def column_widths(self) -> list[int]:
|
|
45
|
+
"""Return a list of integers representing the widths of each table column without padding."""
|
|
46
|
+
if not self.table_data:
|
|
47
|
+
return []
|
|
48
|
+
return max_dimensions(self.table_data)[0]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def ok(self) -> bool: # Too late to change API. # pylint: disable=invalid-name
|
|
52
|
+
"""Return True if the table fits within the terminal width, False if the table breaks."""
|
|
53
|
+
return self.table_width <= terminal_size()[0]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def table_width(self) -> int:
|
|
57
|
+
"""Return the width of the table including padding and borders."""
|
|
58
|
+
outer_widths = max_dimensions(
|
|
59
|
+
self.table_data, self.padding_left, self.padding_right
|
|
60
|
+
)[2]
|
|
61
|
+
outer_border = 2 if self.outer_border else 0
|
|
62
|
+
inner_border = 1 if self.inner_column_border else 0
|
|
63
|
+
return table_width(outer_widths, outer_border, inner_border)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Base table class. Define just the bare minimum to build tables."""
|
|
2
|
+
|
|
3
|
+
from typing import Generator, Optional, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
from terminaltables3.build import build_border, build_row, flatten
|
|
6
|
+
from terminaltables3.width_and_alignment import align_and_pad_cell, max_dimensions
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseTable:
|
|
10
|
+
"""Base table class.
|
|
11
|
+
|
|
12
|
+
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
|
|
13
|
+
:ivar str title: Optional title to show within the top border of the table.
|
|
14
|
+
:ivar bool inner_column_border: Separates columns.
|
|
15
|
+
:ivar bool inner_footing_row_border: Show a border before the last row.
|
|
16
|
+
:ivar bool inner_heading_row_border: Show a border after the first row.
|
|
17
|
+
:ivar bool inner_row_border: Show a border in between every row.
|
|
18
|
+
:ivar bool outer_border: Show the top, left, right, and bottom border.
|
|
19
|
+
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
|
|
20
|
+
:ivar int padding_left: Number of spaces to pad on the left side of every cell.
|
|
21
|
+
:ivar int padding_right: Number of spaces to pad on the right side of every cell.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
CHAR_F_INNER_HORIZONTAL = "-"
|
|
25
|
+
CHAR_F_INNER_INTERSECT = "+"
|
|
26
|
+
CHAR_F_INNER_VERTICAL = "|"
|
|
27
|
+
CHAR_F_OUTER_LEFT_INTERSECT = "+"
|
|
28
|
+
CHAR_F_OUTER_LEFT_VERTICAL = "|"
|
|
29
|
+
CHAR_F_OUTER_RIGHT_INTERSECT = "+"
|
|
30
|
+
CHAR_F_OUTER_RIGHT_VERTICAL = "|"
|
|
31
|
+
CHAR_H_INNER_HORIZONTAL = "-"
|
|
32
|
+
CHAR_H_INNER_INTERSECT = "+"
|
|
33
|
+
CHAR_H_INNER_VERTICAL = "|"
|
|
34
|
+
CHAR_H_OUTER_LEFT_INTERSECT = "+"
|
|
35
|
+
CHAR_H_OUTER_LEFT_VERTICAL = "|"
|
|
36
|
+
CHAR_H_OUTER_RIGHT_INTERSECT = "+"
|
|
37
|
+
CHAR_H_OUTER_RIGHT_VERTICAL = "|"
|
|
38
|
+
CHAR_INNER_HORIZONTAL = "-"
|
|
39
|
+
CHAR_INNER_INTERSECT = "+"
|
|
40
|
+
CHAR_INNER_VERTICAL = "|"
|
|
41
|
+
CHAR_OUTER_BOTTOM_HORIZONTAL = "-"
|
|
42
|
+
CHAR_OUTER_BOTTOM_INTERSECT = "+"
|
|
43
|
+
CHAR_OUTER_BOTTOM_LEFT = "+"
|
|
44
|
+
CHAR_OUTER_BOTTOM_RIGHT = "+"
|
|
45
|
+
CHAR_OUTER_LEFT_INTERSECT = "+"
|
|
46
|
+
CHAR_OUTER_LEFT_VERTICAL = "|"
|
|
47
|
+
CHAR_OUTER_RIGHT_INTERSECT = "+"
|
|
48
|
+
CHAR_OUTER_RIGHT_VERTICAL = "|"
|
|
49
|
+
CHAR_OUTER_TOP_HORIZONTAL = "-"
|
|
50
|
+
CHAR_OUTER_TOP_INTERSECT = "+"
|
|
51
|
+
CHAR_OUTER_TOP_LEFT = "+"
|
|
52
|
+
CHAR_OUTER_TOP_RIGHT = "+"
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self, table_data: Sequence[Sequence[str]], title: Optional[str] = None
|
|
56
|
+
):
|
|
57
|
+
"""Constructor.
|
|
58
|
+
|
|
59
|
+
:param iter table_data: List (empty or list of lists of strings) representing the table.
|
|
60
|
+
:param title: Optional title to show within the top border of the table.
|
|
61
|
+
"""
|
|
62
|
+
self.table_data = table_data
|
|
63
|
+
self.title = title
|
|
64
|
+
|
|
65
|
+
self.inner_column_border = True
|
|
66
|
+
self.inner_footing_row_border = False
|
|
67
|
+
self.inner_heading_row_border = True
|
|
68
|
+
self.inner_row_border = False
|
|
69
|
+
self.outer_border = True
|
|
70
|
+
|
|
71
|
+
self.justify_columns = {} # {0: 'right', 1: 'left', 2: 'center'}
|
|
72
|
+
self.padding_left = 1
|
|
73
|
+
self.padding_right = 1
|
|
74
|
+
|
|
75
|
+
def horizontal_border(
|
|
76
|
+
self, style: str, outer_widths: Sequence[int]
|
|
77
|
+
) -> Tuple[str, ...]:
|
|
78
|
+
"""Build any kind of horizontal border for the table.
|
|
79
|
+
|
|
80
|
+
:param str style: Type of border to return.
|
|
81
|
+
:param iter outer_widths: List of widths (with padding) for each column.
|
|
82
|
+
|
|
83
|
+
:return: Prepared border as a tuple of strings.
|
|
84
|
+
:rtype: tuple
|
|
85
|
+
"""
|
|
86
|
+
if style == "top":
|
|
87
|
+
horizontal = self.CHAR_OUTER_TOP_HORIZONTAL
|
|
88
|
+
left = self.CHAR_OUTER_TOP_LEFT
|
|
89
|
+
intersect = (
|
|
90
|
+
self.CHAR_OUTER_TOP_INTERSECT if self.inner_column_border else ""
|
|
91
|
+
)
|
|
92
|
+
right = self.CHAR_OUTER_TOP_RIGHT
|
|
93
|
+
title = self.title
|
|
94
|
+
elif style == "bottom":
|
|
95
|
+
horizontal = self.CHAR_OUTER_BOTTOM_HORIZONTAL
|
|
96
|
+
left = self.CHAR_OUTER_BOTTOM_LEFT
|
|
97
|
+
intersect = (
|
|
98
|
+
self.CHAR_OUTER_BOTTOM_INTERSECT if self.inner_column_border else ""
|
|
99
|
+
)
|
|
100
|
+
right = self.CHAR_OUTER_BOTTOM_RIGHT
|
|
101
|
+
title = None
|
|
102
|
+
elif style == "heading":
|
|
103
|
+
horizontal = self.CHAR_H_INNER_HORIZONTAL
|
|
104
|
+
left = self.CHAR_H_OUTER_LEFT_INTERSECT if self.outer_border else ""
|
|
105
|
+
intersect = self.CHAR_H_INNER_INTERSECT if self.inner_column_border else ""
|
|
106
|
+
right = self.CHAR_H_OUTER_RIGHT_INTERSECT if self.outer_border else ""
|
|
107
|
+
title = None
|
|
108
|
+
elif style == "footing":
|
|
109
|
+
horizontal = self.CHAR_F_INNER_HORIZONTAL
|
|
110
|
+
left = self.CHAR_F_OUTER_LEFT_INTERSECT if self.outer_border else ""
|
|
111
|
+
intersect = self.CHAR_F_INNER_INTERSECT if self.inner_column_border else ""
|
|
112
|
+
right = self.CHAR_F_OUTER_RIGHT_INTERSECT if self.outer_border else ""
|
|
113
|
+
title = None
|
|
114
|
+
else:
|
|
115
|
+
horizontal = self.CHAR_INNER_HORIZONTAL
|
|
116
|
+
left = self.CHAR_OUTER_LEFT_INTERSECT if self.outer_border else ""
|
|
117
|
+
intersect = self.CHAR_INNER_INTERSECT if self.inner_column_border else ""
|
|
118
|
+
right = self.CHAR_OUTER_RIGHT_INTERSECT if self.outer_border else ""
|
|
119
|
+
title = None
|
|
120
|
+
return build_border(outer_widths, horizontal, left, intersect, right, title)
|
|
121
|
+
|
|
122
|
+
def gen_row_lines(
|
|
123
|
+
self, row: Sequence[str], style: str, inner_widths: Sequence[int], height: int
|
|
124
|
+
) -> Generator[Tuple[str, ...], None, None]:
|
|
125
|
+
r"""Combine cells in row and group them into lines with vertical borders.
|
|
126
|
+
|
|
127
|
+
Caller is expected to pass yielded lines to ''.join() to combine them into a printable line. Caller must append
|
|
128
|
+
newline character to the end of joined line.
|
|
129
|
+
|
|
130
|
+
In:
|
|
131
|
+
['Row One Column One', 'Two', 'Three']
|
|
132
|
+
Out:
|
|
133
|
+
[
|
|
134
|
+
('|', ' Row One Column One ', '|', ' Two ', '|', ' Three ', '|'),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
In:
|
|
138
|
+
['Row One\nColumn One', 'Two', 'Three'],
|
|
139
|
+
Out:
|
|
140
|
+
[
|
|
141
|
+
('|', ' Row One ', '|', ' Two ', '|', ' Three ', '|'),
|
|
142
|
+
('|', ' Column One ', '|', ' ', '|', ' ', '|'),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
:param iter row: One row in the table. List of cells.
|
|
146
|
+
:param str style: Type of border characters to use.
|
|
147
|
+
:param iter inner_widths: List of widths (no padding) for each column.
|
|
148
|
+
:param int height: Inner height (no padding) (number of lines) to expand row to.
|
|
149
|
+
|
|
150
|
+
:return: Yields lines split into components in a list. Caller must ''.join() line.
|
|
151
|
+
"""
|
|
152
|
+
cells_in_row = []
|
|
153
|
+
|
|
154
|
+
# Resize row if it doesn't have enough cells.
|
|
155
|
+
if len(row) != len(inner_widths):
|
|
156
|
+
row = row + [""] * (len(inner_widths) - len(row))
|
|
157
|
+
|
|
158
|
+
# Pad and align each cell. Split each cell into lines to support multi-line cells.
|
|
159
|
+
for i, cell in enumerate(row):
|
|
160
|
+
align = (self.justify_columns.get(i),)
|
|
161
|
+
inner_dimensions = (inner_widths[i], height)
|
|
162
|
+
padding = (self.padding_left, self.padding_right, 0, 0)
|
|
163
|
+
cells_in_row.append(
|
|
164
|
+
align_and_pad_cell(cell, align, inner_dimensions, padding)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Determine border characters.
|
|
168
|
+
if style == "heading":
|
|
169
|
+
left = self.CHAR_H_OUTER_LEFT_VERTICAL if self.outer_border else ""
|
|
170
|
+
center = self.CHAR_H_INNER_VERTICAL if self.inner_column_border else ""
|
|
171
|
+
right = self.CHAR_H_OUTER_RIGHT_VERTICAL if self.outer_border else ""
|
|
172
|
+
elif style == "footing":
|
|
173
|
+
left = self.CHAR_F_OUTER_LEFT_VERTICAL if self.outer_border else ""
|
|
174
|
+
center = self.CHAR_F_INNER_VERTICAL if self.inner_column_border else ""
|
|
175
|
+
right = self.CHAR_F_OUTER_RIGHT_VERTICAL if self.outer_border else ""
|
|
176
|
+
else:
|
|
177
|
+
left = self.CHAR_OUTER_LEFT_VERTICAL if self.outer_border else ""
|
|
178
|
+
center = self.CHAR_INNER_VERTICAL if self.inner_column_border else ""
|
|
179
|
+
right = self.CHAR_OUTER_RIGHT_VERTICAL if self.outer_border else ""
|
|
180
|
+
|
|
181
|
+
# Yield each line.
|
|
182
|
+
yield from build_row(cells_in_row, left, center, right)
|
|
183
|
+
|
|
184
|
+
def gen_table(
|
|
185
|
+
self,
|
|
186
|
+
inner_widths: Sequence[int],
|
|
187
|
+
inner_heights: Sequence[int],
|
|
188
|
+
outer_widths: Sequence[int],
|
|
189
|
+
) -> Generator[Tuple[str, ...], None, None]:
|
|
190
|
+
"""Combine everything and yield every line of the entire table with borders.
|
|
191
|
+
|
|
192
|
+
:param iter inner_widths: List of widths (no padding) for each column.
|
|
193
|
+
:param iter inner_heights: List of heights (no padding) for each row.
|
|
194
|
+
:param iter outer_widths: List of widths (with padding) for each column.
|
|
195
|
+
:return:
|
|
196
|
+
"""
|
|
197
|
+
# Yield top border.
|
|
198
|
+
if self.outer_border:
|
|
199
|
+
yield self.horizontal_border("top", outer_widths)
|
|
200
|
+
|
|
201
|
+
# Yield table body.
|
|
202
|
+
row_count = len(self.table_data)
|
|
203
|
+
last_row_index, before_last_row_index = row_count - 1, row_count - 2
|
|
204
|
+
for i, row in enumerate(self.table_data):
|
|
205
|
+
# Yield the row line by line (e.g. multi-line rows).
|
|
206
|
+
if self.inner_heading_row_border and i == 0:
|
|
207
|
+
style = "heading"
|
|
208
|
+
elif self.inner_footing_row_border and i == last_row_index:
|
|
209
|
+
style = "footing"
|
|
210
|
+
else:
|
|
211
|
+
style = "row"
|
|
212
|
+
yield from self.gen_row_lines(row, style, inner_widths, inner_heights[i])
|
|
213
|
+
# If this is the last row then break. No separator needed.
|
|
214
|
+
if i == last_row_index:
|
|
215
|
+
break
|
|
216
|
+
# Yield heading separator.
|
|
217
|
+
if self.inner_heading_row_border and i == 0:
|
|
218
|
+
yield self.horizontal_border("heading", outer_widths)
|
|
219
|
+
# Yield footing separator.
|
|
220
|
+
elif self.inner_footing_row_border and i == before_last_row_index:
|
|
221
|
+
yield self.horizontal_border("footing", outer_widths)
|
|
222
|
+
# Yield row separator.
|
|
223
|
+
elif self.inner_row_border:
|
|
224
|
+
yield self.horizontal_border("row", outer_widths)
|
|
225
|
+
|
|
226
|
+
# Yield bottom border.
|
|
227
|
+
if self.outer_border:
|
|
228
|
+
yield self.horizontal_border("bottom", outer_widths)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def table(self) -> str:
|
|
232
|
+
"""Return a large string of the entire table ready to be printed to the terminal."""
|
|
233
|
+
dimensions = max_dimensions(
|
|
234
|
+
self.table_data, self.padding_left, self.padding_right
|
|
235
|
+
)[:3]
|
|
236
|
+
return flatten(self.gen_table(*dimensions))
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Combine cells into rows."""
|
|
2
|
+
|
|
3
|
+
from typing import Generator, Iterator, Optional, Sequence, Union
|
|
4
|
+
|
|
5
|
+
from terminaltables3.width_and_alignment import visible_width
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def combine(
|
|
9
|
+
line: Union[
|
|
10
|
+
Generator[Union[int, str], None, None], Iterator[Optional[Union[int, str]]]
|
|
11
|
+
],
|
|
12
|
+
left: str,
|
|
13
|
+
intersect: Optional[str],
|
|
14
|
+
right: str,
|
|
15
|
+
) -> Generator[int, None, None]:
|
|
16
|
+
"""Zip borders between items in `line`.
|
|
17
|
+
|
|
18
|
+
e.g. ('l', '1', 'c', '2', 'c', '3', 'r')
|
|
19
|
+
|
|
20
|
+
:param iter line: List to iterate.
|
|
21
|
+
:param left: Left border.
|
|
22
|
+
:param intersect: Column separator.
|
|
23
|
+
:param right: Right border.
|
|
24
|
+
|
|
25
|
+
:return: Yields combined objects.
|
|
26
|
+
"""
|
|
27
|
+
# Yield left border.
|
|
28
|
+
if left:
|
|
29
|
+
yield left
|
|
30
|
+
|
|
31
|
+
# Yield items with intersect characters.
|
|
32
|
+
if intersect:
|
|
33
|
+
try:
|
|
34
|
+
for j, i in enumerate(line, start=-len(line) + 1):
|
|
35
|
+
yield i
|
|
36
|
+
if j:
|
|
37
|
+
yield intersect
|
|
38
|
+
except TypeError: # Generator.
|
|
39
|
+
try:
|
|
40
|
+
item = next(line)
|
|
41
|
+
except StopIteration: # Was empty all along.
|
|
42
|
+
pass
|
|
43
|
+
else:
|
|
44
|
+
while True:
|
|
45
|
+
yield item
|
|
46
|
+
try:
|
|
47
|
+
peek = next(line)
|
|
48
|
+
except StopIteration:
|
|
49
|
+
break
|
|
50
|
+
yield intersect
|
|
51
|
+
item = peek
|
|
52
|
+
else:
|
|
53
|
+
yield from line
|
|
54
|
+
|
|
55
|
+
# Yield right border.
|
|
56
|
+
if right:
|
|
57
|
+
yield right
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_border(
|
|
61
|
+
outer_widths: Sequence[int],
|
|
62
|
+
horizontal: str,
|
|
63
|
+
left: str,
|
|
64
|
+
intersect: str,
|
|
65
|
+
right: str,
|
|
66
|
+
title: Optional[str] = None,
|
|
67
|
+
):
|
|
68
|
+
"""Build the top/bottom/middle row. Optionally embed the table title within the border.
|
|
69
|
+
|
|
70
|
+
Title is hidden if it doesn't fit between the left/right characters/edges.
|
|
71
|
+
|
|
72
|
+
Example return value:
|
|
73
|
+
('<', '-----', '+', '------', '+', '-------', '>')
|
|
74
|
+
('<', 'My Table', '----', '+', '------->')
|
|
75
|
+
|
|
76
|
+
:param iter outer_widths: List of widths (with padding) for each column.
|
|
77
|
+
:param str horizontal: Character to stretch across each column.
|
|
78
|
+
:param str left: Left border.
|
|
79
|
+
:param str intersect: Column separator.
|
|
80
|
+
:param str right: Right border.
|
|
81
|
+
:param title: Overlay the title on the border between the left and right characters.
|
|
82
|
+
|
|
83
|
+
:return: Returns a generator of strings representing a border.
|
|
84
|
+
:rtype: iter
|
|
85
|
+
"""
|
|
86
|
+
length = 0
|
|
87
|
+
|
|
88
|
+
# Hide title if it doesn't fit.
|
|
89
|
+
if title is not None and outer_widths:
|
|
90
|
+
try:
|
|
91
|
+
length = visible_width(title)
|
|
92
|
+
except TypeError:
|
|
93
|
+
title = str(title)
|
|
94
|
+
length = visible_width(title)
|
|
95
|
+
if length > sum(outer_widths) + len(intersect) * (len(outer_widths) - 1):
|
|
96
|
+
title = None
|
|
97
|
+
|
|
98
|
+
# Handle no title.
|
|
99
|
+
if title is None or not outer_widths or not horizontal:
|
|
100
|
+
return combine((horizontal * c for c in outer_widths), left, intersect, right)
|
|
101
|
+
|
|
102
|
+
# Handle title fitting in the first column.
|
|
103
|
+
if length == outer_widths[0]:
|
|
104
|
+
return combine(
|
|
105
|
+
[title] + [horizontal * c for c in outer_widths[1:]], left, intersect, right
|
|
106
|
+
)
|
|
107
|
+
if length < outer_widths[0]:
|
|
108
|
+
columns = [title + horizontal * (outer_widths[0] - length)] + [
|
|
109
|
+
horizontal * c for c in outer_widths[1:]
|
|
110
|
+
]
|
|
111
|
+
return combine(columns, left, intersect, right)
|
|
112
|
+
|
|
113
|
+
# Handle wide titles/narrow columns.
|
|
114
|
+
columns_and_intersects = [title]
|
|
115
|
+
for width in combine(outer_widths, None, bool(intersect), None):
|
|
116
|
+
# If title is taken care of.
|
|
117
|
+
if length < 1:
|
|
118
|
+
columns_and_intersects.append(
|
|
119
|
+
intersect if width is True else horizontal * width
|
|
120
|
+
)
|
|
121
|
+
# If title's last character overrides an intersect character.
|
|
122
|
+
elif width is True and length == 1:
|
|
123
|
+
length = 0
|
|
124
|
+
# If this is an intersect character that is overridden by the title.
|
|
125
|
+
elif width is True:
|
|
126
|
+
length -= 1
|
|
127
|
+
# If title's last character is within a column.
|
|
128
|
+
elif width >= length:
|
|
129
|
+
columns_and_intersects[0] += horizontal * (
|
|
130
|
+
width - length
|
|
131
|
+
) # Append horizontal chars to title.
|
|
132
|
+
length = 0
|
|
133
|
+
# If remainder of title won't fit in a column.
|
|
134
|
+
else:
|
|
135
|
+
length -= width
|
|
136
|
+
|
|
137
|
+
return combine(columns_and_intersects, left, None, right)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_row(row, left, center, right):
|
|
141
|
+
"""Combine single or multi-lined cells into a single row of list of lists including borders.
|
|
142
|
+
|
|
143
|
+
Row must already be padded and extended so each cell has the same number of lines.
|
|
144
|
+
|
|
145
|
+
Example return value:
|
|
146
|
+
[
|
|
147
|
+
['>', 'Left ', '|', 'Center', '|', 'Right', '<'],
|
|
148
|
+
['>', 'Cell1', '|', 'Cell2 ', '|', 'Cell3', '<'],
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
:param iter row: List of cells for one row.
|
|
152
|
+
:param str left: Left border.
|
|
153
|
+
:param str center: Column separator.
|
|
154
|
+
:param str right: Right border.
|
|
155
|
+
|
|
156
|
+
:return: Yields other generators that yield strings.
|
|
157
|
+
:rtype: iter
|
|
158
|
+
"""
|
|
159
|
+
if not row or not row[0]:
|
|
160
|
+
yield combine((), left, center, right)
|
|
161
|
+
return
|
|
162
|
+
for row_index in range(len(row[0])):
|
|
163
|
+
yield combine((c[row_index] for c in row), left, center, right)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def flatten(table):
|
|
167
|
+
"""Flatten table data into a single string with newlines.
|
|
168
|
+
|
|
169
|
+
:param iter table: Padded and bordered table data.
|
|
170
|
+
|
|
171
|
+
:return: Joined rows/cells.
|
|
172
|
+
:rtype: str
|
|
173
|
+
"""
|
|
174
|
+
return "\n".join("".join(r) for r in table)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""GithubFlavoredMarkdownTable class."""
|
|
2
|
+
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
from terminaltables3.ascii_table import AsciiTable
|
|
6
|
+
from terminaltables3.build import combine
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GithubFlavoredMarkdownTable(AsciiTable):
|
|
10
|
+
"""Github flavored markdown table.
|
|
11
|
+
|
|
12
|
+
https://help.github.com/articles/github-flavored-markdown/#tables
|
|
13
|
+
|
|
14
|
+
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
|
|
15
|
+
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, table_data: Sequence[Sequence[str]]):
|
|
19
|
+
"""Constructor.
|
|
20
|
+
|
|
21
|
+
:param iter table_data: List (empty or list of lists of strings) representing the table.
|
|
22
|
+
"""
|
|
23
|
+
# Github flavored markdown table won't support title.
|
|
24
|
+
super().__init__(table_data)
|
|
25
|
+
|
|
26
|
+
def horizontal_border(self, _, outer_widths):
|
|
27
|
+
"""Handle the GitHub heading border.
|
|
28
|
+
|
|
29
|
+
E.g.:
|
|
30
|
+
|:---|:---:|---:|----|
|
|
31
|
+
|
|
32
|
+
:param _: Unused.
|
|
33
|
+
:param iter outer_widths: List of widths (with padding) for each column.
|
|
34
|
+
|
|
35
|
+
:return: Prepared border strings in a generator.
|
|
36
|
+
:rtype: iter
|
|
37
|
+
"""
|
|
38
|
+
horizontal = str(self.CHAR_INNER_HORIZONTAL)
|
|
39
|
+
left = self.CHAR_OUTER_LEFT_VERTICAL
|
|
40
|
+
intersect = self.CHAR_INNER_VERTICAL
|
|
41
|
+
right = self.CHAR_OUTER_RIGHT_VERTICAL
|
|
42
|
+
|
|
43
|
+
columns = []
|
|
44
|
+
for i, width in enumerate(outer_widths):
|
|
45
|
+
justify = self.justify_columns.get(i)
|
|
46
|
+
width = max(
|
|
47
|
+
3, width
|
|
48
|
+
) # Width should be at least 3 so justification can be applied.
|
|
49
|
+
if justify == "left":
|
|
50
|
+
columns.append(":" + horizontal * (width - 1))
|
|
51
|
+
elif justify == "right":
|
|
52
|
+
columns.append(horizontal * (width - 1) + ":")
|
|
53
|
+
elif justify == "center":
|
|
54
|
+
columns.append(":" + horizontal * (width - 2) + ":")
|
|
55
|
+
else:
|
|
56
|
+
columns.append(horizontal * width)
|
|
57
|
+
|
|
58
|
+
return combine(columns, left, intersect, right)
|
|
59
|
+
|
|
60
|
+
def gen_table(self, inner_widths, inner_heights, outer_widths):
|
|
61
|
+
"""Combine everything and yield every line of the entire table with borders.
|
|
62
|
+
|
|
63
|
+
:param iter inner_widths: List of widths (no padding) for each column.
|
|
64
|
+
:param iter inner_heights: List of heights (no padding) for each row.
|
|
65
|
+
:param iter outer_widths: List of widths (with padding) for each column.
|
|
66
|
+
:return:
|
|
67
|
+
"""
|
|
68
|
+
for i, row in enumerate(self.table_data):
|
|
69
|
+
# Yield the row line by line (e.g. multi-line rows).
|
|
70
|
+
yield from self.gen_row_lines(row, "row", inner_widths, inner_heights[i])
|
|
71
|
+
# Yield heading separator.
|
|
72
|
+
if i == 0:
|
|
73
|
+
yield self.horizontal_border(None, outer_widths)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Additional simple tables defined here."""
|
|
2
|
+
|
|
3
|
+
from terminaltables3.ascii_table import AsciiTable
|
|
4
|
+
from terminaltables3.terminal_io import IS_WINDOWS
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UnixTable(AsciiTable):
|
|
8
|
+
"""Draw a table using box-drawing characters on Unix platforms. Table borders won't have any gaps between lines.
|
|
9
|
+
|
|
10
|
+
Similar to the tables shown on PC BIOS boot messages, but not double-lined.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
CHAR_F_INNER_HORIZONTAL = "\033(0\x71\033(B"
|
|
14
|
+
CHAR_F_INNER_INTERSECT = "\033(0\x6e\033(B"
|
|
15
|
+
CHAR_F_INNER_VERTICAL = "\033(0\x78\033(B"
|
|
16
|
+
CHAR_F_OUTER_LEFT_INTERSECT = "\033(0\x74\033(B"
|
|
17
|
+
CHAR_F_OUTER_LEFT_VERTICAL = "\033(0\x78\033(B"
|
|
18
|
+
CHAR_F_OUTER_RIGHT_INTERSECT = "\033(0\x75\033(B"
|
|
19
|
+
CHAR_F_OUTER_RIGHT_VERTICAL = "\033(0\x78\033(B"
|
|
20
|
+
CHAR_H_INNER_HORIZONTAL = "\033(0\x71\033(B"
|
|
21
|
+
CHAR_H_INNER_INTERSECT = "\033(0\x6e\033(B"
|
|
22
|
+
CHAR_H_INNER_VERTICAL = "\033(0\x78\033(B"
|
|
23
|
+
CHAR_H_OUTER_LEFT_INTERSECT = "\033(0\x74\033(B"
|
|
24
|
+
CHAR_H_OUTER_LEFT_VERTICAL = "\033(0\x78\033(B"
|
|
25
|
+
CHAR_H_OUTER_RIGHT_INTERSECT = "\033(0\x75\033(B"
|
|
26
|
+
CHAR_H_OUTER_RIGHT_VERTICAL = "\033(0\x78\033(B"
|
|
27
|
+
CHAR_INNER_HORIZONTAL = "\033(0\x71\033(B"
|
|
28
|
+
CHAR_INNER_INTERSECT = "\033(0\x6e\033(B"
|
|
29
|
+
CHAR_INNER_VERTICAL = "\033(0\x78\033(B"
|
|
30
|
+
CHAR_OUTER_BOTTOM_HORIZONTAL = "\033(0\x71\033(B"
|
|
31
|
+
CHAR_OUTER_BOTTOM_INTERSECT = "\033(0\x76\033(B"
|
|
32
|
+
CHAR_OUTER_BOTTOM_LEFT = "\033(0\x6d\033(B"
|
|
33
|
+
CHAR_OUTER_BOTTOM_RIGHT = "\033(0\x6a\033(B"
|
|
34
|
+
CHAR_OUTER_LEFT_INTERSECT = "\033(0\x74\033(B"
|
|
35
|
+
CHAR_OUTER_LEFT_VERTICAL = "\033(0\x78\033(B"
|
|
36
|
+
CHAR_OUTER_RIGHT_INTERSECT = "\033(0\x75\033(B"
|
|
37
|
+
CHAR_OUTER_RIGHT_VERTICAL = "\033(0\x78\033(B"
|
|
38
|
+
CHAR_OUTER_TOP_HORIZONTAL = "\033(0\x71\033(B"
|
|
39
|
+
CHAR_OUTER_TOP_INTERSECT = "\033(0\x77\033(B"
|
|
40
|
+
CHAR_OUTER_TOP_LEFT = "\033(0\x6c\033(B"
|
|
41
|
+
CHAR_OUTER_TOP_RIGHT = "\033(0\x6b\033(B"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def table(self):
|
|
45
|
+
"""Return a large string of the entire table ready to be printed to the terminal."""
|
|
46
|
+
ascii_table = super().table
|
|
47
|
+
optimized = ascii_table.replace("\033(B\033(0", "")
|
|
48
|
+
return optimized
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class WindowsTable(AsciiTable):
|
|
52
|
+
"""Draw a table using box-drawing characters on Windows platforms. This uses Code Page 437. Single-line borders.
|
|
53
|
+
|
|
54
|
+
From: http://en.wikipedia.org/wiki/Code_page_437#Characters
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
CHAR_F_INNER_HORIZONTAL = b"\xc4".decode("ibm437")
|
|
58
|
+
CHAR_F_INNER_INTERSECT = b"\xc5".decode("ibm437")
|
|
59
|
+
CHAR_F_INNER_VERTICAL = b"\xb3".decode("ibm437")
|
|
60
|
+
CHAR_F_OUTER_LEFT_INTERSECT = b"\xc3".decode("ibm437")
|
|
61
|
+
CHAR_F_OUTER_LEFT_VERTICAL = b"\xb3".decode("ibm437")
|
|
62
|
+
CHAR_F_OUTER_RIGHT_INTERSECT = b"\xb4".decode("ibm437")
|
|
63
|
+
CHAR_F_OUTER_RIGHT_VERTICAL = b"\xb3".decode("ibm437")
|
|
64
|
+
CHAR_H_INNER_HORIZONTAL = b"\xc4".decode("ibm437")
|
|
65
|
+
CHAR_H_INNER_INTERSECT = b"\xc5".decode("ibm437")
|
|
66
|
+
CHAR_H_INNER_VERTICAL = b"\xb3".decode("ibm437")
|
|
67
|
+
CHAR_H_OUTER_LEFT_INTERSECT = b"\xc3".decode("ibm437")
|
|
68
|
+
CHAR_H_OUTER_LEFT_VERTICAL = b"\xb3".decode("ibm437")
|
|
69
|
+
CHAR_H_OUTER_RIGHT_INTERSECT = b"\xb4".decode("ibm437")
|
|
70
|
+
CHAR_H_OUTER_RIGHT_VERTICAL = b"\xb3".decode("ibm437")
|
|
71
|
+
CHAR_INNER_HORIZONTAL = b"\xc4".decode("ibm437")
|
|
72
|
+
CHAR_INNER_INTERSECT = b"\xc5".decode("ibm437")
|
|
73
|
+
CHAR_INNER_VERTICAL = b"\xb3".decode("ibm437")
|
|
74
|
+
CHAR_OUTER_BOTTOM_HORIZONTAL = b"\xc4".decode("ibm437")
|
|
75
|
+
CHAR_OUTER_BOTTOM_INTERSECT = b"\xc1".decode("ibm437")
|
|
76
|
+
CHAR_OUTER_BOTTOM_LEFT = b"\xc0".decode("ibm437")
|
|
77
|
+
CHAR_OUTER_BOTTOM_RIGHT = b"\xd9".decode("ibm437")
|
|
78
|
+
CHAR_OUTER_LEFT_INTERSECT = b"\xc3".decode("ibm437")
|
|
79
|
+
CHAR_OUTER_LEFT_VERTICAL = b"\xb3".decode("ibm437")
|
|
80
|
+
CHAR_OUTER_RIGHT_INTERSECT = b"\xb4".decode("ibm437")
|
|
81
|
+
CHAR_OUTER_RIGHT_VERTICAL = b"\xb3".decode("ibm437")
|
|
82
|
+
CHAR_OUTER_TOP_HORIZONTAL = b"\xc4".decode("ibm437")
|
|
83
|
+
CHAR_OUTER_TOP_INTERSECT = b"\xc2".decode("ibm437")
|
|
84
|
+
CHAR_OUTER_TOP_LEFT = b"\xda".decode("ibm437")
|
|
85
|
+
CHAR_OUTER_TOP_RIGHT = b"\xbf".decode("ibm437")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class WindowsTableDouble(AsciiTable):
|
|
89
|
+
"""Draw a table using box-drawing characters on Windows platforms. This uses Code Page 437. Double-line borders."""
|
|
90
|
+
|
|
91
|
+
CHAR_F_INNER_HORIZONTAL = b"\xcd".decode("ibm437")
|
|
92
|
+
CHAR_F_INNER_INTERSECT = b"\xce".decode("ibm437")
|
|
93
|
+
CHAR_F_INNER_VERTICAL = b"\xba".decode("ibm437")
|
|
94
|
+
CHAR_F_OUTER_LEFT_INTERSECT = b"\xcc".decode("ibm437")
|
|
95
|
+
CHAR_F_OUTER_LEFT_VERTICAL = b"\xba".decode("ibm437")
|
|
96
|
+
CHAR_F_OUTER_RIGHT_INTERSECT = b"\xb9".decode("ibm437")
|
|
97
|
+
CHAR_F_OUTER_RIGHT_VERTICAL = b"\xba".decode("ibm437")
|
|
98
|
+
CHAR_H_INNER_HORIZONTAL = b"\xcd".decode("ibm437")
|
|
99
|
+
CHAR_H_INNER_INTERSECT = b"\xce".decode("ibm437")
|
|
100
|
+
CHAR_H_INNER_VERTICAL = b"\xba".decode("ibm437")
|
|
101
|
+
CHAR_H_OUTER_LEFT_INTERSECT = b"\xcc".decode("ibm437")
|
|
102
|
+
CHAR_H_OUTER_LEFT_VERTICAL = b"\xba".decode("ibm437")
|
|
103
|
+
CHAR_H_OUTER_RIGHT_INTERSECT = b"\xb9".decode("ibm437")
|
|
104
|
+
CHAR_H_OUTER_RIGHT_VERTICAL = b"\xba".decode("ibm437")
|
|
105
|
+
CHAR_INNER_HORIZONTAL = b"\xcd".decode("ibm437")
|
|
106
|
+
CHAR_INNER_INTERSECT = b"\xce".decode("ibm437")
|
|
107
|
+
CHAR_INNER_VERTICAL = b"\xba".decode("ibm437")
|
|
108
|
+
CHAR_OUTER_BOTTOM_HORIZONTAL = b"\xcd".decode("ibm437")
|
|
109
|
+
CHAR_OUTER_BOTTOM_INTERSECT = b"\xca".decode("ibm437")
|
|
110
|
+
CHAR_OUTER_BOTTOM_LEFT = b"\xc8".decode("ibm437")
|
|
111
|
+
CHAR_OUTER_BOTTOM_RIGHT = b"\xbc".decode("ibm437")
|
|
112
|
+
CHAR_OUTER_LEFT_INTERSECT = b"\xcc".decode("ibm437")
|
|
113
|
+
CHAR_OUTER_LEFT_VERTICAL = b"\xba".decode("ibm437")
|
|
114
|
+
CHAR_OUTER_RIGHT_INTERSECT = b"\xb9".decode("ibm437")
|
|
115
|
+
CHAR_OUTER_RIGHT_VERTICAL = b"\xba".decode("ibm437")
|
|
116
|
+
CHAR_OUTER_TOP_HORIZONTAL = b"\xcd".decode("ibm437")
|
|
117
|
+
CHAR_OUTER_TOP_INTERSECT = b"\xcb".decode("ibm437")
|
|
118
|
+
CHAR_OUTER_TOP_LEFT = b"\xc9".decode("ibm437")
|
|
119
|
+
CHAR_OUTER_TOP_RIGHT = b"\xbb".decode("ibm437")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SingleTable(WindowsTable if IS_WINDOWS else UnixTable):
|
|
123
|
+
"""Cross-platform table with single-line box-drawing characters.
|
|
124
|
+
|
|
125
|
+
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
|
|
126
|
+
:ivar str title: Optional title to show within the top border of the table.
|
|
127
|
+
:ivar bool inner_column_border: Separates columns.
|
|
128
|
+
:ivar bool inner_footing_row_border: Show a border before the last row.
|
|
129
|
+
:ivar bool inner_heading_row_border: Show a border after the first row.
|
|
130
|
+
:ivar bool inner_row_border: Show a border in between every row.
|
|
131
|
+
:ivar bool outer_border: Show the top, left, right, and bottom border.
|
|
132
|
+
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
|
|
133
|
+
:ivar int padding_left: Number of spaces to pad on the left side of every cell.
|
|
134
|
+
:ivar int padding_right: Number of spaces to pad on the right side of every cell.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class DoubleTable(WindowsTableDouble):
|
|
139
|
+
"""Cross-platform table with box-drawing characters. On Windows it's double borders, on Linux/OSX it's unicode.
|
|
140
|
+
|
|
141
|
+
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
|
|
142
|
+
:ivar str title: Optional title to show within the top border of the table.
|
|
143
|
+
:ivar bool inner_column_border: Separates columns.
|
|
144
|
+
:ivar bool inner_footing_row_border: Show a border before the last row.
|
|
145
|
+
:ivar bool inner_heading_row_border: Show a border after the first row.
|
|
146
|
+
:ivar bool inner_row_border: Show a border in between every row.
|
|
147
|
+
:ivar bool outer_border: Show the top, left, right, and bottom border.
|
|
148
|
+
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
|
|
149
|
+
:ivar int padding_left: Number of spaces to pad on the left side of every cell.
|
|
150
|
+
:ivar int padding_right: Number of spaces to pad on the right side of every cell.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class PorcelainTable(AsciiTable):
|
|
155
|
+
"""An AsciiTable stripped to a minimum.
|
|
156
|
+
|
|
157
|
+
Meant to be machine passable and roughly follow format set by git --porcelain option (hence the name).
|
|
158
|
+
|
|
159
|
+
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(self, table_data):
|
|
163
|
+
"""Constructor.
|
|
164
|
+
|
|
165
|
+
:param iter table_data: List (empty or list of lists of strings) representing the table.
|
|
166
|
+
"""
|
|
167
|
+
# Porcelain table won't support title since it has no outer birders.
|
|
168
|
+
super().__init__(table_data)
|
|
169
|
+
|
|
170
|
+
# Removes outer border, and inner footing and header row borders.
|
|
171
|
+
self.inner_footing_row_border = False
|
|
172
|
+
self.inner_heading_row_border = False
|
|
173
|
+
self.outer_border = False
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Get info about the current terminal window/screen buffer."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import struct
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Tuple, Union
|
|
7
|
+
|
|
8
|
+
DEFAULT_HEIGHT = 24
|
|
9
|
+
DEFAULT_WIDTH = 79
|
|
10
|
+
INVALID_HANDLE_VALUE = -1
|
|
11
|
+
IS_WINDOWS = sys.platform == "win32"
|
|
12
|
+
STD_ERROR_HANDLE = -12
|
|
13
|
+
STD_OUTPUT_HANDLE = -11
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_console_info(kernel32, handle: int) -> Tuple[int, int]:
|
|
17
|
+
"""Get information about this current console window (Windows only).
|
|
18
|
+
|
|
19
|
+
https://github.com/Robpol86/colorclass/blob/ab42da59/colorclass/windows.py#L111
|
|
20
|
+
|
|
21
|
+
:raise OSError: When handle is invalid or GetConsoleScreenBufferInfo API call fails.
|
|
22
|
+
|
|
23
|
+
:param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance.
|
|
24
|
+
:param int handle: stderr or stdout handle.
|
|
25
|
+
|
|
26
|
+
:return: Width (number of characters) and height (number of lines) of the terminal.
|
|
27
|
+
:rtype: tuple
|
|
28
|
+
"""
|
|
29
|
+
if handle == INVALID_HANDLE_VALUE:
|
|
30
|
+
raise OSError("Invalid handle.")
|
|
31
|
+
|
|
32
|
+
# Query Win32 API.
|
|
33
|
+
lpcsbi = ctypes.create_string_buffer(22) # Populated by GetConsoleScreenBufferInfo.
|
|
34
|
+
if not kernel32.GetConsoleScreenBufferInfo(handle, lpcsbi):
|
|
35
|
+
raise ctypes.WinError() # Subclass of OSError.
|
|
36
|
+
|
|
37
|
+
# Parse data.
|
|
38
|
+
left, top, right, bottom = struct.unpack("hhhhHhhhhhh", lpcsbi.raw)[5:-2]
|
|
39
|
+
width, height = right - left, bottom - top
|
|
40
|
+
return width, height
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def terminal_size(kernel32=None) -> Tuple[int, int]:
|
|
44
|
+
"""Get the width and height of the terminal.
|
|
45
|
+
|
|
46
|
+
http://code.activestate.com/recipes/440694-determine-size-of-console-window-on-windows/
|
|
47
|
+
http://stackoverflow.com/questions/17993814/why-the-irrelevant-code-made-a-difference
|
|
48
|
+
|
|
49
|
+
:param kernel32: Optional mock kernel32 object. For testing.
|
|
50
|
+
|
|
51
|
+
:return: Width (number of characters) and height (number of lines) of the terminal.
|
|
52
|
+
:rtype: tuple
|
|
53
|
+
"""
|
|
54
|
+
if IS_WINDOWS:
|
|
55
|
+
kernel32 = kernel32 or ctypes.windll.kernel32
|
|
56
|
+
try:
|
|
57
|
+
return get_console_info(kernel32, kernel32.GetStdHandle(STD_ERROR_HANDLE))
|
|
58
|
+
except OSError:
|
|
59
|
+
try:
|
|
60
|
+
return get_console_info(
|
|
61
|
+
kernel32, kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
|
|
62
|
+
)
|
|
63
|
+
except OSError:
|
|
64
|
+
return DEFAULT_WIDTH, DEFAULT_HEIGHT
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
device = __import__("fcntl").ioctl(
|
|
68
|
+
0, __import__("termios").TIOCGWINSZ, "\0\0\0\0\0\0\0\0"
|
|
69
|
+
)
|
|
70
|
+
except OSError:
|
|
71
|
+
return DEFAULT_WIDTH, DEFAULT_HEIGHT
|
|
72
|
+
height, width = struct.unpack("hhhh", device)[:2]
|
|
73
|
+
return width, height
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def set_terminal_title(title: Union[str, bytes], kernel32=None) -> bool:
|
|
77
|
+
"""Set the terminal title.
|
|
78
|
+
|
|
79
|
+
:param title: The title to set (string, unicode, bytes accepted).
|
|
80
|
+
:param kernel32: Optional mock kernel32 object. For testing.
|
|
81
|
+
|
|
82
|
+
:return: If title changed successfully (Windows only, always True on Linux/OSX).
|
|
83
|
+
:rtype: bool
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
title_bytes = title.encode("utf-8")
|
|
87
|
+
except AttributeError:
|
|
88
|
+
title_bytes = title
|
|
89
|
+
|
|
90
|
+
if IS_WINDOWS:
|
|
91
|
+
kernel32 = kernel32 or ctypes.windll.kernel32
|
|
92
|
+
try:
|
|
93
|
+
is_ascii = all(ord(c) < 128 for c in title) # str/unicode.
|
|
94
|
+
except TypeError:
|
|
95
|
+
is_ascii = all(c < 128 for c in title) # bytes.
|
|
96
|
+
if is_ascii:
|
|
97
|
+
return kernel32.SetConsoleTitleA(title_bytes) != 0
|
|
98
|
+
return kernel32.SetConsoleTitleW(title) != 0
|
|
99
|
+
|
|
100
|
+
# Linux/OSX.
|
|
101
|
+
sys.stdout.write(b"\033]0;" + title_bytes + b"\007")
|
|
102
|
+
return True
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Functions that handle alignment, padding, widths, etc."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import unicodedata
|
|
5
|
+
from typing import Sequence, Tuple
|
|
6
|
+
|
|
7
|
+
from terminaltables3.terminal_io import terminal_size
|
|
8
|
+
|
|
9
|
+
RE_COLOR_ANSI = re.compile(r"(\033\[[\d;]+m)")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def visible_width(string: str) -> int:
|
|
13
|
+
"""Get the visible width of a unicode string.
|
|
14
|
+
|
|
15
|
+
Some CJK unicode characters are more than one byte unlike ASCII and latin unicode characters.
|
|
16
|
+
|
|
17
|
+
From: https://github.com/Robpol86/terminaltables3/pull/9
|
|
18
|
+
|
|
19
|
+
:param str string: String to measure.
|
|
20
|
+
|
|
21
|
+
:return: String's width.
|
|
22
|
+
:rtype: int
|
|
23
|
+
"""
|
|
24
|
+
if "\033" in string:
|
|
25
|
+
string = RE_COLOR_ANSI.sub("", string)
|
|
26
|
+
|
|
27
|
+
# Convert to unicode.
|
|
28
|
+
try:
|
|
29
|
+
string = string.decode("u8")
|
|
30
|
+
except (AttributeError, UnicodeEncodeError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
width = 0
|
|
34
|
+
for char in string:
|
|
35
|
+
if unicodedata.east_asian_width(char) in ("F", "W"):
|
|
36
|
+
width += 2
|
|
37
|
+
else:
|
|
38
|
+
width += 1
|
|
39
|
+
|
|
40
|
+
return width
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def align_and_pad_cell(
|
|
44
|
+
string: str,
|
|
45
|
+
align: Tuple,
|
|
46
|
+
inner_dimensions: Tuple,
|
|
47
|
+
padding: Sequence[int],
|
|
48
|
+
space: str = " ",
|
|
49
|
+
) -> list[str]:
|
|
50
|
+
"""Align a string horizontally and vertically. Also add additional padding in both dimensions.
|
|
51
|
+
|
|
52
|
+
:param str string: Input string to operate on.
|
|
53
|
+
:param tuple align: Tuple that contains one of left/center/right and/or top/middle/bottom.
|
|
54
|
+
:param tuple inner_dimensions: Width and height ints to expand string to without padding.
|
|
55
|
+
:param iter padding: Number of space chars for left, right, top, and bottom (4 ints).
|
|
56
|
+
:param str space: Character to use as white space for resizing/padding (use single visible chars only).
|
|
57
|
+
|
|
58
|
+
:return: Padded cell split into lines.
|
|
59
|
+
:rtype: list
|
|
60
|
+
"""
|
|
61
|
+
if not hasattr(string, "splitlines"):
|
|
62
|
+
string = str(string)
|
|
63
|
+
|
|
64
|
+
# Handle trailing newlines or empty strings, str.splitlines() does not satisfy.
|
|
65
|
+
lines = string.splitlines() or [""]
|
|
66
|
+
if string.endswith("\n"):
|
|
67
|
+
lines.append("")
|
|
68
|
+
|
|
69
|
+
# Vertically align and pad.
|
|
70
|
+
if "bottom" in align:
|
|
71
|
+
lines = (
|
|
72
|
+
([""] * (inner_dimensions[1] - len(lines) + padding[2]))
|
|
73
|
+
+ lines
|
|
74
|
+
+ ([""] * padding[3])
|
|
75
|
+
)
|
|
76
|
+
elif "middle" in align:
|
|
77
|
+
delta = inner_dimensions[1] - len(lines)
|
|
78
|
+
lines = (
|
|
79
|
+
([""] * (delta // 2 + delta % 2 + padding[2]))
|
|
80
|
+
+ lines
|
|
81
|
+
+ ([""] * (delta // 2 + padding[3]))
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
lines = (
|
|
85
|
+
([""] * padding[2])
|
|
86
|
+
+ lines
|
|
87
|
+
+ ([""] * (inner_dimensions[1] - len(lines) + padding[3]))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Horizontally align and pad.
|
|
91
|
+
for i, line in enumerate(lines):
|
|
92
|
+
new_width = inner_dimensions[0] + len(line) - visible_width(line)
|
|
93
|
+
if "right" in align:
|
|
94
|
+
lines[i] = line.rjust(padding[0] + new_width, space) + (space * padding[1])
|
|
95
|
+
elif "center" in align:
|
|
96
|
+
lines[i] = (
|
|
97
|
+
(space * padding[0])
|
|
98
|
+
+ line.center(new_width, space)
|
|
99
|
+
+ (space * padding[1])
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
lines[i] = (space * padding[0]) + line.ljust(new_width + padding[1], space)
|
|
103
|
+
|
|
104
|
+
return lines
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def max_dimensions(
|
|
108
|
+
table_data, padding_left=0, padding_right=0, padding_top=0, padding_bottom=0
|
|
109
|
+
):
|
|
110
|
+
"""Get maximum widths of each column and maximum height of each row.
|
|
111
|
+
|
|
112
|
+
:param iter table_data: List of list of strings (unmodified table data).
|
|
113
|
+
:param int padding_left: Number of space chars on left side of cell.
|
|
114
|
+
:param int padding_right: Number of space chars on right side of cell.
|
|
115
|
+
:param int padding_top: Number of empty lines on top side of cell.
|
|
116
|
+
:param int padding_bottom: Number of empty lines on bottom side of cell.
|
|
117
|
+
|
|
118
|
+
:return: 4-item tuple of n-item lists. Inner column widths and row heights, outer column widths and row heights.
|
|
119
|
+
:rtype: tuple
|
|
120
|
+
"""
|
|
121
|
+
inner_widths = [0] * (max(len(r) for r in table_data) if table_data else 0)
|
|
122
|
+
inner_heights = [0] * len(table_data)
|
|
123
|
+
|
|
124
|
+
# Find max width and heights.
|
|
125
|
+
for j, row in enumerate(table_data):
|
|
126
|
+
for i, cell in enumerate(row):
|
|
127
|
+
if not hasattr(cell, "count") or not hasattr(cell, "splitlines"):
|
|
128
|
+
cell = str(cell)
|
|
129
|
+
if not cell:
|
|
130
|
+
continue
|
|
131
|
+
inner_heights[j] = max(inner_heights[j], cell.count("\n") + 1)
|
|
132
|
+
inner_widths[i] = max(
|
|
133
|
+
inner_widths[i],
|
|
134
|
+
*[visible_width(the_line) for the_line in cell.splitlines()]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Calculate with padding.
|
|
138
|
+
outer_widths = [padding_left + i + padding_right for i in inner_widths]
|
|
139
|
+
outer_heights = [padding_top + i + padding_bottom for i in inner_heights]
|
|
140
|
+
|
|
141
|
+
return inner_widths, inner_heights, outer_widths, outer_heights
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def column_max_width(
|
|
145
|
+
inner_widths: Sequence[int],
|
|
146
|
+
column_number: int,
|
|
147
|
+
outer_border: int,
|
|
148
|
+
inner_border: int,
|
|
149
|
+
padding: int,
|
|
150
|
+
) -> int:
|
|
151
|
+
"""Determine the maximum width of a column based on the current terminal width.
|
|
152
|
+
|
|
153
|
+
:param iter inner_widths: List of widths (no padding) for each column.
|
|
154
|
+
:param int column_number: The column number to query.
|
|
155
|
+
:param int outer_border: Sum of left and right outer border visible widths.
|
|
156
|
+
:param int inner_border: Visible width of the inner border character.
|
|
157
|
+
:param int padding: Total padding per cell (left + right padding).
|
|
158
|
+
|
|
159
|
+
:return: The maximum width the column can be without causing line wrapping.
|
|
160
|
+
"""
|
|
161
|
+
column_count = len(inner_widths)
|
|
162
|
+
terminal_width = terminal_size()[0]
|
|
163
|
+
|
|
164
|
+
# Count how much space padding, outer, and inner borders take up.
|
|
165
|
+
non_data_space = outer_border
|
|
166
|
+
non_data_space += inner_border * (column_count - 1)
|
|
167
|
+
non_data_space += column_count * padding
|
|
168
|
+
|
|
169
|
+
# Exclude selected column's width.
|
|
170
|
+
data_space = sum(inner_widths) - inner_widths[column_number]
|
|
171
|
+
|
|
172
|
+
return terminal_width - data_space - non_data_space
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def table_width(
|
|
176
|
+
outer_widths: Sequence[int], outer_border: int, inner_border: int
|
|
177
|
+
) -> int:
|
|
178
|
+
"""Determine the width of the entire table including borders and padding.
|
|
179
|
+
|
|
180
|
+
:param iter outer_widths: List of widths (with padding) for each column.
|
|
181
|
+
:param int outer_border: Sum of left and right outer border visible widths.
|
|
182
|
+
:param int inner_border: Visible width of the inner border character.
|
|
183
|
+
|
|
184
|
+
:return: The width of the table.
|
|
185
|
+
:rtype: int
|
|
186
|
+
"""
|
|
187
|
+
column_count = len(outer_widths)
|
|
188
|
+
|
|
189
|
+
# Count how much space outer and inner borders take up.
|
|
190
|
+
non_data_space = outer_border
|
|
191
|
+
if column_count:
|
|
192
|
+
non_data_space += inner_border * (column_count - 1)
|
|
193
|
+
|
|
194
|
+
# Space of all columns and their padding.
|
|
195
|
+
data_space = sum(outer_widths)
|
|
196
|
+
return data_space + non_data_space
|