Repository: mne-tools/mne-python

Issue:
report.add_figure bad tags error not specific enough
#### Describe the bug
The report class complains about a bad tag but does not say what characters are bad. Furthermore, I think it should allow numeric characters. I get:

```py
ValueError: The following tags contained invalid characters: 'Subject 2002'
```

#### Steps to reproduce
Just do:

```py
import matplotlib.pyplot as plt
from mne.report import Report

rep = Report()
subject = '2002'
fig = plt.figure()
rep.add_figure(fig, title=f'Figure {i}', tags=(f'Subject {subject}',))
```

#### Expected results
Report is produced with tag "subject 2002"

#### Actual results
Numeric subjects are quite common and I'd imagine a common usecase would be to use numeric tags. I think they should work ... did work previously in `rep.add_figs_to_section`, so I'd consider this a regression.

#### Additional information
I'm on latest master branch



Discussion:
Also the docstring talks about "collection of str". I thought "collection" was a matplotlib object? It actually crosslinks to matplotlib. Is that what we mean? Shouldn't it be `tuple | list` or something like that?
Another issue is that the `rep.add_figure` closes the figure. I don't think this should happen as user may still like to interact with the matplotlib figure while having it in a report for easier digestion.
can you give a try to the fix?

> Message ID: ***@***.***>
>

> Also the docstring talks about "collection of str". I thought "collection" was a matplotlib object? It actually crosslinks to matplotlib. Is that what we mean?

We mean a Python [collection](https://docs.python.org/3/library/collections.html) / container type, i.e., `list`, `set`, `tuple`, `array`. In the dev docstring, this has already been changed to `array-like`: https://mne.tools/dev/generated/mne.Report.html

However we should adjust the error message as well.

We don't allow for [certain characters](https://github.com/mne-tools/mne-python/blob/fecf5411d28f47dcb4ff379976a1b4813edc45a9/mne/report/report.py#L304) in tags, including spaces.

[`_fig_to_img()`](https://github.com/mne-tools/mne-python/blob/fecf5411d28f47dcb4ff379976a1b4813edc45a9/mne/report/report.py#L344) has an `auto_close` parameter (defaults to `True`). We could expose this via the `add_figure()` API. WDYT?
no strong feelings about exposing `auto_close`. I think if MNE-report creates a figure, it should auto-close, but if user created the figure (as in `add_figure`), it should not close, by default. 
@jasmainak I think this makes sense, yes!

Resolving patch:
diff --git a/mne/report/report.py b/mne/report/report.py
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -265,19 +265,24 @@ class _ContentElement:
 
 def _check_tags(tags) -> Tuple[str]:
     # Must be iterable, but not a string
-    if (isinstance(tags, str) or not isinstance(tags, (Sequence, np.ndarray))):
+    if isinstance(tags, str):
+        tags = (tags,)
+    elif isinstance(tags, (Sequence, np.ndarray)):
+        tags = tuple(tags)
+    else:
         raise TypeError(
-            f'tags must be a collection of str, but got {type(tags)} '
+            f'tags must be a string (without spaces or special characters) or '
+            f'an array-like object of such strings, but got {type(tags)} '
             f'instead: {tags}'
         )
-    tags = tuple(tags)
 
     # Check for invalid dtypes
     bad_tags = [tag for tag in tags
                 if not isinstance(tag, str)]
     if bad_tags:
         raise TypeError(
-            f'tags must be strings, but got the following instead: '
+            f'All tags must be strings without spaces or special characters, '
+            f'but got the following instead: '
             f'{", ".join([str(tag) for tag in bad_tags])}'
         )
 
@@ -1747,7 +1752,7 @@ def remove(self, *, title=None, tags=None, remove_all=False):
             The title of the element(s) to remove.
 
             .. versionadded:: 0.24.0
-        tags : array-like of str | None
+        tags : array-like of str | str | None
              If supplied, restrict the operation to elements with the supplied
              tags.
 
diff --git a/mne/utils/docs.py b/mne/utils/docs.py
--- a/mne/utils/docs.py
+++ b/mne/utils/docs.py
@@ -2378,8 +2378,8 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
     instantiation.
 """
 docdict['report_tags'] = """
-tags : array-like of str
-    Tags to add for later interactive filtering.
+tags : array-like of str | str
+    Tags to add for later interactive filtering. Must not contain spaces.
 """
 docdict['report_replace'] = """
 replace : bool


Test patch:
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
--- a/mne/report/tests/test_report.py
+++ b/mne/report/tests/test_report.py
@@ -884,33 +884,32 @@ def test_sorting(tmp_path):
 
 
 @pytest.mark.parametrize(
-    ('tags', 'not_a_collection', 'wrong_dtype', 'invalid_chars'),
+    ('tags', 'str_or_array', 'wrong_dtype', 'invalid_chars'),
     [
-        # not a collection
-        ('foo', True, False, False),
-        (123, True, False, False),
         # wrong dtype
-        ([1, 2, 3], False, True, False),
-        (['foo', 1], False, True, False),
+        (123, False, True, False),
+        ([1, 2, 3], True, True, False),
+        (['foo', 1], True, True, False),
         # invalid characters
-        (['foo bar'], False, False, True),
-        (['foo"'], False, False, True),
-        (['foo\n'], False, False, True),
+        (['foo bar'], True, False, True),
+        (['foo"'], True, False, True),
+        (['foo\n'], True, False, True),
         # all good
-        (['foo'], False, False, False),
-        (['foo', 'bar'], False, False, False),
-        (np.array(['foo', 'bar']), False, False, False)
+        ('foo', True, False, False),
+        (['foo'], True, False, False),
+        (['foo', 'bar'], True, False, False),
+        (np.array(['foo', 'bar']), True, False, False)
     ]
 )
-def test_tags(tags, not_a_collection, wrong_dtype, invalid_chars):
+def test_tags(tags, str_or_array, wrong_dtype, invalid_chars):
     """Test handling of invalid tags."""
     r = Report()
 
-    if not_a_collection:
-        with pytest.raises(TypeError, match='must be a collection of str'):
+    if not str_or_array:
+        with pytest.raises(TypeError, match='must be a string.*or an array.*'):
             r.add_code(code='foo', title='bar', tags=tags)
     elif wrong_dtype:
-        with pytest.raises(TypeError, match='must be strings'):
+        with pytest.raises(TypeError, match='tags must be strings'):
             r.add_code(code='foo', title='bar', tags=tags)
     elif invalid_chars:
         with pytest.raises(ValueError, match='contained invalid characters'):