summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKenneth Reitz <me@kennethreitz.org>2016-02-07 06:08:23 -0500
committerKenneth Reitz <me@kennethreitz.org>2016-02-07 06:08:23 -0500
commit32cbc36fc181dcdb551325529a3c582a6acb30bd (patch)
treef8ff6fbd0c3b69c288e63118ff0bcbe8e860aff7
parent8bded88559552241d2a5e6d735f97eb4d69175fb (diff)
parent79dc4524a05e7a89b0e65e9d5b78fa6a56652848 (diff)
downloadtablib-32cbc36fc181dcdb551325529a3c582a6acb30bd.tar.gz
Merge branch 'latex-export' of https://github.com/mloesch/tablib into develop
-rw-r--r--AUTHORS1
-rw-r--r--tablib/core.py10
-rw-r--r--tablib/formats/__init__.py3
-rw-r--r--tablib/formats/_latex.py134
-rwxr-xr-xtest_tablib.py62
5 files changed, 209 insertions, 1 deletions
diff --git a/AUTHORS b/AUTHORS
index 7f9036e..7d150db 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -30,3 +30,4 @@ Patches and Suggestions
- James Douglass
- Tommy Anthony
- Marco Dallagiacoma
+- Mathias Loesch
diff --git a/tablib/core.py b/tablib/core.py
index 1571b5e..7f36d8b 100644
--- a/tablib/core.py
+++ b/tablib/core.py
@@ -584,6 +584,16 @@ class Dataset(object):
pass
+ @property
+ def latex():
+ """A LaTeX booktabs representation of the :class:`Dataset` object. If a
+ title has been set, it will be exported as the table caption.
+
+ .. note:: This method can be used for export only.
+ """
+ pass
+
+
# ----
# Rows
# ----
diff --git a/tablib/formats/__init__.py b/tablib/formats/__init__.py
index 1eda107..5cca19f 100644
--- a/tablib/formats/__init__.py
+++ b/tablib/formats/__init__.py
@@ -12,5 +12,6 @@ from . import _html as html
from . import _xlsx as xlsx
from . import _ods as ods
from . import _dbf as dbf
+from . import _latex as latex
-available = (json, xls, yaml, csv, dbf, tsv, html, xlsx, ods)
+available = (json, xls, yaml, csv, dbf, tsv, html, latex, xlsx, ods)
diff --git a/tablib/formats/_latex.py b/tablib/formats/_latex.py
new file mode 100644
index 0000000..44ee101
--- /dev/null
+++ b/tablib/formats/_latex.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+
+"""Tablib - LaTeX table export support.
+
+ Generates a LaTeX booktabs-style table from the dataset.
+"""
+import re
+
+from tablib.compat import unicode
+
+title = 'latex'
+extensions = ('tex',)
+
+TABLE_TEMPLATE = """\
+%% Note: add \\usepackage{booktabs} to your preamble
+%%
+\\begin{table}[!htbp]
+ \\centering
+ %(CAPTION)s
+ \\begin{tabular}{%(COLSPEC)s}
+ \\toprule
+%(HEADER)s
+ %(MIDRULE)s
+%(BODY)s
+ \\bottomrule
+ \\end{tabular}
+\\end{table}
+"""
+
+TEX_RESERVED_SYMBOLS_MAP = dict([
+ ('\\', '\\textbackslash{}'),
+ ('{', '\\{'),
+ ('}', '\\}'),
+ ('$', '\\$'),
+ ('&', '\\&'),
+ ('#', '\\#'),
+ ('^', '\\textasciicircum{}'),
+ ('_', '\\_'),
+ ('~', '\\textasciitilde{}'),
+ ('%', '\\%'),
+])
+
+TEX_RESERVED_SYMBOLS_RE = re.compile(
+ '(%s)' % '|'.join(map(re.escape, TEX_RESERVED_SYMBOLS_MAP.keys())))
+
+
+def export_set(dataset):
+ """Returns LaTeX representation of dataset
+
+ :param dataset: dataset to serialize
+ :type dataset: tablib.core.Dataset
+ """
+
+ caption = '\\caption{%s}' % dataset.title if dataset.title else '%'
+ colspec = _colspec(dataset.width)
+ header = _serialize_row(dataset.headers) if dataset.headers else ''
+ midrule = _midrule(dataset.width)
+ body = '\n'.join([_serialize_row(row) for row in dataset])
+ return TABLE_TEMPLATE % dict(CAPTION=caption, COLSPEC=colspec,
+ HEADER=header, MIDRULE=midrule, BODY=body)
+
+
+def _colspec(dataset_width):
+ """Generates the column specification for the LaTeX `tabular` environment
+ based on the dataset width.
+
+ The first column is justified to the left, all further columns are aligned
+ to the right.
+
+ .. note:: This is only a heuristic and most probably has to be fine-tuned
+ post export. Column alignment should depend on the data type, e.g., textual
+ content should usually be aligned to the left while numeric content almost
+ always should be aligned to the right.
+
+ :param dataset_width: width of the dataset
+ """
+
+ spec = 'l'
+ for _ in range(1, dataset_width):
+ spec += 'r'
+ return spec
+
+
+def _midrule(dataset_width):
+ """Generates the table `midrule`, which may be composed of several
+ `cmidrules`.
+
+ :param dataset_width: width of the dataset to serialize
+ """
+
+ if not dataset_width or dataset_width == 1:
+ return '\\midrule'
+ return ' '.join([_cmidrule(colindex, dataset_width) for colindex in
+ range(1, dataset_width + 1)])
+
+
+def _cmidrule(colindex, dataset_width):
+ """Generates the `cmidrule` for a single column with appropriate trimming
+ based on the column position.
+
+ :param colindex: Column index
+ :param dataset_width: width of the dataset
+ """
+
+ rule = '\\cmidrule(%s){%d-%d}'
+ if colindex == 1:
+ # Rule of first column is trimmed on the right
+ return rule % ('r', colindex, colindex)
+ if colindex == dataset_width:
+ # Rule of last column is trimmed on the left
+ return rule % ('l', colindex, colindex)
+ # Inner columns are trimmed on the left and right
+ return rule % ('lr', colindex, colindex)
+
+
+def _serialize_row(row):
+ """Returns string representation of a single row.
+
+ :param row: single dataset row
+ """
+
+ new_row = [_escape_tex_reserved_symbols(unicode(item)) if item else '' for
+ item in row]
+ return 6 * ' ' + ' & '.join(new_row) + ' \\\\'
+
+
+def _escape_tex_reserved_symbols(input):
+ """Escapes all TeX reserved symbols ('_', '~', etc.) in a string.
+
+ :param input: String to escape
+ """
+ def replace(match):
+ return TEX_RESERVED_SYMBOLS_MAP[match.group()]
+ return TEX_RESERVED_SYMBOLS_RE.sub(replace, input)
diff --git a/test_tablib.py b/test_tablib.py
index a1f581c..9c37b8e 100755
--- a/test_tablib.py
+++ b/test_tablib.py
@@ -319,6 +319,67 @@ class TablibTestCase(unittest.TestCase):
self.assertEqual(html, d.html)
+ def test_latex_export(self):
+ """LaTeX export"""
+
+ expected = """\
+% Note: add \\usepackage{booktabs} to your preamble
+%
+\\begin{table}[!htbp]
+ \\centering
+ \\caption{Founders}
+ \\begin{tabular}{lrr}
+ \\toprule
+ first\\_name & last\\_name & gpa \\\\
+ \\cmidrule(r){1-1} \\cmidrule(lr){2-2} \\cmidrule(l){3-3}
+ John & Adams & 90 \\\\
+ George & Washington & 67 \\\\
+ Thomas & Jefferson & 50 \\\\
+ \\bottomrule
+ \\end{tabular}
+\\end{table}
+"""
+ output = self.founders.latex
+ self.assertEqual(output, expected)
+
+
+ def test_latex_export_empty_dataset(self):
+ self.assertTrue(tablib.Dataset().latex is not None)
+
+
+ def test_latex_export_no_headers(self):
+ d = tablib.Dataset()
+ d.append(('one', 'two', 'three'))
+ self.assertTrue('one' in d.latex)
+
+
+ def test_latex_export_caption(self):
+ d = tablib.Dataset()
+ d.append(('one', 'two', 'three'))
+ self.assertFalse('caption' in d.latex)
+
+ d.title = 'Title'
+ self.assertTrue('\\caption{Title}' in d.latex)
+
+
+ def test_latex_export_none_values(self):
+ headers = ['foo', None, 'bar']
+ d = tablib.Dataset(['foo', None, 'bar'], headers=headers)
+ output = d.latex
+ self.assertTrue('foo' in output)
+ self.assertFalse('None' in output)
+
+
+ def test_latex_escaping(self):
+ d = tablib.Dataset(['~', '^'])
+ output = d.latex
+
+ self.assertFalse('~' in output)
+ self.assertTrue('textasciitilde' in output)
+ self.assertFalse('^' in output)
+ self.assertTrue('textasciicircum' in output)
+
+
def test_unicode_append(self):
"""Passes in a single unicode character and exports."""
@@ -338,6 +399,7 @@ class TablibTestCase(unittest.TestCase):
data.xlsx
data.ods
data.html
+ data.latex
def test_book_export_no_exceptions(self):