diff options
| author | luke@maurits.id.au <luke@maurits.id.au@0f58610c-415a-11de-9c03-5d6cfad8e937> | 2013-02-19 04:36:20 +0000 |
|---|---|---|
| committer | luke@maurits.id.au <luke@maurits.id.au@0f58610c-415a-11de-9c03-5d6cfad8e937> | 2013-02-19 04:36:20 +0000 |
| commit | 91c233ca7ac3dce72af85a78b37381ff0da2cda8 (patch) | |
| tree | 9ba0883ddf605b42b4bf0ca157862b7f60c76a33 | |
| parent | 0866bd03a147d0f1708be7c6feb96426bad55908 (diff) | |
| download | python-prettytable-ptable-91c233ca7ac3dce72af85a78b37381ff0da2cda8.tar.gz | |
Copied 0.7-RELEASE code from 0.7 branch to trunk.\n\nDon't count ANSI color codes when calculating string length.
| -rw-r--r-- | prettytable.py | 208 | ||||
| -rw-r--r-- | prettytable_test.py | 121 | ||||
| -rw-r--r-- | setup.py | 2 |
3 files changed, 247 insertions, 84 deletions
diff --git a/prettytable.py b/prettytable.py index e08e2ca..f9bcbdf 100644 --- a/prettytable.py +++ b/prettytable.py @@ -29,11 +29,12 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -__version__ = "TRUNK" +__version__ = "trunk" import copy import csv import random +import re import sys import textwrap import itertools @@ -46,10 +47,12 @@ if py3k: 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 @@ -68,6 +71,8 @@ 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) @@ -87,7 +92,6 @@ class PrettyTable(object): 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) - fields - names of fields (columns) to include 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) @@ -106,17 +110,14 @@ class PrettyTable(object): valign - default valign for each row (None, "t", "m" or "b") reversesort - True or False to sort in descending or ascending order""" - if "encoding" in kwargs: - self.encoding = kwargs["encoding"] - else: - self.encoding = "UTF-8" + self.encoding = kwargs.get("encoding", "UTF-8") # Data self._field_names = [] self._align = {} + self._valign = {} self._max_width = {} self._rows = [] - self._valign = [] if field_names: self.field_names = field_names else: @@ -212,14 +213,19 @@ class PrettyTable(object): def __getitem__(self, index): - newtable = copy.deepcopy(self) + 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): - newtable._rows = self._rows[index] + for row in self._rows[index]: + new.add_row(row) elif isinstance(index, int): - newtable._rows = [self._rows[index],] + new.add_row(self._rows[index]) else: raise Exception("Index %s is invalid, must be an integer or slice" % str(index)) - return newtable + return new if py3k: def __str__(self): @@ -406,10 +412,20 @@ class PrettyTable(object): for old_name, new_name in zip(old_names, val): self._align[new_name] = self._align[old_name] for old_name in old_names: - self._align.pop(old_name) + 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): @@ -424,8 +440,8 @@ class PrettyTable(object): return self._valign def _set_valign(self, val): self._validate_valign(val) - for ri in range(len(self._rows)): - self._valign[ri] = val + for field in self._field_names: + self._valign[field] = val valign = property(_get_valign, _set_valign) def _get_max_width(self): @@ -436,6 +452,18 @@ class PrettyTable(object): 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 @@ -760,25 +788,21 @@ class PrettyTable(object): # DATA INPUT METHODS # ############################## - def add_row(self, row, valign=None): + 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 - valign - vertical alignment, must be in (None, "t", "m" or "b") has fields""" - self._validate_valign(valign) 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)) - self._valign.append(valign) - def del_row(self, row_index): """Delete a row to the table @@ -790,9 +814,8 @@ class PrettyTable(object): 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] - del self._valign[row_index] - def add_column(self, fieldname, column, align="c", valign=None): + def add_column(self, fieldname, column, align="c", valign="t"): """Add a column to the table. @@ -802,17 +825,17 @@ class PrettyTable(object): 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 - None for don't care, "t" for top, "m" for middle and "b" for bottom""" + 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._valign.append(valign) self._rows[i].append(column[i]) else: raise Exception("Column length %d does not match number of rows %d!" % (len(column), len(self._rows))) @@ -822,14 +845,12 @@ class PrettyTable(object): """Delete all rows from the table but keep the current field names""" self._rows = [] - self._valign = [] def clear(self): """Delete all rows and field names from the table, maintaining nothing but styling options""" self._rows = [] - self._valign = [] self._field_names = [] self._widths = [] @@ -846,9 +867,9 @@ class PrettyTable(object): def _format_value(self, field, value): if isinstance(value, int) and field in self._int_format: - value = self._unicode(("{0:" + self._int_format[field] + "}").format(value)) + value = self._unicode(("%%%sd" % self._int_format[field]) % value) elif isinstance(value, float) and field in self._float_format: - value = self._unicode(("{0:" + self._float_format[field] + "}").format(value)) + value = self._unicode(("%%%sf" % self._float_format[field]) % value) return self._unicode(value) def _compute_widths(self, rows, options): @@ -958,8 +979,8 @@ class PrettyTable(object): lines.append(self._hrule) # Add rows - for row, valign in zip(formatted_rows, self._valign): - lines.append(self._stringify_row(row, valign, options)) + for row in formatted_rows: + lines.append(self._stringify_row(row, options)) # Add bottom of border if options["border"] and options["hrules"] == FRAME: @@ -1021,7 +1042,7 @@ class PrettyTable(object): bits.append(self._hrule) return "".join(bits) - def _stringify_row(self, row, valign, options): + 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 @@ -1053,6 +1074,7 @@ class PrettyTable(object): 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: @@ -1207,16 +1229,13 @@ class PrettyTable(object): valigns = [] for field in self._field_names: aligns.append({ "l" : "left", "r" : "right", "c" : "center" }[self._align[field]]) - for row, valign in zip(formatted_rows, self._valign): - if valign == None: - valign = "" - else: - valign = " vertical-align: %s;" % ({"t" : "top", "m" : "middle", "b" : "bottom"}[valign]) + valigns.append({"t" : "top", "m" : "middle", "b" : "bottom"}[self._valign[field]]) + for row in formatted_rows: lines.append(" <tr>") - for field, datum, align in zip(self._field_names, row, aligns): + 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(" <td style=\"padding-left: %dem; padding-right: %dem;%s text-align: %s\">%s</td>" % (lpad, rpad, valign, align, escape(datum).replace("\n", "<br />"))) + lines.append(" <td style=\"padding-left: %dem; padding-right: %dem; text-align: %s; vertical-align: %s\">%s</td>" % (lpad, rpad, align, valign, escape(datum).replace("\n", "<br />"))) lines.append(" </tr>") lines.append("</table>") @@ -1261,36 +1280,129 @@ def _char_block_width(char): def _str_block_width(val): - return sum(itermap(_char_block_width, itermap(ord, val))) + return sum(itermap(_char_block_width, itermap(ord, _re.sub("", val)))) ############################## # TABLE FACTORIES # ############################## -def from_csv(fp, field_names = None): +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() + table = PrettyTable(**kwargs) if field_names: table.field_names = field_names else: - table.field_names = [x.strip() for x in next(reader)] + 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): - - table = PrettyTable() - table.field_names = [col[0] for col in cursor.description] - for row in cursor.fetchall(): - table.add_row(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 <table> 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 <table> + """ + + tables = from_html(html_code, **kwargs) + try: + assert len(tables) == 1 + except AssertionError: + raise Exception("More than one <table> in provided HTML code! Use from_html instead.") + return tables[0] ############################## # MAIN (TEST FUNCTION) # diff --git a/prettytable_test.py b/prettytable_test.py index 2262115..1938de2 100644 --- a/prettytable_test.py +++ b/prettytable_test.py @@ -1,10 +1,20 @@ # coding=UTF-8 -import unittest +from prettytable import * + import sys -sys.path.append("../src/") +py3k = sys.version_info[0] >= 3 +try: + import sqlite3 + _have_sqlite = True +except ImportError: + _have_sqlite = False +if py3k: + import io as StringIO +else: + import StringIO from math import pi, e, sqrt -from prettytable import * +import unittest class BuildEquivelanceTest(unittest.TestCase): @@ -125,7 +135,9 @@ class OptionOverrideTests(CityDataTest): class OptionAttributeTests(CityDataTest): - """Make sure all options which have an attribute interface work as they should.""" + """Make sure all options which have an attribute interface work as they should. + Also make sure option settings are copied correctly when a table is cloned by + slicing.""" def testSetForAllColumns(self): self.x.field_names = sorted(self.x.field_names) @@ -148,12 +160,14 @@ class OptionAttributeTests(CityDataTest): self.x.junction_char = "*" self.x.format = True self.x.attributes = {"class" : "prettytable"} + assert self.x.get_string() == self.x[:].get_string() def testSetForOneColumn(self): self.x.align["Rainfall"] = "l" self.x.max_width["Name"] = 10 self.x.int_format["Population"] = "4" self.x.float_format["Area"] = "2.2" + assert self.x.get_string() == self.x[:].get_string() class BasicTests(CityDataTest): @@ -222,6 +236,10 @@ class SlicingTests(CityDataTest): def setUp(self): CityDataTest.setUp(self) + def testSliceAll(self): + y = self.x[:] + assert self.x.get_string() == y.get_string() + def testSliceFirstTwoRows(self): y = self.x[0:2] string = y.get_string() @@ -384,29 +402,6 @@ class BreakLineTests(unittest.TestCase): +------------+-------------+ """.strip() - t = PrettyTable(['Field 1', 'Field 2']) - t.add_row(['value 1', 'value2\nsecond line\nthird line'], valign = "m") - t.add_row(['value 3\nsecond line\nthirdline', 'value4'], valign = "b") - t.add_row(['value 3\nsecond line\nthirdline', 'value4'], valign = "t") - result = t.get_string(hrules=ALL) - assert result.strip() == """ -+-------------+-------------+ -| Field 1 | Field 2 | -+-------------+-------------+ -| | value2 | -| value 1 | second line | -| | third line | -+-------------+-------------+ -| value 3 | | -| second line | | -| thirdline | value4 | -+-------------+-------------+ -| value 3 | value4 | -| second line | | -| thirdline | | -+-------------+-------------+ -""".strip() - def testHtmlBreakLine(self): t = PrettyTable(['Field 1', 'Field 2']) t.add_row(['value 1', 'value2\nsecond line']) @@ -430,6 +425,7 @@ class BreakLineTests(unittest.TestCase): """.strip() class HtmlOutputTests(unittest.TestCase): + def testHtmlOutput(self): t = PrettyTable(['Field 1', 'Field 2', 'Field 3']) t.add_row(['value 1', 'value2', 'value3']) @@ -475,23 +471,76 @@ class HtmlOutputTests(unittest.TestCase): <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 3</th> </tr> <tr> - <td style="padding-left: 1em; padding-right: 1em; text-align: center">value 1</td> - <td style="padding-left: 1em; padding-right: 1em; text-align: center">value2</td> - <td style="padding-left: 1em; padding-right: 1em; text-align: center">value3</td> + <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 1</td> + <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value2</td> + <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value3</td> </tr> <tr> - <td style="padding-left: 1em; padding-right: 1em; text-align: center">value 4</td> - <td style="padding-left: 1em; padding-right: 1em; text-align: center">value5</td> - <td style="padding-left: 1em; padding-right: 1em; text-align: center">value6</td> + <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 4</td> + <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value5</td> + <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value6</td> </tr> <tr> - <td style="padding-left: 1em; padding-right: 1em; text-align: center">value 7</td> - <td style="padding-left: 1em; padding-right: 1em; text-align: center">value8</td> - <td style="padding-left: 1em; padding-right: 1em; text-align: center">value9</td> + <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 7</td> + <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value8</td> + <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value9</td> </tr> </table> """.strip() +class CsvConstructorTest(BasicTests): + + def setUp(self): + + csv_string = """City name, Area , Population , Annual Rainfall + Sydney, 2058 , 4336374 , 1214.8 + Melbourne, 1566 , 3806092 , 646.9 + Brisbane, 5905 , 1857594 , 1146.4 + Perth, 5386 , 1554769 , 869.4 + Adelaide, 1295 , 1158259 , 600.5 + Hobart, 1357 , 205556 , 619.5 + Darwin, 0112 , 120900 , 1714.7""" + csv_fp = StringIO.StringIO(csv_string) + self.x = from_csv(csv_fp) + +if _have_sqlite: + class DatabaseConstructorTest(BasicTests): + + def setUp(self): + self.conn = sqlite3.connect(":memory:") + self.cur = self.conn.cursor() + self.cur.execute("CREATE TABLE cities (name TEXT, area INTEGER, population INTEGER, rainfall REAL)") + self.cur.execute("INSERT INTO cities VALUES (\"Adelaide\", 1295, 1158259, 600.5)") + self.cur.execute("INSERT INTO cities VALUES (\"Brisbane\", 5905, 1857594, 1146.4)") + self.cur.execute("INSERT INTO cities VALUES (\"Darwin\", 112, 120900, 1714.7)") + self.cur.execute("INSERT INTO cities VALUES (\"Hobart\", 1357, 205556, 619.5)") + self.cur.execute("INSERT INTO cities VALUES (\"Sydney\", 2058, 4336374, 1214.8)") + self.cur.execute("INSERT INTO cities VALUES (\"Melbourne\", 1566, 3806092, 646.9)") + self.cur.execute("INSERT INTO cities VALUES (\"Perth\", 5386, 1554769, 869.4)") + self.cur.execute("SELECT * FROM cities") + self.x = from_db_cursor(self.cur) + + def testNonSelectCurosr(self): + self.cur.execute("INSERT INTO cities VALUES (\"Adelaide\", 1295, 1158259, 600.5)") + assert from_db_cursor(self.cur) is None + +class HtmlConstructorTest(CityDataTest): + + def testHtmlAndBack(self): + html_string = self.x.get_html_string() + new_table = from_html(html_string)[0] + assert new_table.get_string() == self.x.get_string() + + def testHtmlOneAndBack(self): + html_string = self.x.get_html_string() + new_table = from_html_one(html_string) + assert new_table.get_string() == self.x.get_string() + + def testHtmlOneFailOnMany(self): + html_string = self.x.get_html_string() + html_string += self.x.get_html_string() + self.assertRaises(Exception, from_html_one, html_string) + class PrintEnglishTest(CityDataTest): def testPrint(self): @@ -7,6 +7,8 @@ setup( version=version, classifiers=[ 'Programming Language :: Python', + 'Programming Language :: Python :: 2.4', + 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', |
