From fedcde2099e1ae08667b00fd583155700b09ae3b Mon Sep 17 00:00:00 2001 From: Philippe Lagadec Date: Sun, 30 Nov 2014 23:03:48 +0100 Subject: [PATCH] added prettytable, used in oleid and oletimes to improve output --- oletools/oleid.py | 15 +++++++++++++-- oletools/oletimes.py | 31 +++++++++++++++++++++++++++---- oletools/thirdparty/prettytable/CHANGELOG | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ oletools/thirdparty/prettytable/COPYING | 30 ++++++++++++++++++++++++++++++ oletools/thirdparty/prettytable/README | 498 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ oletools/thirdparty/prettytable/__init__.py | 0 oletools/thirdparty/prettytable/prettytable.py | 1475 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 2185 insertions(+), 6 deletions(-) create mode 100644 oletools/thirdparty/prettytable/CHANGELOG create mode 100644 oletools/thirdparty/prettytable/COPYING create mode 100644 oletools/thirdparty/prettytable/README create mode 100644 oletools/thirdparty/prettytable/__init__.py create mode 100644 oletools/thirdparty/prettytable/prettytable.py diff --git a/oletools/oleid.py b/oletools/oleid.py index 39354de..2b977fb 100644 --- a/oletools/oleid.py +++ b/oletools/oleid.py @@ -47,8 +47,9 @@ http://www.decalage.info/python/oletools # 2012-10-29 v0.01 PL: - first version # 2014-11-29 v0.02 PL: - use olefile instead of OleFileIO_PL # - improved usage display with -h +# 2014-11-30 v0.03 PL: - improved output with prettytable -__version__ = '0.02' +__version__ = '0.03' #------------------------------------------------------------------------------ @@ -71,6 +72,7 @@ __version__ = '0.02' import optparse, sys, os, re, zlib, struct import thirdparty.olefile as olefile +from thirdparty.prettytable import prettytable #=== FUNCTIONS =============================================================== @@ -276,8 +278,17 @@ def main(): print '\nFilename:', filename oleid = OleID(filename) indicators = oleid.check() + + t = prettytable.PrettyTable(['Indicator', 'Value']) + t.align = 'l' + t.max_width = 39 + #t.border = False + for indicator in indicators: - print '%s: %s' % (indicator.name, indicator.value) + #print '%s: %s' % (indicator.name, indicator.value) + t.add_row((indicator.name, indicator.value)) + + print t if __name__ == '__main__': main() diff --git a/oletools/oletimes.py b/oletools/oletimes.py index ba16a66..67e4841 100644 --- a/oletools/oletimes.py +++ b/oletools/oletimes.py @@ -45,8 +45,9 @@ http://www.decalage.info/python/oletools # 2013-07-24 v0.01 PL: - first version # 2014-11-29 v0.02 PL: - use olefile instead of OleFileIO_PL # - improved usage display +# 2014-11-30 v0.03 PL: - improved output with prettytable -__version__ = '0.02' +__version__ = '0.03' #------------------------------------------------------------------------------ # TODO: @@ -57,8 +58,9 @@ __version__ = '0.02' #=== IMPORTS ================================================================= -import sys +import sys, datetime import thirdparty.olefile as olefile +from thirdparty.prettytable import prettytable #=== MAIN ================================================================= @@ -68,9 +70,30 @@ try: except IndexError: sys.exit(__doc__) -print'- Root mtime=%s ctime=%s' % (ole.root.getmtime(), ole.root.getctime()) +def dt2str (dt): + """ + Convert a datetime object to a string for display, without microseconds + + :param dt: datetime.datetime object, or None + :return: str, or None + """ + if dt is None: + return None + dt = dt.replace(microsecond = 0) + return str(dt) + +t = prettytable.PrettyTable(['Stream/Storage name', 'Modification Time', 'Creation Time']) +t.align = 'l' +t.max_width = 26 +#t.border = False + +#print'- Root mtime=%s ctime=%s' % (ole.root.getmtime(), ole.root.getctime()) +t.add_row(('Root', dt2str(ole.root.getmtime()), dt2str(ole.root.getctime()))) for obj in ole.listdir(streams=True, storages=True): - print '- %s: mtime=%s ctime=%s' % (repr('/'.join(obj)), ole.getmtime(obj), ole.getctime(obj)) + #print '- %s: mtime=%s ctime=%s' % (repr('/'.join(obj)), ole.getmtime(obj), ole.getctime(obj)) + t.add_row((repr('/'.join(obj)), dt2str(ole.getmtime(obj)), dt2str(ole.getctime(obj)))) + +print t ole.close() diff --git a/oletools/thirdparty/prettytable/CHANGELOG b/oletools/thirdparty/prettytable/CHANGELOG new file mode 100644 index 0000000..a790f1f --- /dev/null +++ b/oletools/thirdparty/prettytable/CHANGELOG @@ -0,0 +1,142 @@ +########## PrettyTable 0.7 - Feb 17, 2013 ########### + +* Improved Python 2 and 3 compatibility (2.4-3.2). +* Improved support for non-Latin characters. Table widths should + now be calculated correctly for tables with e.g. Japanese text. +* Table contents can now be read in from a .csv file +* Table contents can now be read in from a DB-API compatible cursor +* Table contents can now be read in from a string containing a + HTML table (thanks to Christoph Robbert for submitting this patch!) +* new valign attribute controls vertical alignment of text when + some cells in a row have multiple lines of text and others don't. + (thanks to Google Code user maartendb for submitting this patch!) +* hrules attribute can now be set to HEADER, which draws a rule only + under the header row +* new vrules attribute controls drawing of vertical rules and can + be set to FRAME, ALL or NONE +* new header_style attribute controls formatting of text in table + headers and can be set to "cap", "title", "upper", "lower" or None +* Fixed a simple bug regarding validation of max_width (thanks to + Anthony Toole for pointing out this bug and providing a patch). +* Fixed a simple bug regarding initialisation of int_format value + for new tables (thanks to Ingo Schmiegel for pointing out this + bug!) +* Fixed a bug regarding some constructor keywords, such as "border", + being ignored (thanks to Google Code user antonio.s.messina for + reporting this bug). + +########## PrettyTable 0.6 - May 5, 2012 ########## + +* Code is now simultaneously compatible with Python 2 and 3 +* Replaced all setter methods with managed attributes +* All styling options can now be set persistently as managed attributes +* Added "add_style" method to make setting style options easily +* Added "del_row", "clear_rows" and "clear" methods to facilitate + removal of data from table. +* Added "copy" method to facilitate cloning of a table. +* Removed caching functionality, which added complexity and fragility + for relatively little gain +* Removed methods that just printed strings produced by get_string and + get_html_string - just use inbuilt print! +* Improved unicode support (thanks to Google Code user ru.w31rd0 for + patch!) +* Added support for decimal and floating point number formatting + support (thanks to Google Code user willfurnass for the suggestion!) +* Added support for using a custom key sorting methods (thanks to + Google Code user amannijhawan for the suggestion!) +* Added support for line breaks in data (suggested and implemented by + Klein Stephane) +* Added support for max column widths (thanks to Tibor Arpas for the + suggestion!) +* Fixed table slicing +* Fixed bug where closing tags in HTML tables were not printed + (thanks to Google Code user kehander for reporting this bug!) +* Fixed HTML table sorting bug (thanks to Google Code user dougbeal + for reporting this bug!) +* Fixed bug whereby changing field_names did not recompute widths + (thanks to Google Code user denilsonsa for reporting this bug!) + +########## PrettyTable 0.5 - May 26, 2009 ########## + +* Fixed a bug whereby printing with headers=False and border=False + would introduce an extraneous newline. Thanks to Alexander Lamaison + for reporting this bug. +* When printing with headers=False, column widths will now be reduced + as appropriate in columns where the field name is wider than the + data. Thanks to Alexander Lamaison for suggesting this behaviour. +* Support for Unicode has improved. Thanks to Chris Clark for + submitting this improvement. +* The value of the "border" argument now correctly controls the + presence of a border when printing HTML tables with print_html or + get_html_string, instead of being incorrectly ignored. Thanks to + Chris Clark for fixing this. +* The print_html and get_html_string methods now accept an + "attributes" argument which is a dictionary of name/value pairs to be + placed inside the tag (so you can, e.g. set class, name or id + values in order to style your table with CSS). Thanks to Chris Clark + for submitting this feature. +* The print_html and get_html_string methods now, by default, do their + best to match the various formatting options in their HTML output. + They use inline CSS to adjust the alignment of data in columns, the + padding widths of columns and in some cases the border settings. You + can give either method a "format=False" attribute to turn this + behaviour off if you want to do your own styling. With "format=False" + the methods print a "bare bones" table, similar to the default + behaviour in 0.4. + +########## PrettyTable 0.4 - May 13, 2009 ########## + +* Added "add_column" method to enable building tables up column-by-column. +* Added "print_HTML" and "get_HTML_string" methods to enable HTML table + production. +* Added "set_border_chars" method to enable control over characters used to + draw the table border. +* Added "set_left_padding" and "set_right_padding" methods to allow + independent padding control for both sides of a column. +* Added "sortby" option to enable column sorting. +* Added "header" option to enable switching off field name printing at top of + table. +* Modified "hrules" option to enable greater control over presence of + horizontal lines. +* Added "border" option to enable switching off all line printing. + +Thanks to Tim Cera, Chris Clark, Alexander Lamaison for suggesting and helping +to test many of the new features in this release. + +########## PrettyTable 0.3 - May 01, 2009 ########## + +* Added "padding_width" option to control the number of spaces between the + vertical line rules at the edges of a column and its content. This can be + set as a keyword argument to the constructor or after instantiation using + the "set_padding_width" method. The value is set to 1 by defaut. If your + table is too wide for a small screen with this value, setting it to 0 might + help you squeeze it in. + +Thanks to Chris Clark for contributing a patch against 0.2.1 to add this +feature! + +########## PrettyTable 0.2.1 - April 29, 2009 ########## + +* Caching no longer breaks when using the "printt(fields=[...])" syntax. The + list of fields was not hashable and hence could not be used as a dictionary + key. I fixed this using the output of the "cPickle" module's "dumps" + function as the dictionary key instead. +* Horizontal lines are now the appropriate length when the above syntax is + used. + +Thanks to Julien Koesten for reporting these bugs and testing the fixes almost +immediately after the release of 0.2! + +########## PrettyTable 0.2 - April 29, 2009 ########## + +* Added "get_string" method. +* Added "__str__" method (which just calls "get_string") to enable nice + "print x" syntax. +* Can now pass field names as a constructor argument. +* Return values of "get_string" are cached in a dictionary that is only + cleared after a call to "add_row" or something else which invalidates the + cache. + +########## PrettyTable 0.1 - February 26, 2009 ######### + +* Original release diff --git a/oletools/thirdparty/prettytable/COPYING b/oletools/thirdparty/prettytable/COPYING new file mode 100644 index 0000000..7de41fb --- /dev/null +++ b/oletools/thirdparty/prettytable/COPYING @@ -0,0 +1,30 @@ +# Copyright (c) 2009-2013 Luke Maurits +# All rights reserved. +# With contributions from: +# * Chris Clark +# * Christoph Robbert +# * Klein Stephane +# * "maartendb" +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/oletools/thirdparty/prettytable/README b/oletools/thirdparty/prettytable/README new file mode 100644 index 0000000..5f85d05 --- /dev/null +++ b/oletools/thirdparty/prettytable/README @@ -0,0 +1,498 @@ +TUTORIAL ON HOW TO USE THE PRETTYTABLE 0.6+ API + +*** This tutorial is distributed with PrettyTable and is meant to serve +as a "quick start" guide for the lazy or impatient. It is not an +exhaustive description of the whole API, and it is not guaranteed to be +100% up to date. For more complete and update documentation, check the +PrettyTable wiki at http://code.google.com/p/prettytable/w/list *** + += Getting your data into (and out of) the table = + +Let's suppose you have a shiny new PrettyTable: + +from prettytable import PrettyTable +x = PrettyTable() + +and you want to put some data into it. You have a few options. + +== Row by row == + +You can add data one row at a time. To do this you can set the field names +first using the `field_names` attribute, and then add the rows one at a time +using the `add_row` method: + +x.field_names = ["City name", "Area", "Population", "Annual Rainfall"] +x.add_row(["Adelaide",1295, 1158259, 600.5]) +x.add_row(["Brisbane",5905, 1857594, 1146.4]) +x.add_row(["Darwin", 112, 120900, 1714.7]) +x.add_row(["Hobart", 1357, 205556, 619.5]) +x.add_row(["Sydney", 2058, 4336374, 1214.8]) +x.add_row(["Melbourne", 1566, 3806092, 646.9]) +x.add_row(["Perth", 5386, 1554769, 869.4]) + +== Column by column == + +You can add data one column at a time as well. To do this you use the +`add_column` method, which takes two arguments - a string which is the name for +the field the column you are adding corresponds to, and a list or tuple which +contains the column data" + +x.add_column("City name", +["Adelaide","Brisbane","Darwin","Hobart","Sydney","Melbourne","Perth"]) +x.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386]) +x.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, +1554769]) +x.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, +869.4]) + +== Mixing and matching == + +If you really want to, you can even mix and match `add_row` and `add_column` +and build some of your table in one way and some of it in the other. There's a +unit test which makes sure that doing things this way will always work out +nicely as if you'd done it using just one of the two approaches. Tables built +this way are kind of confusing for other people to read, though, so don't do +this unless you have a good reason. + +== Importing data from a CSV file == + +If you have your table data in a comma separated values file (.csv), you can +read this data into a PrettyTable like this: + +from prettytable import from_csv +fp = open("myfile.csv", "r") +mytable = from_csv(fp) +fp.close() + +== Importing data from a HTML string == + +If you have a string containing a HTML
, you can read this data into a +PrettyTable like this: + +from prettytable import from_html +mytable = from_html(html_string) + +== Importing data from a database cursor == + +If you have your table data in a database which you can access using a library +which confirms to the Python DB-API (e.g. an SQLite database accessible using +the sqlite module), then you can build a PrettyTable using a cursor object, +like this: + +import sqlite3 +from prettytable import from_db_cursor + +connection = sqlite3.connect("mydb.db") +cursor = connection.cursor() +cursor.execute("SELECT field1, field2, field3 FROM my_table") +mytable = from_db_cursor(cursor) + +== Getting data out == + +There are three ways to get data out of a PrettyTable, in increasing order of +completeness: + + * The `del_row` method takes an integer index of a single row to delete. + * The `clear_rows` method takes no arguments and deletes all the rows in the +table - but keeps the field names as they were so you that you can repopulate +it with the same kind of data. + * The `clear` method takes no arguments and deletes all rows and all field +names. It's not quite the same as creating a fresh table instance, though - +style related settings, discussed later, are maintained. + += Displaying your table in ASCII form = + +PrettyTable's main goal is to let you print tables in an attractive ASCII form, +like this: + ++-----------+------+------------+-----------------+ +| City name | Area | Population | Annual Rainfall | ++-----------+------+------------+-----------------+ +| Adelaide | 1295 | 1158259 | 600.5 | +| Brisbane | 5905 | 1857594 | 1146.4 | +| Darwin | 112 | 120900 | 1714.7 | +| Hobart | 1357 | 205556 | 619.5 | +| Melbourne | 1566 | 3806092 | 646.9 | +| Perth | 5386 | 1554769 | 869.4 | +| Sydney | 2058 | 4336374 | 1214.8 | ++-----------+------+------------+-----------------+ + +You can print tables like this to `stdout` or get string representations of +them. + +== Printing == + +To print a table in ASCII form, you can just do this: + +print x + +in Python 2.x or: + +print(x) + +in Python 3.x. + +The old x.printt() method from versions 0.5 and earlier has been removed. + +To pass options changing the look of the table, use the get_string() method +documented below: + +print x.get_string() + +== Stringing == + +If you don't want to actually print your table in ASCII form but just get a +string containing what _would_ be printed if you use "print x", you can use +the `get_string` method: + +mystring = x.get_string() + +This string is guaranteed to look exactly the same as what would be printed by +doing "print x". You can now do all the usual things you can do with a +string, like write your table to a file or insert it into a GUI. + +== Controlling which data gets displayed == + +If you like, you can restrict the output of `print x` or `x.get_string` to +only the fields or rows you like. + +The `fields` argument to these methods takes a list of field names to be +printed: + +print x.get_string(fields=["City name", "Population"]) + +gives: + ++-----------+------------+ +| City name | Population | ++-----------+------------+ +| Adelaide | 1158259 | +| Brisbane | 1857594 | +| Darwin | 120900 | +| Hobart | 205556 | +| Melbourne | 3806092 | +| Perth | 1554769 | +| Sydney | 4336374 | ++-----------+------------+ + +The `start` and `end` arguments take the index of the first and last row to +print respectively. Note that the indexing works like Python list slicing - to +print the 2nd, 3rd and 4th rows of the table, set `start` to 1 (the first row +is row 0, so the second is row 1) and set `end` to 4 (the index of the 4th row, +plus 1): + +print x.get_string(start=1,end=4) + +prints: + ++-----------+------+------------+-----------------+ +| City name | Area | Population | Annual Rainfall | ++-----------+------+------------+-----------------+ +| Brisbane | 5905 | 1857594 | 1146.4 | +| Darwin | 112 | 120900 | 1714.7 | +| Hobart | 1357 | 205556 | 619.5 | ++-----------+------+------------+-----------------+ + +== Changing the alignment of columns == + +By default, all columns in a table are centre aligned. + +=== All columns at once === + +You can change the alignment of all the columns in a table at once by assigning +a one character string to the `align` attribute. The allowed strings are "l", +"r" and "c" for left, right and centre alignment, respectively: + +x.align = "r" +print x + +gives: + ++-----------+------+------------+-----------------+ +| City name | Area | Population | Annual Rainfall | ++-----------+------+------------+-----------------+ +| Adelaide | 1295 | 1158259 | 600.5 | +| Brisbane | 5905 | 1857594 | 1146.4 | +| Darwin | 112 | 120900 | 1714.7 | +| Hobart | 1357 | 205556 | 619.5 | +| Melbourne | 1566 | 3806092 | 646.9 | +| Perth | 5386 | 1554769 | 869.4 | +| Sydney | 2058 | 4336374 | 1214.8 | ++-----------+------+------------+-----------------+ + +=== One column at a time === + +You can also change the alignment of individual columns based on the +corresponding field name by treating the `align` attribute as if it were a +dictionary. + +x.align["City name"] = "l" +x.align["Area"] = "c" +x.align["Population"] = "r" +x.align["Annual Rainfall"] = "c" +print x + +gives: + ++-----------+------+------------+-----------------+ +| City name | Area | Population | Annual Rainfall | ++-----------+------+------------+-----------------+ +| Adelaide | 1295 | 1158259 | 600.5 | +| Brisbane | 5905 | 1857594 | 1146.4 | +| Darwin | 112 | 120900 | 1714.7 | +| Hobart | 1357 | 205556 | 619.5 | +| Melbourne | 1566 | 3806092 | 646.9 | +| Perth | 5386 | 1554769 | 869.4 | +| Sydney | 2058 | 4336374 | 1214.8 | ++-----------+------+------------+-----------------+ + +== Sorting your table by a field == + +You can make sure that your ASCII tables are produced with the data sorted by +one particular field by giving `get_string` a `sortby` keyword argument, which + must be a string containing the name of one field. + +For example, to print the example table we built earlier of Australian capital +city data, so that the most populated city comes last, we can do this: + +print x.get_string(sortby="Population") + +to get + ++-----------+------+------------+-----------------+ +| City name | Area | Population | Annual Rainfall | ++-----------+------+------------+-----------------+ +| Darwin | 112 | 120900 | 1714.7 | +| Hobart | 1357 | 205556 | 619.5 | +| Adelaide | 1295 | 1158259 | 600.5 | +| Perth | 5386 | 1554769 | 869.4 | +| Brisbane | 5905 | 1857594 | 1146.4 | +| Melbourne | 1566 | 3806092 | 646.9 | +| Sydney | 2058 | 4336374 | 1214.8 | ++-----------+------+------------+-----------------+ + +If we want the most populated city to come _first_, we can also give a +`reversesort=True` argument. + +If you _always_ want your tables to be sorted in a certain way, you can make +the setting long term like this: + +x.sortby = "Population" +print x +print x +print x + +All three tables printed by this code will be sorted by population (you could +do `x.reversesort = True` as well, if you wanted). The behaviour will persist +until you turn it off: + +x.sortby = None + +If you want to specify a custom sorting function, you can use the `sort_key` +keyword argument. Pass this a function which accepts two lists of values +and returns a negative or positive value depending on whether the first list +should appeare before or after the second one. If your table has n columns, +each list will have n+1 elements. Each list corresponds to one row of the +table. The first element will be whatever data is in the relevant row, in +the column specified by the `sort_by` argument. The remaining n elements +are the data in each of the table's columns, in order, including a repeated +instance of the data in the `sort_by` column. + += Changing the appearance of your table - the easy way = + +By default, PrettyTable produces ASCII tables that look like the ones used in +SQL database shells. But if can print them in a variety of other formats as +well. If the format you want to use is common, PrettyTable makes this very +easy for you to do using the `set_style` method. If you want to produce an +uncommon table, you'll have to do things slightly harder (see later). + +== Setting a table style == + +You can set the style for your table using the `set_style` method before any +calls to `print` or `get_string`. Here's how to print a table in a format +which works nicely with Microsoft Word's "Convert to table" feature: + +from prettytable import MSWORD_FRIENDLY +x.set_style(MSWORD_FRIENDLY) +print x + +In addition to `MSWORD_FRIENDLY` there are currently two other in-built styles +you can use for your tables: + + * `DEFAULT` - The default look, used to undo any style changes you may have +made + * `PLAIN_COLUMN` - A borderless style that works well with command line +programs for columnar data + +Other styles are likely to appear in future releases. + += Changing the appearance of your table - the hard way = + +If you want to display your table in a style other than one of the in-built +styles listed above, you'll have to set things up the hard way. + +Don't worry, it's not really that hard! + +== Style options == + +PrettyTable has a number of style options which control various aspects of how +tables are displayed. You have the freedom to set each of these options +individually to whatever you prefer. The `set_style` method just does this +automatically for you. + +The options are these: + + * `border` - A boolean option (must be `True` or `False`). Controls whether + or not a border is drawn around the table. + * `header` - A boolean option (must be `True` or `False`). Controls whether + or not the first row of the table is a header showing the names of all the + fields. + * `hrules` - Controls printing of horizontal rules after rows. Allowed + values: FRAME, HEADER, ALL, NONE - note that these are variables defined + inside the `prettytable` module so make sure you import them or use + `prettytable.FRAME` etc. + * `vrules` - Controls printing of vertical rules between columns. Allowed + values: FRAME, ALL, NONE. + * `int_format` - A string which controls the way integer data is printed. + This works like: print "%d" % data + * `float_format` - A string which controls the way floating point data is + printed. This works like: print "%f" % data + * `padding_width` - Number of spaces on either side of column data (only used + if left and right paddings are None). + * `left_padding_width` - Number of spaces on left hand side of column data. + * `right_padding_width` - Number of spaces on right hand side of column data. + * `vertical_char` - Single character string used to draw vertical lines. + Default is `|`. + * `horizontal_char` - Single character string used to draw horizontal lines. + Default is `-`. + * `junction_char` - Single character string used to draw line junctions. + Default is `+`. + +You can set the style options to your own settings in two ways: + +== Setting style options for the long term == + +If you want to print your table with a different style several times, you can +set your option for the "long term" just by changing the appropriate +attributes. If you never want your tables to have borders you can do this: + +x.border = False +print x +print x +print x + +Neither of the 3 tables printed by this will have borders, even if you do +things like add extra rows inbetween them. The lack of borders will last until +you do: + +x.border = True + +to turn them on again. This sort of long term setting is exactly how +`set_style` works. `set_style` just sets a bunch of attributes to pre-set +values for you. + +Note that if you know what style options you want at the moment you are +creating your table, you can specify them using keyword arguments to the +constructor. For example, the following two code blocks are equivalent: + +x = PrettyTable() +x.border = False +x.header = False +x.padding_width = 5 + +x = PrettyTable(border=False, header=False, padding_width=5) + +== Changing style options just once == + +If you don't want to make long term style changes by changing an attribute like +in the previous section, you can make changes that last for just one +``get_string`` by giving those methods keyword arguments. To print two +"normal" tables with one borderless table between them, you could do this: + +print x +print x.get_string(border=False) +print x + += Displaying your table in HTML form = + +PrettyTable will also print your tables in HTML form, as `
`s. Just like +in ASCII form, you can actually print your table - just use `print_html()` - or +get a string representation - just use `get_html_string()`. HTML printing +supports the `fields`, `start`, `end`, `sortby` and `reversesort` arguments in +exactly the same way as ASCII printing. + +== Styling HTML tables == + +By default, PrettyTable outputs HTML for "vanilla" tables. The HTML code is +quite simple. It looks like this: + +
+ + + + + + + + + + + + + + + + + ... + ... + ... +
City nameAreaPopulationAnnual Rainfall
Adelaide12951158259600.5
Brisbane590518575941146.4
+ +If you like, you can ask PrettyTable to do its best to mimick the style options +that your table has set using inline CSS. This is done by giving a +`format=True` keyword argument to either the `print_html` or `get_html_string` +methods. Note that if you _always_ want to print formatted HTML you can do: + +x.format = True + +and the setting will persist until you turn it off. + +Just like with ASCII tables, if you want to change the table's style for just +one `print_html` or one `get_html_string` you can pass those methods keyword +arguments - exactly like `print` and `get_string`. + +== Setting HTML attributes == + +You can provide a dictionary of HTML attribute name/value pairs to the +`print_html` and `get_html_string` methods using the `attributes` keyword +argument. This lets you specify common HTML attributes like `name`, `id` and +`class` that can be used for linking to your tables or customising their +appearance using CSS. For example: + +x.print_html(attributes={"name":"my_table", "class":"red_table"}) + +will print: + + + + + + + + + ... + ... + ... +
City nameAreaPopulationAnnual Rainfall
+ += Miscellaneous things = + +== Copying a table == + +You can call the `copy` method on a PrettyTable object without arguments to +return an identical independent copy of the table. + +If you want a copy of a PrettyTable object with just a subset of the rows, +you can use list slicing notation: + +new_table = old_table[0:5] diff --git a/oletools/thirdparty/prettytable/__init__.py b/oletools/thirdparty/prettytable/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/oletools/thirdparty/prettytable/__init__.py diff --git a/oletools/thirdparty/prettytable/prettytable.py b/oletools/thirdparty/prettytable/prettytable.py new file mode 100644 index 0000000..8abb952 --- /dev/null +++ b/oletools/thirdparty/prettytable/prettytable.py @@ -0,0 +1,1475 @@ +#!/usr/bin/env python +# +# Copyright (c) 2009-2013, Luke Maurits +# All rights reserved. +# With contributions from: +# * Chris Clark +# * Klein Stephane +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +__version__ = "0.7.2" + +import copy +import csv +import random +import re +import sys +import textwrap +import itertools +import unicodedata + +py3k = sys.version_info[0] >= 3 +if py3k: + unicode = str + basestring = str + itermap = map + iterzip = zip + uni_chr = chr + from html.parser import HTMLParser +else: + itermap = itertools.imap + iterzip = itertools.izip + uni_chr = unichr + from HTMLParser import HTMLParser + +if py3k and sys.version_info[1] >= 2: + from html import escape +else: + from cgi import escape + +# hrule styles +FRAME = 0 +ALL = 1 +NONE = 2 +HEADER = 3 + +# Table styles +DEFAULT = 10 +MSWORD_FRIENDLY = 11 +PLAIN_COLUMNS = 12 +RANDOM = 20 + +_re = re.compile("\033\[[0-9;]*m") + +def _get_size(text): + lines = text.split("\n") + height = len(lines) + width = max([_str_block_width(line) for line in lines]) + return (width, height) + +class PrettyTable(object): + + def __init__(self, field_names=None, **kwargs): + + """Return a new PrettyTable instance + + Arguments: + + encoding - Unicode encoding scheme used to decode any encoded input + field_names - list or tuple of field names + fields - list or tuple of field names to include in displays + start - index of first data row to include in output + end - index of last data row to include in output PLUS ONE (list slice style) + header - print a header showing field names (True or False) + header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None) + border - print a border around the table (True or False) + hrules - controls printing of horizontal rules after rows. Allowed values: FRAME, HEADER, ALL, NONE + vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE + int_format - controls formatting of integer data + float_format - controls formatting of floating point data + padding_width - number of spaces on either side of column data (only used if left and right paddings are None) + left_padding_width - number of spaces on left hand side of column data + right_padding_width - number of spaces on right hand side of column data + vertical_char - single character string used to draw vertical lines + horizontal_char - single character string used to draw horizontal lines + junction_char - single character string used to draw line junctions + sortby - name of field to sort rows by + sort_key - sorting key function, applied to data points before sorting + valign - default valign for each row (None, "t", "m" or "b") + reversesort - True or False to sort in descending or ascending order""" + + self.encoding = kwargs.get("encoding", "UTF-8") + + # Data + self._field_names = [] + self._align = {} + self._valign = {} + self._max_width = {} + self._rows = [] + if field_names: + self.field_names = field_names + else: + self._widths = [] + + # Options + self._options = "start end fields header border sortby reversesort sort_key attributes format hrules vrules".split() + self._options.extend("int_format float_format padding_width left_padding_width right_padding_width".split()) + self._options.extend("vertical_char horizontal_char junction_char header_style valign xhtml print_empty".split()) + for option in self._options: + if option in kwargs: + self._validate_option(option, kwargs[option]) + else: + kwargs[option] = None + + self._start = kwargs["start"] or 0 + self._end = kwargs["end"] or None + self._fields = kwargs["fields"] or None + + if kwargs["header"] in (True, False): + self._header = kwargs["header"] + else: + self._header = True + self._header_style = kwargs["header_style"] or None + if kwargs["border"] in (True, False): + self._border = kwargs["border"] + else: + self._border = True + self._hrules = kwargs["hrules"] or FRAME + self._vrules = kwargs["vrules"] or ALL + + self._sortby = kwargs["sortby"] or None + if kwargs["reversesort"] in (True, False): + self._reversesort = kwargs["reversesort"] + else: + self._reversesort = False + self._sort_key = kwargs["sort_key"] or (lambda x: x) + + self._int_format = kwargs["int_format"] or {} + self._float_format = kwargs["float_format"] or {} + self._padding_width = kwargs["padding_width"] or 1 + self._left_padding_width = kwargs["left_padding_width"] or None + self._right_padding_width = kwargs["right_padding_width"] or None + + self._vertical_char = kwargs["vertical_char"] or self._unicode("|") + self._horizontal_char = kwargs["horizontal_char"] or self._unicode("-") + self._junction_char = kwargs["junction_char"] or self._unicode("+") + + if kwargs["print_empty"] in (True, False): + self._print_empty = kwargs["print_empty"] + else: + self._print_empty = True + self._format = kwargs["format"] or False + self._xhtml = kwargs["xhtml"] or False + self._attributes = kwargs["attributes"] or {} + + def _unicode(self, value): + if not isinstance(value, basestring): + value = str(value) + if not isinstance(value, unicode): + value = unicode(value, self.encoding, "strict") + return value + + def _justify(self, text, width, align): + excess = width - _str_block_width(text) + if align == "l": + return text + excess * " " + elif align == "r": + return excess * " " + text + else: + if excess % 2: + # Uneven padding + # Put more space on right if text is of odd length... + if _str_block_width(text) % 2: + return (excess//2)*" " + text + (excess//2 + 1)*" " + # and more space on left if text is of even length + else: + return (excess//2 + 1)*" " + text + (excess//2)*" " + # Why distribute extra space this way? To match the behaviour of + # the inbuilt str.center() method. + else: + # Equal padding on either side + return (excess//2)*" " + text + (excess//2)*" " + + def __getattr__(self, name): + + if name == "rowcount": + return len(self._rows) + elif name == "colcount": + if self._field_names: + return len(self._field_names) + elif self._rows: + return len(self._rows[0]) + else: + return 0 + else: + raise AttributeError(name) + + def __getitem__(self, index): + + new = PrettyTable() + new.field_names = self.field_names + for attr in self._options: + setattr(new, "_"+attr, getattr(self, "_"+attr)) + setattr(new, "_align", getattr(self, "_align")) + if isinstance(index, slice): + for row in self._rows[index]: + new.add_row(row) + elif isinstance(index, int): + new.add_row(self._rows[index]) + else: + raise Exception("Index %s is invalid, must be an integer or slice" % str(index)) + return new + + if py3k: + def __str__(self): + return self.__unicode__() + else: + def __str__(self): + return self.__unicode__().encode(self.encoding) + + def __unicode__(self): + return self.get_string() + + ############################## + # ATTRIBUTE VALIDATORS # + ############################## + + # The method _validate_option is all that should be used elsewhere in the code base to validate options. + # It will call the appropriate validation method for that option. The individual validation methods should + # never need to be called directly (although nothing bad will happen if they *are*). + # Validation happens in TWO places. + # Firstly, in the property setters defined in the ATTRIBUTE MANAGMENT section. + # Secondly, in the _get_options method, where keyword arguments are mixed with persistent settings + + def _validate_option(self, option, val): + if option in ("field_names"): + self._validate_field_names(val) + elif option in ("start", "end", "max_width", "padding_width", "left_padding_width", "right_padding_width", "format"): + self._validate_nonnegative_int(option, val) + elif option in ("sortby"): + self._validate_field_name(option, val) + elif option in ("sort_key"): + self._validate_function(option, val) + elif option in ("hrules"): + self._validate_hrules(option, val) + elif option in ("vrules"): + self._validate_vrules(option, val) + elif option in ("fields"): + self._validate_all_field_names(option, val) + elif option in ("header", "border", "reversesort", "xhtml", "print_empty"): + self._validate_true_or_false(option, val) + elif option in ("header_style"): + self._validate_header_style(val) + elif option in ("int_format"): + self._validate_int_format(option, val) + elif option in ("float_format"): + self._validate_float_format(option, val) + elif option in ("vertical_char", "horizontal_char", "junction_char"): + self._validate_single_char(option, val) + elif option in ("attributes"): + self._validate_attributes(option, val) + else: + raise Exception("Unrecognised option: %s!" % option) + + def _validate_field_names(self, val): + # Check for appropriate length + if self._field_names: + try: + assert len(val) == len(self._field_names) + except AssertionError: + raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._field_names))) + if self._rows: + try: + assert len(val) == len(self._rows[0]) + except AssertionError: + raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._rows[0]))) + # Check for uniqueness + try: + assert len(val) == len(set(val)) + except AssertionError: + raise Exception("Field names must be unique!") + + def _validate_header_style(self, val): + try: + assert val in ("cap", "title", "upper", "lower", None) + except AssertionError: + raise Exception("Invalid header style, use cap, title, upper, lower or None!") + + def _validate_align(self, val): + try: + assert val in ["l","c","r"] + except AssertionError: + raise Exception("Alignment %s is invalid, use l, c or r!" % val) + + def _validate_valign(self, val): + try: + assert val in ["t","m","b",None] + except AssertionError: + raise Exception("Alignment %s is invalid, use t, m, b or None!" % val) + + def _validate_nonnegative_int(self, name, val): + try: + assert int(val) >= 0 + except AssertionError: + raise Exception("Invalid value for %s: %s!" % (name, self._unicode(val))) + + def _validate_true_or_false(self, name, val): + try: + assert val in (True, False) + except AssertionError: + raise Exception("Invalid value for %s! Must be True or False." % name) + + def _validate_int_format(self, name, val): + if val == "": + return + try: + assert type(val) in (str, unicode) + assert val.isdigit() + except AssertionError: + raise Exception("Invalid value for %s! Must be an integer format string." % name) + + def _validate_float_format(self, name, val): + if val == "": + return + try: + assert type(val) in (str, unicode) + assert "." in val + bits = val.split(".") + assert len(bits) <= 2 + assert bits[0] == "" or bits[0].isdigit() + assert bits[1] == "" or bits[1].isdigit() + except AssertionError: + raise Exception("Invalid value for %s! Must be a float format string." % name) + + def _validate_function(self, name, val): + try: + assert hasattr(val, "__call__") + except AssertionError: + raise Exception("Invalid value for %s! Must be a function." % name) + + def _validate_hrules(self, name, val): + try: + assert val in (ALL, FRAME, HEADER, NONE) + except AssertionError: + raise Exception("Invalid value for %s! Must be ALL, FRAME, HEADER or NONE." % name) + + def _validate_vrules(self, name, val): + try: + assert val in (ALL, FRAME, NONE) + except AssertionError: + raise Exception("Invalid value for %s! Must be ALL, FRAME, or NONE." % name) + + def _validate_field_name(self, name, val): + try: + assert (val in self._field_names) or (val is None) + except AssertionError: + raise Exception("Invalid field name: %s!" % val) + + def _validate_all_field_names(self, name, val): + try: + for x in val: + self._validate_field_name(name, x) + except AssertionError: + raise Exception("fields must be a sequence of field names!") + + def _validate_single_char(self, name, val): + try: + assert _str_block_width(val) == 1 + except AssertionError: + raise Exception("Invalid value for %s! Must be a string of length 1." % name) + + def _validate_attributes(self, name, val): + try: + assert isinstance(val, dict) + except AssertionError: + raise Exception("attributes must be a dictionary of name/value pairs!") + + ############################## + # ATTRIBUTE MANAGEMENT # + ############################## + + def _get_field_names(self): + return self._field_names + """The names of the fields + + Arguments: + + fields - list or tuple of field names""" + def _set_field_names(self, val): + val = [self._unicode(x) for x in val] + self._validate_option("field_names", val) + if self._field_names: + old_names = self._field_names[:] + self._field_names = val + if self._align and old_names: + for old_name, new_name in zip(old_names, val): + self._align[new_name] = self._align[old_name] + for old_name in old_names: + if old_name not in self._align: + self._align.pop(old_name) + else: + for field in self._field_names: + self._align[field] = "c" + if self._valign and old_names: + for old_name, new_name in zip(old_names, val): + self._valign[new_name] = self._valign[old_name] + for old_name in old_names: + if old_name not in self._valign: + self._valign.pop(old_name) + else: + for field in self._field_names: + self._valign[field] = "t" + field_names = property(_get_field_names, _set_field_names) + + def _get_align(self): + return self._align + def _set_align(self, val): + self._validate_align(val) + for field in self._field_names: + self._align[field] = val + align = property(_get_align, _set_align) + + def _get_valign(self): + return self._valign + def _set_valign(self, val): + self._validate_valign(val) + for field in self._field_names: + self._valign[field] = val + valign = property(_get_valign, _set_valign) + + def _get_max_width(self): + return self._max_width + def _set_max_width(self, val): + self._validate_option("max_width", val) + for field in self._field_names: + self._max_width[field] = val + max_width = property(_get_max_width, _set_max_width) + + def _get_fields(self): + """List or tuple of field names to include in displays + + Arguments: + + fields - list or tuple of field names to include in displays""" + return self._fields + def _set_fields(self, val): + self._validate_option("fields", val) + self._fields = val + fields = property(_get_fields, _set_fields) + + def _get_start(self): + """Start index of the range of rows to print + + Arguments: + + start - index of first data row to include in output""" + return self._start + + def _set_start(self, val): + self._validate_option("start", val) + self._start = val + start = property(_get_start, _set_start) + + def _get_end(self): + """End index of the range of rows to print + + Arguments: + + end - index of last data row to include in output PLUS ONE (list slice style)""" + return self._end + def _set_end(self, val): + self._validate_option("end", val) + self._end = val + end = property(_get_end, _set_end) + + def _get_sortby(self): + """Name of field by which to sort rows + + Arguments: + + sortby - field name to sort by""" + return self._sortby + def _set_sortby(self, val): + self._validate_option("sortby", val) + self._sortby = val + sortby = property(_get_sortby, _set_sortby) + + def _get_reversesort(self): + """Controls direction of sorting (ascending vs descending) + + Arguments: + + reveresort - set to True to sort by descending order, or False to sort by ascending order""" + return self._reversesort + def _set_reversesort(self, val): + self._validate_option("reversesort", val) + self._reversesort = val + reversesort = property(_get_reversesort, _set_reversesort) + + def _get_sort_key(self): + """Sorting key function, applied to data points before sorting + + Arguments: + + sort_key - a function which takes one argument and returns something to be sorted""" + return self._sort_key + def _set_sort_key(self, val): + self._validate_option("sort_key", val) + self._sort_key = val + sort_key = property(_get_sort_key, _set_sort_key) + + def _get_header(self): + """Controls printing of table header with field names + + Arguments: + + header - print a header showing field names (True or False)""" + return self._header + def _set_header(self, val): + self._validate_option("header", val) + self._header = val + header = property(_get_header, _set_header) + + def _get_header_style(self): + """Controls stylisation applied to field names in header + + Arguments: + + header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None)""" + return self._header_style + def _set_header_style(self, val): + self._validate_header_style(val) + self._header_style = val + header_style = property(_get_header_style, _set_header_style) + + def _get_border(self): + """Controls printing of border around table + + Arguments: + + border - print a border around the table (True or False)""" + return self._border + def _set_border(self, val): + self._validate_option("border", val) + self._border = val + border = property(_get_border, _set_border) + + def _get_hrules(self): + """Controls printing of horizontal rules after rows + + Arguments: + + hrules - horizontal rules style. Allowed values: FRAME, ALL, HEADER, NONE""" + return self._hrules + def _set_hrules(self, val): + self._validate_option("hrules", val) + self._hrules = val + hrules = property(_get_hrules, _set_hrules) + + def _get_vrules(self): + """Controls printing of vertical rules between columns + + Arguments: + + vrules - vertical rules style. Allowed values: FRAME, ALL, NONE""" + return self._vrules + def _set_vrules(self, val): + self._validate_option("vrules", val) + self._vrules = val + vrules = property(_get_vrules, _set_vrules) + + def _get_int_format(self): + """Controls formatting of integer data + Arguments: + + int_format - integer format string""" + return self._int_format + def _set_int_format(self, val): +# self._validate_option("int_format", val) + for field in self._field_names: + self._int_format[field] = val + int_format = property(_get_int_format, _set_int_format) + + def _get_float_format(self): + """Controls formatting of floating point data + Arguments: + + float_format - floating point format string""" + return self._float_format + def _set_float_format(self, val): +# self._validate_option("float_format", val) + for field in self._field_names: + self._float_format[field] = val + float_format = property(_get_float_format, _set_float_format) + + def _get_padding_width(self): + """The number of empty spaces between a column's edge and its content + + Arguments: + + padding_width - number of spaces, must be a positive integer""" + return self._padding_width + def _set_padding_width(self, val): + self._validate_option("padding_width", val) + self._padding_width = val + padding_width = property(_get_padding_width, _set_padding_width) + + def _get_left_padding_width(self): + """The number of empty spaces between a column's left edge and its content + + Arguments: + + left_padding - number of spaces, must be a positive integer""" + return self._left_padding_width + def _set_left_padding_width(self, val): + self._validate_option("left_padding_width", val) + self._left_padding_width = val + left_padding_width = property(_get_left_padding_width, _set_left_padding_width) + + def _get_right_padding_width(self): + """The number of empty spaces between a column's right edge and its content + + Arguments: + + right_padding - number of spaces, must be a positive integer""" + return self._right_padding_width + def _set_right_padding_width(self, val): + self._validate_option("right_padding_width", val) + self._right_padding_width = val + right_padding_width = property(_get_right_padding_width, _set_right_padding_width) + + def _get_vertical_char(self): + """The charcter used when printing table borders to draw vertical lines + + Arguments: + + vertical_char - single character string used to draw vertical lines""" + return self._vertical_char + def _set_vertical_char(self, val): + val = self._unicode(val) + self._validate_option("vertical_char", val) + self._vertical_char = val + vertical_char = property(_get_vertical_char, _set_vertical_char) + + def _get_horizontal_char(self): + """The charcter used when printing table borders to draw horizontal lines + + Arguments: + + horizontal_char - single character string used to draw horizontal lines""" + return self._horizontal_char + def _set_horizontal_char(self, val): + val = self._unicode(val) + self._validate_option("horizontal_char", val) + self._horizontal_char = val + horizontal_char = property(_get_horizontal_char, _set_horizontal_char) + + def _get_junction_char(self): + """The charcter used when printing table borders to draw line junctions + + Arguments: + + junction_char - single character string used to draw line junctions""" + return self._junction_char + def _set_junction_char(self, val): + val = self._unicode(val) + self._validate_option("vertical_char", val) + self._junction_char = val + junction_char = property(_get_junction_char, _set_junction_char) + + def _get_format(self): + """Controls whether or not HTML tables are formatted to match styling options + + Arguments: + + format - True or False""" + return self._format + def _set_format(self, val): + self._validate_option("format", val) + self._format = val + format = property(_get_format, _set_format) + + def _get_print_empty(self): + """Controls whether or not empty tables produce a header and frame or just an empty string + + Arguments: + + print_empty - True or False""" + return self._print_empty + def _set_print_empty(self, val): + self._validate_option("print_empty", val) + self._print_empty = val + print_empty = property(_get_print_empty, _set_print_empty) + + def _get_attributes(self): + """A dictionary of HTML attribute name/value pairs to be included in the tag when printing HTML + + Arguments: + + attributes - dictionary of attributes""" + return self._attributes + def _set_attributes(self, val): + self._validate_option("attributes", val) + self._attributes = val + attributes = property(_get_attributes, _set_attributes) + + ############################## + # OPTION MIXER # + ############################## + + def _get_options(self, kwargs): + + options = {} + for option in self._options: + if option in kwargs: + self._validate_option(option, kwargs[option]) + options[option] = kwargs[option] + else: + options[option] = getattr(self, "_"+option) + return options + + ############################## + # PRESET STYLE LOGIC # + ############################## + + def set_style(self, style): + + if style == DEFAULT: + self._set_default_style() + elif style == MSWORD_FRIENDLY: + self._set_msword_style() + elif style == PLAIN_COLUMNS: + self._set_columns_style() + elif style == RANDOM: + self._set_random_style() + else: + raise Exception("Invalid pre-set style!") + + def _set_default_style(self): + + self.header = True + self.border = True + self._hrules = FRAME + self._vrules = ALL + self.padding_width = 1 + self.left_padding_width = 1 + self.right_padding_width = 1 + self.vertical_char = "|" + self.horizontal_char = "-" + self.junction_char = "+" + + def _set_msword_style(self): + + self.header = True + self.border = True + self._hrules = NONE + self.padding_width = 1 + self.left_padding_width = 1 + self.right_padding_width = 1 + self.vertical_char = "|" + + def _set_columns_style(self): + + self.header = True + self.border = False + self.padding_width = 1 + self.left_padding_width = 0 + self.right_padding_width = 8 + + def _set_random_style(self): + + # Just for fun! + self.header = random.choice((True, False)) + self.border = random.choice((True, False)) + self._hrules = random.choice((ALL, FRAME, HEADER, NONE)) + self._vrules = random.choice((ALL, FRAME, NONE)) + self.left_padding_width = random.randint(0,5) + self.right_padding_width = random.randint(0,5) + self.vertical_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") + self.horizontal_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") + self.junction_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") + + ############################## + # DATA INPUT METHODS # + ############################## + + def add_row(self, row): + + """Add a row to the table + + Arguments: + + row - row of data, should be a list with as many elements as the table + has fields""" + + if self._field_names and len(row) != len(self._field_names): + raise Exception("Row has incorrect number of values, (actual) %d!=%d (expected)" %(len(row),len(self._field_names))) + if not self._field_names: + self.field_names = [("Field %d" % (n+1)) for n in range(0,len(row))] + self._rows.append(list(row)) + + def del_row(self, row_index): + + """Delete a row to the table + + Arguments: + + row_index - The index of the row you want to delete. Indexing starts at 0.""" + + if row_index > len(self._rows)-1: + raise Exception("Cant delete row at index %d, table only has %d rows!" % (row_index, len(self._rows))) + del self._rows[row_index] + + def add_column(self, fieldname, column, align="c", valign="t"): + + """Add a column to the table. + + Arguments: + + fieldname - name of the field to contain the new column of data + column - column of data, should be a list with as many elements as the + table has rows + align - desired alignment for this column - "l" for left, "c" for centre and "r" for right + valign - desired vertical alignment for new columns - "t" for top, "m" for middle and "b" for bottom""" + + if len(self._rows) in (0, len(column)): + self._validate_align(align) + self._validate_valign(valign) + self._field_names.append(fieldname) + self._align[fieldname] = align + self._valign[fieldname] = valign + for i in range(0, len(column)): + if len(self._rows) < i+1: + self._rows.append([]) + self._rows[i].append(column[i]) + else: + raise Exception("Column length %d does not match number of rows %d!" % (len(column), len(self._rows))) + + def clear_rows(self): + + """Delete all rows from the table but keep the current field names""" + + self._rows = [] + + def clear(self): + + """Delete all rows and field names from the table, maintaining nothing but styling options""" + + self._rows = [] + self._field_names = [] + self._widths = [] + + ############################## + # MISC PUBLIC METHODS # + ############################## + + def copy(self): + return copy.deepcopy(self) + + ############################## + # MISC PRIVATE METHODS # + ############################## + + def _format_value(self, field, value): + if isinstance(value, int) and field in self._int_format: + value = self._unicode(("%%%sd" % self._int_format[field]) % value) + elif isinstance(value, float) and field in self._float_format: + value = self._unicode(("%%%sf" % self._float_format[field]) % value) + return self._unicode(value) + + def _compute_widths(self, rows, options): + if options["header"]: + widths = [_get_size(field)[0] for field in self._field_names] + else: + widths = len(self.field_names) * [0] + for row in rows: + for index, value in enumerate(row): + fieldname = self.field_names[index] + if fieldname in self.max_width: + widths[index] = max(widths[index], min(_get_size(value)[0], self.max_width[fieldname])) + else: + widths[index] = max(widths[index], _get_size(value)[0]) + self._widths = widths + + def _get_padding_widths(self, options): + + if options["left_padding_width"] is not None: + lpad = options["left_padding_width"] + else: + lpad = options["padding_width"] + if options["right_padding_width"] is not None: + rpad = options["right_padding_width"] + else: + rpad = options["padding_width"] + return lpad, rpad + + def _get_rows(self, options): + """Return only those data rows that should be printed, based on slicing and sorting. + + Arguments: + + options - dictionary of option settings.""" + + # Make a copy of only those rows in the slice range + rows = copy.deepcopy(self._rows[options["start"]:options["end"]]) + # Sort if necessary + if options["sortby"]: + sortindex = self._field_names.index(options["sortby"]) + # Decorate + rows = [[row[sortindex]]+row for row in rows] + # Sort + rows.sort(reverse=options["reversesort"], key=options["sort_key"]) + # Undecorate + rows = [row[1:] for row in rows] + return rows + + def _format_row(self, row, options): + return [self._format_value(field, value) for (field, value) in zip(self._field_names, row)] + + def _format_rows(self, rows, options): + return [self._format_row(row, options) for row in rows] + + ############################## + # PLAIN TEXT STRING METHODS # + ############################## + + def get_string(self, **kwargs): + + """Return string representation of table in current state. + + Arguments: + + start - index of first data row to include in output + end - index of last data row to include in output PLUS ONE (list slice style) + fields - names of fields (columns) to include + header - print a header showing field names (True or False) + border - print a border around the table (True or False) + hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE + vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE + int_format - controls formatting of integer data + float_format - controls formatting of floating point data + padding_width - number of spaces on either side of column data (only used if left and right paddings are None) + left_padding_width - number of spaces on left hand side of column data + right_padding_width - number of spaces on right hand side of column data + vertical_char - single character string used to draw vertical lines + horizontal_char - single character string used to draw horizontal lines + junction_char - single character string used to draw line junctions + sortby - name of field to sort rows by + sort_key - sorting key function, applied to data points before sorting + reversesort - True or False to sort in descending or ascending order + print empty - if True, stringify just the header for an empty table, if False return an empty string """ + + options = self._get_options(kwargs) + + lines = [] + + # Don't think too hard about an empty table + # Is this the desired behaviour? Maybe we should still print the header? + if self.rowcount == 0 and (not options["print_empty"] or not options["border"]): + return "" + + # Get the rows we need to print, taking into account slicing, sorting, etc. + rows = self._get_rows(options) + + # Turn all data in all rows into Unicode, formatted as desired + formatted_rows = self._format_rows(rows, options) + + # Compute column widths + self._compute_widths(formatted_rows, options) + + # Add header or top of border + self._hrule = self._stringify_hrule(options) + if options["header"]: + lines.append(self._stringify_header(options)) + elif options["border"] and options["hrules"] in (ALL, FRAME): + lines.append(self._hrule) + + # Add rows + for row in formatted_rows: + lines.append(self._stringify_row(row, options)) + + # Add bottom of border + if options["border"] and options["hrules"] == FRAME: + lines.append(self._hrule) + + return self._unicode("\n").join(lines) + + def _stringify_hrule(self, options): + + if not options["border"]: + return "" + lpad, rpad = self._get_padding_widths(options) + if options['vrules'] in (ALL, FRAME): + bits = [options["junction_char"]] + else: + bits = [options["horizontal_char"]] + # For tables with no data or fieldnames + if not self._field_names: + bits.append(options["junction_char"]) + return "".join(bits) + for field, width in zip(self._field_names, self._widths): + if options["fields"] and field not in options["fields"]: + continue + bits.append((width+lpad+rpad)*options["horizontal_char"]) + if options['vrules'] == ALL: + bits.append(options["junction_char"]) + else: + bits.append(options["horizontal_char"]) + if options["vrules"] == FRAME: + bits.pop() + bits.append(options["junction_char"]) + return "".join(bits) + + def _stringify_header(self, options): + + bits = [] + lpad, rpad = self._get_padding_widths(options) + if options["border"]: + if options["hrules"] in (ALL, FRAME): + bits.append(self._hrule) + bits.append("\n") + if options["vrules"] in (ALL, FRAME): + bits.append(options["vertical_char"]) + else: + bits.append(" ") + # For tables with no data or field names + if not self._field_names: + if options["vrules"] in (ALL, FRAME): + bits.append(options["vertical_char"]) + else: + bits.append(" ") + for field, width, in zip(self._field_names, self._widths): + if options["fields"] and field not in options["fields"]: + continue + if self._header_style == "cap": + fieldname = field.capitalize() + elif self._header_style == "title": + fieldname = field.title() + elif self._header_style == "upper": + fieldname = field.upper() + elif self._header_style == "lower": + fieldname = field.lower() + else: + fieldname = field + bits.append(" " * lpad + self._justify(fieldname, width, self._align[field]) + " " * rpad) + if options["border"]: + if options["vrules"] == ALL: + bits.append(options["vertical_char"]) + else: + bits.append(" ") + # If vrules is FRAME, then we just appended a space at the end + # of the last field, when we really want a vertical character + if options["border"] and options["vrules"] == FRAME: + bits.pop() + bits.append(options["vertical_char"]) + if options["border"] and options["hrules"] != NONE: + bits.append("\n") + bits.append(self._hrule) + return "".join(bits) + + def _stringify_row(self, row, options): + + for index, field, value, width, in zip(range(0,len(row)), self._field_names, row, self._widths): + # Enforce max widths + lines = value.split("\n") + new_lines = [] + for line in lines: + if _str_block_width(line) > width: + line = textwrap.fill(line, width) + new_lines.append(line) + lines = new_lines + value = "\n".join(lines) + row[index] = value + + row_height = 0 + for c in row: + h = _get_size(c)[1] + if h > row_height: + row_height = h + + bits = [] + lpad, rpad = self._get_padding_widths(options) + for y in range(0, row_height): + bits.append([]) + if options["border"]: + if options["vrules"] in (ALL, FRAME): + bits[y].append(self.vertical_char) + else: + bits[y].append(" ") + + for field, value, width, in zip(self._field_names, row, self._widths): + + valign = self._valign[field] + lines = value.split("\n") + dHeight = row_height - len(lines) + if dHeight: + if valign == "m": + lines = [""] * int(dHeight / 2) + lines + [""] * (dHeight - int(dHeight / 2)) + elif valign == "b": + lines = [""] * dHeight + lines + else: + lines = lines + [""] * dHeight + + y = 0 + for l in lines: + if options["fields"] and field not in options["fields"]: + continue + + bits[y].append(" " * lpad + self._justify(l, width, self._align[field]) + " " * rpad) + if options["border"]: + if options["vrules"] == ALL: + bits[y].append(self.vertical_char) + else: + bits[y].append(" ") + y += 1 + + # If vrules is FRAME, then we just appended a space at the end + # of the last field, when we really want a vertical character + for y in range(0, row_height): + if options["border"] and options["vrules"] == FRAME: + bits[y].pop() + bits[y].append(options["vertical_char"]) + + if options["border"] and options["hrules"]== ALL: + bits[row_height-1].append("\n") + bits[row_height-1].append(self._hrule) + + for y in range(0, row_height): + bits[y] = "".join(bits[y]) + + return "\n".join(bits) + + ############################## + # HTML STRING METHODS # + ############################## + + def get_html_string(self, **kwargs): + + """Return string representation of HTML formatted version of table in current state. + + Arguments: + + start - index of first data row to include in output + end - index of last data row to include in output PLUS ONE (list slice style) + fields - names of fields (columns) to include + header - print a header showing field names (True or False) + border - print a border around the table (True or False) + hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE + vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE + int_format - controls formatting of integer data + float_format - controls formatting of floating point data + padding_width - number of spaces on either side of column data (only used if left and right paddings are None) + left_padding_width - number of spaces on left hand side of column data + right_padding_width - number of spaces on right hand side of column data + sortby - name of field to sort rows by + sort_key - sorting key function, applied to data points before sorting + attributes - dictionary of name/value pairs to include as HTML attributes in the
tag + xhtml - print
tags if True,
tags if false""" + + options = self._get_options(kwargs) + + if options["format"]: + string = self._get_formatted_html_string(options) + else: + string = self._get_simple_html_string(options) + + return string + + def _get_simple_html_string(self, options): + + lines = [] + if options["xhtml"]: + linebreak = "
" + else: + linebreak = "
" + + open_tag = [] + open_tag.append("") + lines.append("".join(open_tag)) + + # Headers + if options["header"]: + lines.append(" ") + for field in self._field_names: + if options["fields"] and field not in options["fields"]: + continue + lines.append(" " % escape(field).replace("\n", linebreak)) + lines.append(" ") + + # Data + rows = self._get_rows(options) + formatted_rows = self._format_rows(rows, options) + for row in formatted_rows: + lines.append(" ") + for field, datum in zip(self._field_names, row): + if options["fields"] and field not in options["fields"]: + continue + lines.append(" " % escape(datum).replace("\n", linebreak)) + lines.append(" ") + + lines.append("
%s
%s
") + + return self._unicode("\n").join(lines) + + def _get_formatted_html_string(self, options): + + lines = [] + lpad, rpad = self._get_padding_widths(options) + if options["xhtml"]: + linebreak = "
" + else: + linebreak = "
" + + open_tag = [] + open_tag.append("") + lines.append("".join(open_tag)) + + # Headers + if options["header"]: + lines.append(" ") + for field in self._field_names: + if options["fields"] and field not in options["fields"]: + continue + lines.append(" %s" % (lpad, rpad, escape(field).replace("\n", linebreak))) + lines.append(" ") + + # Data + rows = self._get_rows(options) + formatted_rows = self._format_rows(rows, options) + aligns = [] + valigns = [] + for field in self._field_names: + aligns.append({ "l" : "left", "r" : "right", "c" : "center" }[self._align[field]]) + valigns.append({"t" : "top", "m" : "middle", "b" : "bottom"}[self._valign[field]]) + for row in formatted_rows: + lines.append(" ") + for field, datum, align, valign in zip(self._field_names, row, aligns, valigns): + if options["fields"] and field not in options["fields"]: + continue + lines.append(" %s" % (lpad, rpad, align, valign, escape(datum).replace("\n", linebreak))) + lines.append(" ") + lines.append("") + + return self._unicode("\n").join(lines) + +############################## +# UNICODE WIDTH FUNCTIONS # +############################## + +def _char_block_width(char): + # Basic Latin, which is probably the most common case + #if char in xrange(0x0021, 0x007e): + #if char >= 0x0021 and char <= 0x007e: + if 0x0021 <= char <= 0x007e: + return 1 + # Chinese, Japanese, Korean (common) + if 0x4e00 <= char <= 0x9fff: + return 2 + # Hangul + if 0xac00 <= char <= 0xd7af: + return 2 + # Combining? + if unicodedata.combining(uni_chr(char)): + return 0 + # Hiragana and Katakana + if 0x3040 <= char <= 0x309f or 0x30a0 <= char <= 0x30ff: + return 2 + # Full-width Latin characters + if 0xff01 <= char <= 0xff60: + return 2 + # CJK punctuation + if 0x3000 <= char <= 0x303e: + return 2 + # Backspace and delete + if char in (0x0008, 0x007f): + return -1 + # Other control characters + elif char in (0x0000, 0x001f): + return 0 + # Take a guess + return 1 + +def _str_block_width(val): + + return sum(itermap(_char_block_width, itermap(ord, _re.sub("", val)))) + +############################## +# TABLE FACTORIES # +############################## + +def from_csv(fp, field_names = None, **kwargs): + + dialect = csv.Sniffer().sniff(fp.read(1024)) + fp.seek(0) + reader = csv.reader(fp, dialect) + + table = PrettyTable(**kwargs) + if field_names: + table.field_names = field_names + else: + if py3k: + table.field_names = [x.strip() for x in next(reader)] + else: + table.field_names = [x.strip() for x in reader.next()] + + for row in reader: + table.add_row([x.strip() for x in row]) + + return table + +def from_db_cursor(cursor, **kwargs): + + if cursor.description: + table = PrettyTable(**kwargs) + table.field_names = [col[0] for col in cursor.description] + for row in cursor.fetchall(): + table.add_row(row) + return table + +class TableHandler(HTMLParser): + + def __init__(self, **kwargs): + HTMLParser.__init__(self) + self.kwargs = kwargs + self.tables = [] + self.last_row = [] + self.rows = [] + self.max_row_width = 0 + self.active = None + self.last_content = "" + self.is_last_row_header = False + + def handle_starttag(self,tag, attrs): + self.active = tag + if tag == "th": + self.is_last_row_header = True + + def handle_endtag(self,tag): + if tag in ["th", "td"]: + stripped_content = self.last_content.strip() + self.last_row.append(stripped_content) + if tag == "tr": + self.rows.append( + (self.last_row, self.is_last_row_header)) + self.max_row_width = max(self.max_row_width, len(self.last_row)) + self.last_row = [] + self.is_last_row_header = False + if tag == "table": + table = self.generate_table(self.rows) + self.tables.append(table) + self.rows = [] + self.last_content = " " + self.active = None + + + def handle_data(self, data): + self.last_content += data + + def generate_table(self, rows): + """ + Generates from a list of rows a PrettyTable object. + """ + table = PrettyTable(**self.kwargs) + for row in self.rows: + if len(row[0]) < self.max_row_width: + appends = self.max_row_width - len(row[0]) + for i in range(1,appends): + row[0].append("-") + + if row[1] == True: + self.make_fields_unique(row[0]) + table.field_names = row[0] + else: + table.add_row(row[0]) + return table + + def make_fields_unique(self, fields): + """ + iterates over the row and make each field unique + """ + for i in range(0, len(fields)): + for j in range(i+1, len(fields)): + if fields[i] == fields[j]: + fields[j] += "'" + +def from_html(html_code, **kwargs): + """ + Generates a list of PrettyTables from a string of HTML code. Each in + the HTML becomes one PrettyTable object. + """ + + parser = TableHandler(**kwargs) + parser.feed(html_code) + return parser.tables + +def from_html_one(html_code, **kwargs): + """ + Generates a PrettyTables from a string of HTML code which contains only a + single
+ """ + + tables = from_html(html_code, **kwargs) + try: + assert len(tables) == 1 + except AssertionError: + raise Exception("More than one
in provided HTML code! Use from_html instead.") + return tables[0] + +############################## +# MAIN (TEST FUNCTION) # +############################## + +def main(): + + x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"]) + x.sortby = "Population" + x.reversesort = True + x.int_format["Area"] = "04d" + x.float_format = "6.1f" + x.align["City name"] = "l" # Left align city names + x.add_row(["Adelaide", 1295, 1158259, 600.5]) + x.add_row(["Brisbane", 5905, 1857594, 1146.4]) + x.add_row(["Darwin", 112, 120900, 1714.7]) + x.add_row(["Hobart", 1357, 205556, 619.5]) + x.add_row(["Sydney", 2058, 4336374, 1214.8]) + x.add_row(["Melbourne", 1566, 3806092, 646.9]) + x.add_row(["Perth", 5386, 1554769, 869.4]) + print(x) + +if __name__ == "__main__": + main() -- libgit2 0.21.4