summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--HISTORY.rst18
-rw-r--r--LICENSE2
-rw-r--r--NOTICE6
-rw-r--r--README.rst1
-rw-r--r--TODO.rst9
-rw-r--r--docs/_themes/LICENSE2
-rw-r--r--docs/conf.py2
-rw-r--r--setup.py2
-rw-r--r--tablib/core.py65
-rw-r--r--tablib/formats/__init__.py3
-rw-r--r--tablib/formats/_html.py53
-rw-r--r--tablib/formats/_json.py6
-rw-r--r--tablib/packages/markup.py484
-rwxr-xr-xtest_tablib.py39
14 files changed, 670 insertions, 22 deletions
diff --git a/HISTORY.rst b/HISTORY.rst
index c64d7b5..95b9328 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,19 +1,27 @@
History
-------
+0.9.3 (2011-01-31)
+++++++++++++++++++
+
+* Databook duplication leak fix.
+* HTML Table output.
+* Added column sorting.
+
+
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
+* 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)
++++++++++++++++++
-* Minor reference shadowing bugfix
+* Minor reference shadowing bugfix.
0.9.0 (2010-11-04)
diff --git a/LICENSE b/LICENSE
index 717ff16..ea8c217 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2010 Kenneth Reitz.
+Copyright (c) 2011 Kenneth Reitz.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/NOTICE b/NOTICE
index 88d5d2d..c8428e8 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,6 +1,12 @@
Tablib includes some vendorized python libraries: ordereddict, pyyaml,
simplejson, and xlwt.
+Markup License
+==============
+
+Markup is in the public domain.
+
+
OrderedDict License
===================
diff --git a/README.rst b/README.rst
index 00b6345..f974248 100644
--- a/README.rst
+++ b/README.rst
@@ -18,6 +18,7 @@ Output formats supported:
- Excel (Sets + Books)
- JSON (Sets + Books)
- YAML (Sets + Books)
+- HTML (Sets)
- TSV (Sets)
- CSV (Sets)
diff --git a/TODO.rst b/TODO.rst
index 231f03e..9f8c99f 100644
--- a/TODO.rst
+++ b/TODO.rst
@@ -1,8 +1,13 @@
+* Add seperator support to HTML out
+* Hooks System
+ - pre/post-append
+ - pre/post-import
+ - pre/post-export
+* Big Data
* Backwards-compatible OrderedDict support
* Write more exhausive unit-tests.
* Write stress tests.
* Make CSV write customizable.
-* HTML Table exports.
* Integrate django-tablib
* Mention django-tablib in Documention
-* Dataset title usage in documentation (#17) \ No newline at end of file
+* Dataset title usage in documentation (#17)
diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE
index 81f4d30..b160a8e 100644
--- a/docs/_themes/LICENSE
+++ b/docs/_themes/LICENSE
@@ -1,6 +1,6 @@
Modifications:
-Copyright (c) 2010 Kenneth Reitz.
+Copyright (c) 2011 Kenneth Reitz.
Original Project:
diff --git a/docs/conf.py b/docs/conf.py
index 325002c..2a642c9 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -42,7 +42,7 @@ master_doc = 'index'
# General information about the project.
project = u'Tablib'
-copyright = u'2010, Kenneth Reitz. Styles (modified) © Armin Ronacher'
+copyright = u'2011, Kenneth Reitz. Styles (modified) © Armin Ronacher'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
diff --git a/setup.py b/setup.py
index e779457..c851751 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@ required = []
setup(
name='tablib',
- version='0.9.2',
+ version='0.9.3',
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 bd2d4ba..9d36970 100644
--- a/tablib/core.py
+++ b/tablib/core.py
@@ -5,21 +5,22 @@
This module implements the central tablib objects.
- :copyright: (c) 2010 by Kenneth Reitz.
+ :copyright: (c) 2011 by Kenneth Reitz.
:license: MIT, see LICENSE for more details.
"""
from copy import copy
+from operator import itemgetter
from tablib import formats
__title__ = 'tablib'
-__version__ = '0.9.2'
-__build__ = 0x000902
+__version__ = '0.9.3'
+__build__ = 0x000903
__author__ = 'Kenneth Reitz'
__license__ = 'MIT'
-__copyright__ = 'Copyright 2010 Kenneth Reitz'
+__copyright__ = 'Copyright 2011 Kenneth Reitz'
class Row(object):
@@ -425,6 +426,14 @@ class Dataset(object):
Import assumes (for now) that headers exist.
"""
+ @property
+ def html():
+ """A HTML table representation of the :class:`Dataset` object. If
+ headers have been set, they will be used as table headers.
+
+ ..notice:: This method can be used for export only.
+ """
+ pass
def append(self, row=None, col=None, header=None, tags=list()):
"""Adds a row or column to the :class:`Dataset`.
@@ -511,6 +520,7 @@ class Dataset(object):
else:
self._data = [Row([row]) for row in col]
+
def filter(self, tag):
"""Returns a new instance of the :class:`Dataset`, excluding any rows
that do not contain the given :ref:`tags <tags>`.
@@ -520,6 +530,43 @@ class Dataset(object):
return _dset
+
+ def sort(self, col, reverse=False):
+ """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."""
+
+ if isinstance(col, basestring):
+
+ if not self.headers:
+ raise HeadersNeeded
+
+ _sorted = sorted(self.dict, key=itemgetter(col), reverse=reverse)
+ _dset = Dataset(headers=self.headers)
+
+ for item in _sorted:
+ row = [item[key] for key in self.headers]
+ _dset.append(row=row)
+
+ else:
+ if self.headers:
+ col = self.headers[col]
+
+ _sorted = sorted(self.dict, key=itemgetter(col), reverse=reverse)
+ _dset = Dataset(headers=self.headers)
+
+ for item in _sorted:
+ if self.headers:
+ row = [item[key] for key in self.headers]
+ else:
+ row = item
+ _dset.append(row=row)
+
+
+ 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
@@ -615,10 +662,14 @@ class Databook(object):
"""A book of :class:`Dataset` objects.
"""
- def __init__(self, sets=[]):
- self._datasets = sets
- self._register_formats()
+ def __init__(self, sets=None):
+ if sets is None:
+ self._datasets = list()
+ else:
+ self._datasets = sets
+
+ self._register_formats()
def __repr__(self):
try:
diff --git a/tablib/formats/__init__.py b/tablib/formats/__init__.py
index f5960b8..147df31 100644
--- a/tablib/formats/__init__.py
+++ b/tablib/formats/__init__.py
@@ -8,5 +8,6 @@ import _json as json
import _xls as xls
import _yaml as yaml
import _tsv as tsv
+import _html as html
-available = (json, xls, yaml, csv, tsv)
+available = (json, xls, yaml, csv, tsv, html)
diff --git a/tablib/formats/_html.py b/tablib/formats/_html.py
new file mode 100644
index 0000000..13dc055
--- /dev/null
+++ b/tablib/formats/_html.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+
+""" Tablib - HTML export support.
+"""
+
+from StringIO import StringIO
+
+from tablib.packages import markup
+import tablib
+
+BOOK_ENDINGS = 'h3'
+
+title = 'html'
+extentions = ('html', )
+
+
+def export_set(dataset):
+ """HTML representation of a Dataset."""
+
+ stream = StringIO()
+
+ page = markup.page()
+ page.table.open()
+
+ if dataset.headers is not None:
+ page.thead.open()
+ headers = markup.oneliner.th(dataset.headers)
+ page.tr(headers)
+ page.thead.close()
+
+ for row in dataset:
+ html_row = markup.oneliner.td(row)
+ page.tr(html_row)
+
+ page.table.close()
+
+ stream.writelines(str(page))
+
+ return stream.getvalue()
+
+
+def export_book(databook):
+ """HTML representation of a Databook."""
+
+ stream = StringIO()
+
+ for i, dset in enumerate(databook._datasets):
+ title = (dset.title if dset.title else 'Set %s' % (i))
+ stream.write('<%s>%s</%s>\n' % (BOOK_ENDINGS, title, BOOK_ENDINGS))
+ stream.write(dset.html)
+ stream.write('\n')
+
+ return stream.getvalue()
diff --git a/tablib/formats/_json.py b/tablib/formats/_json.py
index da31b23..7f31ee5 100644
--- a/tablib/formats/_json.py
+++ b/tablib/formats/_json.py
@@ -26,11 +26,11 @@ def export_set(dataset):
def export_book(databook):
"""Returns JSON representation of Databook."""
return json.dumps(databook._package())
-
+
def import_set(dset, in_stream):
"""Returns dataset from JSON stream."""
-
+
dset.wipe()
dset.dict = json.loads(in_stream)
@@ -52,4 +52,4 @@ def detect(stream):
json.loads(stream)
return True
except ValueError:
- return False \ No newline at end of file
+ return False
diff --git a/tablib/packages/markup.py b/tablib/packages/markup.py
new file mode 100644
index 0000000..98d9b1d
--- /dev/null
+++ b/tablib/packages/markup.py
@@ -0,0 +1,484 @@
+# This code is in the public domain, it comes
+# with absolutely no warranty and you can do
+# absolutely whatever you want with it.
+
+__date__ = '17 May 2007'
+__version__ = '1.7'
+__doc__= """
+This is markup.py - a Python module that attempts to
+make it easier to generate HTML/XML from a Python program
+in an intuitive, lightweight, customizable and pythonic way.
+
+The code is in the public domain.
+
+Version: %s as of %s.
+
+Documentation and further info is at http://markup.sourceforge.net/
+
+Please send bug reports, feature requests, enhancement
+ideas or questions to nogradi at gmail dot com.
+
+Installation: drop markup.py somewhere into your Python path.
+""" % ( __version__, __date__ )
+
+import string
+
+class element:
+ """This class handles the addition of a new element."""
+
+ def __init__( self, tag, case='lower', parent=None ):
+ self.parent = parent
+
+ if case == 'lower':
+ self.tag = tag.lower( )
+ else:
+ self.tag = tag.upper( )
+
+ def __call__( self, *args, **kwargs ):
+ if len( args ) > 1:
+ raise ArgumentError( self.tag )
+
+ # if class_ was defined in parent it should be added to every element
+ if self.parent is not None and self.parent.class_ is not None:
+ if 'class_' not in kwargs:
+ kwargs['class_'] = self.parent.class_
+
+ if self.parent is None and len( args ) == 1:
+ x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
+ return '\n'.join( x )
+ elif self.parent is None and len( args ) == 0:
+ x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
+ return '\n'.join( x )
+
+ if self.tag in self.parent.twotags:
+ for myarg, mydict in _argsdicts( args, kwargs ):
+ self.render( self.tag, False, myarg, mydict )
+ elif self.tag in self.parent.onetags:
+ if len( args ) == 0:
+ for myarg, mydict in _argsdicts( args, kwargs ):
+ self.render( self.tag, True, myarg, mydict ) # here myarg is always None, because len( args ) = 0
+ else:
+ raise ClosingError( self.tag )
+ elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
+ raise DeprecationError( self.tag )
+ else:
+ raise InvalidElementError( self.tag, self.parent.mode )
+
+ def render( self, tag, single, between, kwargs ):
+ """Append the actual tags to content."""
+
+ out = "<%s" % tag
+ for key, value in kwargs.iteritems( ):
+ if value is not None: # when value is None that means stuff like <... checked>
+ key = key.strip('_') # strip this so class_ will mean class, etc.
+ if key == 'http_equiv': # special cases, maybe change _ to - overall?
+ key = 'http-equiv'
+ elif key == 'accept_charset':
+ key = 'accept-charset'
+ out = "%s %s=\"%s\"" % ( out, key, escape( value ) )
+ else:
+ out = "%s %s" % ( out, key )
+ if between is not None:
+ out = "%s>%s</%s>" % ( out, between, tag )
+ else:
+ if single:
+ out = "%s />" % out
+ else:
+ out = "%s>" % out
+ if self.parent is not None:
+ self.parent.content.append( out )
+ else:
+ return out
+
+ def close( self ):
+ """Append a closing tag unless element has only opening tag."""
+
+ if self.tag in self.parent.twotags:
+ self.parent.content.append( "</%s>" % self.tag )
+ elif self.tag in self.parent.onetags:
+ raise ClosingError( self.tag )
+ elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
+ raise DeprecationError( self.tag )
+
+ def open( self, **kwargs ):
+ """Append an opening tag."""
+
+ if self.tag in self.parent.twotags or self.tag in self.parent.onetags:
+ self.render( self.tag, False, None, kwargs )
+ elif self.mode == 'strict_html' and self.tag in self.parent.deptags:
+ raise DeprecationError( self.tag )
+
+class page:
+ """This is our main class representing a document. Elements are added
+ as attributes of an instance of this class."""
+
+ def __init__( self, mode='strict_html', case='lower', onetags=None, twotags=None, separator='\n', class_=None ):
+ """Stuff that effects the whole document.
+
+ mode -- 'strict_html' for HTML 4.01 (default)
+ 'html' alias for 'strict_html'
+ 'loose_html' to allow some deprecated elements
+ 'xml' to allow arbitrary elements
+
+ case -- 'lower' element names will be printed in lower case (default)
+ 'upper' they will be printed in upper case
+
+ onetags -- list or tuple of valid elements with opening tags only
+ twotags -- list or tuple of valid elements with both opening and closing tags
+ these two keyword arguments may be used to select
+ the set of valid elements in 'xml' mode
+ invalid elements will raise appropriate exceptions
+
+ separator -- string to place between added elements, defaults to newline
+
+ class_ -- a class that will be added to every element if defined"""
+
+ valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ]
+ valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON",
+ "CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET",
+ "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HTML", "I", "IFRAME", "INS",
+ "KBD", "LABEL", "LEGEND", "LI", "MAP", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP",
+ "OPTION", "P", "PRE", "Q", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "STYLE",
+ "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR",
+ "TT", "UL", "VAR" ]
+ deprecated_onetags = [ "BASEFONT", "ISINDEX" ]
+ deprecated_twotags = [ "APPLET", "CENTER", "DIR", "FONT", "MENU", "S", "STRIKE", "U" ]
+
+ self.header = [ ]
+ self.content = [ ]
+ self.footer = [ ]
+ self.case = case
+ self.separator = separator
+
+ # init( ) sets it to True so we know that </body></html> has to be printed at the end
+ self._full = False
+ self.class_= class_
+
+ if mode == 'strict_html' or mode == 'html':
+ self.onetags = valid_onetags
+ self.onetags += map( string.lower, self.onetags )
+ self.twotags = valid_twotags
+ self.twotags += map( string.lower, self.twotags )
+ self.deptags = deprecated_onetags + deprecated_twotags
+ self.deptags += map( string.lower, self.deptags )
+ self.mode = 'strict_html'
+ elif mode == 'loose_html':
+ self.onetags = valid_onetags + deprecated_onetags
+ self.onetags += map( string.lower, self.onetags )
+ self.twotags = valid_twotags + deprecated_twotags
+ self.twotags += map( string.lower, self.twotags )
+ self.mode = mode
+ elif mode == 'xml':
+ if onetags and twotags:
+ self.onetags = onetags
+ self.twotags = twotags
+ elif ( onetags and not twotags ) or ( twotags and not onetags ):
+ raise CustomizationError( )
+ else:
+ self.onetags = russell( )
+ self.twotags = russell( )
+ self.mode = mode
+ else:
+ raise ModeError( mode )
+
+ def __getattr__( self, attr ):
+ if attr.startswith("__") and attr.endswith("__"):
+ raise AttributeError, attr
+ return element( attr, case=self.case, parent=self )
+
+ def __str__( self ):
+
+ if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ):
+ end = [ '</body>', '</html>' ]
+ else:
+ end = [ ]
+
+ return self.separator.join( self.header + self.content + self.footer + end )
+
+ def __call__( self, escape=False ):
+ """Return the document as a string.
+
+ escape -- False print normally
+ True replace < and > by &lt; and &gt;
+ the default escape sequences in most browsers"""
+
+ if escape:
+ return _escape( self.__str__( ) )
+ else:
+ return self.__str__( )
+
+ def add( self, text ):
+ """This is an alias to addcontent."""
+ self.addcontent( text )
+
+ def addfooter( self, text ):
+ """Add some text to the bottom of the document"""
+ self.footer.append( text )
+
+ def addheader( self, text ):
+ """Add some text to the top of the document"""
+ self.header.append( text )
+
+ def addcontent( self, text ):
+ """Add some text to the main part of the document"""
+ self.content.append( text )
+
+
+ def init( self, lang='en', css=None, metainfo=None, title=None, header=None,
+ footer=None, charset=None, encoding=None, doctype=None, bodyattrs=None, script=None ):
+ """This method is used for complete documents with appropriate
+ doctype, encoding, title, etc information. For an HTML/XML snippet
+ omit this method.
+
+ lang -- language, usually a two character string, will appear
+ as <html lang='en'> in html mode (ignored in xml mode)
+
+ css -- Cascading Style Sheet filename as a string or a list of
+ strings for multiple css files (ignored in xml mode)
+
+ metainfo -- a dictionary in the form { 'name':'content' } to be inserted
+ into meta element(s) as <meta name='name' content='content'>
+ (ignored in xml mode)
+
+ bodyattrs --a dictionary in the form { 'key':'value', ... } which will be added
+ as attributes of the <body> element as <body key='value' ... >
+ (ignored in xml mode)
+
+ script -- dictionary containing src:type pairs, <script type='text/type' src=src></script>
+
+ title -- the title of the document as a string to be inserted into
+ a title element as <title>my title</title> (ignored in xml mode)
+
+ header -- some text to be inserted right after the <body> element
+ (ignored in xml mode)
+
+ footer -- some text to be inserted right before the </body> element
+ (ignored in xml mode)
+
+ charset -- a string defining the character set, will be inserted into a
+ <meta http-equiv='Content-Type' content='text/html; charset=myset'>
+ element (ignored in xml mode)
+
+ encoding -- a string defining the encoding, will be put into to first line of
+ the document as <?xml version='1.0' encoding='myencoding' ?> in
+ xml mode (ignored in html mode)
+
+ doctype -- the document type string, defaults to
+ <!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>
+ in html mode (ignored in xml mode)"""
+
+ self._full = True
+
+ if self.mode == 'strict_html' or self.mode == 'loose_html':
+ if doctype is None:
+ doctype = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>"
+ self.header.append( doctype )
+ self.html( lang=lang )
+ self.head( )
+ if charset is not None:
+ self.meta( http_equiv='Content-Type', content="text/html; charset=%s" % charset )
+ if metainfo is not None:
+ self.metainfo( metainfo )
+ if css is not None:
+ self.css( css )
+ if title is not None:
+ self.title( title )
+ if script is not None:
+ self.scripts( script )
+ self.head.close()
+ if bodyattrs is not None:
+ self.body( **bodyattrs )
+ else:
+ self.body( )
+ if header is not None:
+ self.content.append( header )
+ if footer is not None:
+ self.footer.append( footer )
+
+ elif self.mode == 'xml':
+ if doctype is None:
+ if encoding is not None:
+ doctype = "<?xml version='1.0' encoding='%s' ?>" % encoding
+ else:
+ doctype = "<?xml version='1.0' ?>"
+ self.header.append( doctype )
+
+ def css( self, filelist ):
+ """This convenience function is only useful for html.
+ It adds css stylesheet(s) to the document via the <link> element."""
+
+ if isinstance( filelist, basestring ):
+ self.link( href=filelist, rel='stylesheet', type='text/css', media='all' )
+ else:
+ for file in filelist:
+ self.link( href=file, rel='stylesheet', type='text/css', media='all' )
+
+ def metainfo( self, mydict ):
+ """This convenience function is only useful for html.
+ It adds meta information via the <meta> element, the argument is
+ a dictionary of the form { 'name':'content' }."""
+
+ if isinstance( mydict, dict ):
+ for name, content in mydict.iteritems( ):
+ self.meta( name=name, content=content )
+ else:
+ raise TypeError, "Metainfo should be called with a dictionary argument of name:content pairs."
+
+ def scripts( self, mydict ):
+ """Only useful in html, mydict is dictionary of src:type pairs will
+ be rendered as <script type='text/type' src=src></script>"""
+
+ if isinstance( mydict, dict ):
+ for src, type in mydict.iteritems( ):
+ self.script( '', src=src, type='text/%s' % type )
+ else:
+ raise TypeError, "Script should be given a dictionary of src:type pairs."
+
+
+class _oneliner:
+ """An instance of oneliner returns a string corresponding to one element.
+ This class can be used to write 'oneliners' that return a string
+ immediately so there is no need to instantiate the page class."""
+
+ def __init__( self, case='lower' ):
+ self.case = case
+
+ def __getattr__( self, attr ):
+ if attr.startswith("__") and attr.endswith("__"):
+ raise AttributeError, attr
+ return element( attr, case=self.case, parent=None )
+
+oneliner = _oneliner( case='lower' )
+upper_oneliner = _oneliner( case='upper' )
+
+def _argsdicts( args, mydict ):
+ """A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1."""
+
+ if len( args ) == 0:
+ args = None,
+ elif len( args ) == 1:
+ args = _totuple( args[0] )
+ else:
+ raise Exception, "We should have never gotten here."
+
+ mykeys = mydict.keys( )
+ myvalues = map( _totuple, mydict.values( ) )
+
+ maxlength = max( map( len, [ args ] + myvalues ) )
+
+ for i in xrange( maxlength ):
+ thisdict = { }
+ for key, value in zip( mykeys, myvalues ):
+ try:
+ thisdict[ key ] = value[i]
+ except IndexError:
+ thisdict[ key ] = value[-1]
+ try:
+ thisarg = args[i]
+ except IndexError:
+ thisarg = args[-1]
+
+ yield thisarg, thisdict
+
+def _totuple( x ):
+ """Utility stuff to convert string, int, float, None or anything to a usable tuple."""
+
+ if isinstance( x, basestring ):
+ out = x,
+ elif isinstance( x, ( int, float ) ):
+ out = str( x ),
+ elif x is None:
+ out = None,
+ else:
+ out = tuple( x )
+
+ return out
+
+def escape( text, newline=False ):
+ """Escape special html characters."""
+
+ if isinstance( text, basestring ):
+ if '&' in text:
+ text = text.replace( '&', '&amp;' )
+ if '>' in text:
+ text = text.replace( '>', '&gt;' )
+ if '<' in text:
+ text = text.replace( '<', '&lt;' )
+ if '\"' in text:
+ text = text.replace( '\"', '&quot;' )
+ if '\'' in text:
+ text = text.replace( '\'', '&quot;' )
+ if newline:
+ if '\n' in text:
+ text = text.replace( '\n', '<br>' )
+
+ return text
+
+_escape = escape
+
+def unescape( text ):
+ """Inverse of escape."""
+
+ if isinstance( text, basestring ):
+ if '&amp;' in text:
+ text = text.replace( '&amp;', '&' )
+ if '&gt;' in text:
+ text = text.replace( '&gt;', '>' )
+ if '&lt;' in text:
+ text = text.replace( '&lt;', '<' )
+ if '&quot;' in text:
+ text = text.replace( '&quot;', '\"' )
+
+ return text
+
+class dummy:
+ """A dummy class for attaching attributes."""
+ pass
+
+doctype = dummy( )
+doctype.frameset = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Frameset//EN' 'http://www.w3.org/TR/html4/frameset.dtd'>"
+doctype.strict = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01//EN' 'http://www.w3.org/TR/html4/strict.dtd'>"
+doctype.loose = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN' 'http://www.w3.org/TR/html4/loose.dtd'>"
+
+class russell:
+ """A dummy class that contains anything."""
+
+ def __contains__( self, item ):
+ return True
+
+
+class MarkupError( Exception ):
+ """All our exceptions subclass this."""
+ def __str__( self ):
+ return self.message
+
+class ClosingError( MarkupError ):
+ def __init__( self, tag ):
+ self.message = "The element '%s' does not accept non-keyword arguments (has no closing tag)." % tag
+
+class OpeningError( MarkupError ):
+ def __init__( self, tag ):
+ self.message = "The element '%s' can not be opened." % tag
+
+class ArgumentError( MarkupError ):
+ def __init__( self, tag ):
+ self.message = "The element '%s' was called with more than one non-keyword argument." % tag
+
+class InvalidElementError( MarkupError ):
+ def __init__( self, tag, mode ):
+ self.message = "The element '%s' is not valid for your mode '%s'." % ( tag, mode )
+
+class DeprecationError( MarkupError ):
+ def __init__( self, tag ):
+ self.message = "The element '%s' is deprecated, instantiate markup.page with mode='loose_html' to allow it." % tag
+
+class ModeError( MarkupError ):
+ def __init__( self, mode ):
+ self.message = "Mode '%s' is invalid, possible values: strict_html, loose_html, xml." % mode
+
+class CustomizationError( MarkupError ):
+ def __init__( self ):
+ self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'."
+
+if __name__ == '__main__':
+ print __doc__
diff --git a/test_tablib.py b/test_tablib.py
index 8e2454f..15630f2 100755
--- a/test_tablib.py
+++ b/test_tablib.py
@@ -5,6 +5,8 @@
import unittest
+from tablib.packages import markup
+
import tablib
@@ -182,6 +184,27 @@ class TablibTestCase(unittest.TestCase):
self.assertEqual(tsv, self.founders.tsv)
+ def test_html_export(self):
+
+ """HTML export"""
+
+ html = markup.page()
+ html.table.open()
+ html.thead.open()
+
+ html.tr(markup.oneliner.th(self.founders.headers))
+ html.thead.close()
+
+ for founder in self.founders:
+
+ html.tr(markup.oneliner.td(founder))
+
+ html.table.close()
+ html = str(html)
+
+ self.assertEqual(html, self.founders.html)
+
+
def test_unicode_append(self):
"""Passes in a single unicode charecter and exports."""
@@ -402,6 +425,22 @@ class TablibTestCase(unittest.TestCase):
self.assertEqual(column_stacked[0],
("John", "Adams", 90, "John", "Adams", 90))
+ def test_sorting(self):
+
+ """Sort columns."""
+
+ sorted_data = self.founders.sort(col="first_name")
+
+ first_row = sorted_data[0]
+ second_row = sorted_data[2]
+ third_row = sorted_data[1]
+ expected_first = self.founders[1]
+ expected_second = self.founders[2]
+ expected_third = self.founders[0]
+
+ self.assertEqual(first_row, expected_first)
+ self.assertEqual(second_row, expected_second)
+ self.assertEqual(third_row, expected_third)
def test_wipe(self):
"""Purge a dataset."""