diff options
author | luke.maurits <luke.maurits@0f58610c-415a-11de-9c03-5d6cfad8e937> | 2009-05-26 07:50:55 +0000 |
---|---|---|
committer | luke.maurits <luke.maurits@0f58610c-415a-11de-9c03-5d6cfad8e937> | 2009-05-26 07:50:55 +0000 |
commit | f8a45724391ba5d26877f7ae9f4cb145d95876c9 (patch) | |
tree | 6c106dd9794353994b5d088c3483e9e1f58753aa | |
download | python-prettytable-f8a45724391ba5d26877f7ae9f4cb145d95876c9.tar.gz |
Initial import to Google code repository, after release of 0.5
git-svn-id: http://prettytable.googlecode.com/svn/trunk/trunk@3 0f58610c-415a-11de-9c03-5d6cfad8e937
-rw-r--r-- | COPYING | 27 | ||||
-rw-r--r-- | src/prettytable.py | 625 | ||||
-rw-r--r-- | src/setup.py | 13 |
3 files changed, 665 insertions, 0 deletions
@@ -0,0 +1,27 @@ +# Copyright (c) 2009, Luke Maurits <luke@maurits.id.au> +# All rights reserved. +# With contributions from: +# * Chris Clark +# +# 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/src/prettytable.py b/src/prettytable.py new file mode 100644 index 0000000..bd00f6b --- /dev/null +++ b/src/prettytable.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python +# +# PrettyTable TRUNK +# Copyright (c) 2009, Luke Maurits <luke@maurits.id.au> +# All rights reserved. +# With contributions from: +# * Chris Clark +# +# 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. + +import cgi +import copy +import cPickle +import sys + +FRAME = 0 +ALL = 1 +NONE = 2 + +class PrettyTable: + + def __init__(self, fields=None, caching=True, padding_width=1, left_padding=None, right_padding=None): + + """Return a new PrettyTable instance + + Arguments: + + fields - list or tuple of field names + caching - boolean value to turn string caching on/off + padding width - number of spaces between column lines and content""" + + # Data + self.fields = [] + if fields: + self.set_field_names(fields) + else: + self.widths = [] + self.aligns = [] + self.set_padding_width(padding_width) + self.rows = [] + self.cache = {} + self.html_cache = {} + + # Options + self.hrules = FRAME + self.caching = caching + self.padding_width = padding_width + self.left_padding = left_padding + self.right_padding = right_padding + self.vertical_char = "|" + self.horizontal_char = "-" + self.junction_char = "+" + + def __getslice__(self, i, j): + + """Return a new PrettyTable whose data rows are a slice of this one's + + Arguments: + + i - beginning slice index + j - ending slice index""" + + newtable = copy.deepcopy(self) + newtable.rows = self.rows[i:j] + return newtable + + def __str__(self): + + return self.get_string() + + ############################## + # ATTRIBUTE SETTERS # + ############################## + + def set_field_names(self, fields): + + """Set the names of the fields + + Arguments: + + fields - list or tuple of field names""" + + # We *may* need to change the widths if this isn't the first time + # setting the field names. This could certainly be done more + # efficiently. + if self.fields: + self.widths = [len(field) for field in fields] + for row in self.rows: + for i in range(0,len(row)): + if len(unicode(row[i])) > self.widths[i]: + self.widths[i] = len(unicode(row[i])) + else: + self.widths = [len(field) for field in fields] + self.fields = fields + self.aligns = len(fields)*["c"] + self.cache = {} + self.html_cache = {} + + def set_field_align(self, fieldname, align): + + """Set the alignment of a field by its fieldname + + Arguments: + + fieldname - name of the field whose alignment is to be changed + align - desired alignment - "l" for left, "c" for centre and "r" for right""" + + if fieldname not in self.fields: + raise Exception("No field %s exists!" % fieldname) + if align not in ["l","c","r"]: + raise Exception("Alignment %s is invalid, use l, c or r!" % align) + self.aligns[self.fields.index(fieldname)] = align + self.cache = {} + self.html_cache = {} + + def set_padding_width(self, padding_width): + + """Set the number of empty spaces between a column's edge and its content + + Arguments: + + padding_width - number of spaces, must be a positive integer""" + + try: + assert int(padding_width) >= 0 + except AssertionError: + raise Exception("Invalid value for padding_width: %s!" % unicode(padding_width)) + + self.padding_width = padding_width + self.cache = {} + self.html_cache = {} + + def set_left_padding(self, left_padding): + + """Set 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""" + + try: + assert left_padding == None or int(left_padding) >= 0 + except AssertionError: + raise Exception("Invalid value for left_padding: %s!" % unicode(left_padding)) + + self.left_padding = left_padding + self.cache = {} + self.html_cache = {} + + def set_right_padding(self, right_padding): + + """Set 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""" + + try: + assert right_padding == None or int(right_padding) >= 0 + except AssertionError: + raise Exception("Invalid value for right_padding: %s!" % unicode(right_padding)) + + self.right_padding = right_padding + self.cache = {} + self.html_cache = {} + + def set_border_chars(self, vertical="|", horizontal="-", junction="+"): + + """Set the characters to use when drawing the table border + + Arguments: + + vertical - character used to draw a vertical line segment. Default is | + horizontal - character used to draw a horizontal line segment. Default is - + junction - character used to draw a line junction. Default is +""" + + if len(vertical) > 1 or len(horizontal) > 1 or len(junction) > 1: + raise Exception("All border characters must be strings of length ONE!") + self.vertical_char = vertical + self.horizontal_char = horizontal + self.junction_char = junction + self.cache = {} + + ############################## + # 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 len(row) != len(self.fields): + raise Exception("Row has incorrect number of values, (actual) %d!=%d (expected)" %(len(row),len(self.fields))) + self.rows.append(row) + for i in range(0,len(row)): + if len(unicode(row[i])) > self.widths[i]: + self.widths[i] = len(unicode(row[i])) + self.html_cache = {} + + def add_column(self, fieldname, column, align="c"): + + """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""" + + if len(self.rows) in (0, len(column)): + if align not in ["l","c","r"]: + raise Exception("Alignment %s is invalid, use l, c or r!" % align) + self.fields.append(fieldname) + self.widths.append(len(fieldname)) + self.aligns.append(align) + for i in range(0, len(column)): + if len(self.rows) < i+1: + self.rows.append([]) + self.rows[i].append(column[i]) + if len(unicode(column[i])) > self.widths[-1]: + self.widths[-1] = len(unicode(column[i])) + else: + raise Exception("Column length %d does not match number of rows %d!" % (len(column), len(self.rows))) + + ############################## + # MISC PRIVATE METHODS # + ############################## + + def _get_sorted_rows(self, start, end, sortby, reversesort): + # Sort rows using the "Decorate, Sort, Undecorate" (DSU) paradigm + rows = copy.deepcopy(self.rows[start:end]) + sortindex = self.fields.index(sortby) + # Decorate + rows = [[row[sortindex]]+row for row in rows] + # Sort + rows.sort(reverse=reversesort) + # Undecorate + rows = [row[1:] for row in rows] + return rows + + def _get_paddings(self): + + if self.left_padding is not None: + lpad = self.left_padding + else: + lpad = self.padding_width + if self.right_padding is not None: + rpad = self.right_padding + else: + rpad = self.padding_width + return lpad, rpad + + ############################## + # ASCII PRINT/STRING METHODS # + ############################## + + def printt(self, start=0, end=None, fields=None, header=True, border=True, hrules=FRAME, sortby=None, reversesort=False): + + """Print table in current state to stdout. + + 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 + sortby - name of field to sort rows by + reversesort - True or False to sort in descending or ascending order + border - should be True or False to print or not print borders + hrules - controls printing of horizontal rules after each row. Allowed values: FRAME, ALL, NONE""" + + print self.get_string(start, end, fields, header, border, hrules, sortby, reversesort) + + def get_string(self, start=0, end=None, fields=None, header=True, border=True, hrules=FRAME, sortby=None, reversesort=False): + + """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 + sortby - name of field to sort rows by + reversesort - True or False to sort in descending or ascending order + border - should be True or False to print or not print borders + hrules - controls printing of horizontal rules after each row. Allowed values: FRAME, ALL, NONE""" + + if self.caching: + key = cPickle.dumps((start, end, fields, header, border, hrules, sortby, reversesort)) + if key in self.cache: + return self.cache[key] + + hrule = hrules or self.hrules + bits = [] + if not self.fields: + return "" + if not header: + # Recalculate widths - avoids tables with long field names but narrow data looking odd + old_widths = self.widths[:] + self.widths = [0]*len(self.fields) + for row in self.rows: + for i in range(0,len(row)): + if len(unicode(row[i])) > self.widths[i]: + self.widths[i] = len(unicode(row[i])) + if header: + bits.append(self._stringify_header(fields, border, hrules)) + elif border and hrules != NONE: + bits.append(self._stringify_hrule(fields, border)) + if sortby: + rows = self._get_sorted_rows(start, end, sortby, reversesort) + else: + rows = self.rows[start:end] + for row in rows: + bits.append(self._stringify_row(row, fields, border, hrule)) + if border and not hrule: + bits.append(self._stringify_hrule(fields, border)) + string = "\n".join(bits) + + if self.caching: + self.cache[key] = string + + if not header: + # Restore previous widths + self.widths = old_widths + for row in self.rows: + for i in range(0,len(row)): + if len(unicode(row[i])) > self.widths[i]: + self.widths[i] = len(unicode(row[i])) + + return string + + def _stringify_hrule(self, fields=None, border=True): + + if not border: + return "" + lpad, rpad = self._get_paddings() + padding_width = lpad+rpad + bits = [self.junction_char] + for field, width in zip(self.fields, self.widths): + if fields and field not in fields: + continue + bits.append((width+padding_width)*self.horizontal_char) + bits.append(self.junction_char) + return "".join(bits) + + def _stringify_header(self, fields=None, border=True, hrules=FRAME): + + lpad, rpad = self._get_paddings() + bits = [] + if border: + if hrules != NONE: + bits.append(self._stringify_hrule(fields, border)) + bits.append("\n") + bits.append(self.vertical_char) + for field, width in zip(self.fields, self.widths): + if fields and field not in fields: + continue + bits.append(" " * lpad + field.center(width) + " " * rpad) + if border: + bits.append(self.vertical_char) + if border and hrules != NONE: + bits.append("\n") + bits.append(self._stringify_hrule(fields, border)) + return "".join(bits) + + def _stringify_row(self, row, fields=None, border=True, hrule=False): + + lpad, rpad = self._get_paddings() + bits = [] + if border: + bits.append(self.vertical_char) + for field, value, width, align in zip(self.fields, row, self.widths, self.aligns): + if fields and field not in fields: + continue + if align == "l": + bits.append(" " * lpad + unicode(value).ljust(width) + " " * rpad) + elif align == "r": + bits.append(" " * lpad + unicode(value).rjust(width) + " " * rpad) + else: + bits.append(" " * lpad + unicode(value).center(width) + " " * rpad) + if border: + bits.append(self.vertical_char) + if border and hrule == ALL: + bits.append("\n") + bits.append(self._stringify_hrule(fields, border)) + return "".join(bits) + + ############################## + # HTML PRINT/STRING METHODS # + ############################## + + def print_html(self, start=0, end=None, fields=None, sortby=None, reversesort=False, format=True, header=True, border=True, hrules=FRAME, attributes=None): + + """Print HTML formatted version of table in current state to stdout. + + 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 + sortby - name of field to sort rows by + format - should be True or False to attempt to format alignmet, padding, etc. or not + header - should be True or False to print a header showing field names or not + border - should be True or False to print or not print borders + hrules - include horizontal rule after each row + attributes - dictionary of name/value pairs to include as HTML attributes in the <table> tag""" + + print self.get_html_string(start, end, fields, sortby, reversesort, format, header, border, hrules, attributes) + + def get_html_string(self, start=0, end=None, fields=None, sortby=None, reversesort=False, format=True, header=True, border=True, hrules=FRAME, attributes=None): + + """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 + sortby - name of + border - should be True or False to print or not print borders + format - should be True or False to attempt to format alignmet, padding, etc. or not + header - should be True or False to print a header showing field names or not + border - should be True or False to print or not print borders + hrules - include horizontal rule after each row + attributes - dictionary of name/value pairs to include as HTML attributes in the <table> tag""" + + if self.caching: + key = cPickle.dumps((start, end, fields, format, header, border, hrules, sortby, reversesort, attributes)) + if key in self.html_cache: + return self.html_cache[key] + + if format: + tmp_html_func=self._get_formatted_html_string + else: + tmp_html_func=self._get_simple_html_string + string = tmp_html_func(start, end, fields, sortby, reversesort, header, border, hrules, attributes) + + if self.caching: + self.html_cache[key] = string + + return string + + def _get_simple_html_string(self, start, end, fields, sortby, reversesort, header, border, hrules, attributes): + + bits = [] + # Slow but works + table_tag = '<table' + if border: + table_tag += ' border="1"' + if attributes: + for attr_name in attributes: + table_tag += ' %s="%s"' % (attr_name, attributes[attr_name]) + table_tag += '>' + bits.append(table_tag) + # Headers + bits.append(" <tr>") + for field in self.fields: + if fields and field not in fields: + continue + bits.append(" <th>%s</th>" % cgi.escape(unicode(field))) + bits.append(" </tr>") + # Data + if sortby: + rows = self._get_sorted_rows(stard, end, sortby, reversesort) + else: + rows = self.rows + for row in self.rows: + bits.append(" <tr>") + for field, datum in zip(self.fields, row): + if fields and field not in fields: + continue + bits.append(" <td>%s</td>" % cgi.escape(unicode(datum))) + bits.append(" </tr>") + bits.append("</table>") + string = "\n".join(bits) + + return string + + def _get_formatted_html_string(self, start, end, fields, sortby, reversesort, header, border, hrules, attributes): + + bits = [] + # Slow but works + table_tag = '<table' + if border: + table_tag += ' border="1"' + if hrules == NONE: + table_tag += ' frame="vsides" rules="cols"' + if attributes: + for attr_name in attributes: + table_tag += ' %s="%s"' % (attr_name, attributes[attr_name]) + table_tag += '>' + bits.append(table_tag) + # Headers + lpad, rpad = self._get_paddings() + if header: + bits.append(" <tr>") + for field in self.fields: + if fields and field not in fields: + continue + bits.append(" <th style=\"padding-left: %dem; padding-right: %dem; text-align: center\">%s</th>" % (lpad, rpad, cgi.escape(unicode(field)))) + bits.append(" </tr>") + # Data + if sortby: + rows = self._get_sorted_rows(start, end, sortby, reversesort) + else: + rows = self.rows + for row in self.rows: + bits.append(" <tr>") + for field, align, datum in zip(self.fields, self.aligns, row): + if fields and field not in fields: + continue + if align == "l": + bits.append(" <td style=\"padding-left: %dem; padding-right: %dem; text-align: left\">%s</td>" % (lpad, rpad, cgi.escape(unicode(datum)))) + elif align == "r": + bits.append(" <td style=\"padding-left: %dem; padding-right: %dem; text-align: right\">%s</td>" % (lpad, rpad, cgi.escape(unicode(datum)))) + else: + bits.append(" <td style=\"padding-left: %dem; padding-right: %dem; text-align: center\">%s</td>" % (lpad, rpad, cgi.escape(unicode(datum)))) + bits.append(" </tr>") + bits.append("</table>") + string = "\n".join(bits) + + return string + +def main(): + + x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"]) + x.set_field_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 len(sys.argv) > 1 and sys.argv[1] == "test": + + # This "test suite" is hideous and provides poor, arbitrary coverage. + # I'll replace it with some proper unit tests Sometime Soon (TM). + # Promise. + print "Testing field subset selection:" + x.printt(fields=["City name","Population"]) + print "Testing row subset selection:" + x.printt(start=2, end=5) + print "Testing hrules settings:" + print "FRAME:" + x.printt(hrules=FRAME) + print "ALL:" + x.printt(hrules=ALL) + print "NONE:" + x.printt(hrules=NONE) + print "Testing lack of headers:" + x.printt(header=False) + x.printt(header=False, border=False) + print "Testing lack of borders:" + x.printt(border=False) + print "Testing sorting:" + x.printt(sortby="City name") + x.printt(sortby="Annual Rainfall") + x.printt(sortby="Annual Rainfall", reversesort=True) + print "Testing padding parameter:" + x.set_padding_width(0) + x.printt() + x.set_padding_width(5) + x.printt() + x.set_left_padding(5) + x.set_right_padding(0) + x.printt() + x.set_right_padding(20) + x.printt() + x.set_left_padding(None) + x.set_right_padding(None) + x.set_padding_width(2) + print "Testing changing characters" + x.set_border_chars("*","*","*") + x.printt() + x.set_border_chars("!","~","o") + x.printt() + x.set_border_chars("|","-","+") + print "Testing everything at once:" + x.printt(start=2, end=5, fields=["City name","Population"], border=False, hrules=True) + print "Rebuilding by columns:" + x = PrettyTable() + x.add_column("City name", ["Adelaide", "Brisbane", "Darwin", "Hobart", "Sydney", "Melbourne", "Perth"]) + x.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5385]) + 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]) + x.printt() + print "Testing HTML:" + x.print_html() + x.print_html(border=False) + x.print_html(border=True) + x.print_html(format=False) + x.print_html(attributes={"name": "table", "id": "table"}) + +if __name__ == "__main__": + main() + diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..e773ec0 --- /dev/null +++ b/src/setup.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +from setuptools import setup + +setup( + name='prettytable', + version='TRUNK', + description='A simple Python library for easily displaying tabular data in a visually appealing ASCII table format', + author='Luke Maurits', + author_email='luke@maurits.id.au', + url='http://www.luke.maurits.id.au/software/prettytable', + license='http://www.luke.maurits.id.au/software/bsdlicense.txt', + py_modules=['prettytable'] +) |