Repository: jazzband/tablib (python)

Issue:
"Invalid character / found in sheet title" during export to XLSX
Hi there, 

it looks like tablib could automatically replace some characters when exporting to XLS, what do you think? The problem occurs when I export a sheet with a slash inside its title:

```
  File "/Users/mpasternak/.virtualenvs/bpp/lib/python3.8/site-packages/tablib/formats/_xlsx.py", line 53, in export_book
    ws.title = dset.title if dset.title else 'Sheet%s' % (i)
  File "/Users/mpasternak/.virtualenvs/bpp/lib/python3.8/site-packages/openpyxl/workbook/child.py", line 93, in title
    raise ValueError(msg)
ValueError: Invalid character / found in sheet title
```

Devs, please let me know if this is desired and I'll think about a patch.

Pull request description:
Fix "Invalid character / found in sheet title" during export to XLSX
Fixes #489.

Make sure there are no bad characters in sheet names!

Patch:
diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py
index 34911e9..a6cb158 100644
--- a/src/tablib/formats/_xlsx.py
+++ b/src/tablib/formats/_xlsx.py
@@ -1,6 +1,7 @@
 """ Tablib - XLSX Support.
 """
 
+import re
 from io import BytesIO
 
 from openpyxl.reader.excel import ExcelReader, load_workbook
@@ -11,6 +12,11 @@ from openpyxl.writer.excel import ExcelWriter
 
 import tablib
 
+INVALID_TITLE_REGEX = re.compile(r'[\\*?:/\[\]]')
+
+def safe_xlsx_sheet_title(s, replace="-"):
+    return re.sub(INVALID_TITLE_REGEX, s, replace)[:31]
+
 
 class XLSXFormat:
     title = 'xlsx'
@@ -29,11 +35,20 @@ class XLSXFormat:
             return False
 
     @classmethod
-    def export_set(cls, dataset, freeze_panes=True):
-        """Returns XLSX representation of Dataset."""
+    def export_set(cls, dataset, freeze_panes=True, invalid_char_subst="-"):
+        """Returns XLSX representation of Dataset.
+
+        If dataset.title contains characters which are considered invalid for an XLSX file
+        sheet name (http://www.excelcodex.com/2012/06/worksheets-naming-conventions/), it will
+        be replaced with `invalid_char_subst`.
+        """
         wb = Workbook()
         ws = wb.worksheets[0]
-        ws.title = dataset.title if dataset.title else 'Tablib Dataset'
+
+        ws.title = (
+            safe_xlsx_sheet_title(dataset.title, invalid_char_subst)
+            if dataset.title else 'Tablib Dataset'
+        )
 
         cls.dset_sheet(dataset, ws, freeze_panes=freeze_panes)
 


Test patch:
diff --git a/tests/test_tablib.py b/tests/test_tablib.py
index 96a7386..e89eb0c 100755
--- a/tests/test_tablib.py
+++ b/tests/test_tablib.py
@@ -1018,6 +1018,10 @@ class XLSXTests(BaseTestCase):
         self.assertEqual(data.dict[0]['float'], 21.55)
         self.assertEqual(data.dict[0]['date/time'], date_time)
 
+    def test_xlsx_bad_chars_sheet_name(self):
+        data.title = "this / is / good"
+        _xlsx = data.xlsx
+
     def test_xlsx_import_set_ragged(self):
         """Import XLSX file when not all rows have the same length."""
         xlsx_source = Path(__file__).parent / 'files' / 'ragged.xlsx'