From 9f4ce72e87f3824d7f3577dd408b7e3e210e41a2 Mon Sep 17 00:00:00 2001 From: kigland Date: Sat, 30 May 2026 02:08:51 +0800 Subject: [PATCH 1/3] Preserve falsy values (0, False) in jira and latex exports The jira and latex serializers used a truthiness test (`if item`) to decide whether a cell is empty, so 0, 0.0, and False were exported as blank cells and silently lost. Only None and empty strings should render as blank, which is what the html format already does (`if item is not None`). Check `item not in (None, '')` instead so real falsy values are kept. --- AUTHORS | 1 + src/tablib/formats/_jira.py | 2 +- src/tablib/formats/_latex.py | 2 +- tests/test_tablib.py | 9 +++++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index d645dfc9..01f47885 100644 --- a/AUTHORS +++ b/AUTHORS @@ -20,6 +20,7 @@ Here is a list of past and present much-appreciated contributors: Joel Friedly Josh Ourisman Kenneth Reitz + kigland Luca Beltrame Luke Lee Marc Abramowitz diff --git a/src/tablib/formats/_jira.py b/src/tablib/formats/_jira.py index 9bb62e79..f0338bc6 100644 --- a/src/tablib/formats/_jira.py +++ b/src/tablib/formats/_jira.py @@ -35,6 +35,6 @@ def _get_header(cls, headers): def _serialize_row(cls, row, delimiter='|'): return '{}{}{}'.format( delimiter, - delimiter.join([str(item) if item else ' ' for item in row]), + delimiter.join([str(item) if item not in (None, '') else ' ' for item in row]), delimiter ) diff --git a/src/tablib/formats/_latex.py b/src/tablib/formats/_latex.py index 67b5b004..45ced361 100644 --- a/src/tablib/formats/_latex.py +++ b/src/tablib/formats/_latex.py @@ -117,7 +117,7 @@ def _serialize_row(cls, row): :param row: single dataset row """ - new_row = [cls._escape_tex_reserved_symbols(str(item)) if item else '' + new_row = [cls._escape_tex_reserved_symbols(str(item)) if item not in (None, '') else '' for item in row] return 6 * ' ' + ' & '.join(new_row) + ' \\\\' diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 2bc397ba..46d8b926 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -1772,6 +1772,11 @@ def test_latex_export_none_values(self): self.assertIn('foo', output) self.assertNotIn('None', output) + def test_latex_export_falsy_values(self): + # 0 and False are real values and must be rendered, not dropped. + output = tablib.Dataset([0, False, 90]).latex + self.assertIn('0 & False & 90', output) + def test_latex_escaping(self): d = tablib.Dataset(['~', '^']) output = d.latex @@ -1902,6 +1907,10 @@ def test_jira_export_no_headers(self): def test_jira_export_none_and_empty_values(self): self.assertEqual('| | |c|', tablib.Dataset(['', None, 'c']).jira) + def test_jira_export_falsy_values(self): + # 0 and False are real values, not empty cells. + self.assertEqual('|0|False|c|', tablib.Dataset([0, False, 'c']).jira) + def test_jira_export_empty_dataset(self): self.assertIsNotNone(tablib.Dataset().jira) From 2db3ddc0aedbea6d319ed900b44dfea87d42c8cb Mon Sep 17 00:00:00 2001 From: kigland Date: Wed, 8 Jul 2026 18:50:14 +0800 Subject: [PATCH 2/3] Narrow empty cell handling in jira and latex exports --- src/tablib/formats/_jira.py | 12 +++++++++++- src/tablib/formats/_latex.py | 13 ++++++++++++- tests/test_tablib.py | 11 +++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/tablib/formats/_jira.py b/src/tablib/formats/_jira.py index f0338bc6..71c23a1f 100644 --- a/src/tablib/formats/_jira.py +++ b/src/tablib/formats/_jira.py @@ -35,6 +35,16 @@ def _get_header(cls, headers): def _serialize_row(cls, row, delimiter='|'): return '{}{}{}'.format( delimiter, - delimiter.join([str(item) if item not in (None, '') else ' ' for item in row]), + delimiter.join([' ' if cls._is_empty_value(item) else str(item) for item in row]), delimiter ) + + @classmethod + def _is_empty_value(cls, item): + if item is None: + return True + if isinstance(item, (str, bytes, bytearray)): + return len(item) == 0 + if isinstance(item, (list, tuple, dict, set, frozenset)): + return len(item) == 0 + return False diff --git a/src/tablib/formats/_latex.py b/src/tablib/formats/_latex.py index 45ced361..9339238a 100644 --- a/src/tablib/formats/_latex.py +++ b/src/tablib/formats/_latex.py @@ -117,10 +117,21 @@ def _serialize_row(cls, row): :param row: single dataset row """ - new_row = [cls._escape_tex_reserved_symbols(str(item)) if item not in (None, '') else '' + new_row = ['' if cls._is_empty_value(item) + else cls._escape_tex_reserved_symbols(str(item)) for item in row] return 6 * ' ' + ' & '.join(new_row) + ' \\\\' + @classmethod + def _is_empty_value(cls, item): + if item is None: + return True + if isinstance(item, (str, bytes, bytearray)): + return len(item) == 0 + if isinstance(item, (list, tuple, dict, set, frozenset)): + return len(item) == 0 + return False + @classmethod def _escape_tex_reserved_symbols(cls, string): """Escapes all TeX reserved symbols ('_', '~', etc.) in a string. diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 46d8b926..5a9cb6c4 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -1777,6 +1777,13 @@ def test_latex_export_falsy_values(self): output = tablib.Dataset([0, False, 90]).latex self.assertIn('0 & False & 90', output) + def test_latex_export_empty_containers_and_bytes(self): + output = tablib.Dataset([[], {}, b'']).latex + self.assertIn(' & & ', output) + self.assertNotIn('[]', output) + self.assertNotIn('{}', output) + self.assertNotIn("b''", output) + def test_latex_escaping(self): d = tablib.Dataset(['~', '^']) output = d.latex @@ -1911,6 +1918,10 @@ def test_jira_export_falsy_values(self): # 0 and False are real values, not empty cells. self.assertEqual('|0|False|c|', tablib.Dataset([0, False, 'c']).jira) + def test_jira_export_empty_containers_and_bytes(self): + self.assertEqual('| | |c|', tablib.Dataset([[], {}, 'c']).jira) + self.assertEqual('| |c|', tablib.Dataset([b'', 'c']).jira) + def test_jira_export_empty_dataset(self): self.assertIsNotNone(tablib.Dataset().jira) From ed17557b5f49fe11dd55de02cb36aa9cc4a4120e Mon Sep 17 00:00:00 2001 From: kigland Date: Wed, 8 Jul 2026 21:18:55 +0800 Subject: [PATCH 3/3] Deduplicate empty export cell handling --- src/tablib/formats/_jira.py | 13 ++----------- src/tablib/formats/_latex.py | 14 +++----------- src/tablib/utils.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/tablib/formats/_jira.py b/src/tablib/formats/_jira.py index 71c23a1f..36fb6773 100644 --- a/src/tablib/formats/_jira.py +++ b/src/tablib/formats/_jira.py @@ -2,6 +2,7 @@ Generates a Jira table from the dataset. """ +from tablib.utils import is_empty_cell class JIRAFormat: @@ -35,16 +36,6 @@ def _get_header(cls, headers): def _serialize_row(cls, row, delimiter='|'): return '{}{}{}'.format( delimiter, - delimiter.join([' ' if cls._is_empty_value(item) else str(item) for item in row]), + delimiter.join([' ' if is_empty_cell(item) else str(item) for item in row]), delimiter ) - - @classmethod - def _is_empty_value(cls, item): - if item is None: - return True - if isinstance(item, (str, bytes, bytearray)): - return len(item) == 0 - if isinstance(item, (list, tuple, dict, set, frozenset)): - return len(item) == 0 - return False diff --git a/src/tablib/formats/_latex.py b/src/tablib/formats/_latex.py index 9339238a..c57fb12d 100644 --- a/src/tablib/formats/_latex.py +++ b/src/tablib/formats/_latex.py @@ -4,6 +4,8 @@ """ import re +from tablib.utils import is_empty_cell + class LATEXFormat: title = 'latex' @@ -117,21 +119,11 @@ def _serialize_row(cls, row): :param row: single dataset row """ - new_row = ['' if cls._is_empty_value(item) + new_row = ['' if is_empty_cell(item) else cls._escape_tex_reserved_symbols(str(item)) for item in row] return 6 * ' ' + ' & '.join(new_row) + ' \\\\' - @classmethod - def _is_empty_value(cls, item): - if item is None: - return True - if isinstance(item, (str, bytes, bytearray)): - return len(item) == 0 - if isinstance(item, (list, tuple, dict, set, frozenset)): - return len(item) == 0 - return False - @classmethod def _escape_tex_reserved_symbols(cls, string): """Escapes all TeX reserved symbols ('_', '~', etc.) in a string. diff --git a/src/tablib/utils.py b/src/tablib/utils.py index 97bae72e..2084723b 100644 --- a/src/tablib/utils.py +++ b/src/tablib/utils.py @@ -1,6 +1,16 @@ from io import BytesIO, StringIO +def is_empty_cell(value): + return ( + value is None + or ( + isinstance(value, (str, bytes, bytearray, list, tuple, dict, set, frozenset)) + and len(value) == 0 + ) + ) + + def normalize_input(stream): """ Accept either a str/bytes stream or a file-like object and always return a