mirror of
https://github.com/kennethreitz/tablib.git
synced 2026-06-05 23:10:17 +00:00
+209
-121
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user