summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorluke@maurits.id.au <luke@maurits.id.au@0f58610c-415a-11de-9c03-5d6cfad8e937>2013-02-19 04:36:20 +0000
committerluke@maurits.id.au <luke@maurits.id.au@0f58610c-415a-11de-9c03-5d6cfad8e937>2013-02-19 04:36:20 +0000
commit91c233ca7ac3dce72af85a78b37381ff0da2cda8 (patch)
tree9ba0883ddf605b42b4bf0ca157862b7f60c76a33
parent0866bd03a147d0f1708be7c6feb96426bad55908 (diff)
downloadpython-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.py208
-rw-r--r--prettytable_test.py121
-rw-r--r--setup.py2
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):
diff --git a/setup.py b/setup.py
index 157b620..20a2322 100644
--- a/setup.py
+++ b/setup.py
@@ -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',