Major API Changes

Related #21
This commit is contained in:
Kenneth Reitz
2011-06-21 19:42:56 -04:00
parent d4923533eb
commit 004b3da680
2 changed files with 224 additions and 136 deletions
+209 -121
View File
@@ -62,8 +62,14 @@ class Row(object):
def __setstate__(self, state):
for (k, v) in list(state.items()): setattr(self, k, v)
def rpush(self, value):
self.insert(0, value)
def lpush(self, value):
self.insert(len(value), value)
def append(self, value):
self._row.append(value)
self.rpush(value)
def insert(self, index, value):
self._row.insert(index, value)
@@ -200,6 +206,10 @@ class Dataset(object):
return '<dataset object>'
# ---------
# Internals
# ---------
@classmethod
def _register_formats(cls):
"""Adds format properties."""
@@ -236,6 +246,7 @@ class Dataset(object):
def _package(self, dicts=True, ordered=True):
"""Packages Dataset into lists of dictionaries for transmission."""
# TODO: Dicts default to false?
_data = list(self._data)
@@ -269,46 +280,6 @@ class Dataset(object):
return data
def _clean_col(self, col):
"""Prepares the given column for insert/append."""
col = list(col)
if self.headers:
header = [col.pop(0)]
else:
header = []
if len(col) == 1 and hasattr(col[0], '__call__'):
col = list(map(col[0], self._data))
col = tuple(header + col)
return col
@property
def height(self):
"""The number of rows currently in the :class:`Dataset`.
Cannot be directly modified.
"""
return len(self._data)
@property
def width(self):
"""The number of columns currently in the :class:`Dataset`.
Cannot be directly modified.
"""
try:
return len(self._data[0])
except IndexError:
try:
return len(self.headers)
except TypeError:
return 0
def _get_headers(self):
"""An *optional* list of strings to be used for header rows and attribute names.
@@ -332,6 +303,7 @@ class Dataset(object):
headers = property(_get_headers, _set_headers)
def _get_dict(self):
"""A native Python representation of the :class:`Dataset` object. If headers have
been set, a list of Python dictionaries will be returned. If no headers have been set,
@@ -379,6 +351,52 @@ class Dataset(object):
dict = property(_get_dict, _set_dict)
def _clean_col(self, col):
"""Prepares the given column for insert/append."""
col = list(col)
if self.headers:
header = [col.pop(0)]
else:
header = []
if len(col) == 1 and hasattr(col[0], '__call__'):
col = list(map(col[0], self._data))
col = tuple(header + col)
return col
@property
def height(self):
"""The number of rows currently in the :class:`Dataset`.
Cannot be directly modified.
"""
return len(self._data)
@property
def width(self):
"""The number of columns currently in the :class:`Dataset`.
Cannot be directly modified.
"""
try:
return len(self._data[0])
except IndexError:
try:
return len(self.headers)
except TypeError:
return 0
# -------
# Formats
# -------
@property
def xls():
"""A Legacy Excel Spreadsheet representation of the :class:`Dataset` object, with :ref:`separators`. Cannot be set.
@@ -493,61 +511,12 @@ class Dataset(object):
pass
def append(self, row=None, col=None, header=None, tags=list()):
"""Adds a row or column to the :class:`Dataset`.
Usage is :class:`Dataset.insert` for documentation.
"""
# ----
# Rows
# ----
if row is not None:
self.insert(self.height, row=row, tags=tags)
elif col is not None:
self.insert(self.width, col=col, header=header)
def insert_separator(self, index, text='-'):
"""Adds a separator to :class:`Dataset` at given index."""
sep = (index, text)
self._separators.append(sep)
def append_separator(self, text='-'):
"""Adds a :ref:`separator <separators>` to the :class:`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 add_formatter(self, col, handler):
"""Adds a :ref:`formatter` to the :class:`Dataset`.
.. versionadded:: 0.9.5
:param col: column to. Accepts index int or header str.
:param handler: reference to callback function to execute
against each cell value.
"""
if isinstance(col, str):
if col in self.headers:
col = self.headers.index(col) # get 'key' index from each data
else:
raise KeyError
if not col > self.width:
self._formatters.append((col, handler))
else:
raise InvalidDatasetIndex
return True
def insert(self, index, row=None, col=None, header=None, tags=list()):
"""Inserts a row or column to the :class:`Dataset` at the given index.
def insert(self, index, row, tags=list()):
"""Inserts a row to the :class:`Dataset` at the given index.
Rows and columns inserted must be the correct size (height or width).
@@ -572,35 +541,152 @@ class Dataset(object):
If inserting a row, you can add :ref:`tags <tags>` to the row you are inserting.
This gives you the ability to :class:`filter <Dataset.filter>` your
:class:`Dataset` later.
"""
if row:
self._validate(row)
self._data.insert(index, Row(row, tags=tags))
elif col:
col = list(col)
# Callable Columns...
if len(col) == 1 and hasattr(col[0], '__call__'):
col = list(map(col[0], self._data))
self._validate(row)
self._data.insert(index, Row(row, tags=tags))
col = self._clean_col(col)
self._validate(col=col)
if self.headers:
# pop the first item off, add to headers
if not header:
raise HeadersNeeded()
self.headers.insert(index, header)
def rpush(self, row, tags=list()):
"""Adds a row to the end of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation.
"""
if self.height and self.width:
self.insert(self.height, row=row, tags=tags)
for i, row in enumerate(self._data):
row.insert(index, col[i])
self._data[i] = row
def lpush(self, row, tags=list()):
"""Adds a row to the top of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation.
"""
self.insert(0, row=row, tags=tags)
def append(self, row, tags=list()):
"""Adds a row to the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation.
"""
self.rpush(row, tags)
# -------
# Columns
# -------
def insert_col(self, index, col=None, header=None):
"""Inserts a column to the :class:`Dataset` at the given index.
Columns inserted must be the correct height.
You can also insert a column of a single callable object, which will
add a new column with the return values of the callable each as an
item in the column. ::
data.append_col(col=random.randint)
If inserting a column, and :class:`Dataset.headers` is set, the
header attribute must be set, and will be considered the header for
that row.
See :ref:`dyncols` for an in-depth example.
"""
col = list(col)
# Callable Columns...
if len(col) == 1 and hasattr(col[0], '__call__'):
col = list(map(col[0], self._data))
col = self._clean_col(col)
self._validate(col=col)
if self.headers:
# pop the first item off, add to headers
if not header:
raise HeadersNeeded()
self.headers.insert(index, header)
if self.height and self.width:
for i, row in enumerate(self._data):
row.insert(index, col[i])
self._data[i] = row
else:
self._data = [Row([row]) for row in col]
def rpush_col(self, col, header=None):
"""Adds a column to the end of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation.
"""
self.insert_col(self.width, col, header=header)
def lpush_col(self, col, header=None):
"""Adds a column to the top of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation.
"""
self.insert_col(0, col, header=header)
def insert_separator(self, index, text='-'):
"""Adds a separator to :class:`Dataset` at given index."""
sep = (index, text)
self._separators.append(sep)
def append_separator(self, text='-'):
"""Adds a :ref:`separator <separators>` to the :class:`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 append_col(self, col, header=None):
"""Adds a column to the :class:`Dataset`.
See :class:`Dataset.insert_col` for additional documentation.
"""
self.rpush_col(col, header)
# ----
# Misc
# ----
def add_formatter(self, col, handler):
"""Adds a :ref:`formatter` to the :class:`Dataset`.
.. versionadded:: 0.9.5
:param col: column to. Accepts index int or header str.
:param handler: reference to callback function to execute
against each cell value.
"""
if isinstance(col, str):
if col in self.headers:
col = self.headers.index(col) # get 'key' index from each data
else:
self._data = [Row([row]) for row in col]
raise KeyError
if not col > self.width:
self._formatters.append((col, handler))
else:
raise InvalidDatasetIndex
return True
def filter(self, tag):
@@ -617,8 +703,10 @@ class Dataset(object):
"""Sort a :class:`Dataset` by a specific column, given string (for
header) or integer (for column index). The order can be reversed by
setting ``reverse`` to ``True``.
Returns a new :class:`Dataset` instance where columns have been
sorted."""
sorted.
"""
if isinstance(col, str):
@@ -679,7 +767,7 @@ class Dataset(object):
return _dset
def stack_rows(self, other):
def stack(self, other):
"""Stack two :class:`Dataset` instances together by
joining at the row level, and return new combined
``Dataset`` instance."""
@@ -702,7 +790,7 @@ class Dataset(object):
return _dset
def stack_columns(self, other):
def stack_cols(self, other):
"""Stack two :class:`Dataset` instances together by
joining at the column level, and return a new
combined ``Dataset`` instance. If either ``Dataset``
@@ -726,10 +814,10 @@ class Dataset(object):
_dset = Dataset()
for column in self.headers:
_dset.append(col=self[column])
_dset.append_col(col=self[column])
for column in other.headers:
_dset.append(col=other[column])
_dset.append_col(col=other[column])
_dset.headers = new_headers
+15 -15
View File
@@ -73,7 +73,7 @@ class TablibTestCase(unittest.TestCase):
new_col = ['reitz', 'monke']
data.append(col=new_col)
data.append_col(new_col)
self.assertEquals(data[0], ('kenneth', 'reitz'))
self.assertEquals(data.width, 2)
@@ -81,7 +81,7 @@ class TablibTestCase(unittest.TestCase):
# With Headers
data.headers = ('fname', 'lname')
new_col = [21, 22]
data.append(col=new_col, header='age')
data.append_col(new_col, header='age')
self.assertEquals(data['age'], new_col)
@@ -91,7 +91,7 @@ class TablibTestCase(unittest.TestCase):
new_col = ('reitz', 'monke')
data.append(col=new_col)
data.append_col(new_col)
self.assertEquals(data[0], tuple([new_col[0]]))
self.assertEquals(data.width, 1)
@@ -100,21 +100,23 @@ class TablibTestCase(unittest.TestCase):
def test_add_callable_column(self):
"""Verify adding column with values specified as callable."""
new_col = [lambda x: x[0]]
self.founders.append(col=new_col, header='first_again')
#
# self.assertTrue(map(lambda x: x[0] == x[-1], self.founders))
self.founders.append_col(new_col, header='first_again')
def test_header_slicing(self):
"""Verify slicing by headers."""
self.assertEqual(self.founders['first_name'],
[self.john[0], self.george[0], self.tom[0]])
[self.john[0], self.george[0], self.tom[0]])
self.assertEqual(self.founders['last_name'],
[self.john[1], self.george[1], self.tom[1]])
[self.john[1], self.george[1], self.tom[1]])
self.assertEqual(self.founders['gpa'],
[self.john[2], self.george[2], self.tom[2]])
[self.john[2], self.george[2], self.tom[2]])
def test_data_slicing(self):
@@ -174,6 +176,7 @@ class TablibTestCase(unittest.TestCase):
self.assertEqual(csv, self.founders.csv)
def test_tsv_export(self):
"""Verify exporting dataset object as CSV."""
@@ -191,8 +194,8 @@ class TablibTestCase(unittest.TestCase):
self.assertEqual(tsv, self.founders.tsv)
def test_html_export(self):
def test_html_export(self):
"""HTML export"""
html = markup.page()
@@ -421,7 +424,6 @@ class TablibTestCase(unittest.TestCase):
def test_row_stacking(self):
"""Row stacking."""
to_join = tablib.Dataset(headers=self.founders.headers)
@@ -429,7 +431,7 @@ class TablibTestCase(unittest.TestCase):
for row in self.founders:
to_join.append(row=row)
row_stacked = self.founders.stack_rows(to_join)
row_stacked = self.founders.stack(to_join)
for column in row_stacked.headers:
@@ -439,7 +441,6 @@ class TablibTestCase(unittest.TestCase):
def test_column_stacking(self):
"""Column stacking"""
to_join = tablib.Dataset(headers=self.founders.headers)
@@ -447,7 +448,7 @@ class TablibTestCase(unittest.TestCase):
for row in self.founders:
to_join.append(row=row)
column_stacked = self.founders.stack_columns(to_join)
column_stacked = self.founders.stack_cols(to_join)
for index, row in enumerate(column_stacked):
@@ -460,7 +461,6 @@ class TablibTestCase(unittest.TestCase):
def test_sorting(self):
"""Sort columns."""
sorted_data = self.founders.sort(col="first_name")