├── .coveragerc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── PKG-INFO ├── README.md ├── setup.py ├── tests.py ├── texttable.py ├── texttable.pyi └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | 3 | [report] 4 | # Regexes for lines to exclude from consideration 5 | exclude_lines = 6 | # Have to re-enable the standard pragma: 7 | pragma: no cover 8 | 9 | # Don't complain if non-runnable code isn't run: 10 | if __name__ == .__main__.: 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache/ 2 | /.coverage 3 | /.tox/ 4 | /MANIFEST 5 | /__pycache__/ 6 | *.egg-info/ 7 | *.pyc 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version History 2 | 3 | v1.7.0 (2023-10-03) 4 | * Add boolean formatting option (https://github.com/foutaise/texttable/pull/89) 5 | 6 | v1.6.7 (2022-11-23) 7 | * Get rid of stub file in wheel package (https://github.com/foutaise/texttable/issues/84) 8 | 9 | v1.6.6 (2022-11-22) 10 | * Fix regression introduced in 1.6.5 release (https://github.com/foutaise/texttable/issues/83) 11 | 12 | v1.6.5 (2022-11-20) 13 | * Modify setup.py to include missing stub file in wheel package (https://github.com/foutaise/texttable/issues/82) 14 | 15 | v1.6.4 (2021-07-13) 16 | * Fix alignment bug when deco is modified (https://github.com/foutaise/texttable/issues/76) 17 | 18 | v1.6.3 (2020-09-06) 19 | * Improve int conversion (https://github.com/foutaise/texttable/issues/70) 20 | 21 | v1.6.2 (2019-07-01) 22 | * Fix auto-formatting NaN (https://github.com/foutaise/texttable/pull/60) 23 | 24 | v1.6.1 (2019-02-15) 25 | * Include tests, license in source tarball (https://github.com/foutaise/texttable/issues/58) 26 | * Add changelog 27 | 28 | v1.6.0 (2019-01-17) 29 | * Add basic emoji support (https://github.com/foutaise/texttable/issues/55) 30 | 31 | v1.5.0 (2018-11-02) 32 | * Create a method for redefining the max_width (https://github.com/foutaise/texttable/issues/54) 33 | * Use setuptools instead of distutils to upload metadata to PyPI (https://github.com/foutaise/texttable/issues/49) 34 | * Switch to MIT license 35 | 36 | v1.4.0 (2018-06-22) 37 | * Add set_header_align() method (https://github.com/foutaise/texttable/issues/45) 38 | 39 | v1.3.1 (2018-06-12) 40 | * Fix missing textwrapper command when cjkwrap is not used (https://github.com/foutaise/texttable/issues/43) 41 | 42 | v1.3.0 (2018-06-11) 43 | * Remove redundant code for unsupported/EOL Python (https://github.com/foutaise/texttable/pull/31) 44 | 45 | v1.2.1 (2018-01-03) 46 | * Use test_cjkwrap only when cjkwrap is available (https://github.com/foutaise/texttable/issues/35) 47 | 48 | v1.2.0 (2018-01-03) 49 | * Use cjkwrap for better CJK text support (https://github.com/foutaise/texttable/issues/34) 50 | 51 | v1.1.1 (2017-10-26) 52 | * Fallback to text on TypeError (https://github.com/foutaise/texttable/issues/28) 53 | 54 | v1.1.0 (2017-10-22) 55 | * Easier formatting, allow callable as a column datatype (PR https://github.com/foutaise/texttable/pull/27) 56 | 57 | v1.0.0 (2017-10-14) 58 | * Fix bug in wide chars handling (https://github.com/foutaise/texttable/issues/9) 59 | * Avoid use of sys.version to obtain Python version (https://github.com/foutaise/texttable/pull/24) 60 | 61 | v0.9.1 (2017-06-27) 62 | * Add support for combining characters (https://github.com/foutaise/texttable/pull/19) 63 | 64 | v0.9.0 (2017-05-16) 65 | * Fix width of table exceeds max_width parameter (https://github.com/foutaise/texttable/pull/15) 66 | 67 | v0.8.8 (2017-03-30) 68 | * Add east asian support (https://github.com/foutaise/texttable/pull/12) 69 | * Relative col widths improvements + unit tests (https://github.com/foutaise/texttable/pull/13) 70 | 71 | v0.8.7 (2016-11-14) 72 | * Proper handling of unicode in headers (https://github.com/foutaise/texttable/issues/9) 73 | 74 | v0.8.6 (2016-10-21) 75 | * Preserve empty lines (https://github.com/foutaise/texttable/pull/8) 76 | 77 | v0.8.5 (2016-10-16) 78 | * Better handling of unicode encodings (https://github.com/foutaise/texttable/pull/6) 79 | 80 | v0.8.4 (2015-11-16) 81 | * Fix pypi url 82 | 83 | v0.8.3 (2015-11-16) 84 | * Update README.md 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Gerome Fournier 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. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.py 3 | include *.pyi 4 | include LICENSE 5 | include tox.ini 6 | include .coveragerc 7 | 8 | global-exclude *.pyc 9 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: texttable 3 | Version: 1.7.0 4 | Summary: module to create simple ASCII tables 5 | Home-page: https://github.com/foutaise/texttable/ 6 | Author: Gerome Fournier 7 | Author-email: jef@foutaise.org 8 | License: MIT 9 | Download-URL: https://github.com/foutaise/texttable/archive/v1.7.0.tar.gz 10 | Description: texttable is a module to generate a formatted text table, using ASCII 11 | characters. 12 | Platform: any 13 | Classifier: Development Status :: 5 - Production/Stable 14 | Classifier: Environment :: Console 15 | Classifier: Intended Audience :: Developers 16 | Classifier: Intended Audience :: End Users/Desktop 17 | Classifier: License :: OSI Approved :: MIT License 18 | Classifier: Operating System :: Microsoft :: Windows 19 | Classifier: Operating System :: POSIX 20 | Classifier: Operating System :: MacOS 21 | Classifier: Programming Language :: Python :: 2 22 | Classifier: Programming Language :: Python :: 2.7 23 | Classifier: Programming Language :: Python :: 3 24 | Classifier: Programming Language :: Python :: 3.4 25 | Classifier: Programming Language :: Python :: 3.5 26 | Classifier: Programming Language :: Python :: 3.6 27 | Classifier: Programming Language :: Python :: 3.7 28 | Classifier: Programming Language :: Python :: 3.8 29 | Classifier: Programming Language :: Python :: 3.9 30 | Classifier: Topic :: Software Development :: Libraries :: Python Modules 31 | Classifier: Topic :: Text Processing 32 | Classifier: Topic :: Utilities 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # texttable 2 | 3 | Python module to create simple ASCII tables 4 | 5 | ## Availability 6 | 7 | This module is available on [PyPI](https://pypi.org/project/texttable/), and has been packaged for several Linux/Unix platforms 8 | ([Debian](https://packages.debian.org/search?&searchon=names&keywords=python-texttable+), 9 | [FreeBSD](https://www.freebsd.org/cgi/ports.cgi?query=texttable&stype=all), Fedora, Suse...). 10 | 11 | ## Dependencies 12 | 13 | If available, [cjkwrap](https://github.com/fgallaire/cjkwrap) library is used instead of textwrap, for a better wrapping of CJK text. 14 | 15 | If available, [wcwidth](https://github.com/jquast/wcwidth) library is used for a better rendering (basic emoji support). 16 | 17 | ## Documentation 18 | 19 | ``` 20 | NAME 21 | texttable - module to create simple ASCII tables 22 | 23 | FILE 24 | /usr/local/lib/python2.7/dist-packages/texttable.py 25 | 26 | DESCRIPTION 27 | 28 | Example: 29 | 30 | table = Texttable() 31 | table.set_cols_align(["l", "r", "c"]) 32 | table.set_cols_valign(["t", "m", "b"]) 33 | table.add_rows([["Name", "Age", "Nickname"], 34 | ["Mr\nXavier\nHuon", 32, "Xav'"], 35 | ["Mr\nBaptiste\nClement", 1, "Baby"], 36 | ["Mme\nLouise\nBourgeau", 28, "Lou\n\nLoue"]]) 37 | print(table.draw()) 38 | print() 39 | 40 | table = Texttable() 41 | table.set_deco(Texttable.HEADER) 42 | table.set_cols_dtype(['t', # text 43 | 'f', # float (decimal) 44 | 'e', # float (exponent) 45 | 'i', # integer 46 | 'a']) # automatic 47 | table.set_cols_align(["l", "r", "r", "r", "l"]) 48 | table.add_rows([["text", "float", "exp", "int", "auto"], 49 | ["abcd", "67", 654, 89, 128.001], 50 | ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], 51 | ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], 52 | ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) 53 | print(table.draw()) 54 | 55 | Result: 56 | 57 | +----------+-----+----------+ 58 | | Name | Age | Nickname | 59 | +==========+=====+==========+ 60 | | Mr | | | 61 | | Xavier | 32 | | 62 | | Huon | | Xav' | 63 | +----------+-----+----------+ 64 | | Mr | | | 65 | | Baptiste | 1 | | 66 | | Clement | | Baby | 67 | +----------+-----+----------+ 68 | | Mme | | Lou | 69 | | Louise | 28 | | 70 | | Bourgeau | | Loue | 71 | +----------+-----+----------+ 72 | 73 | text float exp int auto 74 | ============================================== 75 | abcd 67.000 6.540e+02 89 128.001 76 | efghijk 67.543 6.540e-01 90 1.280e+22 77 | lmn 0.000 5.000e-78 89 0.000 78 | opqrstu 0.023 5.000e+78 92 1.280e+22 79 | 80 | CLASSES 81 | class Texttable 82 | | Methods defined here: 83 | | 84 | | __init__(self, max_width=80) 85 | | Constructor 86 | | 87 | | - max_width is an integer, specifying the maximum width of the table 88 | | - if set to 0, size is unlimited, therefore cells won't be wrapped 89 | | 90 | | add_row(self, array) 91 | | Add a row in the rows stack 92 | | 93 | | - cells can contain newlines and tabs 94 | | 95 | | add_rows(self, rows, header=True) 96 | | Add several rows in the rows stack 97 | | 98 | | - The 'rows' argument can be either an iterator returning arrays, 99 | | or a by-dimensional array 100 | | - 'header' specifies if the first row should be used as the header 101 | | of the table 102 | | 103 | | draw(self) 104 | | Draw the table 105 | | 106 | | - the table is returned as a whole string 107 | | 108 | | header(self, array) 109 | | Specify the header of the table 110 | | 111 | | reset(self) 112 | | Reset the instance 113 | | 114 | | - reset rows and header 115 | | 116 | | set_chars(self, array) 117 | | Set the characters used to draw lines between rows and columns 118 | | 119 | | - the array should contain 4 fields: 120 | | 121 | | [horizontal, vertical, corner, header] 122 | | 123 | | - default is set to: 124 | | 125 | | ['-', '|', '+', '='] 126 | | 127 | | set_cols_align(self, array) 128 | | Set the desired columns alignment 129 | | 130 | | - the elements of the array should be either "l", "c" or "r": 131 | | 132 | | * "l": column flushed left 133 | | * "c": column centered 134 | | * "r": column flushed right 135 | | 136 | | set_cols_dtype(self, array) 137 | | Set the desired columns datatype for the cols. 138 | | 139 | | - the elements of the array should be either a callable or any of 140 | | "a", "t", "f", "e" or "i": 141 | | 142 | | * "a": automatic (try to use the most appropriate datatype) 143 | | * "t": treat as text 144 | | * "f": treat as float in decimal format 145 | | * "e": treat as float in exponential format 146 | | * "i": treat as int 147 | | * "b": treat as boolean 148 | | * a callable: should return formatted string for any value given 149 | | 150 | | - by default, automatic datatyping is used for each column 151 | | 152 | | set_cols_valign(self, array) 153 | | Set the desired columns vertical alignment 154 | | 155 | | - the elements of the array should be either "t", "m" or "b": 156 | | 157 | | * "t": column aligned on the top of the cell 158 | | * "m": column aligned on the middle of the cell 159 | | * "b": column aligned on the bottom of the cell 160 | | 161 | | set_cols_width(self, array) 162 | | Set the desired columns width 163 | | 164 | | - the elements of the array should be integers, specifying the 165 | | width of each column. For example: 166 | | 167 | | [10, 20, 5] 168 | | 169 | | set_deco(self, deco) 170 | | Set the table decoration 171 | | 172 | | - 'deco' can be a combination of: 173 | | 174 | | Texttable.BORDER: Border around the table 175 | | Texttable.HEADER: Horizontal line below the header 176 | | Texttable.HLINES: Horizontal lines between rows 177 | | Texttable.VLINES: Vertical lines between columns 178 | | 179 | | All of them are enabled by default 180 | | 181 | | - example: 182 | | 183 | | Texttable.BORDER | Texttable.HEADER 184 | | 185 | | set_header_align(self, array) 186 | | Set the desired header alignment 187 | | 188 | | - the elements of the array should be either "l", "c" or "r": 189 | | 190 | | * "l": column flushed left 191 | | * "c": column centered 192 | | * "r": column flushed right 193 | | 194 | | set_max_width(self, max_width) 195 | | Set the maximum width of the table 196 | | 197 | | - max_width is an integer, specifying the maximum width of the table 198 | | - if set to 0, size is unlimited, therefore cells won't be wrapped 199 | | 200 | | set_precision(self, width) 201 | | Set the desired precision for float/exponential formats 202 | | 203 | | - width must be an integer >= 0 204 | | 205 | | - default value is set to 3 206 | | 207 | | ---------------------------------------------------------------------- 208 | | Data and other attributes defined here: 209 | | 210 | | BORDER = 1 211 | | 212 | | HEADER = 2 213 | | 214 | | HLINES = 4 215 | | 216 | | VLINES = 8 217 | 218 | DATA 219 | __all__ = ['Texttable', 'ArraySizeError'] 220 | __author__ = 'Gerome Fournier ' 221 | __credits__ = 'Jeff Kowalczyk:\n - textwrap improved import\n ...at... 222 | __license__ = 'MIT' 223 | __version__ = '1.7.0' 224 | 225 | VERSION 226 | 1.7.0 227 | 228 | AUTHOR 229 | Gerome Fournier 230 | 231 | CREDITS 232 | Jeff Kowalczyk: 233 | - textwrap improved import 234 | - comment concerning header output 235 | 236 | Anonymous: 237 | - add_rows method, for adding rows in one go 238 | 239 | Sergey Simonenko: 240 | - redefined len() function to deal with non-ASCII characters 241 | 242 | Roger Lew: 243 | - columns datatype specifications 244 | 245 | Brian Peterson: 246 | - better handling of unicode errors 247 | 248 | Frank Sachsenheim: 249 | - add Python 2/3-compatibility 250 | 251 | Maximilian Hils: 252 | - fix minor bug for Python 3 compatibility 253 | 254 | frinkelpi: 255 | - preserve empty lines 256 | ``` 257 | 258 | ## Forks 259 | 260 | * [latextable](https://github.com/JAEarly/latextable) is a fork of texttable that provide a LaTeX backend. 261 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # texttable - module to create simple ASCII tables 4 | # Copyright (C) 2003-2023 Gerome Fournier 5 | 6 | from setuptools import setup 7 | 8 | DESCRIPTION = "module to create simple ASCII tables" 9 | 10 | with open("README.md") as f: 11 | LONG_DESCRIPTION = f.read() 12 | 13 | setup( 14 | name="texttable", 15 | version="1.7.0", 16 | author="Gerome Fournier", 17 | author_email="jef@foutaise.org", 18 | url="https://github.com/foutaise/texttable/", 19 | download_url="https://github.com/foutaise/texttable/archive/v1.7.0.tar.gz", 20 | license="MIT", 21 | py_modules=["texttable"], 22 | description=DESCRIPTION, 23 | long_description=LONG_DESCRIPTION, 24 | long_description_content_type="text/markdown", 25 | platforms="any", 26 | classifiers=[ 27 | 'Development Status :: 5 - Production/Stable', 28 | 'Environment :: Console', 29 | 'Intended Audience :: Developers', 30 | 'Intended Audience :: End Users/Desktop', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: Microsoft :: Windows', 33 | 'Operating System :: POSIX', 34 | 'Operating System :: MacOS', 35 | 'Topic :: Software Development :: Libraries :: Python Modules', 36 | 'Topic :: Text Processing', 37 | 'Topic :: Utilities', 38 | 'Programming Language :: Python', 39 | 'Programming Language :: Python :: 2', 40 | 'Programming Language :: Python :: 2.7', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.5', 43 | 'Programming Language :: Python :: 3.6', 44 | 'Programming Language :: Python :: 3.7', 45 | 'Programming Language :: Python :: 3.8', 46 | 'Programming Language :: Python :: 3.9', 47 | ], 48 | options={"bdist_wheel": {"universal": "1"}} 49 | ) 50 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | import re 4 | import sys 5 | from textwrap import dedent 6 | from texttable import Texttable 7 | 8 | if sys.version >= '3': 9 | u_dedent = dedent 10 | else: 11 | def u_dedent(b): 12 | return unicode(dedent(b), 'utf-8') 13 | 14 | def clean(text): 15 | return re.sub(r'( +)$', '', text, flags=re.MULTILINE) + '\n' 16 | 17 | def test_texttable(): 18 | table = Texttable() 19 | table.set_cols_align(["l", "r", "c"]) 20 | table.set_cols_valign(["t", "m", "b"]) 21 | table.add_rows([ 22 | ["Name", "Age", "Nickname"], 23 | ["Mr\nXavier\nHuon", 32, "Xav'"], 24 | ["Mr\nBaptiste\nClement", 1, "Baby"], 25 | ["Mme\nLouise\nBourgeau", 28, "Lou\n \nLoue"], 26 | ]) 27 | assert clean(table.draw()) == dedent('''\ 28 | +----------+-----+----------+ 29 | | Name | Age | Nickname | 30 | +==========+=====+==========+ 31 | | Mr | | | 32 | | Xavier | 32 | | 33 | | Huon | | Xav' | 34 | +----------+-----+----------+ 35 | | Mr | | | 36 | | Baptiste | 1 | | 37 | | Clement | | Baby | 38 | +----------+-----+----------+ 39 | | Mme | | Lou | 40 | | Louise | 28 | | 41 | | Bourgeau | | Loue | 42 | +----------+-----+----------+ 43 | ''') 44 | 45 | def test_texttable_header(): 46 | table = Texttable() 47 | table.set_deco(Texttable.HEADER) 48 | table.set_cols_dtype([ 49 | 't', # text 50 | 'f', # float (decimal) 51 | 'e', # float (exponent) 52 | 'i', # integer 53 | 'a', # automatic 54 | ]) 55 | table.set_cols_align(["l", "r", "r", "r", "l"]) 56 | table.add_rows([ 57 | ["text", "float", "exp", "int", "auto"], 58 | ["abcd", "67", 654, 89, 128.001], 59 | ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], 60 | ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], 61 | ["opqrstu", .023, 5e+78, 92., 12800000000000000000000], 62 | ]) 63 | assert clean(table.draw()) == dedent('''\ 64 | text float exp int auto 65 | ============================================== 66 | abcd 67.000 6.540e+02 89 128.001 67 | efghijk 67.543 6.540e-01 90 1.280e+22 68 | lmn 0.000 5.000e-78 89 0.000 69 | opqrstu 0.023 5.000e+78 92 1.280e+22 70 | ''') 71 | 72 | def test_set_cols_width(): 73 | table = Texttable() 74 | table.set_deco(Texttable.HEADER) 75 | table.set_cols_width([10, 10]) 76 | table.add_rows([ 77 | ["key", "value"], 78 | [1, "a"], 79 | [2, "b"], 80 | ]) 81 | assert clean(table.draw()) == dedent('''\ 82 | key value 83 | ======================= 84 | 1 a 85 | 2 b 86 | ''') 87 | 88 | def test_exceeding_max_width(): 89 | table = Texttable(max_width=35) 90 | table.set_deco(Texttable.HEADER) 91 | table.add_rows([ 92 | ["key", "value"], 93 | [1, "a"], 94 | [2, "b"], 95 | [3, "very long, very long, very long"], 96 | ]) 97 | assert clean(table.draw()) == dedent('''\ 98 | key value 99 | =================================== 100 | 1 a 101 | 2 b 102 | 3 very long, very long, very 103 | long 104 | ''') 105 | 106 | def test_exceeding_max_width2(): 107 | table = Texttable(max_width=14) 108 | table.add_rows([ 109 | ["a", "b"], 110 | [1, "+"], 111 | [22, "++++++++"], 112 | ]) 113 | assert clean(table.draw()) == dedent('''\ 114 | +----+-------+ 115 | | a | b | 116 | +====+=======+ 117 | | 1 | + | 118 | +----+-------+ 119 | | 22 | +++++ | 120 | | | +++ | 121 | +----+-------+ 122 | ''') 123 | 124 | def test_exceeding_max_width3(): 125 | table = Texttable() 126 | table.set_max_width(35) 127 | table.set_deco(Texttable.HEADER) 128 | table.add_rows([ 129 | ["key", "value"], 130 | [1, "a"], 131 | [2, "b"], 132 | [3, "very long, very long, very long"], 133 | ]) 134 | assert clean(table.draw()) == dedent('''\ 135 | key value 136 | =================================== 137 | 1 a 138 | 2 b 139 | 3 very long, very long, very 140 | long 141 | ''') 142 | 143 | def test_exceeding_max_width4(): 144 | table = Texttable() 145 | table.set_max_width(14) 146 | table.add_rows([ 147 | ["a", "b"], 148 | [1, "+"], 149 | [22, "++++++++"], 150 | ]) 151 | assert clean(table.draw()) == dedent('''\ 152 | +----+-------+ 153 | | a | b | 154 | +====+=======+ 155 | | 1 | + | 156 | +----+-------+ 157 | | 22 | +++++ | 158 | | | +++ | 159 | +----+-------+ 160 | ''') 161 | 162 | def test_obj2unicode(): 163 | table = Texttable() 164 | table.set_deco(Texttable.HEADER) 165 | table.add_rows([ 166 | ["key", "value"], 167 | [1, "a"], 168 | [2, 1], 169 | [3, None], 170 | ]) 171 | assert clean(table.draw()) == dedent('''\ 172 | key value 173 | =========== 174 | 1 a 175 | 2 1 176 | 3 None 177 | ''') 178 | 179 | def test_combining_char(): 180 | table = Texttable() 181 | table.set_cols_align(["l", "r", "r"]) 182 | table.add_rows([ 183 | ["str", "code-point\nlength", "display\nwidth"], 184 | ["ā", 2, 1], 185 | ["a", 1, 1], 186 | ]) 187 | assert clean(table.draw()) == u_dedent('''\ 188 | +-----+------------+---------+ 189 | | str | code-point | display | 190 | | | length | width | 191 | +=====+============+=========+ 192 | | ā | 2 | 1 | 193 | +-----+------------+---------+ 194 | | a | 1 | 1 | 195 | +-----+------------+---------+ 196 | ''') 197 | 198 | def test_combining_char2(): 199 | table = Texttable() 200 | table.add_rows([ 201 | ["a", "b", "c"], 202 | ["诶诶诶", "bbb", "西西西"], 203 | ], False) 204 | assert clean(table.draw()) == u_dedent('''\ 205 | +--------+-----+--------+ 206 | | a | b | c | 207 | +--------+-----+--------+ 208 | | 诶诶诶 | bbb | 西西西 | 209 | +--------+-----+--------+ 210 | ''') 211 | 212 | 213 | def test_user_dtype(): 214 | table = Texttable() 215 | 216 | table.set_cols_align(["l", "r", "r"]) 217 | table.set_cols_dtype([ 218 | 'a', # automatic 219 | lambda s:str(s)+"s", # user-def 220 | lambda s:('%s'%s) if s>=0 else '[%s]'%(-s), # user-def 221 | ]) 222 | table.add_rows([ 223 | ["str", "code-point\nlength", "display\nwidth"], 224 | ["a", 2, 1], 225 | ["a", 1,-3], 226 | ]) 227 | assert clean(table.draw()) == u_dedent('''\ 228 | +-----+------------+---------+ 229 | | str | code-point | display | 230 | | | length | width | 231 | +=====+============+=========+ 232 | | a | 2s | 1 | 233 | +-----+------------+---------+ 234 | | a | 1s | [3] | 235 | +-----+------------+---------+ 236 | ''') 237 | 238 | def test_cjkwarp(): 239 | try: 240 | import cjkwrap 241 | table = Texttable() 242 | 243 | table.set_cols_align(["r", "l"]) 244 | table.add_rows([ 245 | ["Name", 'Discuz! 6.x/7.x 全局变量防御绕过导致命令执行'], 246 | ["Description", '由于php5.3.x版本里php.ini的设置里request_order默认值为GP,导致Discuz! 6.x/7.x 全局变量防御绕过漏洞'], 247 | ], header = False) 248 | assert clean(table.draw()) == u_dedent('''\ 249 | +-------------+----------------------------------------------------------------+ 250 | | Name | Discuz! 6.x/7.x 全局变量防御绕过导致命令执行 | 251 | +-------------+----------------------------------------------------------------+ 252 | | Description | 由于php5.3.x版本里php.ini的设置里request_order默认值为GP,导致 | 253 | | | Discuz! 6.x/7.x 全局变量防御绕过漏洞 | 254 | +-------------+----------------------------------------------------------------+ 255 | ''') 256 | except ImportError: 257 | True 258 | 259 | def test_chaining(): 260 | table = Texttable() 261 | table.reset() 262 | table.set_max_width(50) 263 | table.set_chars(list('-|+=')) 264 | table.set_deco(Texttable.BORDER) 265 | table.set_header_align(list('lll')) 266 | table.set_cols_align(list('lll')) 267 | table.set_cols_valign(list('mmm')) 268 | table.set_cols_dtype(list('ttt')) 269 | table.set_cols_width([3, 3, 3]) 270 | table.set_precision(3) 271 | table.header(list('abc')) 272 | table.add_row(list('def')) 273 | table.add_rows([list('ghi')], False) 274 | s1 = table.draw() 275 | s2 = (Texttable() 276 | .reset() 277 | .set_max_width(50) 278 | .set_chars(list('-|+=')) 279 | .set_deco(Texttable.BORDER) 280 | .set_header_align(list('lll')) 281 | .set_cols_align(list('lll')) 282 | .set_cols_valign(list('mmm')) 283 | .set_cols_dtype(list('ttt')) 284 | .set_cols_width([3, 3, 3]) 285 | .set_precision(3) 286 | .header(list('abc')) 287 | .add_row(list('def')) 288 | .add_rows([list('ghi')], False) 289 | .draw()) 290 | assert s1 == s2 291 | 292 | def test_nan(): 293 | table = Texttable() 294 | table.set_cols_align(["l"]) 295 | table.add_rows([ 296 | ["A NaN"], 297 | ["NaN"], 298 | ]) 299 | assert clean(table.draw()) == u_dedent('''\ 300 | +-------+ 301 | | A NaN | 302 | +=======+ 303 | | NaN | 304 | +-------+ 305 | ''') 306 | 307 | def test_bool(): 308 | table = Texttable() 309 | table.set_cols_align(["l", "l"]) 310 | table.set_cols_dtype(["a", "b"]) 311 | table.set_deco(0) 312 | table.add_rows([ 313 | [True, True], 314 | [False, False], 315 | ["test", 0], 316 | [12, "true"], 317 | [12, ""], 318 | [34.2, 1.0], 319 | ], header=False) 320 | assert clean(table.draw()) == u_dedent('''\ 321 | True True 322 | False False 323 | test False 324 | 12 True 325 | 12 False 326 | 34.200 True 327 | ''') 328 | -------------------------------------------------------------------------------- /texttable.py: -------------------------------------------------------------------------------- 1 | # texttable - module to create simple ASCII tables 2 | # Copyright (C) 2003-2023 Gerome Fournier 3 | 4 | """module to create simple ASCII tables 5 | 6 | 7 | Example: 8 | 9 | table = Texttable() 10 | table.set_cols_align(["l", "r", "c"]) 11 | table.set_cols_valign(["t", "m", "b"]) 12 | table.add_rows([["Name", "Age", "Nickname"], 13 | ["Mr\\nXavier\\nHuon", 32, "Xav'"], 14 | ["Mr\\nBaptiste\\nClement", 1, "Baby"], 15 | ["Mme\\nLouise\\nBourgeau", 28, "Lou\\n\\nLoue"]]) 16 | print(table.draw()) 17 | print() 18 | 19 | table = Texttable() 20 | table.set_deco(Texttable.HEADER) 21 | table.set_cols_dtype(['t', # text 22 | 'f', # float (decimal) 23 | 'e', # float (exponent) 24 | 'i', # integer 25 | 'a']) # automatic 26 | table.set_cols_align(["l", "r", "r", "r", "l"]) 27 | table.add_rows([["text", "float", "exp", "int", "auto"], 28 | ["abcd", "67", 654, 89, 128.001], 29 | ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], 30 | ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], 31 | ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) 32 | print(table.draw()) 33 | 34 | Result: 35 | 36 | +----------+-----+----------+ 37 | | Name | Age | Nickname | 38 | +==========+=====+==========+ 39 | | Mr | | | 40 | | Xavier | 32 | | 41 | | Huon | | Xav' | 42 | +----------+-----+----------+ 43 | | Mr | | | 44 | | Baptiste | 1 | | 45 | | Clement | | Baby | 46 | +----------+-----+----------+ 47 | | Mme | | Lou | 48 | | Louise | 28 | | 49 | | Bourgeau | | Loue | 50 | +----------+-----+----------+ 51 | 52 | text float exp int auto 53 | =========================================== 54 | abcd 67.000 6.540e+02 89 128.001 55 | efgh 67.543 6.540e-01 90 1.280e+22 56 | ijkl 0.000 5.000e-78 89 0.000 57 | mnop 0.023 5.000e+78 92 1.280e+22 58 | """ 59 | 60 | from __future__ import division 61 | 62 | __all__ = ["Texttable", "ArraySizeError"] 63 | 64 | __author__ = 'Gerome Fournier ' 65 | __license__ = 'MIT' 66 | __version__ = '1.7.0' 67 | __credits__ = """\ 68 | Jeff Kowalczyk: 69 | - textwrap improved import 70 | - comment concerning header output 71 | 72 | Anonymous: 73 | - add_rows method, for adding rows in one go 74 | 75 | Sergey Simonenko: 76 | - redefined len() function to deal with non-ASCII characters 77 | 78 | Roger Lew: 79 | - columns datatype specifications 80 | 81 | Brian Peterson: 82 | - better handling of unicode errors 83 | 84 | Frank Sachsenheim: 85 | - add Python 2/3-compatibility 86 | 87 | Maximilian Hils: 88 | - fix minor bug for Python 3 compatibility 89 | 90 | frinkelpi: 91 | - preserve empty lines 92 | """ 93 | 94 | import sys 95 | import unicodedata 96 | 97 | # define a text wrapping function to wrap some text 98 | # to a specific width: 99 | # - use cjkwrap if available (better CJK support) 100 | # - fallback to textwrap otherwise 101 | try: 102 | import cjkwrap 103 | def textwrapper(txt, width): 104 | return cjkwrap.wrap(txt, width) 105 | except ImportError: 106 | try: 107 | import textwrap 108 | def textwrapper(txt, width): 109 | return textwrap.wrap(txt, width) 110 | except ImportError: 111 | sys.stderr.write("Can't import textwrap module!\n") 112 | raise 113 | 114 | # define a function to calculate the rendering width of a unicode character 115 | # - use wcwidth if available 116 | # - fallback to unicodedata information otherwise 117 | try: 118 | import wcwidth 119 | def uchar_width(c): 120 | """Return the rendering width of a unicode character 121 | """ 122 | return max(0, wcwidth.wcwidth(c)) 123 | except ImportError: 124 | def uchar_width(c): 125 | """Return the rendering width of a unicode character 126 | """ 127 | if unicodedata.east_asian_width(c) in 'WF': 128 | return 2 129 | elif unicodedata.combining(c): 130 | return 0 131 | else: 132 | return 1 133 | 134 | from functools import reduce 135 | 136 | if sys.version_info >= (3, 0): 137 | unicode_type = str 138 | bytes_type = bytes 139 | else: 140 | unicode_type = unicode 141 | bytes_type = str 142 | 143 | 144 | def obj2unicode(obj): 145 | """Return a unicode representation of a python object 146 | """ 147 | if isinstance(obj, unicode_type): 148 | return obj 149 | elif isinstance(obj, bytes_type): 150 | try: 151 | return unicode_type(obj, 'utf-8') 152 | except UnicodeDecodeError as strerror: 153 | sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (obj, strerror)) 154 | return unicode_type(obj, 'utf-8', 'replace') 155 | else: 156 | return unicode_type(obj) 157 | 158 | 159 | def len(iterable): 160 | """Redefining len here so it will be able to work with non-ASCII characters 161 | """ 162 | if isinstance(iterable, bytes_type) or isinstance(iterable, unicode_type): 163 | return sum([uchar_width(c) for c in obj2unicode(iterable)]) 164 | else: 165 | return iterable.__len__() 166 | 167 | 168 | class ArraySizeError(Exception): 169 | """Exception raised when specified rows don't fit the required size 170 | """ 171 | 172 | def __init__(self, msg): 173 | self.msg = msg 174 | Exception.__init__(self, msg, '') 175 | 176 | def __str__(self): 177 | return self.msg 178 | 179 | 180 | class FallbackToText(Exception): 181 | """Used for failed conversion to float""" 182 | pass 183 | 184 | 185 | class Texttable: 186 | 187 | BORDER = 1 188 | HEADER = 1 << 1 189 | HLINES = 1 << 2 190 | VLINES = 1 << 3 191 | 192 | def __init__(self, max_width=80): 193 | """Constructor 194 | 195 | - max_width is an integer, specifying the maximum width of the table 196 | - if set to 0, size is unlimited, therefore cells won't be wrapped 197 | """ 198 | 199 | self.set_max_width(max_width) 200 | self._precision = 3 201 | 202 | self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | \ 203 | Texttable.HEADER 204 | self.set_chars(['-', '|', '+', '=']) 205 | self.reset() 206 | 207 | def reset(self): 208 | """Reset the instance 209 | 210 | - reset rows and header 211 | """ 212 | 213 | self._hline_string = None 214 | self._row_size = None 215 | self._header = [] 216 | self._rows = [] 217 | return self 218 | 219 | def set_max_width(self, max_width): 220 | """Set the maximum width of the table 221 | 222 | - max_width is an integer, specifying the maximum width of the table 223 | - if set to 0, size is unlimited, therefore cells won't be wrapped 224 | """ 225 | self._max_width = max_width if max_width > 0 else False 226 | return self 227 | 228 | def set_chars(self, array): 229 | """Set the characters used to draw lines between rows and columns 230 | 231 | - the array should contain 4 fields: 232 | 233 | [horizontal, vertical, corner, header] 234 | 235 | - default is set to: 236 | 237 | ['-', '|', '+', '='] 238 | """ 239 | 240 | if len(array) != 4: 241 | raise ArraySizeError("array should contain 4 characters") 242 | array = [ x[:1] for x in [ str(s) for s in array ] ] 243 | (self._char_horiz, self._char_vert, 244 | self._char_corner, self._char_header) = array 245 | return self 246 | 247 | def set_deco(self, deco): 248 | """Set the table decoration 249 | 250 | - 'deco' can be a combination of: 251 | 252 | Texttable.BORDER: Border around the table 253 | Texttable.HEADER: Horizontal line below the header 254 | Texttable.HLINES: Horizontal lines between rows 255 | Texttable.VLINES: Vertical lines between columns 256 | 257 | All of them are enabled by default 258 | 259 | - example: 260 | 261 | Texttable.BORDER | Texttable.HEADER 262 | """ 263 | 264 | self._deco = deco 265 | self._hline_string = None 266 | return self 267 | 268 | def set_header_align(self, array): 269 | """Set the desired header alignment 270 | 271 | - the elements of the array should be either "l", "c" or "r": 272 | 273 | * "l": column flushed left 274 | * "c": column centered 275 | * "r": column flushed right 276 | """ 277 | 278 | self._check_row_size(array) 279 | self._header_align = array 280 | return self 281 | 282 | def set_cols_align(self, array): 283 | """Set the desired columns alignment 284 | 285 | - the elements of the array should be either "l", "c" or "r": 286 | 287 | * "l": column flushed left 288 | * "c": column centered 289 | * "r": column flushed right 290 | """ 291 | 292 | self._check_row_size(array) 293 | self._align = array 294 | return self 295 | 296 | def set_cols_valign(self, array): 297 | """Set the desired columns vertical alignment 298 | 299 | - the elements of the array should be either "t", "m" or "b": 300 | 301 | * "t": column aligned on the top of the cell 302 | * "m": column aligned on the middle of the cell 303 | * "b": column aligned on the bottom of the cell 304 | """ 305 | 306 | self._check_row_size(array) 307 | self._valign = array 308 | return self 309 | 310 | def set_cols_dtype(self, array): 311 | """Set the desired columns datatype for the cols. 312 | 313 | - the elements of the array should be either a callable or any of 314 | "a", "t", "f", "e", "i" or "b": 315 | 316 | * "a": automatic (try to use the most appropriate datatype) 317 | * "t": treat as text 318 | * "f": treat as float in decimal format 319 | * "e": treat as float in exponential format 320 | * "i": treat as int 321 | * "b": treat as boolean 322 | * a callable: should return formatted string for any value given 323 | 324 | - by default, automatic datatyping is used for each column 325 | """ 326 | 327 | self._check_row_size(array) 328 | self._dtype = array 329 | return self 330 | 331 | def set_cols_width(self, array): 332 | """Set the desired columns width 333 | 334 | - the elements of the array should be integers, specifying the 335 | width of each column. For example: 336 | 337 | [10, 20, 5] 338 | """ 339 | 340 | self._check_row_size(array) 341 | try: 342 | array = list(map(int, array)) 343 | if reduce(min, array) <= 0: 344 | raise ValueError 345 | except ValueError: 346 | sys.stderr.write("Wrong argument in column width specification\n") 347 | raise 348 | self._width = array 349 | return self 350 | 351 | def set_precision(self, width): 352 | """Set the desired precision for float/exponential formats 353 | 354 | - width must be an integer >= 0 355 | 356 | - default value is set to 3 357 | """ 358 | 359 | if not type(width) is int or width < 0: 360 | raise ValueError('width must be an integer greater then 0') 361 | self._precision = width 362 | return self 363 | 364 | def header(self, array): 365 | """Specify the header of the table 366 | """ 367 | 368 | self._check_row_size(array) 369 | self._header = list(map(obj2unicode, array)) 370 | return self 371 | 372 | def add_row(self, array): 373 | """Add a row in the rows stack 374 | 375 | - cells can contain newlines and tabs 376 | """ 377 | 378 | self._check_row_size(array) 379 | 380 | if not hasattr(self, "_dtype"): 381 | self._dtype = ["a"] * self._row_size 382 | 383 | cells = [] 384 | for i, x in enumerate(array): 385 | cells.append(self._str(i, x)) 386 | self._rows.append(cells) 387 | return self 388 | 389 | def add_rows(self, rows, header=True): 390 | """Add several rows in the rows stack 391 | 392 | - The 'rows' argument can be either an iterator returning arrays, 393 | or a by-dimensional array 394 | - 'header' specifies if the first row should be used as the header 395 | of the table 396 | """ 397 | 398 | # nb: don't use 'iter' on by-dimensional arrays, to get a 399 | # usable code for python 2.1 400 | if header: 401 | if hasattr(rows, '__iter__') and hasattr(rows, 'next'): 402 | self.header(rows.next()) 403 | else: 404 | self.header(rows[0]) 405 | rows = rows[1:] 406 | for row in rows: 407 | self.add_row(row) 408 | return self 409 | 410 | def draw(self): 411 | """Draw the table 412 | 413 | - the table is returned as a whole string 414 | """ 415 | 416 | if not self._header and not self._rows: 417 | return 418 | self._compute_cols_width() 419 | self._check_align() 420 | out = "" 421 | if self._has_border(): 422 | out += self._hline() 423 | if self._header: 424 | out += self._draw_line(self._header, isheader=True) 425 | if self._has_header(): 426 | out += self._hline_header() 427 | length = 0 428 | for row in self._rows: 429 | length += 1 430 | out += self._draw_line(row) 431 | if self._has_hlines() and length < len(self._rows): 432 | out += self._hline() 433 | if self._has_border(): 434 | out += self._hline() 435 | return out[:-1] 436 | 437 | @classmethod 438 | def _to_float(cls, x): 439 | if x is None: 440 | raise FallbackToText() 441 | try: 442 | return float(x) 443 | except (TypeError, ValueError): 444 | raise FallbackToText() 445 | 446 | @classmethod 447 | def _fmt_int(cls, x, **kw): 448 | """Integer formatting class-method. 449 | """ 450 | if type(x) == int: 451 | return str(x) 452 | else: 453 | return str(int(round(cls._to_float(x)))) 454 | 455 | @classmethod 456 | def _fmt_float(cls, x, **kw): 457 | """Float formatting class-method. 458 | 459 | - x parameter is ignored. Instead kw-argument f being x float-converted 460 | will be used. 461 | 462 | - precision will be taken from `n` kw-argument. 463 | """ 464 | n = kw.get('n') 465 | return '%.*f' % (n, cls._to_float(x)) 466 | 467 | @classmethod 468 | def _fmt_exp(cls, x, **kw): 469 | """Exponential formatting class-method. 470 | 471 | - x parameter is ignored. Instead kw-argument f being x float-converted 472 | will be used. 473 | 474 | - precision will be taken from `n` kw-argument. 475 | """ 476 | n = kw.get('n') 477 | return '%.*e' % (n, cls._to_float(x)) 478 | 479 | @classmethod 480 | def _fmt_text(cls, x, **kw): 481 | """String formatting class-method.""" 482 | return obj2unicode(x) 483 | 484 | @classmethod 485 | def _fmt_bool(cls, x, **kw): 486 | """Boolean formatting class-method""" 487 | return str(bool(x)) 488 | 489 | @classmethod 490 | def _fmt_auto(cls, x, **kw): 491 | """auto formatting class-method.""" 492 | f = cls._to_float(x) 493 | if abs(f) > 1e8: 494 | fn = cls._fmt_exp 495 | elif f != f: # NaN 496 | fn = cls._fmt_text 497 | elif f - round(f) == 0: 498 | fn = cls._fmt_bool if isinstance(x, bool) else cls._fmt_int 499 | else: 500 | fn = cls._fmt_float 501 | return fn(x, **kw) 502 | 503 | def _str(self, i, x): 504 | """Handles string formatting of cell data 505 | 506 | i - index of the cell datatype in self._dtype 507 | x - cell data to format 508 | """ 509 | FMT = { 510 | 'a':self._fmt_auto, 511 | 'i':self._fmt_int, 512 | 'b':self._fmt_bool, 513 | 'f':self._fmt_float, 514 | 'e':self._fmt_exp, 515 | 't':self._fmt_text, 516 | } 517 | 518 | n = self._precision 519 | dtype = self._dtype[i] 520 | try: 521 | if callable(dtype): 522 | return dtype(x) 523 | else: 524 | return FMT[dtype](x, n=n) 525 | except FallbackToText: 526 | return self._fmt_text(x) 527 | 528 | def _check_row_size(self, array): 529 | """Check that the specified array fits the previous rows size 530 | """ 531 | 532 | if not self._row_size: 533 | self._row_size = len(array) 534 | elif self._row_size != len(array): 535 | raise ArraySizeError("array should contain %d elements" \ 536 | % self._row_size) 537 | 538 | def _has_vlines(self): 539 | """Return a boolean, if vlines are required or not 540 | """ 541 | 542 | return self._deco & Texttable.VLINES > 0 543 | 544 | def _has_hlines(self): 545 | """Return a boolean, if hlines are required or not 546 | """ 547 | 548 | return self._deco & Texttable.HLINES > 0 549 | 550 | def _has_border(self): 551 | """Return a boolean, if border is required or not 552 | """ 553 | 554 | return self._deco & Texttable.BORDER > 0 555 | 556 | def _has_header(self): 557 | """Return a boolean, if header line is required or not 558 | """ 559 | 560 | return self._deco & Texttable.HEADER > 0 561 | 562 | def _hline_header(self): 563 | """Print header's horizontal line 564 | """ 565 | 566 | return self._build_hline(True) 567 | 568 | def _hline(self): 569 | """Print an horizontal line 570 | """ 571 | 572 | if not self._hline_string: 573 | self._hline_string = self._build_hline() 574 | return self._hline_string 575 | 576 | def _build_hline(self, is_header=False): 577 | """Return a string used to separated rows or separate header from 578 | rows 579 | """ 580 | horiz = self._char_horiz 581 | if (is_header): 582 | horiz = self._char_header 583 | # compute cell separator 584 | s = "%s%s%s" % (horiz, [horiz, self._char_corner][self._has_vlines()], 585 | horiz) 586 | # build the line 587 | l = s.join([horiz * n for n in self._width]) 588 | # add border if needed 589 | if self._has_border(): 590 | l = "%s%s%s%s%s\n" % (self._char_corner, horiz, l, horiz, 591 | self._char_corner) 592 | else: 593 | l += "\n" 594 | return l 595 | 596 | def _len_cell(self, cell): 597 | """Return the width of the cell 598 | 599 | Special characters are taken into account to return the width of the 600 | cell, such like newlines and tabs 601 | """ 602 | 603 | cell_lines = cell.split('\n') 604 | maxi = 0 605 | for line in cell_lines: 606 | length = 0 607 | parts = line.split('\t') 608 | for part, i in zip(parts, list(range(1, len(parts) + 1))): 609 | length = length + len(part) 610 | if i < len(parts): 611 | length = (length//8 + 1) * 8 612 | maxi = max(maxi, length) 613 | return maxi 614 | 615 | def _compute_cols_width(self): 616 | """Return an array with the width of each column 617 | 618 | If a specific width has been specified, exit. If the total of the 619 | columns width exceed the table desired width, another width will be 620 | computed to fit, and cells will be wrapped. 621 | """ 622 | 623 | if hasattr(self, "_width"): 624 | return 625 | maxi = [] 626 | if self._header: 627 | maxi = [ self._len_cell(x) for x in self._header ] 628 | for row in self._rows: 629 | for cell,i in zip(row, list(range(len(row)))): 630 | try: 631 | maxi[i] = max(maxi[i], self._len_cell(cell)) 632 | except (TypeError, IndexError): 633 | maxi.append(self._len_cell(cell)) 634 | 635 | ncols = len(maxi) 636 | content_width = sum(maxi) 637 | deco_width = 3*(ncols-1) + [0,4][self._has_border()] 638 | if self._max_width and (content_width + deco_width) > self._max_width: 639 | """ content too wide to fit the expected max_width 640 | let's recompute maximum cell width for each cell 641 | """ 642 | if self._max_width < (ncols + deco_width): 643 | raise ValueError('max_width too low to render data') 644 | available_width = self._max_width - deco_width 645 | newmaxi = [0] * ncols 646 | i = 0 647 | while available_width > 0: 648 | if newmaxi[i] < maxi[i]: 649 | newmaxi[i] += 1 650 | available_width -= 1 651 | i = (i + 1) % ncols 652 | maxi = newmaxi 653 | self._width = maxi 654 | 655 | def _check_align(self): 656 | """Check if alignment has been specified, set default one if not 657 | """ 658 | 659 | if not hasattr(self, "_header_align"): 660 | self._header_align = ["c"] * self._row_size 661 | if not hasattr(self, "_align"): 662 | self._align = ["l"] * self._row_size 663 | if not hasattr(self, "_valign"): 664 | self._valign = ["t"] * self._row_size 665 | 666 | def _draw_line(self, line, isheader=False): 667 | """Draw a line 668 | 669 | Loop over a single cell length, over all the cells 670 | """ 671 | 672 | line = self._splitit(line, isheader) 673 | space = " " 674 | out = "" 675 | for i in range(len(line[0])): 676 | if self._has_border(): 677 | out += "%s " % self._char_vert 678 | length = 0 679 | for cell, width, align in zip(line, self._width, self._align): 680 | length += 1 681 | cell_line = cell[i] 682 | fill = width - len(cell_line) 683 | if isheader: 684 | align = self._header_align[length - 1] 685 | if align == "r": 686 | out += fill * space + cell_line 687 | elif align == "c": 688 | out += (int(fill/2) * space + cell_line \ 689 | + int(fill/2 + fill%2) * space) 690 | else: 691 | out += cell_line + fill * space 692 | if length < len(line): 693 | out += " %s " % [space, self._char_vert][self._has_vlines()] 694 | out += "%s\n" % ['', space + self._char_vert][self._has_border()] 695 | return out 696 | 697 | def _splitit(self, line, isheader): 698 | """Split each element of line to fit the column width 699 | 700 | Each element is turned into a list, result of the wrapping of the 701 | string to the desired width 702 | """ 703 | 704 | line_wrapped = [] 705 | for cell, width in zip(line, self._width): 706 | array = [] 707 | for c in cell.split('\n'): 708 | if c.strip() == "": 709 | array.append("") 710 | else: 711 | array.extend(textwrapper(c, width)) 712 | line_wrapped.append(array) 713 | max_cell_lines = reduce(max, list(map(len, line_wrapped))) 714 | for cell, valign in zip(line_wrapped, self._valign): 715 | if isheader: 716 | valign = "t" 717 | if valign == "m": 718 | missing = max_cell_lines - len(cell) 719 | cell[:0] = [""] * int(missing / 2) 720 | cell.extend([""] * int(missing / 2 + missing % 2)) 721 | elif valign == "b": 722 | cell[:0] = [""] * (max_cell_lines - len(cell)) 723 | else: 724 | cell.extend([""] * (max_cell_lines - len(cell))) 725 | return line_wrapped 726 | 727 | 728 | if __name__ == '__main__': 729 | table = Texttable() 730 | table.set_cols_align(["l", "r", "c"]) 731 | table.set_cols_valign(["t", "m", "b"]) 732 | table.add_rows([["Name", "Age", "Nickname"], 733 | ["Mr\nXavier\nHuon", 32, "Xav'"], 734 | ["Mr\nBaptiste\nClement", 1, "Baby"], 735 | ["Mme\nLouise\nBourgeau", 28, "Lou\n \nLoue"]]) 736 | print(table.draw()) 737 | print() 738 | 739 | table = Texttable() 740 | table.set_deco(Texttable.HEADER) 741 | table.set_cols_dtype(['t', # text 742 | 'f', # float (decimal) 743 | 'e', # float (exponent) 744 | 'i', # integer 745 | 'a']) # automatic 746 | table.set_cols_align(["l", "r", "r", "r", "l"]) 747 | table.add_rows([["text", "float", "exp", "int", "auto"], 748 | ["abcd", "67", 654, 89, 128.001], 749 | ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], 750 | ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], 751 | ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) 752 | print(table.draw()) 753 | -------------------------------------------------------------------------------- /texttable.pyi: -------------------------------------------------------------------------------- 1 | from typing import * 2 | 3 | class Texttable: 4 | 5 | BORDER: int 6 | HEADER: int 7 | HLINES: int 8 | VLINES: int 9 | 10 | def __init__(self, max_width: int = ...) -> None: ... 11 | 12 | def reset(self) -> 'Texttable': ... 13 | 14 | def set_max_width(self, max_width: int) -> 'Texttable': ... 15 | 16 | def set_chars(self, array: List[str]) -> 'Texttable': ... 17 | 18 | def set_deco(self, deco: int) -> 'Texttable': ... 19 | 20 | def set_header_align(self, array: List[str]) -> 'Texttable': ... 21 | 22 | def set_cols_align(self, array: List[str]) -> 'Texttable': ... 23 | 24 | def set_cols_valign(self, array: List[str]) -> 'Texttable': ... 25 | 26 | def set_cols_dtype(self, array: List[Union[str, Callable[[Any], str]]]) -> 'Texttable': ... 27 | 28 | def set_cols_width(self, array: List[int]) -> 'Texttable': ... 29 | 30 | def set_precision(self, width: int) -> 'Texttable': ... 31 | 32 | def header(self, array: List[str]) -> 'Texttable': ... 33 | 34 | def add_row(self, array: List[Union[Union[int, str], float]]) -> 'Texttable': ... 35 | 36 | def add_rows(self, rows: List[object], header: bool = ...) -> 'Texttable': ... 37 | 38 | def draw(self) -> str: ... 39 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py35,py36,py37,py38,py39,py311 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | pytest-cov 8 | cjkwrap 9 | commands = pytest --cov-report=term-missing --cov=texttable tests.py 10 | --------------------------------------------------------------------------------