Compare commits

..

1 Commits

Author SHA1 Message Date
Kenneth Reitz 551fc90461 Merge branch 'release/0.6.4' 2010-09-20 09:21:22 -04:00
15 changed files with 129 additions and 757 deletions
+1 -2
View File
@@ -10,5 +10,4 @@ Development Lead
Patches and Suggestions
```````````````````````
- Luke Lee
- Josh Ourisman
- A Lucky Someone
+4 -51
View File
@@ -1,58 +1,11 @@
History
=======
0.8.4 (2010-10-04)
0.6.4 (2010-09-13)
------------------
* Upated XLS output: Only wrap if '\n' in cell.
0.8.3 (2010-10-04)
------------------
* Ability to append new column passing a callable
as the value that will be applied to every row.
0.8.2 (2010-10-04)
------------------
* Added alignment wrapping to written cells.
* Added separator support to XLS.
0.8.1 (2010-09-28)
------------------
* Packaging Fix
0.8.0 (2010-09-25)
------------------
* New format plugin system!
* Imports! ELEGANT Imports!
* Tests. Lots of tests.
0.7.1 (2010-09-20)
------------------
* Reverting methods back to properties.
* Windows bug compenated in documentation.
0.7.0 (2010-09-20)
------------------
* Renamed DataBook Databook for consistiency.
* Export properties changed to methods (XLS filename / StringIO bug).
* Optional Dataset.xls(path='filename') support (for writing on windows).
* Added utf-8 on the worksheet level.
0.6.4 (2010-09-19)
------------------
* Updated unicode export for XLS.
* More exhaustive unit tests.
* Updated unicode export for XLS
* More exhaustive unit tests
0.6.3 (2010-09-14)
+13 -57
View File
@@ -15,27 +15,15 @@ Tablib is a format-agnostic tabular dataset library, written in Python.
Output formats supported:
- Excel (Sets + Books)
- JSON (Sets + Books)
- YAML (Sets + Books)
- CSV (Sets)
- Excel
- JSON
- YAML
- CSV
Import formats supported:
- JSON (Sets + Books)
- YAML (Sets + Books)
- CSV (Sets)
At this time, Tablib supports the **export** of it's powerful Dataset object instances into any of the above formats. Import is underway.
Note that tablib *purposefully* excludes XML support. It always will.
Overview
--------
`tablib.Dataset()`
A Dataset is a table of tabular data. It may or may not have a header row. They can be build and maniuplated as raw Python datatypes (Lists of tuples|dictonaries). Datasets can be imported from JSON, YAML, and CSV; they can be exported to Excel (XLS), JSON, YAML, and CSV.
`tablib.Databook()`
A Databook is a set of Datasets. The most common form of a Databook is an Excel file with multiple spreadsheets. Databooks can be imported from JSON and YAML; they can be exported to Excel (XLS), JSON, and YAML.
Usage
-----
@@ -76,9 +64,6 @@ Easily delete rows: ::
>>> del data[1]
Exports
-------
Drumroll please...........
JSON!
@@ -121,44 +106,11 @@ EXCEL!
++++++
::
>>> open('people.xls', 'wb').write(data.xls)
>>> open('people.xls').write(data.xls)
It's that easy.
Imports!
--------
JSON
++++
::
>>> data.json = '[{"last_name": "Adams","age": 90,"first_name": "John"}]'
>>> print data[0]
('John', 'Adams', 90)
YAML
++++
::
>>> data.yaml = '- {age: 90, first_name: John, last_name: Adams}'
>>> print data[0]
('John', 'Adams', 90)
CSV
+++
::
>>> data.yaml = 'age, first_name, last_name\n90, John, Adams'
>>> print data[0]
('John', 'Adams', 90)
>>> print data.yaml
- {age: 90, first_name: John, last_name: Adams}
Installation
------------
@@ -179,10 +131,14 @@ If you'd like to contribute, simply fork `the repository`_, commit your changes
Roadmap
-------
- Add ability to add/remove full columns
- Import datasets from CSV, JSON, YAML
- Release CLI Interface
- Auto-detect import format
- Add possible other exports (SQL?)
- Possibly plugin-ify format architecture
- Ability to assign types to rows (set, regex=, &c.)
- Plugin support
.. _`the repository`: http://github.com/kennethreitz/tablib
.. _AUTHORS: http://github.com/kennethreitz/tablib/blob/master/AUTHORS
.. _AUTHORS: http://github.com/kennethreitz/tablib/blob/master/AUTHORS
+3 -2
View File
@@ -11,20 +11,21 @@ def publish():
"""Publish to PyPi"""
os.system("python setup.py sdist upload")
if sys.argv[-1] == "publish":
publish()
sys.exit()
setup(
name='tablib',
version='0.8.4',
version='0.6.4',
description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)',
long_description=open('README.rst').read() + '\n\n' +
open('HISTORY.rst').read(),
author='Kenneth Reitz',
author_email='me@kennethreitz.com',
url='http://github.com/kennethreitz/tablib',
packages=['tablib', 'tablib.formats'],
packages=['tablib'],
install_requires=['xlwt', 'simplejson', 'PyYAML'],
license='MIT',
classifiers=(
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tabbed -- CLI for Tablib
Copyright (c) 2010 Kenneth Reitz. MIT License.
"""
import tablib.cli
if __name__ == '__main__':
tablib.cli.start()
+1 -8
View File
@@ -1,8 +1 @@
""" Tablib.
"""
from tablib.core import (
Databook, Dataset, detect, import_set,
InvalidDatasetType, InvalidDimensions, UnsupportedFormat
)
from core import *
-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
""" Tabbed CLI Inteface Application
"""
import io
import sys
import argue
import tablib
from helpers import Struct, piped
FORMATS = [fmt.title for fmt in tablib.formats.FORMATS]
opts = []
opts.append(('v', 'version', False, 'Report tabbed version'))
for format in FORMATS:
opts.append(('', format, False, 'Output to %s' % (format.upper())))
@argue.command(options=opts, usage='[FILE] [--FORMAT | FILE]')
def start(in_file=None, out_file=None, **opts):
"""Covertly convert dataset formats"""
opts = Struct(**opts)
if opts.version:
print('Tabbed, Ver. %s' % tablib.core.__version__)
sys.exit(0)
stdin = piped()
if stdin:
data = tablib.import_set(stdin)
elif in_file:
try:
in_stream =- io.open(in_file, 'r').read()
except Exception, e:
print(' %s cannot be read.' % in_file)
sys.exit(65)
try:
tablib.import_set(in_stream)
except Exception, e:
raise e
print('Import format not supported.')
sys.exit(65)
else:
print('Please provide input.')
sys.exit(65)
_formats_sum = sum(opts[f] for f in FORMATS)
# Multiple output formats given
if _formats_sum > 1:
print('Please specify a single output format.')
sys.exit(64)
# No output formats given
elif _formats_sum < 1:
print('Please specify an output format.')
sys.exit(64)
# fetch options.formats list
# if sum(()) > 1
# log only one data format please
# if sum of formats == 0, specity format
# look for filename
# print opts.__dict__
# print in_file
# print out_file
+106 -131
View File
@@ -1,14 +1,28 @@
# -*- coding: utf-8 -*-
""" Tablib - Core Library.
"""
from tablib.formats import FORMATS as formats
# _____ ______ ______ _________
# __ /_______ ____ /_ ___ /_ _____ ______ /
# _ __/_ __ `/__ __ \__ __ \_ _ \_ __ /
# / /_ / /_/ / _ /_/ /_ /_/ // __// /_/ /
# \__/ \__,_/ /_.___/ /_.___/ \___/ \__,_/
__title__ = 'tablib'
__version__ = '0.8.4'
__build__ = 0x000804
import csv
import cStringIO
import random
import simplejson as json
import xlwt
import yaml
from helpers import *
# __all__ = ['Dataset', 'DataBook']
__name__ = 'tablib'
__version__ = '0.6.4'
__build__ = 0x000604
__author__ = 'Kenneth Reitz'
__license__ = 'MIT'
__copyright__ = 'Copyright 2010 Kenneth Reitz'
@@ -18,25 +32,23 @@ class Dataset(object):
"""Epic Tabular-Dataset object. """
def __init__(self, *args, **kwargs):
self._data = None
self._saved_file = None
self._saved_format = None
self._data = list(args)
self.__headers = None
# ('title', index) tuples
self._separators = []
try:
self.headers = kwargs['headers']
except KeyError:
except KeyError, why:
self.headers = None
try:
self.title = kwargs['title']
except KeyError:
except KeyError, why:
self.title = None
self._register_formats()
def __len__(self):
return self.height
@@ -67,20 +79,6 @@ class Dataset(object):
except AttributeError:
return '<dataset object>'
@classmethod
def _register_formats(cls):
"""Adds format properties."""
for fmt in formats:
try:
try:
setattr(cls, fmt.title, property(fmt.export_set, fmt.import_set))
except AttributeError:
setattr(cls, fmt.title, property(fmt.export_set))
except AttributeError:
pass
def _validate(self, row=None, col=None, safety=False):
"""Assures size of every row in dataset is of proper proportions."""
@@ -115,7 +113,6 @@ class Dataset(object):
return data
@property
def height(self):
"""Returns the height of the Dataset."""
@@ -127,13 +124,12 @@ class Dataset(object):
"""Returns the width of the Dataset."""
try:
return len(self._data[0])
except IndexError:
except IndexError, why:
try:
return len(self.headers)
except TypeError:
except TypeError, e:
return 0
@property
def headers(self):
"""Headers property."""
@@ -147,49 +143,64 @@ class Dataset(object):
if collection:
try:
self.__headers = list(collection)
except TypeError:
except TypeError, why:
raise TypeError
else:
self.__headers = None
@property
def dict(self):
"""Returns python dict of Dataset."""
return self._package()
@dict.setter
def dict(self, pickle):
"""Returns python dict of Dataset."""
if not len(pickle):
return
if isinstance(pickle[0], list):
for row in pickle:
self.append(row)
elif isinstance(pickle[0], dict):
self.headers = pickle[0].keys()
for row in pickle:
self.append(row.values())
else:
raise UnsupportedFormat
@property
def json(self):
"""Returns JSON representation of Dataset."""
return json.dumps(self.dict)
@property
def yaml(self):
"""Returns YAML representation of Dataset."""
return yaml.dump(self.dict)
@property
def csv(self):
"""Returns CSV representation of Dataset."""
stream = cStringIO.StringIO()
_csv = csv.writer(stream)
for row in self._package(dicts=False):
_csv.writerow(row)
return stream.getvalue()
@property
def xls(self):
"""Returns XLS representation of Dataset."""
stream = cStringIO.StringIO()
wb = xlwt.Workbook()
ws = wb.add_sheet(self.title if self.title else 'Tabbed Dataset')
for i, row in enumerate(self._package(dicts=False)):
for j, col in enumerate(row):
ws.write(i, j, col.decode('utf8'))
wb.save(stream)
return stream.getvalue()
def append(self, row=None, col=None):
"""Adds a row to the end of Dataset"""
if row is not None:
if row:
self._validate(row)
self._data.append(tuple(row))
elif col is not None:
col = list(col)
if self.headers:
header = [col.pop(0)]
else:
header = []
if len(col) == 1 and callable(col[0]):
col = map(col[0], self._data)
col = tuple(header + col)
elif col:
self._validate(col=col)
if self.headers:
@@ -207,48 +218,22 @@ class Dataset(object):
self._data = [tuple([row]) for row in col]
def insert_separator(self, index, text='-'):
"""Adds a separator to Dataset at given index."""
sep = (index, text)
self._separators.append(sep)
def append_separator(self, text='-'):
"""Adds a separator to Dataset."""
# change offsets if headers are or aren't defined
if not self.headers:
index = self.height if self.height else 0
else:
index = (self.height + 1) if self.height else 1
self.insert_separator(index, text)
def insert(self, i, row=None):
def insert(self, i, row=None, col=None):
"""Inserts a row at given position in Dataset"""
if row:
self._validate(row)
self._data.insert(i, tuple(row))
elif col:
pass
def wipe(self):
"""Erases all data from Dataset."""
self._data = list()
self.__headers = None
class Databook(object):
class DataBook(object):
"""A book of Dataset objects.
Currently, this exists only for XLS workbook support.
"""
def __init__(self, sets=[]):
self._datasets = sets
self._register_formats()
def __repr__(self):
@@ -258,35 +243,15 @@ class Databook(object):
return '<databook object>'
def wipe(self):
"""Wipe book clean."""
self._datasets = []
@classmethod
def _register_formats(cls):
"""Adds format properties."""
for fmt in formats:
try:
try:
setattr(cls, fmt.title, property(fmt.export_book, fmt.import_book))
except AttributeError:
setattr(cls, fmt.title, property(fmt.export_book))
except AttributeError:
pass
def add_sheet(self, dataset):
"""Adds given dataset."""
"""Add given dataset ."""
if type(dataset) is Dataset:
self._datasets.append(dataset)
else:
raise InvalidDatasetType
def _package(self):
"""Packages Databook for delivery."""
collector = []
for dset in self._datasets:
collector.append(dict(
@@ -302,28 +267,38 @@ class Databook(object):
return len(self._datasets)
def detect(stream):
"""Return (format, stream) of given stream."""
for fmt in formats:
try:
if fmt.detect(stream):
return (fmt, stream)
except AttributeError:
pass
return (None, stream)
def import_set(stream):
"""Return dataset of given stream."""
(format, stream) = detect(stream)
@property
def xls(self):
"""Returns XLS representation of DataBook."""
stream = cStringIO.StringIO()
wb = xlwt.Workbook()
for i, dset in enumerate(self._datasets):
ws = wb.add_sheet(dset.title if dset.title else 'Sheet%s' % (i))
#for row in self._package(dicts=False):
for i, row in enumerate(dset._package(dicts=False)):
for j, col in enumerate(row):
ws.write(i, j, str(col))
wb.save(stream)
return stream.getvalue()
@property
def json(self):
"""Returns JSON representation of Databook."""
return json.dumps(self._package())
@property
def yaml(self):
"""Returns YAML representation of Databook."""
return yaml.dump(self._package())
try:
data = Dataset()
format.import_set(data, stream)
return data
except AttributeError, e:
return None
class InvalidDatasetType(Exception):
-11
View File
@@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
""" Tablib - formats
"""
import _csv as csv
import _json as json
import _xls as xls
import _yaml as yaml
FORMATS = (json, xls, yaml, csv)
-51
View File
@@ -1,51 +0,0 @@
# -*- coding: utf-8 -*-
""" Tablib - CSV Support.
"""
import cStringIO
import csv
import os
import simplejson as json
import tablib
title = 'csv'
extentions = ('csv',)
def export_set(dataset):
"""Returns CSV representation of Dataset."""
stream = cStringIO.StringIO()
_csv = csv.writer(stream)
for row in dataset._package(dicts=False):
_csv.writerow(row)
return stream.getvalue()
def import_set(dset, in_stream, headers=True):
"""Returns dataset from CSV stream."""
dset.wipe()
rows = csv.reader(in_stream.split())
for i, row in enumerate(rows):
if (i == 0) and (headers):
dset.headers = row
else:
dset.append(row)
def detect(stream):
"""Returns True if given stream is valid CSV."""
try:
rows = dialect = csv.Sniffer().sniff(stream)
return True
except csv.Error:
return False
-47
View File
@@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
""" Tablib - JSON Support
"""
import simplejson as json
import tablib.core
title = 'json'
extentions = ('json', 'jsn')
def export_set(dataset):
"""Returns JSON representation of Dataset."""
return json.dumps(dataset.dict)
def export_book(databook):
"""Returns JSON representation of Databook."""
return json.dumps(databook._package())
def import_set(dset, in_stream):
"""Returns dataset from JSON stream."""
dset.wipe()
dset.dict = json.loads(in_stream)
def import_book(dbook, in_stream):
"""Returns databook from JSON stream."""
dbook.wipe()
for sheet in json.loads(in_stream):
data = tablib.core.Dataset()
data.title = sheet['title']
data.dict = sheet['data']
dbook.add_sheet(data)
def detect(stream):
"""Returns True if given stream is valid JSON."""
try:
json.loads(stream)
return True
except json.decoder.JSONDecodeError:
return False
-73
View File
@@ -1,73 +0,0 @@
# -*- coding: utf-8 -*-
""" Tablib - XLS Support.
"""
import xlwt
import cStringIO
title = 'xls'
extentions = ('xls',)
# special styles
wrap = xlwt.easyxf("alignment: wrap on")
bold = xlwt.easyxf("font: bold on")
def export_set(dataset):
"""Returns XLS representation of Dataset."""
wb = xlwt.Workbook(encoding='utf8')
ws = wb.add_sheet(dataset.title if dataset.title else 'Tabbed Dataset')
dset_sheet(dataset, ws)
stream = cStringIO.StringIO()
wb.save(stream)
return stream.getvalue()
def export_book(databook):
"""Returns XLS representation of DataBook."""
wb = xlwt.Workbook(encoding='utf8')
for i, dset in enumerate(databook._datasets):
ws = wb.add_sheet(dset.title if dset.title else 'Sheet%s' % (i))
dset_sheet(dset, ws)
stream = cStringIO.StringIO()
wb.save(stream)
return stream.getvalue()
def dset_sheet(dataset, ws):
"""Completes given worksheet from given Dataset."""
_package = dataset._package(dicts=False)
for i, sep in enumerate(dataset._separators):
_offset = i
_package.insert((sep[0] + _offset), (sep[1],))
for i, row in enumerate(_package):
for j, col in enumerate(row):
# bold headers
if (i == 0) and dataset.headers:
ws.write(i, j, col, bold)
# bold separators
elif len(row) < dataset.width:
ws.write(i, j, col, bold)
# wrap the rest
else:
if '\n' in col:
ws.write(i, j, col, wrap)
else:
ws.write(i, j, col)
-53
View File
@@ -1,53 +0,0 @@
# -*- coding: utf-8 -*-
""" Tablib - YAML Support.
"""
import yaml
import tablib
title = 'yaml'
extentions = ('yaml', 'yml')
def export_set(dataset):
"""Returns YAML representation of Dataset."""
return yaml.dump(dataset.dict)
def export_book(databook):
"""Returns YAML representation of Databook."""
return yaml.dump(databook._package())
def import_set(dset, in_stream):
"""Returns dataset from YAML stream."""
dset.wipe()
dset.dict = yaml.load(in_stream)
def import_book(dbook, in_stream):
"""Returns databook from YAML stream."""
dbook.wipe()
for sheet in yaml.load(in_stream):
data = tablib.core.Dataset()
data.title = sheet['title']
data.dict = sheet['data']
dbook.add_sheet(data)
def detect(stream):
"""Returns True if given stream is valid YAML."""
try:
_yaml = yaml.load(stream)
if isinstance(_yaml, (list, tuple, dict)):
return True
else:
return False
except yaml.parser.ParserError:
return False
-16
View File
@@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
""" Tablib - General Helpers.
"""
import sys
@@ -15,19 +12,6 @@ class Struct(object):
def __getitem__(self, key):
return getattr(self, key, None)
def dictionary(self):
"""Returns dictionary representation of object."""
return self.__dict__
def items(self):
"""Returns items within object."""
return self.__dict__.items()
def keys(self):
"""Returns keys within object."""
return self.__dict__.keys()
def piped():
"""Returns piped input via stdin, else False."""
+1 -157
View File
@@ -13,10 +13,8 @@ class TablibTestCase(unittest.TestCase):
def setUp(self):
"""Create simple data set with headers."""
global data, book
global data
data = tablib.Dataset()
book = tablib.Databook()
self.headers = ('first_name', 'last_name', 'gpa')
self.john = ('John', 'Adams', 90)
@@ -102,13 +100,6 @@ class TablibTestCase(unittest.TestCase):
self.assertRaises(tablib.InvalidDimensions, data.append, col=new_col)
def test_add_callable_column(self):
"""Verify adding column with values specified as callable."""
new_col = ['first_again', lambda x: x[0]]
self.founders.append(col=new_col)
self.assertTrue(map(lambda x: x[0] == x[-1], self.founders))
def test_header_slicing(self):
"""Verify slicing by headers."""
@@ -190,153 +181,6 @@ class TablibTestCase(unittest.TestCase):
data.csv
data.xls
def test_book_export_no_exceptions(self):
"""Test that varoius exports don't error out."""
book = tablib.Databook()
book.add_sheet(data)
book.json
book.yaml
book.xls
def test_json_import_set(self):
"""Generate and import JSON set serialization."""
data.append(self.john)
data.append(self.george)
data.headers = self.headers
_json = data.json
data.json = _json
self.assertEqual(_json, data.json)
def test_json_import_book(self):
"""Generate and import JSON book serialization."""
data.append(self.john)
data.append(self.george)
data.headers = self.headers
book.add_sheet(data)
_json = book.json
book.json = _json
self.assertEqual(_json, book.json)
def test_yaml_import_set(self):
"""Generate and import YAML set serialization."""
data.append(self.john)
data.append(self.george)
data.headers = self.headers
_yaml = data.yaml
data.yaml = _yaml
self.assertEqual(_yaml, data.yaml)
def test_yaml_import_book(self):
"""Generate and import YAML book serialization."""
data.append(self.john)
data.append(self.george)
data.headers = self.headers
book.add_sheet(data)
_yaml = book.yaml
book.yaml = _yaml
self.assertEqual(_yaml, book.yaml)
def test_csv_import_set(self):
"""Generate and import CSV set serialization."""
data.append(self.john)
data.append(self.george)
data.headers = self.headers
_csv = data.csv
data.csv = _csv
self.assertEqual(_csv, data.csv)
def test_csv_format_detect(self):
"""Test CSV format detection."""
_csv = (
'1,2,3\n'
'4,5,6\n'
'7,8,9\n'
)
_bunk = (
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
)
self.assertTrue(tablib.formats.csv.detect(_csv))
self.assertFalse(tablib.formats.csv.detect(_bunk))
def test_json_format_detect(self):
"""Test JSON format detection."""
_json = '[{"last_name": "Adams","age": 90,"first_name": "John"}]'
_bunk = (
'¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
)
self.assertTrue(tablib.formats.json.detect(_json))
self.assertFalse(tablib.formats.json.detect(_bunk))
def test_yaml_format_detect(self):
"""Test YAML format detection."""
_yaml = '- {age: 90, first_name: John, last_name: Adams}'
_bunk = (
'¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
)
self.assertTrue(tablib.formats.yaml.detect(_yaml))
self.assertFalse(tablib.formats.yaml.detect(_bunk))
def test_auto_format_detect(self):
"""Test auto format detection."""
_yaml = '- {age: 90, first_name: John, last_name: Adams}'
_json = '[{"last_name": "Adams","age": 90,"first_name": "John"}]'
_csv = '1,2,3\n4,5,6\n7,8,9\n'
_bunk = '¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
self.assertEqual(tablib.detect(_yaml)[0], tablib.formats.yaml)
self.assertEqual(tablib.detect(_csv)[0], tablib.formats.csv)
self.assertEqual(tablib.detect(_json)[0], tablib.formats.json)
self.assertEqual(tablib.detect(_bunk)[0], None)
def test_wipe(self):
"""Purge a dataset."""
new_row = (1, 2, 3)
data.append(new_row)
# Verify width/data
self.assertTrue(data.width == len(new_row))
self.assertTrue(data[0] == new_row)
data.wipe()
new_row = (1, 2, 3, 4)
data.append(new_row)
self.assertTrue(data.width == len(new_row))
self.assertTrue(data[0] == new_row)
if __name__ == '__main__':
unittest.main()