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:
Correct order of arguments to a regex call in safe_xlsx_sheet_title
PR #490 had non-functional tests and thus an invalid order of arguments to re.sub was unnoticed.
This PR corrects the call, adds tests + applies same treatment to sheet names also when saving a databook.

Fixes #489
Related to #490

Patch:
diff --git a/AUTHORS b/AUTHORS
index 8e54fe9..a3d356d 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -21,6 +21,7 @@ Here is a list of passed and present much-appreciated contributors:
     Luke Lee
     Marc Abramowitz
     Marco Dallagiacoma
+    Maris Nartiss
     Mark Rogers
     Mark Walling
     Mathias Loesch
diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py
index 888b969..b262844 100644
--- a/src/tablib/formats/_xlsx.py
+++ b/src/tablib/formats/_xlsx.py
@@ -15,7 +15,7 @@ import tablib
 INVALID_TITLE_REGEX = re.compile(r'[\\*?:/\[\]]')
 
 def safe_xlsx_sheet_title(s, replace="-"):
-    return re.sub(INVALID_TITLE_REGEX, s, replace)[:31]
+    return re.sub(INVALID_TITLE_REGEX, replace, s)[:31]
 
 
 class XLSXFormat:
@@ -39,7 +39,7 @@ class XLSXFormat:
         """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
+        sheet name (http://www.excelcodex.com/2012/06/worksheets-naming-conventions/), they will
         be replaced with `invalid_char_subst`.
         """
         wb = Workbook()
@@ -57,15 +57,23 @@ class XLSXFormat:
         return stream.getvalue()
 
     @classmethod
-    def export_book(cls, databook, freeze_panes=True):
-        """Returns XLSX representation of DataBook."""
+    def export_book(cls, databook, freeze_panes=True, invalid_char_subst="-"):
+        """Returns XLSX representation of DataBook.
+
+        If dataset.title contains characters which are considered invalid for an XLSX file
+        sheet name (http://www.excelcodex.com/2012/06/worksheets-naming-conventions/), they will
+        be replaced with `invalid_char_subst`.
+        """
 
         wb = Workbook()
         for sheet in wb.worksheets:
             wb.remove(sheet)
         for i, dset in enumerate(databook._datasets):
             ws = wb.create_sheet()
-            ws.title = dset.title if dset.title else 'Sheet%s' % (i)
+            ws.title = (
+                safe_xlsx_sheet_title(dset.title, invalid_char_subst)
+                if dset.title else 'Sheet%s' % (i)
+            )
 
             cls.dset_sheet(dset, ws, freeze_panes=freeze_panes)
 


Test patch:
diff --git a/tests/test_tablib.py b/tests/test_tablib.py
index e9b10fa..0ee4e86 100755
--- a/tests/test_tablib.py
+++ b/tests/test_tablib.py
@@ -1045,8 +1045,22 @@ class XLSXTests(BaseTestCase):
         self.assertEqual(new_data.headers, ['id', 'name', 'description'])
 
     def test_xlsx_bad_chars_sheet_name(self):
-        data.title = "this / is / good"
-        _xlsx = data.xlsx
+        """
+        Sheet names are limited to 30 chars and the following chars
+        are not permitted: \\ / * ? : [ ]
+        """
+        _dataset = tablib.Dataset(
+            title='bad name \\/*?:[]qwertyuiopasdfghjklzxcvbnm'
+        )
+        _xlsx = _dataset.export('xlsx')
+        new_data = tablib.Dataset().load(_xlsx)
+        self.assertEqual(new_data.title, 'bad name -------qwertyuiopasdfg')
+
+        _book = tablib.Databook()
+        _book.add_sheet(_dataset)
+        _xlsx = _book.export('xlsx')
+        new_data = tablib.Databook().load(_xlsx, 'xlsx')
+        self.assertEqual(new_data.sheets()[0].title, 'bad name -------qwertyuiopasdfg')
 
     def test_xlsx_import_set_ragged(self):
         """Import XLSX file when not all rows have the same length."""