├── .gitignore ├── PKG-INFO ├── README.md ├── example.py ├── setup.py ├── test_texttable.py ├── texttable.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Jython 4 | *.class 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | MANIFEST 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | 27 | #Translations 28 | *.mo 29 | 30 | #Mr Developer 31 | .mr.developer.cfg 32 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: texttable 3 | Version: 0.8.1 4 | Summary: module for creating simple ASCII tables 5 | Home-page: http://foutaise.org/code/ 6 | Author: Gerome Fournier 7 | Author-email: jef(at)foutaise.org 8 | License: LGPL 9 | Download-URL: http://foutaise.org/code/texttable/texttable-0.8.1.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 :: 4 - Beta 14 | Classifier: Environment :: Console 15 | Classifier: Intended Audience :: Developers 16 | Classifier: Intended Audience :: End Users/Desktop 17 | Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) 18 | Classifier: Operating System :: Microsoft :: Windows 19 | Classifier: Operating System :: POSIX 20 | Classifier: Operating System :: MacOS 21 | Classifier: Topic :: Software Development :: Libraries :: Python Modules 22 | Classifier: Topic :: Text Processing 23 | Classifier: Topic :: Utilities 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-texttable 2 | ================ 3 | 4 | For easy printing of ascii tables within python 5 | 6 | # Installation: 7 | 8 | ```bash 9 | pip install -U git+http://github.com/bufordtaylor/python-texttable 10 | ``` 11 | 12 | On debian sid 13 | 14 | ```bash 15 | apt-get install python-texttable 16 | ``` 17 | 18 | 19 | # Example: 20 | 21 | ```python 22 | from texttable import Texttable, get_color_string, bcolors 23 | 24 | table = Texttable() 25 | table.set_cols_align(["l", "r", "c"]) 26 | table.set_cols_valign(["t", "m", "b"]) 27 | table.add_rows([ [get_color_string(bcolors.GREEN, "Name Of Person"), "Age", "Nickname"], 28 | ["Mr\nXavier\nHuon", 32, "Xav'"], 29 | [get_color_string(bcolors.BLUE,"Mr\nBaptiste\nClement"), 1, get_color_string(bcolors.RED,"Baby")] ]) 30 | print(table.draw() + "\n") 31 | 32 | table = Texttable() 33 | table.set_deco(Texttable.HEADER) 34 | table.set_cols_dtype(['t', # text 35 | 'f', # float (decimal) 36 | 'e', # float (exponent) 37 | 'i', # integer 38 | 'a']) # automatic 39 | table.set_cols_align(["l", "r", "r", "r", "l"]) 40 | table.add_rows([["text", "float", "exp", "int", "auto"], 41 | ["abcd", "67", 654, 89, 128.001], 42 | ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], 43 | ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], 44 | ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) 45 | print(table.draw()) 46 | ``` 47 | 48 | # Result: 49 | 50 | ![texttable results](http://i.imgur.com/Zu7FB.png) 51 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from texttable import Texttable, get_color_string, bcolors 2 | 3 | table = Texttable() 4 | table.set_cols_align(["l", "r", "c"]) 5 | table.set_cols_valign(["t", "m", "b"]) 6 | table.add_rows([ 7 | [get_color_string(bcolors.GREEN, "Name Of Person"), "Age", "Nickname"], 8 | ["Mr\nXavier\nHuon", 32, "Xav'"], 9 | [get_color_string(bcolors.BLUE,"Mr\nBaptiste\nClement"), 10 | 1, 11 | get_color_string(bcolors.RED,"Baby")] ]) 12 | print(table.draw() + "\n") 13 | 14 | table = Texttable() 15 | table.set_deco(Texttable.HEADER) 16 | table.set_cols_dtype(['t', # text 17 | 'f', # float (decimal) 18 | 'e', # float (exponent) 19 | 'i', # integer 20 | 'a']) # automatic 21 | table.set_cols_align(["l", "r", "r", "r", "l"]) 22 | table.add_rows([["text", "float", "exp", "int", "auto"], 23 | ["abcd", "67", 654, 89, 128.001], 24 | ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], 25 | ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], 26 | ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) 27 | print(table.draw()) 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # texttable - module for creating simple ASCII tables 4 | # Copyright (C) 2003-2011 Gerome Fournier 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | 20 | DESCRIPTION = "module for creating simple ASCII tables" 21 | 22 | LONG_DESCRIPTION = """\ 23 | texttable is a module to generate a formatted text table, using ASCII 24 | characters.""" 25 | 26 | import sys 27 | 28 | from distutils.core import setup 29 | if sys.version < '2.2.3': 30 | from distutils.dist import DistributionMetadata 31 | DistributionMetadata.classifiers = None 32 | DistributionMetadata.download_url = None 33 | 34 | setup( 35 | name = "texttable", 36 | version = "0.8.2", 37 | author = "Gerome Fournier", 38 | author_email = "jef(at)foutaise.org", 39 | url = "http://foutaise.org/code/", 40 | download_url = "http://foutaise.org/code/texttable/texttable-0.8.1.tar.gz", 41 | license = "LGPL", 42 | py_modules = ["texttable"], 43 | description = DESCRIPTION, 44 | long_description = LONG_DESCRIPTION, 45 | platforms = "any", 46 | classifiers = [ 47 | 'Development Status :: 4 - Beta', 48 | 'Environment :: Console', 49 | 'Intended Audience :: Developers', 50 | 'Intended Audience :: End Users/Desktop', 51 | 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 52 | 'Operating System :: Microsoft :: Windows', 53 | 'Operating System :: POSIX', 54 | 'Operating System :: MacOS', 55 | 'Topic :: Software Development :: Libraries :: Python Modules', 56 | 'Topic :: Text Processing', 57 | 'Topic :: Utilities', 58 | ] 59 | ) 60 | -------------------------------------------------------------------------------- /test_texttable.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | from texttable import Texttable, get_color_string, bcolors 4 | 5 | def test_colored(): 6 | table = Texttable() 7 | table.set_cols_align(["l", "r", "c"]) 8 | table.set_cols_valign(["t", "m", "b"]) 9 | table.add_rows([ 10 | [get_color_string(bcolors.GREEN, "Name Of Person"), "Age", "Nickname"], 11 | ["Mr\nXavier\nHuon", 32, "Xav'"], 12 | [get_color_string(bcolors.BLUE,"Mr\nBaptiste\nClement"), 13 | 1, 14 | get_color_string(bcolors.RED,"Baby")] ]) 15 | expected_output = dedent(""" 16 | +----------------+-----+----------+ 17 | | Name Of Person | Age | Nickname | 18 | +================+=====+==========+ 19 | | Mr | | | 20 | | Xavier | 32 | | 21 | | Huon | | Xav' | 22 | +----------------+-----+----------+ 23 | | Mr | | | 24 | | Baptiste | 1 | | 25 | | Clement | | Baby | 26 | +----------------+-----+----------+ 27 | """).strip('\n') 28 | 29 | assert table.draw() == expected_output 30 | 31 | def test_typed_columns(): 32 | table = Texttable() 33 | table.set_deco(Texttable.HEADER) 34 | table.set_cols_dtype(['t', # text 35 | 'f', # float (decimal) 36 | 'e', # float (exponent) 37 | 'i', # integer 38 | 'a']) # automatic 39 | table.set_cols_align(["l", "r", "r", "r", "l"]) 40 | table.add_rows([["text", "float", "exp", "int", "auto"], 41 | ["abcd", "67", 654, 89, 128.001], 42 | ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], 43 | ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], 44 | ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) 45 | expected_output = dedent(""" 46 | text float exp int auto 47 | ============================================== 48 | abcd 67.000 6.540e+02 89 128.001 49 | efghijk 67.543 6.540e-01 90 1.280e+22 50 | lmn 0.000 5.000e-78 89 0.000 51 | opqrstu 0.023 5.000e+78 92 1.280e+22 52 | """).strip('\n') 53 | 54 | assert table.draw() == expected_output 55 | -------------------------------------------------------------------------------- /texttable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # texttable - module for creating simple ASCII tables 4 | # Copyright (C) 2003-2011 Gerome Fournier 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | 20 | """module for creating simple ASCII tables 21 | 22 | 23 | Example: 24 | 25 | table = Texttable() 26 | table.set_cols_align(["l", "r", "c"]) 27 | table.set_cols_valign(["t", "m", "b"]) 28 | table.add_rows([ ["Name", "Age", "Nickname"], 29 | ["Mr\\nXavier\\nHuon", 32, "Xav'"], 30 | ["Mr\\nBaptiste\\nClement", 1, "Baby"] ]) 31 | print table.draw() + "\\n" 32 | 33 | table = Texttable() 34 | table.set_deco(Texttable.HEADER) 35 | table.set_cols_dtype(['t', # text 36 | 'f', # float (decimal) 37 | 'e', # float (exponent) 38 | 'i', # integer 39 | 'a']) # automatic 40 | table.set_cols_align(["l", "r", "r", "r", "l"]) 41 | table.add_rows([["text", "float", "exp", "int", "auto"], 42 | ["abcd", "67", 654, 89, 128.001], 43 | ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], 44 | ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], 45 | ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) 46 | print table.draw() 47 | 48 | Result: 49 | 50 | +----------+-----+----------+ 51 | | Name | Age | Nickname | 52 | +==========+=====+==========+ 53 | | Mr | | | 54 | | Xavier | 32 | | 55 | | Huon | | Xav' | 56 | +----------+-----+----------+ 57 | | Mr | | | 58 | | Baptiste | 1 | | 59 | | Clement | | Baby | 60 | +----------+-----+----------+ 61 | 62 | text float exp int auto 63 | =========================================== 64 | abcd 67.000 6.540e+02 89 128.001 65 | efgh 67.543 6.540e-01 90 1.280e+22 66 | ijkl 0.000 5.000e-78 89 0.000 67 | mnop 0.023 5.000e+78 92 1.280e+22 68 | """ 69 | 70 | __all__ = ["Texttable", "ArraySizeError"] 71 | 72 | __author__ = 'Gerome Fournier ' 73 | __license__ = 'GPL' 74 | __version__ = '0.8.1' 75 | __credits__ = """\ 76 | Jeff Kowalczyk: 77 | - textwrap improved import 78 | - comment concerning header output 79 | 80 | Anonymous: 81 | - add_rows method, for adding rows in one go 82 | 83 | Sergey Simonenko: 84 | - redefined len() function to deal with non-ASCII characters 85 | 86 | Roger Lew: 87 | - columns datatype specifications 88 | 89 | Brian Peterson: 90 | - better handling of unicode errors 91 | """ 92 | 93 | import re 94 | import math 95 | import sys 96 | import string 97 | from functools import reduce 98 | 99 | try: 100 | if sys.version >= '2.3': 101 | import textwrap 102 | elif sys.version >= '2.2': 103 | from optparse import textwrap 104 | else: 105 | from optik import textwrap 106 | except ImportError: 107 | sys.stderr.write("Can't import textwrap module!\n") 108 | raise 109 | 110 | def len(iterable): 111 | """Redefining len here so it will be able to work with non-ASCII characters 112 | """ 113 | if not isinstance(iterable, str): 114 | return iterable.__len__() 115 | 116 | try: 117 | return len(str(iterable, 'utf')) 118 | except: 119 | return iterable.__len__() 120 | 121 | class ArraySizeError(Exception): 122 | """Exception raised when specified rows don't fit the required size 123 | """ 124 | 125 | def __init__(self, msg): 126 | self.msg = msg 127 | Exception.__init__(self, msg, '') 128 | 129 | def __str__(self): 130 | return self.msg 131 | 132 | class bcolors: 133 | PURPLE = '\x1b[95m' 134 | BLUE = '\x1b[94m' 135 | GREEN = '\x1b[92m' 136 | YELLOW = '\x1b[93m' 137 | RED = '\x1b[91m' 138 | ENDC = '\x1b[0m' 139 | WHITE = '' 140 | BOLD = '\x1b[1m' 141 | UNDERLINE = '\x1b[4m' 142 | 143 | def get_color_string(type, string): 144 | end = bcolors.ENDC 145 | if type == bcolors.WHITE: 146 | end = '' 147 | return '%s%s%s' % (type, string, end) 148 | 149 | class Texttable: 150 | 151 | BORDER = 1 152 | HEADER = 1 << 1 153 | HLINES = 1 << 2 154 | VLINES = 1 << 3 155 | 156 | def __init__(self, max_width=80): 157 | """Constructor 158 | 159 | - max_width is an integer, specifying the maximum width of the table 160 | - if set to 0, size is unlimited, therefore cells won't be wrapped 161 | """ 162 | 163 | if max_width <= 0: 164 | max_width = False 165 | self._max_width = max_width 166 | self._precision = 3 167 | 168 | self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | \ 169 | Texttable.HEADER 170 | self.set_chars(['-', '|', '+', '=']) 171 | self.reset() 172 | 173 | def reset(self): 174 | """Reset the instance 175 | 176 | - reset rows and header 177 | """ 178 | 179 | self._hline_string = None 180 | self._row_size = None 181 | self._header = [] 182 | self._rows = [] 183 | 184 | def set_chars(self, array): 185 | """Set the characters used to draw lines between rows and columns 186 | 187 | - the array should contain 4 fields: 188 | 189 | [horizontal, vertical, corner, header] 190 | 191 | - default is set to: 192 | 193 | ['-', '|', '+', '='] 194 | """ 195 | 196 | if len(array) != 4: 197 | raise ArraySizeError("array should contain 4 characters") 198 | array = [ x[:1] for x in [ str(s) for s in array ] ] 199 | (self._char_horiz, self._char_vert, 200 | self._char_corner, self._char_header) = array 201 | 202 | def set_deco(self, deco): 203 | """Set the table decoration 204 | 205 | - 'deco' can be a combinaison of: 206 | 207 | Texttable.BORDER: Border around the table 208 | Texttable.HEADER: Horizontal line below the header 209 | Texttable.HLINES: Horizontal lines between rows 210 | Texttable.VLINES: Vertical lines between columns 211 | 212 | All of them are enabled by default 213 | 214 | - example: 215 | 216 | Texttable.BORDER | Texttable.HEADER 217 | """ 218 | 219 | self._deco = deco 220 | 221 | def set_cols_align(self, array): 222 | """Set the desired columns alignment 223 | 224 | - the elements of the array should be either "l", "c" or "r": 225 | 226 | * "l": column flushed left 227 | * "c": column centered 228 | * "r": column flushed right 229 | """ 230 | 231 | self._check_row_size(array) 232 | self._align = array 233 | 234 | def set_cols_valign(self, array): 235 | """Set the desired columns vertical alignment 236 | 237 | - the elements of the array should be either "t", "m" or "b": 238 | 239 | * "t": column aligned on the top of the cell 240 | * "m": column aligned on the middle of the cell 241 | * "b": column aligned on the bottom of the cell 242 | """ 243 | 244 | self._check_row_size(array) 245 | self._valign = array 246 | 247 | def set_cols_dtype(self, array): 248 | """Set the desired columns datatype for the cols. 249 | 250 | - the elements of the array should be either "a", "t", "f", "e" or "i": 251 | 252 | * "a": automatic (try to use the most appropriate datatype) 253 | * "t": treat as text 254 | * "f": treat as float in decimal format 255 | * "e": treat as float in exponential format 256 | * "i": treat as int 257 | 258 | - by default, automatic datatyping is used for each column 259 | """ 260 | 261 | self._check_row_size(array) 262 | self._dtype = array 263 | 264 | def set_cols_width(self, array): 265 | """Set the desired columns width 266 | 267 | - the elements of the array should be integers, specifying the 268 | width of each column. For example: 269 | 270 | [10, 20, 5] 271 | """ 272 | 273 | self._check_row_size(array) 274 | try: 275 | array = list(map(int, array)) 276 | if reduce(min, array) <= 0: 277 | raise ValueError 278 | except ValueError: 279 | sys.stderr.write("Wrong argument in column width specification\n") 280 | raise 281 | self._width = array 282 | 283 | def set_precision(self, width): 284 | """Set the desired precision for float/exponential formats 285 | 286 | - width must be an integer >= 0 287 | 288 | - default value is set to 3 289 | """ 290 | 291 | if not type(width) is int or width < 0: 292 | raise ValueError('width must be an integer greater then 0') 293 | self._precision = width 294 | 295 | def header(self, array): 296 | """Specify the header of the table 297 | """ 298 | 299 | self._check_row_size(array) 300 | self._header = list(map(str, array)) 301 | 302 | def add_row(self, array): 303 | """Add a row in the rows stack 304 | 305 | - cells can contain newlines and tabs 306 | """ 307 | 308 | self._check_row_size(array) 309 | 310 | if not hasattr(self, "_dtype"): 311 | self._dtype = ["a"] * self._row_size 312 | 313 | cells = [] 314 | for i,x in enumerate(array): 315 | cells.append(self._str(i,x)) 316 | self._rows.append(cells) 317 | 318 | def add_rows(self, rows, header=True): 319 | """Add several rows in the rows stack 320 | 321 | - The 'rows' argument can be either an iterator returning arrays, 322 | or a by-dimensional array 323 | - 'header' specifies if the first row should be used as the header 324 | of the table 325 | """ 326 | 327 | # nb: don't use 'iter' on by-dimensional arrays, to get a 328 | # usable code for python 2.1 329 | if header: 330 | if hasattr(rows, '__iter__') and hasattr(rows, 'next'): 331 | self.header(next(rows)) 332 | else: 333 | self.header(rows[0]) 334 | rows = rows[1:] 335 | for row in rows: 336 | self.add_row(row) 337 | 338 | 339 | def draw(self): 340 | """Draw the table 341 | 342 | - the table is returned as a whole string 343 | """ 344 | 345 | if not self._header and not self._rows: 346 | return 347 | self._compute_cols_width() 348 | self._check_align() 349 | out = "" 350 | if self._has_border(): 351 | out += self._hline() 352 | if self._header: 353 | out += self._draw_line(self._header, isheader=True) 354 | if self._has_header(): 355 | out += self._hline_header() 356 | length = 0 357 | for row in self._rows: 358 | length += 1 359 | out += self._draw_line(row) 360 | if self._has_hlines() and length < len(self._rows): 361 | out += self._hline() 362 | if self._has_border(): 363 | out += self._hline() 364 | return out[:-1] 365 | 366 | def _str(self, i, x): 367 | """Handles string formatting of cell data 368 | 369 | i - index of the cell datatype in self._dtype 370 | x - cell data to format 371 | """ 372 | try: 373 | f = float(x) 374 | n = str(f) 375 | if n == "nan" or n=="inf" or n=="-inf" : raise ValueError('Infinity or NaN considered as string') 376 | except: 377 | if type(x) is str: 378 | return x 379 | else: 380 | if x is None: 381 | return str(x) 382 | else: 383 | return str(x.encode('utf-8')) 384 | 385 | n = self._precision 386 | dtype = self._dtype[i] 387 | 388 | if dtype == 'i': 389 | return str(int(round(f))) 390 | elif dtype == 'f': 391 | return '%.*f' % (n, f) 392 | elif dtype == 'e': 393 | return '%.*e' % (n, f) 394 | elif dtype == 't': 395 | if type(x) is str: 396 | return x 397 | else: 398 | if x is None: 399 | return str(x) 400 | else: 401 | return str(x.encode('utf-8')) 402 | else: 403 | if f - round(f) == 0: 404 | if abs(f) > 1e8: 405 | return '%.*e' % (n, f) 406 | else: 407 | return str(int(round(f))) 408 | else: 409 | if abs(f) > 1e8: 410 | return '%.*e' % (n, f) 411 | else: 412 | return '%.*f' % (n, f) 413 | 414 | def _check_row_size(self, array): 415 | """Check that the specified array fits the previous rows size 416 | """ 417 | 418 | if not self._row_size: 419 | self._row_size = len(array) 420 | elif self._row_size != len(array): 421 | raise ArraySizeError("array should contain %d elements" \ 422 | % self._row_size) 423 | 424 | def _has_vlines(self): 425 | """Return a boolean, if vlines are required or not 426 | """ 427 | 428 | return self._deco & Texttable.VLINES > 0 429 | 430 | def _has_hlines(self): 431 | """Return a boolean, if hlines are required or not 432 | """ 433 | 434 | return self._deco & Texttable.HLINES > 0 435 | 436 | def _has_border(self): 437 | """Return a boolean, if border is required or not 438 | """ 439 | 440 | return self._deco & Texttable.BORDER > 0 441 | 442 | def _has_header(self): 443 | """Return a boolean, if header line is required or not 444 | """ 445 | 446 | return self._deco & Texttable.HEADER > 0 447 | 448 | def _hline_header(self): 449 | """Print header's horizontal line 450 | """ 451 | 452 | return self._build_hline(True) 453 | 454 | def _hline(self): 455 | """Print an horizontal line 456 | """ 457 | 458 | if not self._hline_string: 459 | self._hline_string = self._build_hline() 460 | return self._hline_string 461 | 462 | def _build_hline(self, is_header=False): 463 | """Return a string used to separated rows or separate header from 464 | rows 465 | """ 466 | horiz = self._char_horiz 467 | if (is_header): 468 | horiz = self._char_header 469 | # compute cell separator 470 | s = "%s%s%s" % (horiz, [horiz, self._char_corner][self._has_vlines()], 471 | horiz) 472 | # build the line 473 | l = s.join([horiz * n for n in self._width]) 474 | # add border if needed 475 | if self._has_border(): 476 | l = "%s%s%s%s%s\n" % (self._char_corner, horiz, l, horiz, 477 | self._char_corner) 478 | else: 479 | l += "\n" 480 | return l 481 | 482 | def _len_cell(self, cell): 483 | """Return the width of the cell 484 | 485 | Special characters are taken into account to return the width of the 486 | cell, such like newlines and tabs 487 | """ 488 | 489 | cell = re.compile(r'\x1b[^m]*m').sub('', cell) 490 | 491 | cell_lines = cell.split('\n') 492 | maxi = 0 493 | for line in cell_lines: 494 | length = 0 495 | parts = line.split('\t') 496 | for part, i in zip(parts, list(range(1, len(parts) + 1))): 497 | length = length + len(part) 498 | if i < len(parts): 499 | length = (length//8 + 1) * 8 500 | maxi = max(maxi, length) 501 | return maxi 502 | 503 | def _compute_cols_width(self): 504 | """Return an array with the width of each column 505 | 506 | If a specific width has been specified, exit. If the total of the 507 | columns width exceed the table desired width, another width will be 508 | computed to fit, and cells will be wrapped. 509 | """ 510 | 511 | if hasattr(self, "_width"): 512 | return 513 | maxi = [] 514 | if self._header: 515 | maxi = [ self._len_cell(x) for x in self._header ] 516 | for row in self._rows: 517 | for cell,i in zip(row, list(range(len(row)))): 518 | try: 519 | maxi[i] = max(maxi[i], self._len_cell(cell)) 520 | except (TypeError, IndexError): 521 | maxi.append(self._len_cell(cell)) 522 | items = len(maxi) 523 | length = reduce(lambda x,y: x+y, maxi) 524 | if self._max_width and length + items * 3 + 1 > self._max_width: 525 | max_lengths = maxi 526 | maxi = [(self._max_width - items * 3 -1) // items \ 527 | for n in range(items)] 528 | 529 | # free space to distribute 530 | free = 0 531 | 532 | # how many columns are oversized 533 | oversized = 0 534 | 535 | # reduce size of columns that need less space and calculate how 536 | # much space is freed 537 | for col, max_len in enumerate(max_lengths): 538 | current_length = maxi[col] 539 | 540 | # column needs less space, adjust and 541 | # update free space 542 | if current_length > max_len: 543 | free += current_length - max_len 544 | maxi[col] = max_len 545 | 546 | # column needs more space, count it 547 | elif max_len > current_length: 548 | oversized += 1 549 | 550 | # as long as free space is available, distribute it 551 | while free > 0: 552 | # available free space for each oversized column 553 | free_part = int(math.ceil(float(free) / float(oversized))) 554 | 555 | for col, max_len in enumerate(max_lengths): 556 | current_length = maxi[col] 557 | 558 | # column needs more space 559 | if current_length < max_len: 560 | 561 | # how much space is needed 562 | needed = max_len - current_length 563 | 564 | # enough free space for column 565 | if needed <= free_part: 566 | maxi[col] = max_len 567 | free -= needed 568 | oversized -= 1 569 | 570 | # still oversized after re-sizing 571 | else: 572 | maxi[col] = maxi[col] + free_part 573 | free -= free_part 574 | self._width = maxi 575 | 576 | def _check_align(self): 577 | """Check if alignment has been specified, set default one if not 578 | """ 579 | 580 | if not hasattr(self, "_align"): 581 | self._align = ["l"] * self._row_size 582 | if not hasattr(self, "_valign"): 583 | self._valign = ["t"] * self._row_size 584 | 585 | def _draw_line(self, line, isheader=False): 586 | """Draw a line 587 | 588 | Loop over a single cell length, over all the cells 589 | """ 590 | 591 | line = self._splitit(line, isheader) 592 | space = " " 593 | out = "" 594 | for i in range(len(line[0])): 595 | if self._has_border(): 596 | out += "%s " % self._char_vert 597 | length = 0 598 | for cell, width, align in zip(line, self._width, self._align): 599 | length += 1 600 | cell_line = cell[i] 601 | 602 | fill = width - len(re.compile(r'\x1b[^m]*m').sub('', cell_line)) 603 | if isheader: 604 | align = "c" 605 | if align == "r": 606 | out += "%s " % (fill * space + cell_line) 607 | elif align == "c": 608 | out += "%s " % (fill//2 * space + cell_line \ 609 | + (fill//2 + fill%2) * space) 610 | else: 611 | out += "%s " % (cell_line + fill * space) 612 | if length < len(line): 613 | out += "%s " % [space, self._char_vert][self._has_vlines()] 614 | out += "%s\n" % ['', self._char_vert][self._has_border()] 615 | return out 616 | 617 | def _splitit(self, line, isheader): 618 | """Split each element of line to fit the column width 619 | 620 | Each element is turned into a list, result of the wrapping of the 621 | string to the desired width 622 | """ 623 | 624 | line_wrapped = [] 625 | for cell, width in zip(line, self._width): 626 | array = [] 627 | original_cell = cell 628 | ansi_keep = [] 629 | for c in cell.split('\n'): 630 | c = "".join(ansi_keep) + c 631 | ansi_keep = [] 632 | extra_width = 0 633 | for a in re.findall(r'\x1b[^m]*m', c): 634 | extra_width += len(a) 635 | if a == '\x1b[0m': 636 | if len(ansi_keep) > 0: 637 | ansi_keep.pop() 638 | else: 639 | ansi_keep.append(a) 640 | c = c + '\x1b[0m' * len(ansi_keep) 641 | extra_width += len('\x1b[0m' * len(ansi_keep)) 642 | if type(c) is not str: 643 | try: 644 | c = str(c, 'utf') 645 | except UnicodeDecodeError as strerror: 646 | sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (c, strerror)) 647 | c = str(c, 'utf', 'replace') 648 | array.extend(textwrap.wrap(c, width + extra_width)) 649 | line_wrapped.append(array) 650 | max_cell_lines = reduce(max, list(map(len, line_wrapped))) 651 | for cell, valign in zip(line_wrapped, self._valign): 652 | if isheader: 653 | valign = "t" 654 | if valign == "m": 655 | missing = max_cell_lines - len(cell) 656 | cell[:0] = [""] * (missing // 2) 657 | cell.extend([""] * (missing // 2 + missing % 2)) 658 | elif valign == "b": 659 | cell[:0] = [""] * (max_cell_lines - len(cell)) 660 | else: 661 | cell.extend([""] * (max_cell_lines - len(cell))) 662 | return line_wrapped 663 | 664 | if __name__ == '__main__': 665 | table = Texttable() 666 | table.set_cols_align(["l", "r", "c"]) 667 | table.set_cols_valign(["t", "m", "b"]) 668 | table.add_rows([ [get_color_string(bcolors.GREEN, "Name Of Person"), "Age", get_color_string(bcolors.UNDERLINE, "Nickname")], 669 | ["Mr\n" + get_color_string(bcolors.BOLD, "Xavier\nHuon"), 32, "Xav'"], 670 | [get_color_string(bcolors.BLUE,get_color_string(bcolors.BOLD, "Mr\nBaptiste") + "\n" + get_color_string(bcolors.UNDERLINE, "Clement")), 1, get_color_string(bcolors.RED,"Baby")] ]) 671 | print(table.draw() + "\n") 672 | 673 | table = Texttable() 674 | table.set_deco(Texttable.HEADER) 675 | table.set_cols_dtype(['t', # text 676 | 'f', # float (decimal) 677 | 'e', # float (exponent) 678 | 'i', # integer 679 | 'a']) # automatic 680 | table.set_cols_align(["l", "r", "r", "r", "l"]) 681 | table.add_rows([['text', "float", "exp", "int", "auto"], 682 | ["abcd", "67", 654, 89, 128.001], 683 | ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], 684 | ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], 685 | ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) 686 | print(table.draw()) 687 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py26, py27, py32, py33, py34, pypy 8 | 9 | [testenv] 10 | commands = py.test --cov=texttable --cov-report=term-missing test_texttable.py 11 | deps = 12 | pytest 13 | pytest-cov 14 | --------------------------------------------------------------------------------