From 004b3da680b821d0dcedbe52ee6fb5a2a45c9ad5 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 21 Jun 2011 19:42:56 -0400 Subject: [PATCH] Major API Changes Related #21 --- tablib/core.py | 330 +++++++++++++++++++++++++++++++------------------ test_tablib.py | 30 ++--- 2 files changed, 224 insertions(+), 136 deletions(-) diff --git a/tablib/core.py b/tablib/core.py index 16b3dff..9c561fd 100644 --- a/tablib/core.py +++ b/tablib/core.py @@ -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 '' + # --------- + # 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 ` 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 ` to the row you are inserting. This gives you the ability to :class:`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 ` 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 diff --git a/test_tablib.py b/test_tablib.py index c5210a9..860c5a8 100755 --- a/test_tablib.py +++ b/test_tablib.py @@ -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")