summaryrefslogtreecommitdiff
path: root/tablib/core.py
diff options
context:
space:
mode:
Diffstat (limited to 'tablib/core.py')
-rw-r--r--tablib/core.py169
1 files changed, 124 insertions, 45 deletions
diff --git a/tablib/core.py b/tablib/core.py
index 53f3767..9db46c9 100644
--- a/tablib/core.py
+++ b/tablib/core.py
@@ -5,7 +5,7 @@
This module implements the central Tablib objects.
- :copyright: (c) 2011 by Kenneth Reitz.
+ :copyright: (c) 2014 by Kenneth Reitz.
:license: MIT, see LICENSE for more details.
"""
@@ -14,22 +14,22 @@ from operator import itemgetter
from tablib import formats
-from tablib.compat import OrderedDict
+from tablib.compat import OrderedDict, unicode
__title__ = 'tablib'
-__version__ = '0.9.11'
-__build__ = 0x000911
+__version__ = '0.10.0'
+__build__ = 0x001000
__author__ = 'Kenneth Reitz'
__license__ = 'MIT'
-__copyright__ = 'Copyright 2011 Kenneth Reitz'
+__copyright__ = 'Copyright 2014 Kenneth Reitz'
__docformat__ = 'restructuredtext'
class Row(object):
"""Internal Row object. Mainly used for filtering."""
- __slots__ = ['tuple', '_row', 'tags']
+ __slots__ = ['_row', 'tags']
def __init__(self, row=list(), tags=list()):
self._row = list(row)
@@ -57,7 +57,14 @@ class Row(object):
del self._row[i]
def __getstate__(self):
- return {'slot': [getattr(self, slot) for slot in self.__slots__]}
+
+ slots = dict()
+
+ for slot in self.__slots__:
+ attribute = getattr(self, slot)
+ slots[slot] = attribute
+
+ return slots
def __setstate__(self, state):
for (k, v) in list(state.items()): setattr(self, k, v)
@@ -105,7 +112,7 @@ class Dataset(object):
functionality.
Usually you create a :class:`Dataset` instance in your main module, and append
- rows and columns as you collect data. ::
+ rows as you collect data. ::
data = tablib.Dataset()
data.headers = ('name', 'age')
@@ -113,15 +120,27 @@ class Dataset(object):
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. ::
+
+ Setting columns is similar. The column data length must equal the
+ current height of the data and headers must be set ::
+
+ data = tablib.Dataset()
+ data.headers = ('first_name', 'last_name')
+
+ data.append(('John', 'Adams'))
+ data.append(('George', 'Washington'))
+
+ data.append_col((90, 67), header='age')
+
+
+ You can also set rows and headers upon instantiation. This is useful if
+ dealing with dozens or hundreds 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
@@ -162,7 +181,7 @@ class Dataset(object):
def __getitem__(self, key):
- if isinstance(key, str):
+ if isinstance(key, str) or isinstance(key, unicode):
if key in self.headers:
pos = self.headers.index(key) # get 'key' index from each data
return [row[pos] for row in self._data]
@@ -175,14 +194,13 @@ class Dataset(object):
else:
return [result.tuple for result in _results]
-
def __setitem__(self, key, value):
self._validate(value)
self._data[key] = Row(value)
def __delitem__(self, key):
- if isinstance(key, str):
+ if isinstance(key, str) or isinstance(key, unicode):
if key in self.headers:
@@ -205,6 +223,25 @@ class Dataset(object):
except AttributeError:
return '<dataset object>'
+ def __unicode__(self):
+ result = [self.__headers]
+
+ result.extend(list(map(unicode, row)) for row in self._data)
+
+ # here, we calculate max width for each column
+ lens = (list(map(len, row)) for row in result)
+ field_lens = list(map(max, zip(*lens)))
+
+ # delimiter between header and data
+ result.insert(1, ['-' * length for length in field_lens])
+
+ format_string = '|'.join('{%s:%s}' % item for item in enumerate(field_lens))
+
+ return '\n'.join(format_string.format(*row) for row in result)
+
+ def __str__(self):
+ return self.__unicode__()
+
# ---------
# Internals
@@ -410,7 +447,7 @@ class Dataset(object):
:class:`Dataset.xls` contains binary data, so make sure to write in binary mode::
with open('output.xls', 'wb') as f:
- f.write(data.xls)'
+ f.write(data.xls)
"""
pass
@@ -423,7 +460,7 @@ class Dataset(object):
:class:`Dataset.xlsx` contains binary data, so make sure to write in binary mode::
with open('output.xlsx', 'wb') as f:
- f.write(data.xlsx)'
+ f.write(data.xlsx)
"""
pass
@@ -433,10 +470,10 @@ class Dataset(object):
.. admonition:: Binary Warning
- :class:`Dataset.xlsx` contains binary data, so make sure to write in binary mode::
+ :class:`Dataset.ods` contains binary data, so make sure to write in binary mode::
with open('output.ods', 'wb') as f:
- f.write(data.ods)'
+ f.write(data.ods)
"""
pass
@@ -452,6 +489,17 @@ class Dataset(object):
data.csv = 'age, first_name, last_name\\n90, John, Adams'
Import assumes (for now) that headers exist.
+
+ .. admonition:: Binary Warning
+
+ :class:`Dataset.csv` uses \\r\\n line endings by default, so make
+ sure to write in binary mode::
+
+ with open('output.csv', 'wb') as f:
+ f.write(data.csv)
+
+ If you do not do this, and you export the file on Windows, your
+ CSV file will open in Excel with a blank line between each row.
"""
pass
@@ -477,7 +525,7 @@ class Dataset(object):
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: ::
+ A dataset object can also be imported by setting the :class:`Dataset.yaml` attribute: ::
data = tablib.Dataset()
data.yaml = '- {age: 90, first_name: John, last_name: Adams}'
@@ -500,6 +548,7 @@ class Dataset(object):
Import assumes (for now) that headers exist.
"""
+ pass
@property
def html():
@@ -518,30 +567,11 @@ class Dataset(object):
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).
+ Rows 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
- item in the column. ::
-
- data.append(col=random.randint)
-
- See :ref:`dyncols` for an in-depth example.
-
- .. versionchanged:: 0.9.0
- 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.
-
- .. versionadded:: 0.9.0
- 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.
- """
+ object at the given index.
+ """
self._validate(row)
self._data.insert(index, Row(row, tags=tags))
@@ -570,6 +600,14 @@ class Dataset(object):
self.rpush(row, tags)
+ def extend(self, rows, tags=list()):
+ """Adds a list of rows to the :class:`Dataset` using
+ :class:`Dataset.append`
+ """
+
+ for row in rows:
+ self.append(row, tags)
+
def lpop(self):
"""Removes and returns the first row of the :class:`Dataset`."""
@@ -615,8 +653,22 @@ class Dataset(object):
that row.
See :ref:`dyncols` for an in-depth example.
+
+ .. versionchanged:: 0.9.0
+ 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.
+
+ .. versionadded:: 0.9.0
+ 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 col is None:
+ col = []
+
# Callable Columns...
if hasattr(col, '__call__'):
col = list(map(col, self._data))
@@ -628,8 +680,14 @@ class Dataset(object):
# pop the first item off, add to headers
if not header:
raise HeadersNeeded()
+
+ # corner case - if header is set without data
+ elif header and self.height == 0 and len(col):
+ raise InvalidDimensions
+
self.headers.insert(index, header)
+
if self.height and self.width:
for i, row in enumerate(self._data):
@@ -684,6 +742,12 @@ class Dataset(object):
self.rpush_col(col, header)
+ def get_col(self, index):
+ """Returns the column from the :class:`Dataset` at the given index."""
+
+ return [row[index] for row in self._data]
+
+
# ----
# Misc
# ----
@@ -730,13 +794,13 @@ class Dataset(object):
sorted.
"""
- if isinstance(col, str):
+ if isinstance(col, str) or isinstance(col, unicode):
if not self.headers:
raise HeadersNeeded
_sorted = sorted(self.dict, key=itemgetter(col), reverse=reverse)
- _dset = Dataset(headers=self.headers)
+ _dset = Dataset(headers=self.headers, title=self.title)
for item in _sorted:
row = [item[key] for key in self.headers]
@@ -747,7 +811,7 @@ class Dataset(object):
col = self.headers[col]
_sorted = sorted(self.dict, key=itemgetter(col), reverse=reverse)
- _dset = Dataset(headers=self.headers)
+ _dset = Dataset(headers=self.headers, title=self.title)
for item in _sorted:
if self.headers:
@@ -891,10 +955,12 @@ class Databook(object):
except AttributeError:
pass
+ def sheets(self):
+ return self._datasets
def add_sheet(self, dataset):
"""Adds given :class:`Dataset` to the :class:`Databook`."""
- if type(dataset) is Dataset:
+ if isinstance(dataset, Dataset):
self._datasets.append(dataset)
else:
raise InvalidDatasetType
@@ -947,6 +1013,19 @@ def import_set(stream):
return None
+def import_book(stream):
+ """Return dataset of given stream."""
+ (format, stream) = detect(stream)
+
+ try:
+ databook = Databook()
+ format.import_book(databook, stream)
+ return databook
+
+ except AttributeError:
+ return None
+
+
class InvalidDatasetType(Exception):
"Only Datasets can be added to a DataBook"