Commit fedcde2099e1ae08667b00fd583155700b09ae3b
1 parent
02bebc79
added prettytable, used in oleid and oletimes to improve output
Showing
7 changed files
with
2185 additions
and
6 deletions
oletools/oleid.py
| ... | ... | @@ -47,8 +47,9 @@ http://www.decalage.info/python/oletools |
| 47 | 47 | # 2012-10-29 v0.01 PL: - first version |
| 48 | 48 | # 2014-11-29 v0.02 PL: - use olefile instead of OleFileIO_PL |
| 49 | 49 | # - improved usage display with -h |
| 50 | +# 2014-11-30 v0.03 PL: - improved output with prettytable | |
| 50 | 51 | |
| 51 | -__version__ = '0.02' | |
| 52 | +__version__ = '0.03' | |
| 52 | 53 | |
| 53 | 54 | |
| 54 | 55 | #------------------------------------------------------------------------------ |
| ... | ... | @@ -71,6 +72,7 @@ __version__ = '0.02' |
| 71 | 72 | |
| 72 | 73 | import optparse, sys, os, re, zlib, struct |
| 73 | 74 | import thirdparty.olefile as olefile |
| 75 | +from thirdparty.prettytable import prettytable | |
| 74 | 76 | |
| 75 | 77 | |
| 76 | 78 | #=== FUNCTIONS =============================================================== |
| ... | ... | @@ -276,8 +278,17 @@ def main(): |
| 276 | 278 | print '\nFilename:', filename |
| 277 | 279 | oleid = OleID(filename) |
| 278 | 280 | indicators = oleid.check() |
| 281 | + | |
| 282 | + t = prettytable.PrettyTable(['Indicator', 'Value']) | |
| 283 | + t.align = 'l' | |
| 284 | + t.max_width = 39 | |
| 285 | + #t.border = False | |
| 286 | + | |
| 279 | 287 | for indicator in indicators: |
| 280 | - print '%s: %s' % (indicator.name, indicator.value) | |
| 288 | + #print '%s: %s' % (indicator.name, indicator.value) | |
| 289 | + t.add_row((indicator.name, indicator.value)) | |
| 290 | + | |
| 291 | + print t | |
| 281 | 292 | |
| 282 | 293 | if __name__ == '__main__': |
| 283 | 294 | main() | ... | ... |
oletools/oletimes.py
| ... | ... | @@ -45,8 +45,9 @@ http://www.decalage.info/python/oletools |
| 45 | 45 | # 2013-07-24 v0.01 PL: - first version |
| 46 | 46 | # 2014-11-29 v0.02 PL: - use olefile instead of OleFileIO_PL |
| 47 | 47 | # - improved usage display |
| 48 | +# 2014-11-30 v0.03 PL: - improved output with prettytable | |
| 48 | 49 | |
| 49 | -__version__ = '0.02' | |
| 50 | +__version__ = '0.03' | |
| 50 | 51 | |
| 51 | 52 | #------------------------------------------------------------------------------ |
| 52 | 53 | # TODO: |
| ... | ... | @@ -57,8 +58,9 @@ __version__ = '0.02' |
| 57 | 58 | |
| 58 | 59 | #=== IMPORTS ================================================================= |
| 59 | 60 | |
| 60 | -import sys | |
| 61 | +import sys, datetime | |
| 61 | 62 | import thirdparty.olefile as olefile |
| 63 | +from thirdparty.prettytable import prettytable | |
| 62 | 64 | |
| 63 | 65 | |
| 64 | 66 | #=== MAIN ================================================================= |
| ... | ... | @@ -68,9 +70,30 @@ try: |
| 68 | 70 | except IndexError: |
| 69 | 71 | sys.exit(__doc__) |
| 70 | 72 | |
| 71 | -print'- Root mtime=%s ctime=%s' % (ole.root.getmtime(), ole.root.getctime()) | |
| 73 | +def dt2str (dt): | |
| 74 | + """ | |
| 75 | + Convert a datetime object to a string for display, without microseconds | |
| 76 | + | |
| 77 | + :param dt: datetime.datetime object, or None | |
| 78 | + :return: str, or None | |
| 79 | + """ | |
| 80 | + if dt is None: | |
| 81 | + return None | |
| 82 | + dt = dt.replace(microsecond = 0) | |
| 83 | + return str(dt) | |
| 84 | + | |
| 85 | +t = prettytable.PrettyTable(['Stream/Storage name', 'Modification Time', 'Creation Time']) | |
| 86 | +t.align = 'l' | |
| 87 | +t.max_width = 26 | |
| 88 | +#t.border = False | |
| 89 | + | |
| 90 | +#print'- Root mtime=%s ctime=%s' % (ole.root.getmtime(), ole.root.getctime()) | |
| 91 | +t.add_row(('Root', dt2str(ole.root.getmtime()), dt2str(ole.root.getctime()))) | |
| 72 | 92 | |
| 73 | 93 | for obj in ole.listdir(streams=True, storages=True): |
| 74 | - print '- %s: mtime=%s ctime=%s' % (repr('/'.join(obj)), ole.getmtime(obj), ole.getctime(obj)) | |
| 94 | + #print '- %s: mtime=%s ctime=%s' % (repr('/'.join(obj)), ole.getmtime(obj), ole.getctime(obj)) | |
| 95 | + t.add_row((repr('/'.join(obj)), dt2str(ole.getmtime(obj)), dt2str(ole.getctime(obj)))) | |
| 96 | + | |
| 97 | +print t | |
| 75 | 98 | |
| 76 | 99 | ole.close() | ... | ... |
oletools/thirdparty/prettytable/CHANGELOG
0 → 100644
| 1 | +########## PrettyTable 0.7 - Feb 17, 2013 ########### | |
| 2 | + | |
| 3 | +* Improved Python 2 and 3 compatibility (2.4-3.2). | |
| 4 | +* Improved support for non-Latin characters. Table widths should | |
| 5 | + now be calculated correctly for tables with e.g. Japanese text. | |
| 6 | +* Table contents can now be read in from a .csv file | |
| 7 | +* Table contents can now be read in from a DB-API compatible cursor | |
| 8 | +* Table contents can now be read in from a string containing a | |
| 9 | + HTML table (thanks to Christoph Robbert for submitting this patch!) | |
| 10 | +* new valign attribute controls vertical alignment of text when | |
| 11 | + some cells in a row have multiple lines of text and others don't. | |
| 12 | + (thanks to Google Code user maartendb for submitting this patch!) | |
| 13 | +* hrules attribute can now be set to HEADER, which draws a rule only | |
| 14 | + under the header row | |
| 15 | +* new vrules attribute controls drawing of vertical rules and can | |
| 16 | + be set to FRAME, ALL or NONE | |
| 17 | +* new header_style attribute controls formatting of text in table | |
| 18 | + headers and can be set to "cap", "title", "upper", "lower" or None | |
| 19 | +* Fixed a simple bug regarding validation of max_width (thanks to | |
| 20 | + Anthony Toole for pointing out this bug and providing a patch). | |
| 21 | +* Fixed a simple bug regarding initialisation of int_format value | |
| 22 | + for new tables (thanks to Ingo Schmiegel for pointing out this | |
| 23 | + bug!) | |
| 24 | +* Fixed a bug regarding some constructor keywords, such as "border", | |
| 25 | + being ignored (thanks to Google Code user antonio.s.messina for | |
| 26 | + reporting this bug). | |
| 27 | + | |
| 28 | +########## PrettyTable 0.6 - May 5, 2012 ########## | |
| 29 | + | |
| 30 | +* Code is now simultaneously compatible with Python 2 and 3 | |
| 31 | +* Replaced all setter methods with managed attributes | |
| 32 | +* All styling options can now be set persistently as managed attributes | |
| 33 | +* Added "add_style" method to make setting style options easily | |
| 34 | +* Added "del_row", "clear_rows" and "clear" methods to facilitate | |
| 35 | + removal of data from table. | |
| 36 | +* Added "copy" method to facilitate cloning of a table. | |
| 37 | +* Removed caching functionality, which added complexity and fragility | |
| 38 | + for relatively little gain | |
| 39 | +* Removed methods that just printed strings produced by get_string and | |
| 40 | + get_html_string - just use inbuilt print! | |
| 41 | +* Improved unicode support (thanks to Google Code user ru.w31rd0 for | |
| 42 | + patch!) | |
| 43 | +* Added support for decimal and floating point number formatting | |
| 44 | + support (thanks to Google Code user willfurnass for the suggestion!) | |
| 45 | +* Added support for using a custom key sorting methods (thanks to | |
| 46 | + Google Code user amannijhawan for the suggestion!) | |
| 47 | +* Added support for line breaks in data (suggested and implemented by | |
| 48 | + Klein Stephane) | |
| 49 | +* Added support for max column widths (thanks to Tibor Arpas for the | |
| 50 | + suggestion!) | |
| 51 | +* Fixed table slicing | |
| 52 | +* Fixed bug where closing <tr/> tags in HTML tables were not printed | |
| 53 | + (thanks to Google Code user kehander for reporting this bug!) | |
| 54 | +* Fixed HTML table sorting bug (thanks to Google Code user dougbeal | |
| 55 | + for reporting this bug!) | |
| 56 | +* Fixed bug whereby changing field_names did not recompute widths | |
| 57 | + (thanks to Google Code user denilsonsa for reporting this bug!) | |
| 58 | + | |
| 59 | +########## PrettyTable 0.5 - May 26, 2009 ########## | |
| 60 | + | |
| 61 | +* Fixed a bug whereby printing with headers=False and border=False | |
| 62 | + would introduce an extraneous newline. Thanks to Alexander Lamaison | |
| 63 | + for reporting this bug. | |
| 64 | +* When printing with headers=False, column widths will now be reduced | |
| 65 | + as appropriate in columns where the field name is wider than the | |
| 66 | + data. Thanks to Alexander Lamaison for suggesting this behaviour. | |
| 67 | +* Support for Unicode has improved. Thanks to Chris Clark for | |
| 68 | + submitting this improvement. | |
| 69 | +* The value of the "border" argument now correctly controls the | |
| 70 | + presence of a border when printing HTML tables with print_html or | |
| 71 | + get_html_string, instead of being incorrectly ignored. Thanks to | |
| 72 | + Chris Clark for fixing this. | |
| 73 | +* The print_html and get_html_string methods now accept an | |
| 74 | + "attributes" argument which is a dictionary of name/value pairs to be | |
| 75 | + placed inside the <table> tag (so you can, e.g. set class, name or id | |
| 76 | + values in order to style your table with CSS). Thanks to Chris Clark | |
| 77 | + for submitting this feature. | |
| 78 | +* The print_html and get_html_string methods now, by default, do their | |
| 79 | + best to match the various formatting options in their HTML output. | |
| 80 | + They use inline CSS to adjust the alignment of data in columns, the | |
| 81 | + padding widths of columns and in some cases the border settings. You | |
| 82 | + can give either method a "format=False" attribute to turn this | |
| 83 | + behaviour off if you want to do your own styling. With "format=False" | |
| 84 | + the methods print a "bare bones" table, similar to the default | |
| 85 | + behaviour in 0.4. | |
| 86 | + | |
| 87 | +########## PrettyTable 0.4 - May 13, 2009 ########## | |
| 88 | + | |
| 89 | +* Added "add_column" method to enable building tables up column-by-column. | |
| 90 | +* Added "print_HTML" and "get_HTML_string" methods to enable HTML table | |
| 91 | + production. | |
| 92 | +* Added "set_border_chars" method to enable control over characters used to | |
| 93 | + draw the table border. | |
| 94 | +* Added "set_left_padding" and "set_right_padding" methods to allow | |
| 95 | + independent padding control for both sides of a column. | |
| 96 | +* Added "sortby" option to enable column sorting. | |
| 97 | +* Added "header" option to enable switching off field name printing at top of | |
| 98 | + table. | |
| 99 | +* Modified "hrules" option to enable greater control over presence of | |
| 100 | + horizontal lines. | |
| 101 | +* Added "border" option to enable switching off all line printing. | |
| 102 | + | |
| 103 | +Thanks to Tim Cera, Chris Clark, Alexander Lamaison for suggesting and helping | |
| 104 | +to test many of the new features in this release. | |
| 105 | + | |
| 106 | +########## PrettyTable 0.3 - May 01, 2009 ########## | |
| 107 | + | |
| 108 | +* Added "padding_width" option to control the number of spaces between the | |
| 109 | + vertical line rules at the edges of a column and its content. This can be | |
| 110 | + set as a keyword argument to the constructor or after instantiation using | |
| 111 | + the "set_padding_width" method. The value is set to 1 by defaut. If your | |
| 112 | + table is too wide for a small screen with this value, setting it to 0 might | |
| 113 | + help you squeeze it in. | |
| 114 | + | |
| 115 | +Thanks to Chris Clark for contributing a patch against 0.2.1 to add this | |
| 116 | +feature! | |
| 117 | + | |
| 118 | +########## PrettyTable 0.2.1 - April 29, 2009 ########## | |
| 119 | + | |
| 120 | +* Caching no longer breaks when using the "printt(fields=[...])" syntax. The | |
| 121 | + list of fields was not hashable and hence could not be used as a dictionary | |
| 122 | + key. I fixed this using the output of the "cPickle" module's "dumps" | |
| 123 | + function as the dictionary key instead. | |
| 124 | +* Horizontal lines are now the appropriate length when the above syntax is | |
| 125 | + used. | |
| 126 | + | |
| 127 | +Thanks to Julien Koesten for reporting these bugs and testing the fixes almost | |
| 128 | +immediately after the release of 0.2! | |
| 129 | + | |
| 130 | +########## PrettyTable 0.2 - April 29, 2009 ########## | |
| 131 | + | |
| 132 | +* Added "get_string" method. | |
| 133 | +* Added "__str__" method (which just calls "get_string") to enable nice | |
| 134 | + "print x" syntax. | |
| 135 | +* Can now pass field names as a constructor argument. | |
| 136 | +* Return values of "get_string" are cached in a dictionary that is only | |
| 137 | + cleared after a call to "add_row" or something else which invalidates the | |
| 138 | + cache. | |
| 139 | + | |
| 140 | +########## PrettyTable 0.1 - February 26, 2009 ######### | |
| 141 | + | |
| 142 | +* Original release | ... | ... |
oletools/thirdparty/prettytable/COPYING
0 → 100644
| 1 | +# Copyright (c) 2009-2013 Luke Maurits <luke@maurits.id.au> | |
| 2 | +# All rights reserved. | |
| 3 | +# With contributions from: | |
| 4 | +# * Chris Clark | |
| 5 | +# * Christoph Robbert | |
| 6 | +# * Klein Stephane | |
| 7 | +# * "maartendb" | |
| 8 | +# | |
| 9 | +# Redistribution and use in source and binary forms, with or without | |
| 10 | +# modification, are permitted provided that the following conditions are met: | |
| 11 | +# | |
| 12 | +# * Redistributions of source code must retain the above copyright notice, | |
| 13 | +# this list of conditions and the following disclaimer. | |
| 14 | +# * Redistributions in binary form must reproduce the above copyright notice, | |
| 15 | +# this list of conditions and the following disclaimer in the documentation | |
| 16 | +# and/or other materials provided with the distribution. | |
| 17 | +# * The name of the author may not be used to endorse or promote products | |
| 18 | +# derived from this software without specific prior written permission. | |
| 19 | +# | |
| 20 | +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| 21 | +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| 22 | +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| 23 | +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | |
| 24 | +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| 25 | +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| 26 | +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| 27 | +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| 28 | +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| 29 | +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| 30 | +# POSSIBILITY OF SUCH DAMAGE. | ... | ... |
oletools/thirdparty/prettytable/README
0 → 100644
| 1 | +TUTORIAL ON HOW TO USE THE PRETTYTABLE 0.6+ API | |
| 2 | + | |
| 3 | +*** This tutorial is distributed with PrettyTable and is meant to serve | |
| 4 | +as a "quick start" guide for the lazy or impatient. It is not an | |
| 5 | +exhaustive description of the whole API, and it is not guaranteed to be | |
| 6 | +100% up to date. For more complete and update documentation, check the | |
| 7 | +PrettyTable wiki at http://code.google.com/p/prettytable/w/list *** | |
| 8 | + | |
| 9 | += Getting your data into (and out of) the table = | |
| 10 | + | |
| 11 | +Let's suppose you have a shiny new PrettyTable: | |
| 12 | + | |
| 13 | +from prettytable import PrettyTable | |
| 14 | +x = PrettyTable() | |
| 15 | + | |
| 16 | +and you want to put some data into it. You have a few options. | |
| 17 | + | |
| 18 | +== Row by row == | |
| 19 | + | |
| 20 | +You can add data one row at a time. To do this you can set the field names | |
| 21 | +first using the `field_names` attribute, and then add the rows one at a time | |
| 22 | +using the `add_row` method: | |
| 23 | + | |
| 24 | +x.field_names = ["City name", "Area", "Population", "Annual Rainfall"] | |
| 25 | +x.add_row(["Adelaide",1295, 1158259, 600.5]) | |
| 26 | +x.add_row(["Brisbane",5905, 1857594, 1146.4]) | |
| 27 | +x.add_row(["Darwin", 112, 120900, 1714.7]) | |
| 28 | +x.add_row(["Hobart", 1357, 205556, 619.5]) | |
| 29 | +x.add_row(["Sydney", 2058, 4336374, 1214.8]) | |
| 30 | +x.add_row(["Melbourne", 1566, 3806092, 646.9]) | |
| 31 | +x.add_row(["Perth", 5386, 1554769, 869.4]) | |
| 32 | + | |
| 33 | +== Column by column == | |
| 34 | + | |
| 35 | +You can add data one column at a time as well. To do this you use the | |
| 36 | +`add_column` method, which takes two arguments - a string which is the name for | |
| 37 | +the field the column you are adding corresponds to, and a list or tuple which | |
| 38 | +contains the column data" | |
| 39 | + | |
| 40 | +x.add_column("City name", | |
| 41 | +["Adelaide","Brisbane","Darwin","Hobart","Sydney","Melbourne","Perth"]) | |
| 42 | +x.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386]) | |
| 43 | +x.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, | |
| 44 | +1554769]) | |
| 45 | +x.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, | |
| 46 | +869.4]) | |
| 47 | + | |
| 48 | +== Mixing and matching == | |
| 49 | + | |
| 50 | +If you really want to, you can even mix and match `add_row` and `add_column` | |
| 51 | +and build some of your table in one way and some of it in the other. There's a | |
| 52 | +unit test which makes sure that doing things this way will always work out | |
| 53 | +nicely as if you'd done it using just one of the two approaches. Tables built | |
| 54 | +this way are kind of confusing for other people to read, though, so don't do | |
| 55 | +this unless you have a good reason. | |
| 56 | + | |
| 57 | +== Importing data from a CSV file == | |
| 58 | + | |
| 59 | +If you have your table data in a comma separated values file (.csv), you can | |
| 60 | +read this data into a PrettyTable like this: | |
| 61 | + | |
| 62 | +from prettytable import from_csv | |
| 63 | +fp = open("myfile.csv", "r") | |
| 64 | +mytable = from_csv(fp) | |
| 65 | +fp.close() | |
| 66 | + | |
| 67 | +== Importing data from a HTML string == | |
| 68 | + | |
| 69 | +If you have a string containing a HTML <table>, you can read this data into a | |
| 70 | +PrettyTable like this: | |
| 71 | + | |
| 72 | +from prettytable import from_html | |
| 73 | +mytable = from_html(html_string) | |
| 74 | + | |
| 75 | +== Importing data from a database cursor == | |
| 76 | + | |
| 77 | +If you have your table data in a database which you can access using a library | |
| 78 | +which confirms to the Python DB-API (e.g. an SQLite database accessible using | |
| 79 | +the sqlite module), then you can build a PrettyTable using a cursor object, | |
| 80 | +like this: | |
| 81 | + | |
| 82 | +import sqlite3 | |
| 83 | +from prettytable import from_db_cursor | |
| 84 | + | |
| 85 | +connection = sqlite3.connect("mydb.db") | |
| 86 | +cursor = connection.cursor() | |
| 87 | +cursor.execute("SELECT field1, field2, field3 FROM my_table") | |
| 88 | +mytable = from_db_cursor(cursor) | |
| 89 | + | |
| 90 | +== Getting data out == | |
| 91 | + | |
| 92 | +There are three ways to get data out of a PrettyTable, in increasing order of | |
| 93 | +completeness: | |
| 94 | + | |
| 95 | + * The `del_row` method takes an integer index of a single row to delete. | |
| 96 | + * The `clear_rows` method takes no arguments and deletes all the rows in the | |
| 97 | +table - but keeps the field names as they were so you that you can repopulate | |
| 98 | +it with the same kind of data. | |
| 99 | + * The `clear` method takes no arguments and deletes all rows and all field | |
| 100 | +names. It's not quite the same as creating a fresh table instance, though - | |
| 101 | +style related settings, discussed later, are maintained. | |
| 102 | + | |
| 103 | += Displaying your table in ASCII form = | |
| 104 | + | |
| 105 | +PrettyTable's main goal is to let you print tables in an attractive ASCII form, | |
| 106 | +like this: | |
| 107 | + | |
| 108 | ++-----------+------+------------+-----------------+ | |
| 109 | +| City name | Area | Population | Annual Rainfall | | |
| 110 | ++-----------+------+------------+-----------------+ | |
| 111 | +| Adelaide | 1295 | 1158259 | 600.5 | | |
| 112 | +| Brisbane | 5905 | 1857594 | 1146.4 | | |
| 113 | +| Darwin | 112 | 120900 | 1714.7 | | |
| 114 | +| Hobart | 1357 | 205556 | 619.5 | | |
| 115 | +| Melbourne | 1566 | 3806092 | 646.9 | | |
| 116 | +| Perth | 5386 | 1554769 | 869.4 | | |
| 117 | +| Sydney | 2058 | 4336374 | 1214.8 | | |
| 118 | ++-----------+------+------------+-----------------+ | |
| 119 | + | |
| 120 | +You can print tables like this to `stdout` or get string representations of | |
| 121 | +them. | |
| 122 | + | |
| 123 | +== Printing == | |
| 124 | + | |
| 125 | +To print a table in ASCII form, you can just do this: | |
| 126 | + | |
| 127 | +print x | |
| 128 | + | |
| 129 | +in Python 2.x or: | |
| 130 | + | |
| 131 | +print(x) | |
| 132 | + | |
| 133 | +in Python 3.x. | |
| 134 | + | |
| 135 | +The old x.printt() method from versions 0.5 and earlier has been removed. | |
| 136 | + | |
| 137 | +To pass options changing the look of the table, use the get_string() method | |
| 138 | +documented below: | |
| 139 | + | |
| 140 | +print x.get_string() | |
| 141 | + | |
| 142 | +== Stringing == | |
| 143 | + | |
| 144 | +If you don't want to actually print your table in ASCII form but just get a | |
| 145 | +string containing what _would_ be printed if you use "print x", you can use | |
| 146 | +the `get_string` method: | |
| 147 | + | |
| 148 | +mystring = x.get_string() | |
| 149 | + | |
| 150 | +This string is guaranteed to look exactly the same as what would be printed by | |
| 151 | +doing "print x". You can now do all the usual things you can do with a | |
| 152 | +string, like write your table to a file or insert it into a GUI. | |
| 153 | + | |
| 154 | +== Controlling which data gets displayed == | |
| 155 | + | |
| 156 | +If you like, you can restrict the output of `print x` or `x.get_string` to | |
| 157 | +only the fields or rows you like. | |
| 158 | + | |
| 159 | +The `fields` argument to these methods takes a list of field names to be | |
| 160 | +printed: | |
| 161 | + | |
| 162 | +print x.get_string(fields=["City name", "Population"]) | |
| 163 | + | |
| 164 | +gives: | |
| 165 | + | |
| 166 | ++-----------+------------+ | |
| 167 | +| City name | Population | | |
| 168 | ++-----------+------------+ | |
| 169 | +| Adelaide | 1158259 | | |
| 170 | +| Brisbane | 1857594 | | |
| 171 | +| Darwin | 120900 | | |
| 172 | +| Hobart | 205556 | | |
| 173 | +| Melbourne | 3806092 | | |
| 174 | +| Perth | 1554769 | | |
| 175 | +| Sydney | 4336374 | | |
| 176 | ++-----------+------------+ | |
| 177 | + | |
| 178 | +The `start` and `end` arguments take the index of the first and last row to | |
| 179 | +print respectively. Note that the indexing works like Python list slicing - to | |
| 180 | +print the 2nd, 3rd and 4th rows of the table, set `start` to 1 (the first row | |
| 181 | +is row 0, so the second is row 1) and set `end` to 4 (the index of the 4th row, | |
| 182 | +plus 1): | |
| 183 | + | |
| 184 | +print x.get_string(start=1,end=4) | |
| 185 | + | |
| 186 | +prints: | |
| 187 | + | |
| 188 | ++-----------+------+------------+-----------------+ | |
| 189 | +| City name | Area | Population | Annual Rainfall | | |
| 190 | ++-----------+------+------------+-----------------+ | |
| 191 | +| Brisbane | 5905 | 1857594 | 1146.4 | | |
| 192 | +| Darwin | 112 | 120900 | 1714.7 | | |
| 193 | +| Hobart | 1357 | 205556 | 619.5 | | |
| 194 | ++-----------+------+------------+-----------------+ | |
| 195 | + | |
| 196 | +== Changing the alignment of columns == | |
| 197 | + | |
| 198 | +By default, all columns in a table are centre aligned. | |
| 199 | + | |
| 200 | +=== All columns at once === | |
| 201 | + | |
| 202 | +You can change the alignment of all the columns in a table at once by assigning | |
| 203 | +a one character string to the `align` attribute. The allowed strings are "l", | |
| 204 | +"r" and "c" for left, right and centre alignment, respectively: | |
| 205 | + | |
| 206 | +x.align = "r" | |
| 207 | +print x | |
| 208 | + | |
| 209 | +gives: | |
| 210 | + | |
| 211 | ++-----------+------+------------+-----------------+ | |
| 212 | +| City name | Area | Population | Annual Rainfall | | |
| 213 | ++-----------+------+------------+-----------------+ | |
| 214 | +| Adelaide | 1295 | 1158259 | 600.5 | | |
| 215 | +| Brisbane | 5905 | 1857594 | 1146.4 | | |
| 216 | +| Darwin | 112 | 120900 | 1714.7 | | |
| 217 | +| Hobart | 1357 | 205556 | 619.5 | | |
| 218 | +| Melbourne | 1566 | 3806092 | 646.9 | | |
| 219 | +| Perth | 5386 | 1554769 | 869.4 | | |
| 220 | +| Sydney | 2058 | 4336374 | 1214.8 | | |
| 221 | ++-----------+------+------------+-----------------+ | |
| 222 | + | |
| 223 | +=== One column at a time === | |
| 224 | + | |
| 225 | +You can also change the alignment of individual columns based on the | |
| 226 | +corresponding field name by treating the `align` attribute as if it were a | |
| 227 | +dictionary. | |
| 228 | + | |
| 229 | +x.align["City name"] = "l" | |
| 230 | +x.align["Area"] = "c" | |
| 231 | +x.align["Population"] = "r" | |
| 232 | +x.align["Annual Rainfall"] = "c" | |
| 233 | +print x | |
| 234 | + | |
| 235 | +gives: | |
| 236 | + | |
| 237 | ++-----------+------+------------+-----------------+ | |
| 238 | +| City name | Area | Population | Annual Rainfall | | |
| 239 | ++-----------+------+------------+-----------------+ | |
| 240 | +| Adelaide | 1295 | 1158259 | 600.5 | | |
| 241 | +| Brisbane | 5905 | 1857594 | 1146.4 | | |
| 242 | +| Darwin | 112 | 120900 | 1714.7 | | |
| 243 | +| Hobart | 1357 | 205556 | 619.5 | | |
| 244 | +| Melbourne | 1566 | 3806092 | 646.9 | | |
| 245 | +| Perth | 5386 | 1554769 | 869.4 | | |
| 246 | +| Sydney | 2058 | 4336374 | 1214.8 | | |
| 247 | ++-----------+------+------------+-----------------+ | |
| 248 | + | |
| 249 | +== Sorting your table by a field == | |
| 250 | + | |
| 251 | +You can make sure that your ASCII tables are produced with the data sorted by | |
| 252 | +one particular field by giving `get_string` a `sortby` keyword argument, which | |
| 253 | + must be a string containing the name of one field. | |
| 254 | + | |
| 255 | +For example, to print the example table we built earlier of Australian capital | |
| 256 | +city data, so that the most populated city comes last, we can do this: | |
| 257 | + | |
| 258 | +print x.get_string(sortby="Population") | |
| 259 | + | |
| 260 | +to get | |
| 261 | + | |
| 262 | ++-----------+------+------------+-----------------+ | |
| 263 | +| City name | Area | Population | Annual Rainfall | | |
| 264 | ++-----------+------+------------+-----------------+ | |
| 265 | +| Darwin | 112 | 120900 | 1714.7 | | |
| 266 | +| Hobart | 1357 | 205556 | 619.5 | | |
| 267 | +| Adelaide | 1295 | 1158259 | 600.5 | | |
| 268 | +| Perth | 5386 | 1554769 | 869.4 | | |
| 269 | +| Brisbane | 5905 | 1857594 | 1146.4 | | |
| 270 | +| Melbourne | 1566 | 3806092 | 646.9 | | |
| 271 | +| Sydney | 2058 | 4336374 | 1214.8 | | |
| 272 | ++-----------+------+------------+-----------------+ | |
| 273 | + | |
| 274 | +If we want the most populated city to come _first_, we can also give a | |
| 275 | +`reversesort=True` argument. | |
| 276 | + | |
| 277 | +If you _always_ want your tables to be sorted in a certain way, you can make | |
| 278 | +the setting long term like this: | |
| 279 | + | |
| 280 | +x.sortby = "Population" | |
| 281 | +print x | |
| 282 | +print x | |
| 283 | +print x | |
| 284 | + | |
| 285 | +All three tables printed by this code will be sorted by population (you could | |
| 286 | +do `x.reversesort = True` as well, if you wanted). The behaviour will persist | |
| 287 | +until you turn it off: | |
| 288 | + | |
| 289 | +x.sortby = None | |
| 290 | + | |
| 291 | +If you want to specify a custom sorting function, you can use the `sort_key` | |
| 292 | +keyword argument. Pass this a function which accepts two lists of values | |
| 293 | +and returns a negative or positive value depending on whether the first list | |
| 294 | +should appeare before or after the second one. If your table has n columns, | |
| 295 | +each list will have n+1 elements. Each list corresponds to one row of the | |
| 296 | +table. The first element will be whatever data is in the relevant row, in | |
| 297 | +the column specified by the `sort_by` argument. The remaining n elements | |
| 298 | +are the data in each of the table's columns, in order, including a repeated | |
| 299 | +instance of the data in the `sort_by` column. | |
| 300 | + | |
| 301 | += Changing the appearance of your table - the easy way = | |
| 302 | + | |
| 303 | +By default, PrettyTable produces ASCII tables that look like the ones used in | |
| 304 | +SQL database shells. But if can print them in a variety of other formats as | |
| 305 | +well. If the format you want to use is common, PrettyTable makes this very | |
| 306 | +easy for you to do using the `set_style` method. If you want to produce an | |
| 307 | +uncommon table, you'll have to do things slightly harder (see later). | |
| 308 | + | |
| 309 | +== Setting a table style == | |
| 310 | + | |
| 311 | +You can set the style for your table using the `set_style` method before any | |
| 312 | +calls to `print` or `get_string`. Here's how to print a table in a format | |
| 313 | +which works nicely with Microsoft Word's "Convert to table" feature: | |
| 314 | + | |
| 315 | +from prettytable import MSWORD_FRIENDLY | |
| 316 | +x.set_style(MSWORD_FRIENDLY) | |
| 317 | +print x | |
| 318 | + | |
| 319 | +In addition to `MSWORD_FRIENDLY` there are currently two other in-built styles | |
| 320 | +you can use for your tables: | |
| 321 | + | |
| 322 | + * `DEFAULT` - The default look, used to undo any style changes you may have | |
| 323 | +made | |
| 324 | + * `PLAIN_COLUMN` - A borderless style that works well with command line | |
| 325 | +programs for columnar data | |
| 326 | + | |
| 327 | +Other styles are likely to appear in future releases. | |
| 328 | + | |
| 329 | += Changing the appearance of your table - the hard way = | |
| 330 | + | |
| 331 | +If you want to display your table in a style other than one of the in-built | |
| 332 | +styles listed above, you'll have to set things up the hard way. | |
| 333 | + | |
| 334 | +Don't worry, it's not really that hard! | |
| 335 | + | |
| 336 | +== Style options == | |
| 337 | + | |
| 338 | +PrettyTable has a number of style options which control various aspects of how | |
| 339 | +tables are displayed. You have the freedom to set each of these options | |
| 340 | +individually to whatever you prefer. The `set_style` method just does this | |
| 341 | +automatically for you. | |
| 342 | + | |
| 343 | +The options are these: | |
| 344 | + | |
| 345 | + * `border` - A boolean option (must be `True` or `False`). Controls whether | |
| 346 | + or not a border is drawn around the table. | |
| 347 | + * `header` - A boolean option (must be `True` or `False`). Controls whether | |
| 348 | + or not the first row of the table is a header showing the names of all the | |
| 349 | + fields. | |
| 350 | + * `hrules` - Controls printing of horizontal rules after rows. Allowed | |
| 351 | + values: FRAME, HEADER, ALL, NONE - note that these are variables defined | |
| 352 | + inside the `prettytable` module so make sure you import them or use | |
| 353 | + `prettytable.FRAME` etc. | |
| 354 | + * `vrules` - Controls printing of vertical rules between columns. Allowed | |
| 355 | + values: FRAME, ALL, NONE. | |
| 356 | + * `int_format` - A string which controls the way integer data is printed. | |
| 357 | + This works like: print "%<int_format>d" % data | |
| 358 | + * `float_format` - A string which controls the way floating point data is | |
| 359 | + printed. This works like: print "%<int_format>f" % data | |
| 360 | + * `padding_width` - Number of spaces on either side of column data (only used | |
| 361 | + if left and right paddings are None). | |
| 362 | + * `left_padding_width` - Number of spaces on left hand side of column data. | |
| 363 | + * `right_padding_width` - Number of spaces on right hand side of column data. | |
| 364 | + * `vertical_char` - Single character string used to draw vertical lines. | |
| 365 | + Default is `|`. | |
| 366 | + * `horizontal_char` - Single character string used to draw horizontal lines. | |
| 367 | + Default is `-`. | |
| 368 | + * `junction_char` - Single character string used to draw line junctions. | |
| 369 | + Default is `+`. | |
| 370 | + | |
| 371 | +You can set the style options to your own settings in two ways: | |
| 372 | + | |
| 373 | +== Setting style options for the long term == | |
| 374 | + | |
| 375 | +If you want to print your table with a different style several times, you can | |
| 376 | +set your option for the "long term" just by changing the appropriate | |
| 377 | +attributes. If you never want your tables to have borders you can do this: | |
| 378 | + | |
| 379 | +x.border = False | |
| 380 | +print x | |
| 381 | +print x | |
| 382 | +print x | |
| 383 | + | |
| 384 | +Neither of the 3 tables printed by this will have borders, even if you do | |
| 385 | +things like add extra rows inbetween them. The lack of borders will last until | |
| 386 | +you do: | |
| 387 | + | |
| 388 | +x.border = True | |
| 389 | + | |
| 390 | +to turn them on again. This sort of long term setting is exactly how | |
| 391 | +`set_style` works. `set_style` just sets a bunch of attributes to pre-set | |
| 392 | +values for you. | |
| 393 | + | |
| 394 | +Note that if you know what style options you want at the moment you are | |
| 395 | +creating your table, you can specify them using keyword arguments to the | |
| 396 | +constructor. For example, the following two code blocks are equivalent: | |
| 397 | + | |
| 398 | +x = PrettyTable() | |
| 399 | +x.border = False | |
| 400 | +x.header = False | |
| 401 | +x.padding_width = 5 | |
| 402 | + | |
| 403 | +x = PrettyTable(border=False, header=False, padding_width=5) | |
| 404 | + | |
| 405 | +== Changing style options just once == | |
| 406 | + | |
| 407 | +If you don't want to make long term style changes by changing an attribute like | |
| 408 | +in the previous section, you can make changes that last for just one | |
| 409 | +``get_string`` by giving those methods keyword arguments. To print two | |
| 410 | +"normal" tables with one borderless table between them, you could do this: | |
| 411 | + | |
| 412 | +print x | |
| 413 | +print x.get_string(border=False) | |
| 414 | +print x | |
| 415 | + | |
| 416 | += Displaying your table in HTML form = | |
| 417 | + | |
| 418 | +PrettyTable will also print your tables in HTML form, as `<table>`s. Just like | |
| 419 | +in ASCII form, you can actually print your table - just use `print_html()` - or | |
| 420 | +get a string representation - just use `get_html_string()`. HTML printing | |
| 421 | +supports the `fields`, `start`, `end`, `sortby` and `reversesort` arguments in | |
| 422 | +exactly the same way as ASCII printing. | |
| 423 | + | |
| 424 | +== Styling HTML tables == | |
| 425 | + | |
| 426 | +By default, PrettyTable outputs HTML for "vanilla" tables. The HTML code is | |
| 427 | +quite simple. It looks like this: | |
| 428 | + | |
| 429 | +<table> | |
| 430 | + <tr> | |
| 431 | + <th>City name</th> | |
| 432 | + <th>Area</th> | |
| 433 | + <th>Population</th> | |
| 434 | + <th>Annual Rainfall</th> | |
| 435 | + </tr> | |
| 436 | + <tr> | |
| 437 | + <td>Adelaide</td> | |
| 438 | + <td>1295</td> | |
| 439 | + <td>1158259</td> | |
| 440 | + <td>600.5</td> | |
| 441 | + <tr> | |
| 442 | + <td>Brisbane</td> | |
| 443 | + <td>5905</td> | |
| 444 | + <td>1857594</td> | |
| 445 | + <td>1146.4</td> | |
| 446 | + ... | |
| 447 | + ... | |
| 448 | + ... | |
| 449 | +</table> | |
| 450 | + | |
| 451 | +If you like, you can ask PrettyTable to do its best to mimick the style options | |
| 452 | +that your table has set using inline CSS. This is done by giving a | |
| 453 | +`format=True` keyword argument to either the `print_html` or `get_html_string` | |
| 454 | +methods. Note that if you _always_ want to print formatted HTML you can do: | |
| 455 | + | |
| 456 | +x.format = True | |
| 457 | + | |
| 458 | +and the setting will persist until you turn it off. | |
| 459 | + | |
| 460 | +Just like with ASCII tables, if you want to change the table's style for just | |
| 461 | +one `print_html` or one `get_html_string` you can pass those methods keyword | |
| 462 | +arguments - exactly like `print` and `get_string`. | |
| 463 | + | |
| 464 | +== Setting HTML attributes == | |
| 465 | + | |
| 466 | +You can provide a dictionary of HTML attribute name/value pairs to the | |
| 467 | +`print_html` and `get_html_string` methods using the `attributes` keyword | |
| 468 | +argument. This lets you specify common HTML attributes like `name`, `id` and | |
| 469 | +`class` that can be used for linking to your tables or customising their | |
| 470 | +appearance using CSS. For example: | |
| 471 | + | |
| 472 | +x.print_html(attributes={"name":"my_table", "class":"red_table"}) | |
| 473 | + | |
| 474 | +will print: | |
| 475 | + | |
| 476 | +<table name="my_table" class="red_table"> | |
| 477 | + <tr> | |
| 478 | + <th>City name</th> | |
| 479 | + <th>Area</th> | |
| 480 | + <th>Population</th> | |
| 481 | + <th>Annual Rainfall</th> | |
| 482 | + </tr> | |
| 483 | + ... | |
| 484 | + ... | |
| 485 | + ... | |
| 486 | +</table> | |
| 487 | + | |
| 488 | += Miscellaneous things = | |
| 489 | + | |
| 490 | +== Copying a table == | |
| 491 | + | |
| 492 | +You can call the `copy` method on a PrettyTable object without arguments to | |
| 493 | +return an identical independent copy of the table. | |
| 494 | + | |
| 495 | +If you want a copy of a PrettyTable object with just a subset of the rows, | |
| 496 | +you can use list slicing notation: | |
| 497 | + | |
| 498 | +new_table = old_table[0:5] | ... | ... |
oletools/thirdparty/prettytable/__init__.py
0 → 100644
oletools/thirdparty/prettytable/prettytable.py
0 → 100644
| 1 | +#!/usr/bin/env python | |
| 2 | +# | |
| 3 | +# Copyright (c) 2009-2013, Luke Maurits <luke@maurits.id.au> | |
| 4 | +# All rights reserved. | |
| 5 | +# With contributions from: | |
| 6 | +# * Chris Clark | |
| 7 | +# * Klein Stephane | |
| 8 | +# | |
| 9 | +# Redistribution and use in source and binary forms, with or without | |
| 10 | +# modification, are permitted provided that the following conditions are met: | |
| 11 | +# | |
| 12 | +# * Redistributions of source code must retain the above copyright notice, | |
| 13 | +# this list of conditions and the following disclaimer. | |
| 14 | +# * Redistributions in binary form must reproduce the above copyright notice, | |
| 15 | +# this list of conditions and the following disclaimer in the documentation | |
| 16 | +# and/or other materials provided with the distribution. | |
| 17 | +# * The name of the author may not be used to endorse or promote products | |
| 18 | +# derived from this software without specific prior written permission. | |
| 19 | +# | |
| 20 | +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| 21 | +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| 22 | +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| 23 | +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | |
| 24 | +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| 25 | +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| 26 | +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| 27 | +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| 28 | +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| 29 | +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| 30 | +# POSSIBILITY OF SUCH DAMAGE. | |
| 31 | + | |
| 32 | +__version__ = "0.7.2" | |
| 33 | + | |
| 34 | +import copy | |
| 35 | +import csv | |
| 36 | +import random | |
| 37 | +import re | |
| 38 | +import sys | |
| 39 | +import textwrap | |
| 40 | +import itertools | |
| 41 | +import unicodedata | |
| 42 | + | |
| 43 | +py3k = sys.version_info[0] >= 3 | |
| 44 | +if py3k: | |
| 45 | + unicode = str | |
| 46 | + basestring = str | |
| 47 | + itermap = map | |
| 48 | + iterzip = zip | |
| 49 | + uni_chr = chr | |
| 50 | + from html.parser import HTMLParser | |
| 51 | +else: | |
| 52 | + itermap = itertools.imap | |
| 53 | + iterzip = itertools.izip | |
| 54 | + uni_chr = unichr | |
| 55 | + from HTMLParser import HTMLParser | |
| 56 | + | |
| 57 | +if py3k and sys.version_info[1] >= 2: | |
| 58 | + from html import escape | |
| 59 | +else: | |
| 60 | + from cgi import escape | |
| 61 | + | |
| 62 | +# hrule styles | |
| 63 | +FRAME = 0 | |
| 64 | +ALL = 1 | |
| 65 | +NONE = 2 | |
| 66 | +HEADER = 3 | |
| 67 | + | |
| 68 | +# Table styles | |
| 69 | +DEFAULT = 10 | |
| 70 | +MSWORD_FRIENDLY = 11 | |
| 71 | +PLAIN_COLUMNS = 12 | |
| 72 | +RANDOM = 20 | |
| 73 | + | |
| 74 | +_re = re.compile("\033\[[0-9;]*m") | |
| 75 | + | |
| 76 | +def _get_size(text): | |
| 77 | + lines = text.split("\n") | |
| 78 | + height = len(lines) | |
| 79 | + width = max([_str_block_width(line) for line in lines]) | |
| 80 | + return (width, height) | |
| 81 | + | |
| 82 | +class PrettyTable(object): | |
| 83 | + | |
| 84 | + def __init__(self, field_names=None, **kwargs): | |
| 85 | + | |
| 86 | + """Return a new PrettyTable instance | |
| 87 | + | |
| 88 | + Arguments: | |
| 89 | + | |
| 90 | + encoding - Unicode encoding scheme used to decode any encoded input | |
| 91 | + field_names - list or tuple of field names | |
| 92 | + fields - list or tuple of field names to include in displays | |
| 93 | + start - index of first data row to include in output | |
| 94 | + end - index of last data row to include in output PLUS ONE (list slice style) | |
| 95 | + header - print a header showing field names (True or False) | |
| 96 | + header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None) | |
| 97 | + border - print a border around the table (True or False) | |
| 98 | + hrules - controls printing of horizontal rules after rows. Allowed values: FRAME, HEADER, ALL, NONE | |
| 99 | + vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE | |
| 100 | + int_format - controls formatting of integer data | |
| 101 | + float_format - controls formatting of floating point data | |
| 102 | + padding_width - number of spaces on either side of column data (only used if left and right paddings are None) | |
| 103 | + left_padding_width - number of spaces on left hand side of column data | |
| 104 | + right_padding_width - number of spaces on right hand side of column data | |
| 105 | + vertical_char - single character string used to draw vertical lines | |
| 106 | + horizontal_char - single character string used to draw horizontal lines | |
| 107 | + junction_char - single character string used to draw line junctions | |
| 108 | + sortby - name of field to sort rows by | |
| 109 | + sort_key - sorting key function, applied to data points before sorting | |
| 110 | + valign - default valign for each row (None, "t", "m" or "b") | |
| 111 | + reversesort - True or False to sort in descending or ascending order""" | |
| 112 | + | |
| 113 | + self.encoding = kwargs.get("encoding", "UTF-8") | |
| 114 | + | |
| 115 | + # Data | |
| 116 | + self._field_names = [] | |
| 117 | + self._align = {} | |
| 118 | + self._valign = {} | |
| 119 | + self._max_width = {} | |
| 120 | + self._rows = [] | |
| 121 | + if field_names: | |
| 122 | + self.field_names = field_names | |
| 123 | + else: | |
| 124 | + self._widths = [] | |
| 125 | + | |
| 126 | + # Options | |
| 127 | + self._options = "start end fields header border sortby reversesort sort_key attributes format hrules vrules".split() | |
| 128 | + self._options.extend("int_format float_format padding_width left_padding_width right_padding_width".split()) | |
| 129 | + self._options.extend("vertical_char horizontal_char junction_char header_style valign xhtml print_empty".split()) | |
| 130 | + for option in self._options: | |
| 131 | + if option in kwargs: | |
| 132 | + self._validate_option(option, kwargs[option]) | |
| 133 | + else: | |
| 134 | + kwargs[option] = None | |
| 135 | + | |
| 136 | + self._start = kwargs["start"] or 0 | |
| 137 | + self._end = kwargs["end"] or None | |
| 138 | + self._fields = kwargs["fields"] or None | |
| 139 | + | |
| 140 | + if kwargs["header"] in (True, False): | |
| 141 | + self._header = kwargs["header"] | |
| 142 | + else: | |
| 143 | + self._header = True | |
| 144 | + self._header_style = kwargs["header_style"] or None | |
| 145 | + if kwargs["border"] in (True, False): | |
| 146 | + self._border = kwargs["border"] | |
| 147 | + else: | |
| 148 | + self._border = True | |
| 149 | + self._hrules = kwargs["hrules"] or FRAME | |
| 150 | + self._vrules = kwargs["vrules"] or ALL | |
| 151 | + | |
| 152 | + self._sortby = kwargs["sortby"] or None | |
| 153 | + if kwargs["reversesort"] in (True, False): | |
| 154 | + self._reversesort = kwargs["reversesort"] | |
| 155 | + else: | |
| 156 | + self._reversesort = False | |
| 157 | + self._sort_key = kwargs["sort_key"] or (lambda x: x) | |
| 158 | + | |
| 159 | + self._int_format = kwargs["int_format"] or {} | |
| 160 | + self._float_format = kwargs["float_format"] or {} | |
| 161 | + self._padding_width = kwargs["padding_width"] or 1 | |
| 162 | + self._left_padding_width = kwargs["left_padding_width"] or None | |
| 163 | + self._right_padding_width = kwargs["right_padding_width"] or None | |
| 164 | + | |
| 165 | + self._vertical_char = kwargs["vertical_char"] or self._unicode("|") | |
| 166 | + self._horizontal_char = kwargs["horizontal_char"] or self._unicode("-") | |
| 167 | + self._junction_char = kwargs["junction_char"] or self._unicode("+") | |
| 168 | + | |
| 169 | + if kwargs["print_empty"] in (True, False): | |
| 170 | + self._print_empty = kwargs["print_empty"] | |
| 171 | + else: | |
| 172 | + self._print_empty = True | |
| 173 | + self._format = kwargs["format"] or False | |
| 174 | + self._xhtml = kwargs["xhtml"] or False | |
| 175 | + self._attributes = kwargs["attributes"] or {} | |
| 176 | + | |
| 177 | + def _unicode(self, value): | |
| 178 | + if not isinstance(value, basestring): | |
| 179 | + value = str(value) | |
| 180 | + if not isinstance(value, unicode): | |
| 181 | + value = unicode(value, self.encoding, "strict") | |
| 182 | + return value | |
| 183 | + | |
| 184 | + def _justify(self, text, width, align): | |
| 185 | + excess = width - _str_block_width(text) | |
| 186 | + if align == "l": | |
| 187 | + return text + excess * " " | |
| 188 | + elif align == "r": | |
| 189 | + return excess * " " + text | |
| 190 | + else: | |
| 191 | + if excess % 2: | |
| 192 | + # Uneven padding | |
| 193 | + # Put more space on right if text is of odd length... | |
| 194 | + if _str_block_width(text) % 2: | |
| 195 | + return (excess//2)*" " + text + (excess//2 + 1)*" " | |
| 196 | + # and more space on left if text is of even length | |
| 197 | + else: | |
| 198 | + return (excess//2 + 1)*" " + text + (excess//2)*" " | |
| 199 | + # Why distribute extra space this way? To match the behaviour of | |
| 200 | + # the inbuilt str.center() method. | |
| 201 | + else: | |
| 202 | + # Equal padding on either side | |
| 203 | + return (excess//2)*" " + text + (excess//2)*" " | |
| 204 | + | |
| 205 | + def __getattr__(self, name): | |
| 206 | + | |
| 207 | + if name == "rowcount": | |
| 208 | + return len(self._rows) | |
| 209 | + elif name == "colcount": | |
| 210 | + if self._field_names: | |
| 211 | + return len(self._field_names) | |
| 212 | + elif self._rows: | |
| 213 | + return len(self._rows[0]) | |
| 214 | + else: | |
| 215 | + return 0 | |
| 216 | + else: | |
| 217 | + raise AttributeError(name) | |
| 218 | + | |
| 219 | + def __getitem__(self, index): | |
| 220 | + | |
| 221 | + new = PrettyTable() | |
| 222 | + new.field_names = self.field_names | |
| 223 | + for attr in self._options: | |
| 224 | + setattr(new, "_"+attr, getattr(self, "_"+attr)) | |
| 225 | + setattr(new, "_align", getattr(self, "_align")) | |
| 226 | + if isinstance(index, slice): | |
| 227 | + for row in self._rows[index]: | |
| 228 | + new.add_row(row) | |
| 229 | + elif isinstance(index, int): | |
| 230 | + new.add_row(self._rows[index]) | |
| 231 | + else: | |
| 232 | + raise Exception("Index %s is invalid, must be an integer or slice" % str(index)) | |
| 233 | + return new | |
| 234 | + | |
| 235 | + if py3k: | |
| 236 | + def __str__(self): | |
| 237 | + return self.__unicode__() | |
| 238 | + else: | |
| 239 | + def __str__(self): | |
| 240 | + return self.__unicode__().encode(self.encoding) | |
| 241 | + | |
| 242 | + def __unicode__(self): | |
| 243 | + return self.get_string() | |
| 244 | + | |
| 245 | + ############################## | |
| 246 | + # ATTRIBUTE VALIDATORS # | |
| 247 | + ############################## | |
| 248 | + | |
| 249 | + # The method _validate_option is all that should be used elsewhere in the code base to validate options. | |
| 250 | + # It will call the appropriate validation method for that option. The individual validation methods should | |
| 251 | + # never need to be called directly (although nothing bad will happen if they *are*). | |
| 252 | + # Validation happens in TWO places. | |
| 253 | + # Firstly, in the property setters defined in the ATTRIBUTE MANAGMENT section. | |
| 254 | + # Secondly, in the _get_options method, where keyword arguments are mixed with persistent settings | |
| 255 | + | |
| 256 | + def _validate_option(self, option, val): | |
| 257 | + if option in ("field_names"): | |
| 258 | + self._validate_field_names(val) | |
| 259 | + elif option in ("start", "end", "max_width", "padding_width", "left_padding_width", "right_padding_width", "format"): | |
| 260 | + self._validate_nonnegative_int(option, val) | |
| 261 | + elif option in ("sortby"): | |
| 262 | + self._validate_field_name(option, val) | |
| 263 | + elif option in ("sort_key"): | |
| 264 | + self._validate_function(option, val) | |
| 265 | + elif option in ("hrules"): | |
| 266 | + self._validate_hrules(option, val) | |
| 267 | + elif option in ("vrules"): | |
| 268 | + self._validate_vrules(option, val) | |
| 269 | + elif option in ("fields"): | |
| 270 | + self._validate_all_field_names(option, val) | |
| 271 | + elif option in ("header", "border", "reversesort", "xhtml", "print_empty"): | |
| 272 | + self._validate_true_or_false(option, val) | |
| 273 | + elif option in ("header_style"): | |
| 274 | + self._validate_header_style(val) | |
| 275 | + elif option in ("int_format"): | |
| 276 | + self._validate_int_format(option, val) | |
| 277 | + elif option in ("float_format"): | |
| 278 | + self._validate_float_format(option, val) | |
| 279 | + elif option in ("vertical_char", "horizontal_char", "junction_char"): | |
| 280 | + self._validate_single_char(option, val) | |
| 281 | + elif option in ("attributes"): | |
| 282 | + self._validate_attributes(option, val) | |
| 283 | + else: | |
| 284 | + raise Exception("Unrecognised option: %s!" % option) | |
| 285 | + | |
| 286 | + def _validate_field_names(self, val): | |
| 287 | + # Check for appropriate length | |
| 288 | + if self._field_names: | |
| 289 | + try: | |
| 290 | + assert len(val) == len(self._field_names) | |
| 291 | + except AssertionError: | |
| 292 | + raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._field_names))) | |
| 293 | + if self._rows: | |
| 294 | + try: | |
| 295 | + assert len(val) == len(self._rows[0]) | |
| 296 | + except AssertionError: | |
| 297 | + raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._rows[0]))) | |
| 298 | + # Check for uniqueness | |
| 299 | + try: | |
| 300 | + assert len(val) == len(set(val)) | |
| 301 | + except AssertionError: | |
| 302 | + raise Exception("Field names must be unique!") | |
| 303 | + | |
| 304 | + def _validate_header_style(self, val): | |
| 305 | + try: | |
| 306 | + assert val in ("cap", "title", "upper", "lower", None) | |
| 307 | + except AssertionError: | |
| 308 | + raise Exception("Invalid header style, use cap, title, upper, lower or None!") | |
| 309 | + | |
| 310 | + def _validate_align(self, val): | |
| 311 | + try: | |
| 312 | + assert val in ["l","c","r"] | |
| 313 | + except AssertionError: | |
| 314 | + raise Exception("Alignment %s is invalid, use l, c or r!" % val) | |
| 315 | + | |
| 316 | + def _validate_valign(self, val): | |
| 317 | + try: | |
| 318 | + assert val in ["t","m","b",None] | |
| 319 | + except AssertionError: | |
| 320 | + raise Exception("Alignment %s is invalid, use t, m, b or None!" % val) | |
| 321 | + | |
| 322 | + def _validate_nonnegative_int(self, name, val): | |
| 323 | + try: | |
| 324 | + assert int(val) >= 0 | |
| 325 | + except AssertionError: | |
| 326 | + raise Exception("Invalid value for %s: %s!" % (name, self._unicode(val))) | |
| 327 | + | |
| 328 | + def _validate_true_or_false(self, name, val): | |
| 329 | + try: | |
| 330 | + assert val in (True, False) | |
| 331 | + except AssertionError: | |
| 332 | + raise Exception("Invalid value for %s! Must be True or False." % name) | |
| 333 | + | |
| 334 | + def _validate_int_format(self, name, val): | |
| 335 | + if val == "": | |
| 336 | + return | |
| 337 | + try: | |
| 338 | + assert type(val) in (str, unicode) | |
| 339 | + assert val.isdigit() | |
| 340 | + except AssertionError: | |
| 341 | + raise Exception("Invalid value for %s! Must be an integer format string." % name) | |
| 342 | + | |
| 343 | + def _validate_float_format(self, name, val): | |
| 344 | + if val == "": | |
| 345 | + return | |
| 346 | + try: | |
| 347 | + assert type(val) in (str, unicode) | |
| 348 | + assert "." in val | |
| 349 | + bits = val.split(".") | |
| 350 | + assert len(bits) <= 2 | |
| 351 | + assert bits[0] == "" or bits[0].isdigit() | |
| 352 | + assert bits[1] == "" or bits[1].isdigit() | |
| 353 | + except AssertionError: | |
| 354 | + raise Exception("Invalid value for %s! Must be a float format string." % name) | |
| 355 | + | |
| 356 | + def _validate_function(self, name, val): | |
| 357 | + try: | |
| 358 | + assert hasattr(val, "__call__") | |
| 359 | + except AssertionError: | |
| 360 | + raise Exception("Invalid value for %s! Must be a function." % name) | |
| 361 | + | |
| 362 | + def _validate_hrules(self, name, val): | |
| 363 | + try: | |
| 364 | + assert val in (ALL, FRAME, HEADER, NONE) | |
| 365 | + except AssertionError: | |
| 366 | + raise Exception("Invalid value for %s! Must be ALL, FRAME, HEADER or NONE." % name) | |
| 367 | + | |
| 368 | + def _validate_vrules(self, name, val): | |
| 369 | + try: | |
| 370 | + assert val in (ALL, FRAME, NONE) | |
| 371 | + except AssertionError: | |
| 372 | + raise Exception("Invalid value for %s! Must be ALL, FRAME, or NONE." % name) | |
| 373 | + | |
| 374 | + def _validate_field_name(self, name, val): | |
| 375 | + try: | |
| 376 | + assert (val in self._field_names) or (val is None) | |
| 377 | + except AssertionError: | |
| 378 | + raise Exception("Invalid field name: %s!" % val) | |
| 379 | + | |
| 380 | + def _validate_all_field_names(self, name, val): | |
| 381 | + try: | |
| 382 | + for x in val: | |
| 383 | + self._validate_field_name(name, x) | |
| 384 | + except AssertionError: | |
| 385 | + raise Exception("fields must be a sequence of field names!") | |
| 386 | + | |
| 387 | + def _validate_single_char(self, name, val): | |
| 388 | + try: | |
| 389 | + assert _str_block_width(val) == 1 | |
| 390 | + except AssertionError: | |
| 391 | + raise Exception("Invalid value for %s! Must be a string of length 1." % name) | |
| 392 | + | |
| 393 | + def _validate_attributes(self, name, val): | |
| 394 | + try: | |
| 395 | + assert isinstance(val, dict) | |
| 396 | + except AssertionError: | |
| 397 | + raise Exception("attributes must be a dictionary of name/value pairs!") | |
| 398 | + | |
| 399 | + ############################## | |
| 400 | + # ATTRIBUTE MANAGEMENT # | |
| 401 | + ############################## | |
| 402 | + | |
| 403 | + def _get_field_names(self): | |
| 404 | + return self._field_names | |
| 405 | + """The names of the fields | |
| 406 | + | |
| 407 | + Arguments: | |
| 408 | + | |
| 409 | + fields - list or tuple of field names""" | |
| 410 | + def _set_field_names(self, val): | |
| 411 | + val = [self._unicode(x) for x in val] | |
| 412 | + self._validate_option("field_names", val) | |
| 413 | + if self._field_names: | |
| 414 | + old_names = self._field_names[:] | |
| 415 | + self._field_names = val | |
| 416 | + if self._align and old_names: | |
| 417 | + for old_name, new_name in zip(old_names, val): | |
| 418 | + self._align[new_name] = self._align[old_name] | |
| 419 | + for old_name in old_names: | |
| 420 | + if old_name not in self._align: | |
| 421 | + self._align.pop(old_name) | |
| 422 | + else: | |
| 423 | + for field in self._field_names: | |
| 424 | + self._align[field] = "c" | |
| 425 | + if self._valign and old_names: | |
| 426 | + for old_name, new_name in zip(old_names, val): | |
| 427 | + self._valign[new_name] = self._valign[old_name] | |
| 428 | + for old_name in old_names: | |
| 429 | + if old_name not in self._valign: | |
| 430 | + self._valign.pop(old_name) | |
| 431 | + else: | |
| 432 | + for field in self._field_names: | |
| 433 | + self._valign[field] = "t" | |
| 434 | + field_names = property(_get_field_names, _set_field_names) | |
| 435 | + | |
| 436 | + def _get_align(self): | |
| 437 | + return self._align | |
| 438 | + def _set_align(self, val): | |
| 439 | + self._validate_align(val) | |
| 440 | + for field in self._field_names: | |
| 441 | + self._align[field] = val | |
| 442 | + align = property(_get_align, _set_align) | |
| 443 | + | |
| 444 | + def _get_valign(self): | |
| 445 | + return self._valign | |
| 446 | + def _set_valign(self, val): | |
| 447 | + self._validate_valign(val) | |
| 448 | + for field in self._field_names: | |
| 449 | + self._valign[field] = val | |
| 450 | + valign = property(_get_valign, _set_valign) | |
| 451 | + | |
| 452 | + def _get_max_width(self): | |
| 453 | + return self._max_width | |
| 454 | + def _set_max_width(self, val): | |
| 455 | + self._validate_option("max_width", val) | |
| 456 | + for field in self._field_names: | |
| 457 | + self._max_width[field] = val | |
| 458 | + max_width = property(_get_max_width, _set_max_width) | |
| 459 | + | |
| 460 | + def _get_fields(self): | |
| 461 | + """List or tuple of field names to include in displays | |
| 462 | + | |
| 463 | + Arguments: | |
| 464 | + | |
| 465 | + fields - list or tuple of field names to include in displays""" | |
| 466 | + return self._fields | |
| 467 | + def _set_fields(self, val): | |
| 468 | + self._validate_option("fields", val) | |
| 469 | + self._fields = val | |
| 470 | + fields = property(_get_fields, _set_fields) | |
| 471 | + | |
| 472 | + def _get_start(self): | |
| 473 | + """Start index of the range of rows to print | |
| 474 | + | |
| 475 | + Arguments: | |
| 476 | + | |
| 477 | + start - index of first data row to include in output""" | |
| 478 | + return self._start | |
| 479 | + | |
| 480 | + def _set_start(self, val): | |
| 481 | + self._validate_option("start", val) | |
| 482 | + self._start = val | |
| 483 | + start = property(_get_start, _set_start) | |
| 484 | + | |
| 485 | + def _get_end(self): | |
| 486 | + """End index of the range of rows to print | |
| 487 | + | |
| 488 | + Arguments: | |
| 489 | + | |
| 490 | + end - index of last data row to include in output PLUS ONE (list slice style)""" | |
| 491 | + return self._end | |
| 492 | + def _set_end(self, val): | |
| 493 | + self._validate_option("end", val) | |
| 494 | + self._end = val | |
| 495 | + end = property(_get_end, _set_end) | |
| 496 | + | |
| 497 | + def _get_sortby(self): | |
| 498 | + """Name of field by which to sort rows | |
| 499 | + | |
| 500 | + Arguments: | |
| 501 | + | |
| 502 | + sortby - field name to sort by""" | |
| 503 | + return self._sortby | |
| 504 | + def _set_sortby(self, val): | |
| 505 | + self._validate_option("sortby", val) | |
| 506 | + self._sortby = val | |
| 507 | + sortby = property(_get_sortby, _set_sortby) | |
| 508 | + | |
| 509 | + def _get_reversesort(self): | |
| 510 | + """Controls direction of sorting (ascending vs descending) | |
| 511 | + | |
| 512 | + Arguments: | |
| 513 | + | |
| 514 | + reveresort - set to True to sort by descending order, or False to sort by ascending order""" | |
| 515 | + return self._reversesort | |
| 516 | + def _set_reversesort(self, val): | |
| 517 | + self._validate_option("reversesort", val) | |
| 518 | + self._reversesort = val | |
| 519 | + reversesort = property(_get_reversesort, _set_reversesort) | |
| 520 | + | |
| 521 | + def _get_sort_key(self): | |
| 522 | + """Sorting key function, applied to data points before sorting | |
| 523 | + | |
| 524 | + Arguments: | |
| 525 | + | |
| 526 | + sort_key - a function which takes one argument and returns something to be sorted""" | |
| 527 | + return self._sort_key | |
| 528 | + def _set_sort_key(self, val): | |
| 529 | + self._validate_option("sort_key", val) | |
| 530 | + self._sort_key = val | |
| 531 | + sort_key = property(_get_sort_key, _set_sort_key) | |
| 532 | + | |
| 533 | + def _get_header(self): | |
| 534 | + """Controls printing of table header with field names | |
| 535 | + | |
| 536 | + Arguments: | |
| 537 | + | |
| 538 | + header - print a header showing field names (True or False)""" | |
| 539 | + return self._header | |
| 540 | + def _set_header(self, val): | |
| 541 | + self._validate_option("header", val) | |
| 542 | + self._header = val | |
| 543 | + header = property(_get_header, _set_header) | |
| 544 | + | |
| 545 | + def _get_header_style(self): | |
| 546 | + """Controls stylisation applied to field names in header | |
| 547 | + | |
| 548 | + Arguments: | |
| 549 | + | |
| 550 | + header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None)""" | |
| 551 | + return self._header_style | |
| 552 | + def _set_header_style(self, val): | |
| 553 | + self._validate_header_style(val) | |
| 554 | + self._header_style = val | |
| 555 | + header_style = property(_get_header_style, _set_header_style) | |
| 556 | + | |
| 557 | + def _get_border(self): | |
| 558 | + """Controls printing of border around table | |
| 559 | + | |
| 560 | + Arguments: | |
| 561 | + | |
| 562 | + border - print a border around the table (True or False)""" | |
| 563 | + return self._border | |
| 564 | + def _set_border(self, val): | |
| 565 | + self._validate_option("border", val) | |
| 566 | + self._border = val | |
| 567 | + border = property(_get_border, _set_border) | |
| 568 | + | |
| 569 | + def _get_hrules(self): | |
| 570 | + """Controls printing of horizontal rules after rows | |
| 571 | + | |
| 572 | + Arguments: | |
| 573 | + | |
| 574 | + hrules - horizontal rules style. Allowed values: FRAME, ALL, HEADER, NONE""" | |
| 575 | + return self._hrules | |
| 576 | + def _set_hrules(self, val): | |
| 577 | + self._validate_option("hrules", val) | |
| 578 | + self._hrules = val | |
| 579 | + hrules = property(_get_hrules, _set_hrules) | |
| 580 | + | |
| 581 | + def _get_vrules(self): | |
| 582 | + """Controls printing of vertical rules between columns | |
| 583 | + | |
| 584 | + Arguments: | |
| 585 | + | |
| 586 | + vrules - vertical rules style. Allowed values: FRAME, ALL, NONE""" | |
| 587 | + return self._vrules | |
| 588 | + def _set_vrules(self, val): | |
| 589 | + self._validate_option("vrules", val) | |
| 590 | + self._vrules = val | |
| 591 | + vrules = property(_get_vrules, _set_vrules) | |
| 592 | + | |
| 593 | + def _get_int_format(self): | |
| 594 | + """Controls formatting of integer data | |
| 595 | + Arguments: | |
| 596 | + | |
| 597 | + int_format - integer format string""" | |
| 598 | + return self._int_format | |
| 599 | + def _set_int_format(self, val): | |
| 600 | +# self._validate_option("int_format", val) | |
| 601 | + for field in self._field_names: | |
| 602 | + self._int_format[field] = val | |
| 603 | + int_format = property(_get_int_format, _set_int_format) | |
| 604 | + | |
| 605 | + def _get_float_format(self): | |
| 606 | + """Controls formatting of floating point data | |
| 607 | + Arguments: | |
| 608 | + | |
| 609 | + float_format - floating point format string""" | |
| 610 | + return self._float_format | |
| 611 | + def _set_float_format(self, val): | |
| 612 | +# self._validate_option("float_format", val) | |
| 613 | + for field in self._field_names: | |
| 614 | + self._float_format[field] = val | |
| 615 | + float_format = property(_get_float_format, _set_float_format) | |
| 616 | + | |
| 617 | + def _get_padding_width(self): | |
| 618 | + """The number of empty spaces between a column's edge and its content | |
| 619 | + | |
| 620 | + Arguments: | |
| 621 | + | |
| 622 | + padding_width - number of spaces, must be a positive integer""" | |
| 623 | + return self._padding_width | |
| 624 | + def _set_padding_width(self, val): | |
| 625 | + self._validate_option("padding_width", val) | |
| 626 | + self._padding_width = val | |
| 627 | + padding_width = property(_get_padding_width, _set_padding_width) | |
| 628 | + | |
| 629 | + def _get_left_padding_width(self): | |
| 630 | + """The number of empty spaces between a column's left edge and its content | |
| 631 | + | |
| 632 | + Arguments: | |
| 633 | + | |
| 634 | + left_padding - number of spaces, must be a positive integer""" | |
| 635 | + return self._left_padding_width | |
| 636 | + def _set_left_padding_width(self, val): | |
| 637 | + self._validate_option("left_padding_width", val) | |
| 638 | + self._left_padding_width = val | |
| 639 | + left_padding_width = property(_get_left_padding_width, _set_left_padding_width) | |
| 640 | + | |
| 641 | + def _get_right_padding_width(self): | |
| 642 | + """The number of empty spaces between a column's right edge and its content | |
| 643 | + | |
| 644 | + Arguments: | |
| 645 | + | |
| 646 | + right_padding - number of spaces, must be a positive integer""" | |
| 647 | + return self._right_padding_width | |
| 648 | + def _set_right_padding_width(self, val): | |
| 649 | + self._validate_option("right_padding_width", val) | |
| 650 | + self._right_padding_width = val | |
| 651 | + right_padding_width = property(_get_right_padding_width, _set_right_padding_width) | |
| 652 | + | |
| 653 | + def _get_vertical_char(self): | |
| 654 | + """The charcter used when printing table borders to draw vertical lines | |
| 655 | + | |
| 656 | + Arguments: | |
| 657 | + | |
| 658 | + vertical_char - single character string used to draw vertical lines""" | |
| 659 | + return self._vertical_char | |
| 660 | + def _set_vertical_char(self, val): | |
| 661 | + val = self._unicode(val) | |
| 662 | + self._validate_option("vertical_char", val) | |
| 663 | + self._vertical_char = val | |
| 664 | + vertical_char = property(_get_vertical_char, _set_vertical_char) | |
| 665 | + | |
| 666 | + def _get_horizontal_char(self): | |
| 667 | + """The charcter used when printing table borders to draw horizontal lines | |
| 668 | + | |
| 669 | + Arguments: | |
| 670 | + | |
| 671 | + horizontal_char - single character string used to draw horizontal lines""" | |
| 672 | + return self._horizontal_char | |
| 673 | + def _set_horizontal_char(self, val): | |
| 674 | + val = self._unicode(val) | |
| 675 | + self._validate_option("horizontal_char", val) | |
| 676 | + self._horizontal_char = val | |
| 677 | + horizontal_char = property(_get_horizontal_char, _set_horizontal_char) | |
| 678 | + | |
| 679 | + def _get_junction_char(self): | |
| 680 | + """The charcter used when printing table borders to draw line junctions | |
| 681 | + | |
| 682 | + Arguments: | |
| 683 | + | |
| 684 | + junction_char - single character string used to draw line junctions""" | |
| 685 | + return self._junction_char | |
| 686 | + def _set_junction_char(self, val): | |
| 687 | + val = self._unicode(val) | |
| 688 | + self._validate_option("vertical_char", val) | |
| 689 | + self._junction_char = val | |
| 690 | + junction_char = property(_get_junction_char, _set_junction_char) | |
| 691 | + | |
| 692 | + def _get_format(self): | |
| 693 | + """Controls whether or not HTML tables are formatted to match styling options | |
| 694 | + | |
| 695 | + Arguments: | |
| 696 | + | |
| 697 | + format - True or False""" | |
| 698 | + return self._format | |
| 699 | + def _set_format(self, val): | |
| 700 | + self._validate_option("format", val) | |
| 701 | + self._format = val | |
| 702 | + format = property(_get_format, _set_format) | |
| 703 | + | |
| 704 | + def _get_print_empty(self): | |
| 705 | + """Controls whether or not empty tables produce a header and frame or just an empty string | |
| 706 | + | |
| 707 | + Arguments: | |
| 708 | + | |
| 709 | + print_empty - True or False""" | |
| 710 | + return self._print_empty | |
| 711 | + def _set_print_empty(self, val): | |
| 712 | + self._validate_option("print_empty", val) | |
| 713 | + self._print_empty = val | |
| 714 | + print_empty = property(_get_print_empty, _set_print_empty) | |
| 715 | + | |
| 716 | + def _get_attributes(self): | |
| 717 | + """A dictionary of HTML attribute name/value pairs to be included in the <table> tag when printing HTML | |
| 718 | + | |
| 719 | + Arguments: | |
| 720 | + | |
| 721 | + attributes - dictionary of attributes""" | |
| 722 | + return self._attributes | |
| 723 | + def _set_attributes(self, val): | |
| 724 | + self._validate_option("attributes", val) | |
| 725 | + self._attributes = val | |
| 726 | + attributes = property(_get_attributes, _set_attributes) | |
| 727 | + | |
| 728 | + ############################## | |
| 729 | + # OPTION MIXER # | |
| 730 | + ############################## | |
| 731 | + | |
| 732 | + def _get_options(self, kwargs): | |
| 733 | + | |
| 734 | + options = {} | |
| 735 | + for option in self._options: | |
| 736 | + if option in kwargs: | |
| 737 | + self._validate_option(option, kwargs[option]) | |
| 738 | + options[option] = kwargs[option] | |
| 739 | + else: | |
| 740 | + options[option] = getattr(self, "_"+option) | |
| 741 | + return options | |
| 742 | + | |
| 743 | + ############################## | |
| 744 | + # PRESET STYLE LOGIC # | |
| 745 | + ############################## | |
| 746 | + | |
| 747 | + def set_style(self, style): | |
| 748 | + | |
| 749 | + if style == DEFAULT: | |
| 750 | + self._set_default_style() | |
| 751 | + elif style == MSWORD_FRIENDLY: | |
| 752 | + self._set_msword_style() | |
| 753 | + elif style == PLAIN_COLUMNS: | |
| 754 | + self._set_columns_style() | |
| 755 | + elif style == RANDOM: | |
| 756 | + self._set_random_style() | |
| 757 | + else: | |
| 758 | + raise Exception("Invalid pre-set style!") | |
| 759 | + | |
| 760 | + def _set_default_style(self): | |
| 761 | + | |
| 762 | + self.header = True | |
| 763 | + self.border = True | |
| 764 | + self._hrules = FRAME | |
| 765 | + self._vrules = ALL | |
| 766 | + self.padding_width = 1 | |
| 767 | + self.left_padding_width = 1 | |
| 768 | + self.right_padding_width = 1 | |
| 769 | + self.vertical_char = "|" | |
| 770 | + self.horizontal_char = "-" | |
| 771 | + self.junction_char = "+" | |
| 772 | + | |
| 773 | + def _set_msword_style(self): | |
| 774 | + | |
| 775 | + self.header = True | |
| 776 | + self.border = True | |
| 777 | + self._hrules = NONE | |
| 778 | + self.padding_width = 1 | |
| 779 | + self.left_padding_width = 1 | |
| 780 | + self.right_padding_width = 1 | |
| 781 | + self.vertical_char = "|" | |
| 782 | + | |
| 783 | + def _set_columns_style(self): | |
| 784 | + | |
| 785 | + self.header = True | |
| 786 | + self.border = False | |
| 787 | + self.padding_width = 1 | |
| 788 | + self.left_padding_width = 0 | |
| 789 | + self.right_padding_width = 8 | |
| 790 | + | |
| 791 | + def _set_random_style(self): | |
| 792 | + | |
| 793 | + # Just for fun! | |
| 794 | + self.header = random.choice((True, False)) | |
| 795 | + self.border = random.choice((True, False)) | |
| 796 | + self._hrules = random.choice((ALL, FRAME, HEADER, NONE)) | |
| 797 | + self._vrules = random.choice((ALL, FRAME, NONE)) | |
| 798 | + self.left_padding_width = random.randint(0,5) | |
| 799 | + self.right_padding_width = random.randint(0,5) | |
| 800 | + self.vertical_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") | |
| 801 | + self.horizontal_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") | |
| 802 | + self.junction_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") | |
| 803 | + | |
| 804 | + ############################## | |
| 805 | + # DATA INPUT METHODS # | |
| 806 | + ############################## | |
| 807 | + | |
| 808 | + def add_row(self, row): | |
| 809 | + | |
| 810 | + """Add a row to the table | |
| 811 | + | |
| 812 | + Arguments: | |
| 813 | + | |
| 814 | + row - row of data, should be a list with as many elements as the table | |
| 815 | + has fields""" | |
| 816 | + | |
| 817 | + if self._field_names and len(row) != len(self._field_names): | |
| 818 | + raise Exception("Row has incorrect number of values, (actual) %d!=%d (expected)" %(len(row),len(self._field_names))) | |
| 819 | + if not self._field_names: | |
| 820 | + self.field_names = [("Field %d" % (n+1)) for n in range(0,len(row))] | |
| 821 | + self._rows.append(list(row)) | |
| 822 | + | |
| 823 | + def del_row(self, row_index): | |
| 824 | + | |
| 825 | + """Delete a row to the table | |
| 826 | + | |
| 827 | + Arguments: | |
| 828 | + | |
| 829 | + row_index - The index of the row you want to delete. Indexing starts at 0.""" | |
| 830 | + | |
| 831 | + if row_index > len(self._rows)-1: | |
| 832 | + raise Exception("Cant delete row at index %d, table only has %d rows!" % (row_index, len(self._rows))) | |
| 833 | + del self._rows[row_index] | |
| 834 | + | |
| 835 | + def add_column(self, fieldname, column, align="c", valign="t"): | |
| 836 | + | |
| 837 | + """Add a column to the table. | |
| 838 | + | |
| 839 | + Arguments: | |
| 840 | + | |
| 841 | + fieldname - name of the field to contain the new column of data | |
| 842 | + column - column of data, should be a list with as many elements as the | |
| 843 | + table has rows | |
| 844 | + align - desired alignment for this column - "l" for left, "c" for centre and "r" for right | |
| 845 | + valign - desired vertical alignment for new columns - "t" for top, "m" for middle and "b" for bottom""" | |
| 846 | + | |
| 847 | + if len(self._rows) in (0, len(column)): | |
| 848 | + self._validate_align(align) | |
| 849 | + self._validate_valign(valign) | |
| 850 | + self._field_names.append(fieldname) | |
| 851 | + self._align[fieldname] = align | |
| 852 | + self._valign[fieldname] = valign | |
| 853 | + for i in range(0, len(column)): | |
| 854 | + if len(self._rows) < i+1: | |
| 855 | + self._rows.append([]) | |
| 856 | + self._rows[i].append(column[i]) | |
| 857 | + else: | |
| 858 | + raise Exception("Column length %d does not match number of rows %d!" % (len(column), len(self._rows))) | |
| 859 | + | |
| 860 | + def clear_rows(self): | |
| 861 | + | |
| 862 | + """Delete all rows from the table but keep the current field names""" | |
| 863 | + | |
| 864 | + self._rows = [] | |
| 865 | + | |
| 866 | + def clear(self): | |
| 867 | + | |
| 868 | + """Delete all rows and field names from the table, maintaining nothing but styling options""" | |
| 869 | + | |
| 870 | + self._rows = [] | |
| 871 | + self._field_names = [] | |
| 872 | + self._widths = [] | |
| 873 | + | |
| 874 | + ############################## | |
| 875 | + # MISC PUBLIC METHODS # | |
| 876 | + ############################## | |
| 877 | + | |
| 878 | + def copy(self): | |
| 879 | + return copy.deepcopy(self) | |
| 880 | + | |
| 881 | + ############################## | |
| 882 | + # MISC PRIVATE METHODS # | |
| 883 | + ############################## | |
| 884 | + | |
| 885 | + def _format_value(self, field, value): | |
| 886 | + if isinstance(value, int) and field in self._int_format: | |
| 887 | + value = self._unicode(("%%%sd" % self._int_format[field]) % value) | |
| 888 | + elif isinstance(value, float) and field in self._float_format: | |
| 889 | + value = self._unicode(("%%%sf" % self._float_format[field]) % value) | |
| 890 | + return self._unicode(value) | |
| 891 | + | |
| 892 | + def _compute_widths(self, rows, options): | |
| 893 | + if options["header"]: | |
| 894 | + widths = [_get_size(field)[0] for field in self._field_names] | |
| 895 | + else: | |
| 896 | + widths = len(self.field_names) * [0] | |
| 897 | + for row in rows: | |
| 898 | + for index, value in enumerate(row): | |
| 899 | + fieldname = self.field_names[index] | |
| 900 | + if fieldname in self.max_width: | |
| 901 | + widths[index] = max(widths[index], min(_get_size(value)[0], self.max_width[fieldname])) | |
| 902 | + else: | |
| 903 | + widths[index] = max(widths[index], _get_size(value)[0]) | |
| 904 | + self._widths = widths | |
| 905 | + | |
| 906 | + def _get_padding_widths(self, options): | |
| 907 | + | |
| 908 | + if options["left_padding_width"] is not None: | |
| 909 | + lpad = options["left_padding_width"] | |
| 910 | + else: | |
| 911 | + lpad = options["padding_width"] | |
| 912 | + if options["right_padding_width"] is not None: | |
| 913 | + rpad = options["right_padding_width"] | |
| 914 | + else: | |
| 915 | + rpad = options["padding_width"] | |
| 916 | + return lpad, rpad | |
| 917 | + | |
| 918 | + def _get_rows(self, options): | |
| 919 | + """Return only those data rows that should be printed, based on slicing and sorting. | |
| 920 | + | |
| 921 | + Arguments: | |
| 922 | + | |
| 923 | + options - dictionary of option settings.""" | |
| 924 | + | |
| 925 | + # Make a copy of only those rows in the slice range | |
| 926 | + rows = copy.deepcopy(self._rows[options["start"]:options["end"]]) | |
| 927 | + # Sort if necessary | |
| 928 | + if options["sortby"]: | |
| 929 | + sortindex = self._field_names.index(options["sortby"]) | |
| 930 | + # Decorate | |
| 931 | + rows = [[row[sortindex]]+row for row in rows] | |
| 932 | + # Sort | |
| 933 | + rows.sort(reverse=options["reversesort"], key=options["sort_key"]) | |
| 934 | + # Undecorate | |
| 935 | + rows = [row[1:] for row in rows] | |
| 936 | + return rows | |
| 937 | + | |
| 938 | + def _format_row(self, row, options): | |
| 939 | + return [self._format_value(field, value) for (field, value) in zip(self._field_names, row)] | |
| 940 | + | |
| 941 | + def _format_rows(self, rows, options): | |
| 942 | + return [self._format_row(row, options) for row in rows] | |
| 943 | + | |
| 944 | + ############################## | |
| 945 | + # PLAIN TEXT STRING METHODS # | |
| 946 | + ############################## | |
| 947 | + | |
| 948 | + def get_string(self, **kwargs): | |
| 949 | + | |
| 950 | + """Return string representation of table in current state. | |
| 951 | + | |
| 952 | + Arguments: | |
| 953 | + | |
| 954 | + start - index of first data row to include in output | |
| 955 | + end - index of last data row to include in output PLUS ONE (list slice style) | |
| 956 | + fields - names of fields (columns) to include | |
| 957 | + header - print a header showing field names (True or False) | |
| 958 | + border - print a border around the table (True or False) | |
| 959 | + hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE | |
| 960 | + vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE | |
| 961 | + int_format - controls formatting of integer data | |
| 962 | + float_format - controls formatting of floating point data | |
| 963 | + padding_width - number of spaces on either side of column data (only used if left and right paddings are None) | |
| 964 | + left_padding_width - number of spaces on left hand side of column data | |
| 965 | + right_padding_width - number of spaces on right hand side of column data | |
| 966 | + vertical_char - single character string used to draw vertical lines | |
| 967 | + horizontal_char - single character string used to draw horizontal lines | |
| 968 | + junction_char - single character string used to draw line junctions | |
| 969 | + sortby - name of field to sort rows by | |
| 970 | + sort_key - sorting key function, applied to data points before sorting | |
| 971 | + reversesort - True or False to sort in descending or ascending order | |
| 972 | + print empty - if True, stringify just the header for an empty table, if False return an empty string """ | |
| 973 | + | |
| 974 | + options = self._get_options(kwargs) | |
| 975 | + | |
| 976 | + lines = [] | |
| 977 | + | |
| 978 | + # Don't think too hard about an empty table | |
| 979 | + # Is this the desired behaviour? Maybe we should still print the header? | |
| 980 | + if self.rowcount == 0 and (not options["print_empty"] or not options["border"]): | |
| 981 | + return "" | |
| 982 | + | |
| 983 | + # Get the rows we need to print, taking into account slicing, sorting, etc. | |
| 984 | + rows = self._get_rows(options) | |
| 985 | + | |
| 986 | + # Turn all data in all rows into Unicode, formatted as desired | |
| 987 | + formatted_rows = self._format_rows(rows, options) | |
| 988 | + | |
| 989 | + # Compute column widths | |
| 990 | + self._compute_widths(formatted_rows, options) | |
| 991 | + | |
| 992 | + # Add header or top of border | |
| 993 | + self._hrule = self._stringify_hrule(options) | |
| 994 | + if options["header"]: | |
| 995 | + lines.append(self._stringify_header(options)) | |
| 996 | + elif options["border"] and options["hrules"] in (ALL, FRAME): | |
| 997 | + lines.append(self._hrule) | |
| 998 | + | |
| 999 | + # Add rows | |
| 1000 | + for row in formatted_rows: | |
| 1001 | + lines.append(self._stringify_row(row, options)) | |
| 1002 | + | |
| 1003 | + # Add bottom of border | |
| 1004 | + if options["border"] and options["hrules"] == FRAME: | |
| 1005 | + lines.append(self._hrule) | |
| 1006 | + | |
| 1007 | + return self._unicode("\n").join(lines) | |
| 1008 | + | |
| 1009 | + def _stringify_hrule(self, options): | |
| 1010 | + | |
| 1011 | + if not options["border"]: | |
| 1012 | + return "" | |
| 1013 | + lpad, rpad = self._get_padding_widths(options) | |
| 1014 | + if options['vrules'] in (ALL, FRAME): | |
| 1015 | + bits = [options["junction_char"]] | |
| 1016 | + else: | |
| 1017 | + bits = [options["horizontal_char"]] | |
| 1018 | + # For tables with no data or fieldnames | |
| 1019 | + if not self._field_names: | |
| 1020 | + bits.append(options["junction_char"]) | |
| 1021 | + return "".join(bits) | |
| 1022 | + for field, width in zip(self._field_names, self._widths): | |
| 1023 | + if options["fields"] and field not in options["fields"]: | |
| 1024 | + continue | |
| 1025 | + bits.append((width+lpad+rpad)*options["horizontal_char"]) | |
| 1026 | + if options['vrules'] == ALL: | |
| 1027 | + bits.append(options["junction_char"]) | |
| 1028 | + else: | |
| 1029 | + bits.append(options["horizontal_char"]) | |
| 1030 | + if options["vrules"] == FRAME: | |
| 1031 | + bits.pop() | |
| 1032 | + bits.append(options["junction_char"]) | |
| 1033 | + return "".join(bits) | |
| 1034 | + | |
| 1035 | + def _stringify_header(self, options): | |
| 1036 | + | |
| 1037 | + bits = [] | |
| 1038 | + lpad, rpad = self._get_padding_widths(options) | |
| 1039 | + if options["border"]: | |
| 1040 | + if options["hrules"] in (ALL, FRAME): | |
| 1041 | + bits.append(self._hrule) | |
| 1042 | + bits.append("\n") | |
| 1043 | + if options["vrules"] in (ALL, FRAME): | |
| 1044 | + bits.append(options["vertical_char"]) | |
| 1045 | + else: | |
| 1046 | + bits.append(" ") | |
| 1047 | + # For tables with no data or field names | |
| 1048 | + if not self._field_names: | |
| 1049 | + if options["vrules"] in (ALL, FRAME): | |
| 1050 | + bits.append(options["vertical_char"]) | |
| 1051 | + else: | |
| 1052 | + bits.append(" ") | |
| 1053 | + for field, width, in zip(self._field_names, self._widths): | |
| 1054 | + if options["fields"] and field not in options["fields"]: | |
| 1055 | + continue | |
| 1056 | + if self._header_style == "cap": | |
| 1057 | + fieldname = field.capitalize() | |
| 1058 | + elif self._header_style == "title": | |
| 1059 | + fieldname = field.title() | |
| 1060 | + elif self._header_style == "upper": | |
| 1061 | + fieldname = field.upper() | |
| 1062 | + elif self._header_style == "lower": | |
| 1063 | + fieldname = field.lower() | |
| 1064 | + else: | |
| 1065 | + fieldname = field | |
| 1066 | + bits.append(" " * lpad + self._justify(fieldname, width, self._align[field]) + " " * rpad) | |
| 1067 | + if options["border"]: | |
| 1068 | + if options["vrules"] == ALL: | |
| 1069 | + bits.append(options["vertical_char"]) | |
| 1070 | + else: | |
| 1071 | + bits.append(" ") | |
| 1072 | + # If vrules is FRAME, then we just appended a space at the end | |
| 1073 | + # of the last field, when we really want a vertical character | |
| 1074 | + if options["border"] and options["vrules"] == FRAME: | |
| 1075 | + bits.pop() | |
| 1076 | + bits.append(options["vertical_char"]) | |
| 1077 | + if options["border"] and options["hrules"] != NONE: | |
| 1078 | + bits.append("\n") | |
| 1079 | + bits.append(self._hrule) | |
| 1080 | + return "".join(bits) | |
| 1081 | + | |
| 1082 | + def _stringify_row(self, row, options): | |
| 1083 | + | |
| 1084 | + for index, field, value, width, in zip(range(0,len(row)), self._field_names, row, self._widths): | |
| 1085 | + # Enforce max widths | |
| 1086 | + lines = value.split("\n") | |
| 1087 | + new_lines = [] | |
| 1088 | + for line in lines: | |
| 1089 | + if _str_block_width(line) > width: | |
| 1090 | + line = textwrap.fill(line, width) | |
| 1091 | + new_lines.append(line) | |
| 1092 | + lines = new_lines | |
| 1093 | + value = "\n".join(lines) | |
| 1094 | + row[index] = value | |
| 1095 | + | |
| 1096 | + row_height = 0 | |
| 1097 | + for c in row: | |
| 1098 | + h = _get_size(c)[1] | |
| 1099 | + if h > row_height: | |
| 1100 | + row_height = h | |
| 1101 | + | |
| 1102 | + bits = [] | |
| 1103 | + lpad, rpad = self._get_padding_widths(options) | |
| 1104 | + for y in range(0, row_height): | |
| 1105 | + bits.append([]) | |
| 1106 | + if options["border"]: | |
| 1107 | + if options["vrules"] in (ALL, FRAME): | |
| 1108 | + bits[y].append(self.vertical_char) | |
| 1109 | + else: | |
| 1110 | + bits[y].append(" ") | |
| 1111 | + | |
| 1112 | + for field, value, width, in zip(self._field_names, row, self._widths): | |
| 1113 | + | |
| 1114 | + valign = self._valign[field] | |
| 1115 | + lines = value.split("\n") | |
| 1116 | + dHeight = row_height - len(lines) | |
| 1117 | + if dHeight: | |
| 1118 | + if valign == "m": | |
| 1119 | + lines = [""] * int(dHeight / 2) + lines + [""] * (dHeight - int(dHeight / 2)) | |
| 1120 | + elif valign == "b": | |
| 1121 | + lines = [""] * dHeight + lines | |
| 1122 | + else: | |
| 1123 | + lines = lines + [""] * dHeight | |
| 1124 | + | |
| 1125 | + y = 0 | |
| 1126 | + for l in lines: | |
| 1127 | + if options["fields"] and field not in options["fields"]: | |
| 1128 | + continue | |
| 1129 | + | |
| 1130 | + bits[y].append(" " * lpad + self._justify(l, width, self._align[field]) + " " * rpad) | |
| 1131 | + if options["border"]: | |
| 1132 | + if options["vrules"] == ALL: | |
| 1133 | + bits[y].append(self.vertical_char) | |
| 1134 | + else: | |
| 1135 | + bits[y].append(" ") | |
| 1136 | + y += 1 | |
| 1137 | + | |
| 1138 | + # If vrules is FRAME, then we just appended a space at the end | |
| 1139 | + # of the last field, when we really want a vertical character | |
| 1140 | + for y in range(0, row_height): | |
| 1141 | + if options["border"] and options["vrules"] == FRAME: | |
| 1142 | + bits[y].pop() | |
| 1143 | + bits[y].append(options["vertical_char"]) | |
| 1144 | + | |
| 1145 | + if options["border"] and options["hrules"]== ALL: | |
| 1146 | + bits[row_height-1].append("\n") | |
| 1147 | + bits[row_height-1].append(self._hrule) | |
| 1148 | + | |
| 1149 | + for y in range(0, row_height): | |
| 1150 | + bits[y] = "".join(bits[y]) | |
| 1151 | + | |
| 1152 | + return "\n".join(bits) | |
| 1153 | + | |
| 1154 | + ############################## | |
| 1155 | + # HTML STRING METHODS # | |
| 1156 | + ############################## | |
| 1157 | + | |
| 1158 | + def get_html_string(self, **kwargs): | |
| 1159 | + | |
| 1160 | + """Return string representation of HTML formatted version of table in current state. | |
| 1161 | + | |
| 1162 | + Arguments: | |
| 1163 | + | |
| 1164 | + start - index of first data row to include in output | |
| 1165 | + end - index of last data row to include in output PLUS ONE (list slice style) | |
| 1166 | + fields - names of fields (columns) to include | |
| 1167 | + header - print a header showing field names (True or False) | |
| 1168 | + border - print a border around the table (True or False) | |
| 1169 | + hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE | |
| 1170 | + vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE | |
| 1171 | + int_format - controls formatting of integer data | |
| 1172 | + float_format - controls formatting of floating point data | |
| 1173 | + padding_width - number of spaces on either side of column data (only used if left and right paddings are None) | |
| 1174 | + left_padding_width - number of spaces on left hand side of column data | |
| 1175 | + right_padding_width - number of spaces on right hand side of column data | |
| 1176 | + sortby - name of field to sort rows by | |
| 1177 | + sort_key - sorting key function, applied to data points before sorting | |
| 1178 | + attributes - dictionary of name/value pairs to include as HTML attributes in the <table> tag | |
| 1179 | + xhtml - print <br/> tags if True, <br> tags if false""" | |
| 1180 | + | |
| 1181 | + options = self._get_options(kwargs) | |
| 1182 | + | |
| 1183 | + if options["format"]: | |
| 1184 | + string = self._get_formatted_html_string(options) | |
| 1185 | + else: | |
| 1186 | + string = self._get_simple_html_string(options) | |
| 1187 | + | |
| 1188 | + return string | |
| 1189 | + | |
| 1190 | + def _get_simple_html_string(self, options): | |
| 1191 | + | |
| 1192 | + lines = [] | |
| 1193 | + if options["xhtml"]: | |
| 1194 | + linebreak = "<br/>" | |
| 1195 | + else: | |
| 1196 | + linebreak = "<br>" | |
| 1197 | + | |
| 1198 | + open_tag = [] | |
| 1199 | + open_tag.append("<table") | |
| 1200 | + if options["attributes"]: | |
| 1201 | + for attr_name in options["attributes"]: | |
| 1202 | + open_tag.append(" %s=\"%s\"" % (attr_name, options["attributes"][attr_name])) | |
| 1203 | + open_tag.append(">") | |
| 1204 | + lines.append("".join(open_tag)) | |
| 1205 | + | |
| 1206 | + # Headers | |
| 1207 | + if options["header"]: | |
| 1208 | + lines.append(" <tr>") | |
| 1209 | + for field in self._field_names: | |
| 1210 | + if options["fields"] and field not in options["fields"]: | |
| 1211 | + continue | |
| 1212 | + lines.append(" <th>%s</th>" % escape(field).replace("\n", linebreak)) | |
| 1213 | + lines.append(" </tr>") | |
| 1214 | + | |
| 1215 | + # Data | |
| 1216 | + rows = self._get_rows(options) | |
| 1217 | + formatted_rows = self._format_rows(rows, options) | |
| 1218 | + for row in formatted_rows: | |
| 1219 | + lines.append(" <tr>") | |
| 1220 | + for field, datum in zip(self._field_names, row): | |
| 1221 | + if options["fields"] and field not in options["fields"]: | |
| 1222 | + continue | |
| 1223 | + lines.append(" <td>%s</td>" % escape(datum).replace("\n", linebreak)) | |
| 1224 | + lines.append(" </tr>") | |
| 1225 | + | |
| 1226 | + lines.append("</table>") | |
| 1227 | + | |
| 1228 | + return self._unicode("\n").join(lines) | |
| 1229 | + | |
| 1230 | + def _get_formatted_html_string(self, options): | |
| 1231 | + | |
| 1232 | + lines = [] | |
| 1233 | + lpad, rpad = self._get_padding_widths(options) | |
| 1234 | + if options["xhtml"]: | |
| 1235 | + linebreak = "<br/>" | |
| 1236 | + else: | |
| 1237 | + linebreak = "<br>" | |
| 1238 | + | |
| 1239 | + open_tag = [] | |
| 1240 | + open_tag.append("<table") | |
| 1241 | + if options["border"]: | |
| 1242 | + if options["hrules"] == ALL and options["vrules"] == ALL: | |
| 1243 | + open_tag.append(" frame=\"box\" rules=\"all\"") | |
| 1244 | + elif options["hrules"] == FRAME and options["vrules"] == FRAME: | |
| 1245 | + open_tag.append(" frame=\"box\"") | |
| 1246 | + elif options["hrules"] == FRAME and options["vrules"] == ALL: | |
| 1247 | + open_tag.append(" frame=\"box\" rules=\"cols\"") | |
| 1248 | + elif options["hrules"] == FRAME: | |
| 1249 | + open_tag.append(" frame=\"hsides\"") | |
| 1250 | + elif options["hrules"] == ALL: | |
| 1251 | + open_tag.append(" frame=\"hsides\" rules=\"rows\"") | |
| 1252 | + elif options["vrules"] == FRAME: | |
| 1253 | + open_tag.append(" frame=\"vsides\"") | |
| 1254 | + elif options["vrules"] == ALL: | |
| 1255 | + open_tag.append(" frame=\"vsides\" rules=\"cols\"") | |
| 1256 | + if options["attributes"]: | |
| 1257 | + for attr_name in options["attributes"]: | |
| 1258 | + open_tag.append(" %s=\"%s\"" % (attr_name, options["attributes"][attr_name])) | |
| 1259 | + open_tag.append(">") | |
| 1260 | + lines.append("".join(open_tag)) | |
| 1261 | + | |
| 1262 | + # Headers | |
| 1263 | + if options["header"]: | |
| 1264 | + lines.append(" <tr>") | |
| 1265 | + for field in self._field_names: | |
| 1266 | + if options["fields"] and field not in options["fields"]: | |
| 1267 | + continue | |
| 1268 | + lines.append(" <th style=\"padding-left: %dem; padding-right: %dem; text-align: center\">%s</th>" % (lpad, rpad, escape(field).replace("\n", linebreak))) | |
| 1269 | + lines.append(" </tr>") | |
| 1270 | + | |
| 1271 | + # Data | |
| 1272 | + rows = self._get_rows(options) | |
| 1273 | + formatted_rows = self._format_rows(rows, options) | |
| 1274 | + aligns = [] | |
| 1275 | + valigns = [] | |
| 1276 | + for field in self._field_names: | |
| 1277 | + aligns.append({ "l" : "left", "r" : "right", "c" : "center" }[self._align[field]]) | |
| 1278 | + valigns.append({"t" : "top", "m" : "middle", "b" : "bottom"}[self._valign[field]]) | |
| 1279 | + for row in formatted_rows: | |
| 1280 | + lines.append(" <tr>") | |
| 1281 | + for field, datum, align, valign in zip(self._field_names, row, aligns, valigns): | |
| 1282 | + if options["fields"] and field not in options["fields"]: | |
| 1283 | + continue | |
| 1284 | + lines.append(" <td style=\"padding-left: %dem; padding-right: %dem; text-align: %s; vertical-align: %s\">%s</td>" % (lpad, rpad, align, valign, escape(datum).replace("\n", linebreak))) | |
| 1285 | + lines.append(" </tr>") | |
| 1286 | + lines.append("</table>") | |
| 1287 | + | |
| 1288 | + return self._unicode("\n").join(lines) | |
| 1289 | + | |
| 1290 | +############################## | |
| 1291 | +# UNICODE WIDTH FUNCTIONS # | |
| 1292 | +############################## | |
| 1293 | + | |
| 1294 | +def _char_block_width(char): | |
| 1295 | + # Basic Latin, which is probably the most common case | |
| 1296 | + #if char in xrange(0x0021, 0x007e): | |
| 1297 | + #if char >= 0x0021 and char <= 0x007e: | |
| 1298 | + if 0x0021 <= char <= 0x007e: | |
| 1299 | + return 1 | |
| 1300 | + # Chinese, Japanese, Korean (common) | |
| 1301 | + if 0x4e00 <= char <= 0x9fff: | |
| 1302 | + return 2 | |
| 1303 | + # Hangul | |
| 1304 | + if 0xac00 <= char <= 0xd7af: | |
| 1305 | + return 2 | |
| 1306 | + # Combining? | |
| 1307 | + if unicodedata.combining(uni_chr(char)): | |
| 1308 | + return 0 | |
| 1309 | + # Hiragana and Katakana | |
| 1310 | + if 0x3040 <= char <= 0x309f or 0x30a0 <= char <= 0x30ff: | |
| 1311 | + return 2 | |
| 1312 | + # Full-width Latin characters | |
| 1313 | + if 0xff01 <= char <= 0xff60: | |
| 1314 | + return 2 | |
| 1315 | + # CJK punctuation | |
| 1316 | + if 0x3000 <= char <= 0x303e: | |
| 1317 | + return 2 | |
| 1318 | + # Backspace and delete | |
| 1319 | + if char in (0x0008, 0x007f): | |
| 1320 | + return -1 | |
| 1321 | + # Other control characters | |
| 1322 | + elif char in (0x0000, 0x001f): | |
| 1323 | + return 0 | |
| 1324 | + # Take a guess | |
| 1325 | + return 1 | |
| 1326 | + | |
| 1327 | +def _str_block_width(val): | |
| 1328 | + | |
| 1329 | + return sum(itermap(_char_block_width, itermap(ord, _re.sub("", val)))) | |
| 1330 | + | |
| 1331 | +############################## | |
| 1332 | +# TABLE FACTORIES # | |
| 1333 | +############################## | |
| 1334 | + | |
| 1335 | +def from_csv(fp, field_names = None, **kwargs): | |
| 1336 | + | |
| 1337 | + dialect = csv.Sniffer().sniff(fp.read(1024)) | |
| 1338 | + fp.seek(0) | |
| 1339 | + reader = csv.reader(fp, dialect) | |
| 1340 | + | |
| 1341 | + table = PrettyTable(**kwargs) | |
| 1342 | + if field_names: | |
| 1343 | + table.field_names = field_names | |
| 1344 | + else: | |
| 1345 | + if py3k: | |
| 1346 | + table.field_names = [x.strip() for x in next(reader)] | |
| 1347 | + else: | |
| 1348 | + table.field_names = [x.strip() for x in reader.next()] | |
| 1349 | + | |
| 1350 | + for row in reader: | |
| 1351 | + table.add_row([x.strip() for x in row]) | |
| 1352 | + | |
| 1353 | + return table | |
| 1354 | + | |
| 1355 | +def from_db_cursor(cursor, **kwargs): | |
| 1356 | + | |
| 1357 | + if cursor.description: | |
| 1358 | + table = PrettyTable(**kwargs) | |
| 1359 | + table.field_names = [col[0] for col in cursor.description] | |
| 1360 | + for row in cursor.fetchall(): | |
| 1361 | + table.add_row(row) | |
| 1362 | + return table | |
| 1363 | + | |
| 1364 | +class TableHandler(HTMLParser): | |
| 1365 | + | |
| 1366 | + def __init__(self, **kwargs): | |
| 1367 | + HTMLParser.__init__(self) | |
| 1368 | + self.kwargs = kwargs | |
| 1369 | + self.tables = [] | |
| 1370 | + self.last_row = [] | |
| 1371 | + self.rows = [] | |
| 1372 | + self.max_row_width = 0 | |
| 1373 | + self.active = None | |
| 1374 | + self.last_content = "" | |
| 1375 | + self.is_last_row_header = False | |
| 1376 | + | |
| 1377 | + def handle_starttag(self,tag, attrs): | |
| 1378 | + self.active = tag | |
| 1379 | + if tag == "th": | |
| 1380 | + self.is_last_row_header = True | |
| 1381 | + | |
| 1382 | + def handle_endtag(self,tag): | |
| 1383 | + if tag in ["th", "td"]: | |
| 1384 | + stripped_content = self.last_content.strip() | |
| 1385 | + self.last_row.append(stripped_content) | |
| 1386 | + if tag == "tr": | |
| 1387 | + self.rows.append( | |
| 1388 | + (self.last_row, self.is_last_row_header)) | |
| 1389 | + self.max_row_width = max(self.max_row_width, len(self.last_row)) | |
| 1390 | + self.last_row = [] | |
| 1391 | + self.is_last_row_header = False | |
| 1392 | + if tag == "table": | |
| 1393 | + table = self.generate_table(self.rows) | |
| 1394 | + self.tables.append(table) | |
| 1395 | + self.rows = [] | |
| 1396 | + self.last_content = " " | |
| 1397 | + self.active = None | |
| 1398 | + | |
| 1399 | + | |
| 1400 | + def handle_data(self, data): | |
| 1401 | + self.last_content += data | |
| 1402 | + | |
| 1403 | + def generate_table(self, rows): | |
| 1404 | + """ | |
| 1405 | + Generates from a list of rows a PrettyTable object. | |
| 1406 | + """ | |
| 1407 | + table = PrettyTable(**self.kwargs) | |
| 1408 | + for row in self.rows: | |
| 1409 | + if len(row[0]) < self.max_row_width: | |
| 1410 | + appends = self.max_row_width - len(row[0]) | |
| 1411 | + for i in range(1,appends): | |
| 1412 | + row[0].append("-") | |
| 1413 | + | |
| 1414 | + if row[1] == True: | |
| 1415 | + self.make_fields_unique(row[0]) | |
| 1416 | + table.field_names = row[0] | |
| 1417 | + else: | |
| 1418 | + table.add_row(row[0]) | |
| 1419 | + return table | |
| 1420 | + | |
| 1421 | + def make_fields_unique(self, fields): | |
| 1422 | + """ | |
| 1423 | + iterates over the row and make each field unique | |
| 1424 | + """ | |
| 1425 | + for i in range(0, len(fields)): | |
| 1426 | + for j in range(i+1, len(fields)): | |
| 1427 | + if fields[i] == fields[j]: | |
| 1428 | + fields[j] += "'" | |
| 1429 | + | |
| 1430 | +def from_html(html_code, **kwargs): | |
| 1431 | + """ | |
| 1432 | + Generates a list of PrettyTables from a string of HTML code. Each <table> in | |
| 1433 | + the HTML becomes one PrettyTable object. | |
| 1434 | + """ | |
| 1435 | + | |
| 1436 | + parser = TableHandler(**kwargs) | |
| 1437 | + parser.feed(html_code) | |
| 1438 | + return parser.tables | |
| 1439 | + | |
| 1440 | +def from_html_one(html_code, **kwargs): | |
| 1441 | + """ | |
| 1442 | + Generates a PrettyTables from a string of HTML code which contains only a | |
| 1443 | + single <table> | |
| 1444 | + """ | |
| 1445 | + | |
| 1446 | + tables = from_html(html_code, **kwargs) | |
| 1447 | + try: | |
| 1448 | + assert len(tables) == 1 | |
| 1449 | + except AssertionError: | |
| 1450 | + raise Exception("More than one <table> in provided HTML code! Use from_html instead.") | |
| 1451 | + return tables[0] | |
| 1452 | + | |
| 1453 | +############################## | |
| 1454 | +# MAIN (TEST FUNCTION) # | |
| 1455 | +############################## | |
| 1456 | + | |
| 1457 | +def main(): | |
| 1458 | + | |
| 1459 | + x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"]) | |
| 1460 | + x.sortby = "Population" | |
| 1461 | + x.reversesort = True | |
| 1462 | + x.int_format["Area"] = "04d" | |
| 1463 | + x.float_format = "6.1f" | |
| 1464 | + x.align["City name"] = "l" # Left align city names | |
| 1465 | + x.add_row(["Adelaide", 1295, 1158259, 600.5]) | |
| 1466 | + x.add_row(["Brisbane", 5905, 1857594, 1146.4]) | |
| 1467 | + x.add_row(["Darwin", 112, 120900, 1714.7]) | |
| 1468 | + x.add_row(["Hobart", 1357, 205556, 619.5]) | |
| 1469 | + x.add_row(["Sydney", 2058, 4336374, 1214.8]) | |
| 1470 | + x.add_row(["Melbourne", 1566, 3806092, 646.9]) | |
| 1471 | + x.add_row(["Perth", 5386, 1554769, 869.4]) | |
| 1472 | + print(x) | |
| 1473 | + | |
| 1474 | +if __name__ == "__main__": | |
| 1475 | + main() | ... | ... |