diff options
| -rw-r--r-- | HISTORY.rst | 18 | ||||
| -rw-r--r-- | LICENSE | 2 | ||||
| -rw-r--r-- | NOTICE | 6 | ||||
| -rw-r--r-- | README.rst | 1 | ||||
| -rw-r--r-- | TODO.rst | 9 | ||||
| -rw-r--r-- | docs/_themes/LICENSE | 2 | ||||
| -rw-r--r-- | docs/conf.py | 2 | ||||
| -rw-r--r-- | setup.py | 2 | ||||
| -rw-r--r-- | tablib/core.py | 65 | ||||
| -rw-r--r-- | tablib/formats/__init__.py | 3 | ||||
| -rw-r--r-- | tablib/formats/_html.py | 53 | ||||
| -rw-r--r-- | tablib/formats/_json.py | 6 | ||||
| -rw-r--r-- | tablib/packages/markup.py | 484 | ||||
| -rwxr-xr-x | test_tablib.py | 39 |
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) @@ -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 @@ -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 =================== @@ -18,6 +18,7 @@ Output formats supported: - Excel (Sets + Books) - JSON (Sets + Books) - YAML (Sets + Books) +- HTML (Sets) - TSV (Sets) - CSV (Sets) @@ -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 @@ -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 < and > + 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( '&', '&' ) + if '>' in text: + text = text.replace( '>', '>' ) + if '<' in text: + text = text.replace( '<', '<' ) + if '\"' in text: + text = text.replace( '\"', '"' ) + if '\'' in text: + text = text.replace( '\'', '"' ) + 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 '&' in text: + text = text.replace( '&', '&' ) + if '>' in text: + text = text.replace( '>', '>' ) + if '<' in text: + text = text.replace( '<', '<' ) + if '"' in text: + text = text.replace( '"', '\"' ) + + 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.""" |
