Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/tablib/formats/_xlsx.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import re
from io import BytesIO

from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
from openpyxl.reader.excel import ExcelReader, load_workbook
from openpyxl.styles import Alignment, Font
from openpyxl.utils import get_column_letter
Expand All @@ -23,11 +24,30 @@

INVALID_TITLE_REGEX = re.compile(r'[\\*?:/\[\]]')

# Lone surrogates (e.g. produced by ``surrogateescape`` error handling)
# cannot be encoded to valid XML/UTF-8 either, so they need stripping
# alongside the ASCII control characters openpyxl itself rejects.
SURROGATES_RE = re.compile(r'[\ud800-\udfff]')


def safe_xlsx_sheet_title(s, replace="-"):
return re.sub(INVALID_TITLE_REGEX, replace, s)[:31]


def _sanitize_value(value):
"""Strip characters that openpyxl/XML cannot represent in a cell.

openpyxl raises ``IllegalCharacterError`` (not a ``ValueError``) for
strings containing certain ASCII control characters, and silently
writes an unreadable file for lone surrogate characters. Sanitize
both cases up front so the value can be written safely.
"""
if isinstance(value, str):
value = ILLEGAL_CHARACTERS_RE.sub('', value)
value = SURROGATES_RE.sub('', value)
return value


class XLSXFormat:
title = 'xlsx'
extensions = ('xlsx',)
Expand Down Expand Up @@ -182,7 +202,7 @@ def dset_sheet(cls, dataset, ws, freeze_panes=True, escape=False):
cell.alignment = wrap_text

try:
cell.value = col
cell.value = _sanitize_value(col)
except ValueError:
cell.value = str(col)

Expand Down
10 changes: 10 additions & 0 deletions tests/test_tablib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1644,6 +1644,16 @@ def test_xlsx_raise_ValueError_on_cell_write_during_export(self):
wb = load_workbook(filename=BytesIO(_xlsx))
self.assertEqual('[1]', wb.active['A1'].value)

def test_xlsx_export_strips_illegal_characters(self):
"""Cell values containing XML-illegal ASCII control characters
must be sanitized rather than raising IllegalCharacterError.
See: https://github.com/jazzband/tablib/issues/370
"""
data.append((f'a{chr(31)}b',))
_xlsx = data.export('xlsx')
wb = load_workbook(filename=BytesIO(_xlsx))
self.assertEqual('ab', wb.active['A1'].value)

def test_xlsx_column_width_adaptive(self):
""" Test that column width adapts to value length"""
width_before, width_after = self._helper_export_column_width("adaptive")
Expand Down