diff --git a/HISTORY.rst b/HISTORY.rst index d467712..c64d7b5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,15 @@ History ------- +0.9.2 (2010-11-17) +++++++++++++++++++ + +* Tanspose method added to Datasets +* New frozen top row in Excel output +* Pickling support for Datasets and Rows +* Support for row/column stacking + + 0.9.1 (2010-11-04) ++++++++++++++++++ diff --git a/NOTICE b/NOTICE index f072112..88d5d2d 100644 --- a/NOTICE +++ b/NOTICE @@ -1,4 +1,31 @@ -Tablib includes some vendorized python libraries: pyyaml, simplejson, and xlwt. +Tablib includes some vendorized python libraries: ordereddict, pyyaml, +simplejson, and xlwt. + + +OrderedDict License +=================== + +Copyright (c) 2009 Raymond Hettinger + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.rst b/README.rst index 3d01ed8..00b6345 100644 --- a/README.rst +++ b/README.rst @@ -61,7 +61,7 @@ Intelligently add new rows: :: Intelligently add new columns: :: - >>> data.append(col=('age', 90, 67, 83)) + >>> data.append(col=(90, 67, 83), header='age') Slice rows: :: @@ -171,8 +171,7 @@ To install tablib, simply: :: Or, if you absolutely must: :: $ easy_install tablib - - + Contribute ---------- diff --git a/TODO.rst b/TODO.rst index c28c8b7..231f03e 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,9 +1,8 @@ -* Polish *&* announce http://tablib.org. +* Backwards-compatible OrderedDict support * Write more exhausive unit-tests. * Write stress tests. * Make CSV write customizable. -* HTML Table exports. -* ``Dataset.traspose()`` support? - - - +* HTML Table exports. +* Integrate django-tablib +* Mention django-tablib in Documention +* Dataset title usage in documentation (#17) \ No newline at end of file diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 774158d..dfe10ff 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -87,7 +87,7 @@ Adding Columns Now that we have a basic :class:`Dataset` in place, let's add a column of **ages** to it. :: - data.append(col=['Age', 22, 20]) + data.append(col=[22, 20], header='Age') Let's view the data now. :: @@ -243,7 +243,7 @@ Filtering Datasets with Tags .. versionadded:: 0.9.0 -When constructing a :class:`Dataset` object, you can add tags to rows by speficying the ``tags`` parameter. +When constructing a :class:`Dataset` object, you can add tags to rows by specifying the ``tags`` parameter. This allows you to filter your :class:`Dataset` later. This can be useful so seperate rows of data based on arbitrary criteria (*e.g.* origin) that you don't want to include in your :class:`Dataset`. @@ -350,4 +350,4 @@ The resulting **tests.xls** will have the following layout: ---- -Now, go check out the :ref:`API Documentation ` or begin :ref:`Tablib Development `. \ No newline at end of file +Now, go check out the :ref:`API Documentation ` or begin :ref:`Tablib Development `. diff --git a/setup.py b/setup.py index 577e4d0..e779457 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ required = [] setup( name='tablib', - version='0.9.1', + version='0.9.2', description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)', long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), diff --git a/tablib/core.py b/tablib/core.py index b5c2526..bd2d4ba 100644 --- a/tablib/core.py +++ b/tablib/core.py @@ -15,12 +15,13 @@ from tablib import formats __title__ = 'tablib' -__version__ = '0.9.1' -__build__ = 0x000901 +__version__ = '0.9.2' +__build__ = 0x000902 __author__ = 'Kenneth Reitz' __license__ = 'MIT' __copyright__ = 'Copyright 2010 Kenneth Reitz' + class Row(object): """Internal Row object. Mainly used for filtering.""" @@ -51,6 +52,17 @@ class Row(object): def __delitem__(self, i): del self._row[i] + def __getstate__(self): + result = dict() + result['_row'] = self._row + result['tags'] = self.tags + + return result + + def __setstate__(self, state): + self._row = state['_row'] + self.tags = state['tags'] + def append(self, value): self._row.append(value) @@ -76,44 +88,41 @@ class Row(object): if tag == None: return False elif isinstance(tag, basestring): - return tag in self.tags + return (tag in self.tags) else: - for t in tag: - if t in self.tags: - return True - return False + return True if len(set(tag) & set(self.tags)) else False class Dataset(object): - """The :class:`Dataset` object is the heart of Tablib. It provides all core + """The :class:`Dataset` object is the heart of Tablib. It provides all core functionality. - + Usually you create a :class:`Dataset` instance in your main module, and append rows and columns as you collect data. :: - + data = tablib.Dataset() data.headers = ('name', 'age') - + for (name, age) in some_collector(): data.append((name, age)) - + You can also set rows and headers upon instantiation. This is useful if dealing with dozens or hundres of :class:`Dataset` objects. :: - + headers = ('first_name', 'last_name') data = [('John', 'Adams'), ('George', 'Washington')] - + data = tablib.Dataset(*data, headers=headers) - - + + :param \*args: (optional) list of rows to populate Dataset :param headers: (optional) list strings for Dataset header row .. admonition:: Format Attributes Definition - If you look at the code, the various output/import formats are not - defined within the :class:`Dataset` object. To add support for a new format, see + If you look at the code, the various output/import formats are not + defined within the :class:`Dataset` object. To add support for a new format, see :ref:`Adding New Formats `. """ @@ -121,7 +130,7 @@ class Dataset(object): def __init__(self, *args, **kwargs): self._data = list(Row(arg) for arg in args) self.__headers = None - + # ('title', index) tuples self._separators = [] @@ -137,7 +146,7 @@ class Dataset(object): self._register_formats() - + def __len__(self): return self.height @@ -156,7 +165,7 @@ class Dataset(object): else: return [result.tuple for result in _results] - + def __setitem__(self, key, value): self._validate(value) self._data[key] = Row(value) @@ -166,10 +175,10 @@ class Dataset(object): if isinstance(key, basestring): if key in self.headers: - + pos = self.headers.index(key) del self.headers[pos] - + for i, row in enumerate(self._data): del row[pos] @@ -186,7 +195,7 @@ class Dataset(object): except AttributeError: return '' - + @classmethod def _register_formats(cls): """Adds format properties.""" @@ -196,7 +205,7 @@ class Dataset(object): setattr(cls, fmt.title, property(fmt.export_set, fmt.import_set)) except AttributeError: setattr(cls, fmt.title, property(fmt.export_set)) - + except AttributeError: pass @@ -237,21 +246,21 @@ class Dataset(object): 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 callable(col[0]): col = map(col[0], self._data) col = tuple(header + col) - + return col - + @property def height(self): """The number of rows currently in the :class:`Dataset`. @@ -265,7 +274,7 @@ class Dataset(object): """The number of columns currently in the :class:`Dataset`. Cannot be directly modified. """ - + try: return len(self._data[0]) except IndexError: @@ -278,7 +287,7 @@ class Dataset(object): @property def headers(self): """An *optional* list of strings to be used for header rows and attribute names. - + This must be set manually. The given list length must equal :class:`Dataset.width`. """ @@ -300,9 +309,9 @@ class Dataset(object): @property def dict(self): - """A JSON representation of the :class:`Dataset` object. If headers have been - set, a JSON list of objects will be returned. If no headers have - been set, a JSON list of lists (rows) will be returned instead. + """A JSON representation of the :class:`Dataset` object. If headers have been + set, a JSON list of objects will be returned. If no headers have + been set, a JSON list of lists (rows) will be returned instead. A dataset object can also be imported by setting the `Dataset.json` attribute: :: @@ -312,10 +321,10 @@ class Dataset(object): """ return self._package() - + @dict.setter def dict(self, pickle): - """A native Python representation of the Dataset object. If headers have been + """A native Python representation of the Dataset object. If headers have been set, a list of Python dictionaries will be returned. If no headers have been set, a list of tuples (rows) will be returned instead. @@ -323,7 +332,7 @@ class Dataset(object): data = tablib.Dataset() data.dict = [{'age': 90, 'first_name': 'Kenneth', 'last_name': 'Reitz'}] - + """ if not len(pickle): return @@ -333,7 +342,7 @@ class Dataset(object): self.wipe() for row in pickle: self.append(Row(row)) - + # if list of objects elif isinstance(pickle[0], dict): self.wipe() @@ -356,11 +365,11 @@ class Dataset(object): """ pass - + @property def csv(): - """A CSV representation of the :class:`Dataset` object. The top row will contain - headers, if they have been set. Otherwise, the top row will contain + """A CSV representation of the :class:`Dataset` object. The top row will contain + headers, if they have been set. Otherwise, the top row will contain the first row of the dataset. A dataset object can also be imported by setting the :class:`Dataset.csv` attribute. :: @@ -374,8 +383,8 @@ class Dataset(object): @property def tsv(): - """A TSV representation of the :class:`Dataset` object. The top row will contain - headers, if they have been set. Otherwise, the top row will contain + """A TSV representation of the :class:`Dataset` object. The top row will contain + headers, if they have been set. Otherwise, the top row will contain the first row of the dataset. A dataset object can also be imported by setting the :class:`Dataset.tsv` attribute. :: @@ -388,9 +397,9 @@ class Dataset(object): @property def yaml(): - """A YAML representation of the :class:`Dataset` object. If headers have been - set, a YAML list of objects will be returned. If no headers have - been set, a YAML list of lists (rows) will be returned instead. + """A YAML representation of the :class:`Dataset` object. If headers have been + set, a YAML list of objects will be returned. If no headers have + been set, a YAML list of lists (rows) will be returned instead. A dataset object can also be imported by setting the :class:`Dataset.json` attribute: :: @@ -401,12 +410,12 @@ class Dataset(object): """ pass - + @property def json(): - """A JSON representation of the :class:`Dataset` object. If headers have been - set, a JSON list of objects will be returned. If no headers have - been set, a JSON list of lists (rows) will be returned instead. + """A JSON representation of the :class:`Dataset` object. If headers have been + set, a JSON list of objects will be returned. If no headers have + been set, a JSON list of lists (rows) will be returned instead. A dataset object can also be imported by setting the :class:`Dataset.json` attribute: :: @@ -447,18 +456,18 @@ class Dataset(object): 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. - - Rows and columns inserted must be the correct size (height or width). - + """Inserts a row or column to the :class:`Dataset` at the given index. + + Rows and columns inserted must be the correct size (height or width). + The default behaviour is to insert the given row to the :class:`Dataset` object at the given index. If the ``col`` parameter is given, however, a new column will be insert to the :class:`Dataset` object instead. 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 + add a new column with the return values of the callable each as an item in the column. :: - + data.append(col=random.randint) See :ref:`dyncols` for an in-depth example. @@ -472,7 +481,7 @@ 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) @@ -480,7 +489,7 @@ class Dataset(object): elif col: col = list(col) - # Callable Columns... + # Callable Columns... if len(col) == 1 and callable(col[0]): col = map(col[0], self._data) @@ -492,7 +501,7 @@ class Dataset(object): if not header: raise HeadersNeeded() self.headers.insert(index, header) - + if self.height and self.width: for i, row in enumerate(self._data): @@ -504,13 +513,98 @@ class Dataset(object): def filter(self, tag): """Returns a new instance of the :class:`Dataset`, excluding any rows - that do not contain the given :ref:`tags `. + that do not contain the given :ref:`tags `. """ _dset = copy(self) _dset._data = [row for row in _dset._data if row.has_tag(tag)] - + return _dset - + + def transpose(self): + """Transpose a :class:`Dataset`, turning rows into columns and vice + versa, returning a new ``Dataset`` instance. The first row of the + original instance becomes the new header row.""" + + # Don't transpose if there is no data + if not self: + return + + _dset = Dataset() + # The first element of the headers stays in the headers, + # it is our "hinge" on which we rotate the data + new_headers = [self.headers[0]] + self[self.headers[0]] + + _dset.headers = new_headers + for column in self.headers: + + if column == self.headers[0]: + # It's in the headers, so skip it + continue + + # Adding the column name as now they're a regular column + row_data = [column] + self[column] + row_data = Row(row_data) + _dset.append(row=row_data) + + return _dset + + + def stack_rows(self, other): + """Stack two :class:`Dataset` instances together by + joining at the row level, and return new combined + ``Dataset`` instance.""" + + if not isinstance(other, Dataset): + return + + if self.width != other.width: + raise InvalidDimensions + + # Copy the source data + _dset = copy(self) + + rows_to_stack = [row for row in _dset._data] + other_rows = [row for row in other._data] + + rows_to_stack.extend(other_rows) + _dset._data = rows_to_stack + + return _dset + + + def stack_columns(self, other): + """Stack two :class:`Dataset` instances together by + joining at the column level, and return a new + combined ``Dataset`` instance. If either ``Dataset`` + has headers set, than the other must as well.""" + + if not isinstance(other, Dataset): + return + + if self.headers or other.headers: + if not self.headers or not other.headers: + raise HeadersNeeded + + if self.height != other.height: + raise InvalidDimensions + + try: + new_headers = self.headers + other.headers + except TypeError: + new_headers = None + + _dset = Dataset() + + for column in self.headers: + _dset.append(col=self[column]) + + for column in other.headers: + _dset.append(col=other[column]) + + _dset.headers = new_headers + + return _dset + def wipe(self): """Removes all content and headers from the :class:`Dataset` object.""" self._data = list() @@ -537,7 +631,7 @@ class Databook(object): """Removes all :class:`Dataset` objects from the :class:`Databook`.""" self._datasets = [] - + @classmethod def _register_formats(cls): """Adds format properties.""" @@ -547,7 +641,7 @@ class Databook(object): setattr(cls, fmt.title, property(fmt.export_book, fmt.import_book)) except AttributeError: setattr(cls, fmt.title, property(fmt.export_book)) - + except AttributeError: pass @@ -558,7 +652,7 @@ class Databook(object): self._datasets.append(dataset) else: raise InvalidDatasetType - + def _package(self): """Packages :class:`Databook` for delivery.""" @@ -582,12 +676,12 @@ def detect(stream): for fmt in formats.available: try: if fmt.detect(stream): - return (fmt, stream) + return (fmt, stream) except AttributeError: - pass + pass return (None, stream) - - + + def import_set(stream): """Return dataset of given stream.""" (format, stream) = detect(stream) @@ -596,7 +690,7 @@ def import_set(stream): data = Dataset() format.import_set(data, stream) return data - + except AttributeError, e: return None diff --git a/tablib/formats/_xls.py b/tablib/formats/_xls.py index 97a9580..08bc0f6 100644 --- a/tablib/formats/_xls.py +++ b/tablib/formats/_xls.py @@ -26,7 +26,7 @@ def export_set(dataset): 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() @@ -63,6 +63,11 @@ def dset_sheet(dataset, ws): if (i == 0) and dataset.headers: ws.write(i, j, col, bold) + # frozen header row + ws.panes_frozen = True + ws.horz_split_pos = 1 + + # bold separators elif len(row) < dataset.width: ws.write(i, j, col, bold) @@ -77,4 +82,4 @@ def dset_sheet(dataset, ws): except TypeError: ws.write(i, j, col) - \ No newline at end of file + diff --git a/tablib/packages/ordereddict.py b/tablib/packages/ordereddict.py new file mode 100644 index 0000000..5b0303f --- /dev/null +++ b/tablib/packages/ordereddict.py @@ -0,0 +1,127 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from UserDict import DictMixin + +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/test_tablib.py b/test_tablib.py index 342fd45..8e2454f 100755 --- a/test_tablib.py +++ b/test_tablib.py @@ -8,6 +8,7 @@ import unittest import tablib + class TablibTestCase(unittest.TestCase): """Tablib test cases.""" @@ -15,6 +16,7 @@ class TablibTestCase(unittest.TestCase): """Create simple data set with headers.""" global data, book + data = tablib.Dataset() book = tablib.Databook() @@ -192,7 +194,7 @@ class TablibTestCase(unittest.TestCase): data.tsv data.xls - + def test_book_export_no_exceptions(self): """Test that varoius exports don't error out.""" @@ -243,7 +245,7 @@ class TablibTestCase(unittest.TestCase): self.assertEqual(_yaml, data.yaml) - + def test_yaml_import_book(self): """Generate and import YAML book serialization.""" data.append(self.john) @@ -256,7 +258,7 @@ class TablibTestCase(unittest.TestCase): book.yaml = _yaml self.assertEqual(_yaml, book.yaml) - + def test_csv_import_set(self): """Generate and import CSV set serialization.""" @@ -284,7 +286,7 @@ class TablibTestCase(unittest.TestCase): def test_csv_format_detect(self): """Test CSV format detection.""" - + _csv = ( '1,2,3\n' '4,5,6\n' @@ -293,13 +295,13 @@ class TablibTestCase(unittest.TestCase): _bunk = ( '¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶' ) - + self.assertTrue(tablib.formats.csv.detect(_csv)) self.assertFalse(tablib.formats.csv.detect(_bunk)) def test_tsv_format_detect(self): """Test TSV format detection.""" - + _tsv = ( '1\t2\t3\n' '4\t5\t6\n' @@ -308,7 +310,7 @@ class TablibTestCase(unittest.TestCase): _bunk = ( '¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶' ) - + self.assertTrue(tablib.formats.tsv.detect(_tsv)) self.assertFalse(tablib.formats.tsv.detect(_bunk)) @@ -349,23 +351,74 @@ class TablibTestCase(unittest.TestCase): self.assertEqual(tablib.detect(_json)[0], tablib.formats.json) self.assertEqual(tablib.detect(_bunk)[0], None) + def test_transpose(self): + """Transpose a dataset.""" + + transposed_founders = self.founders.transpose() + first_row = transposed_founders[0] + second_row = transposed_founders[1] + + self.assertEqual(transposed_founders.headers, + ["first_name","John", "George", "Thomas"]) + self.assertEqual(first_row, + ("last_name","Adams", "Washington", "Jefferson")) + self.assertEqual(second_row, + ("gpa",90, 67, 50)) + + def test_row_stacking(self): + + """Row stacking.""" + + to_join = tablib.Dataset(headers=self.founders.headers) + + for row in self.founders: + to_join.append(row=row) + + row_stacked = self.founders.stack_rows(to_join) + + for column in row_stacked.headers: + + original_data = self.founders[column] + expected_data = original_data + original_data + self.assertEqual(row_stacked[column], expected_data) + + def test_column_stacking(self): + + """Column stacking""" + + to_join = tablib.Dataset(headers=self.founders.headers) + + for row in self.founders: + to_join.append(row=row) + + column_stacked = self.founders.stack_columns(to_join) + + for index, row in enumerate(column_stacked): + + original_data = self.founders[index] + expected_data = original_data + original_data + self.assertEqual(row, expected_data) + + self.assertEqual(column_stacked[0], + ("John", "Adams", 90, "John", "Adams", 90)) + 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()