Repository: mne-tools/mne-bids

Issue:
minor enh: allow print_dir_tree to return a str 
if print_dir_tree could return a string, then we could "plot" that string, which could then be used as a thumbnail for the examples gallery.

I think that'd be easy to do, would make the gallery more beautiful, and perhaps even come in handy when one wants to print the dir tree to a file.


Example:

```python
import matplotlib.pyplot as plt
fig, ax = plt.subplots()

# imagine this:
# s = print_dir_tree(".", max_depth=4, return_str=True)

s="""
|eegmmidb_bids_group_conversion/
|--- README
|--- dataset_description.json
|--- participants.json
|--- participants.tsv
|--- sub-001/
|------ ses-01/
|--------- sub-001_ses-01_scans.tsv
|--------- eeg/
|------------ sub-001_ses-01_task-MotorImagery_run-01_channels.tsv
|------------ sub-001_ses-01_task-MotorImagery_run-01_eeg.eeg
|------------ sub-001_ses-01_task-MotorImagery_run-01_eeg.json
|------------ sub-001_ses-01_task-MotorImagery_run-01_eeg.vhdr
|------------ sub-001_ses-01_task-MotorImagery_run-01_eeg.vmrk
|------------ sub-001_ses-01_task-MotorImagery_run-01_events.tsv
|------------ sub-001_ses-01_task-MotorImagery_run-02_channels.tsv
|------------ sub-001_ses-01_task-MotorImagery_run-02_eeg.eeg
|------------ sub-001_ses-01_task-MotorImagery_run-02_eeg.json
"""

ax.text(x=0, y=1, s=s)
ax.axis("off")
fig
```

![image](https://user-images.githubusercontent.com/9084751/96716400-45696a00-13a5-11eb-9e89-d305d1aa4e4a.png)



Discussion:
Generally I think we should have some figure / nice icon for each item in our gallery.

But how would we generate a plot like you propose without adding additional code to the examples?
> But how would we generate a plot like you propose without adding additional code to the examples?

without additional code, we'd have to generate images "behind the scenes" during the doc build process, and then link them in the sphinx-gallery config ... or add lots of assets (which I wouldn't like)

however, I think that my proposal with it's 4 added lines would work quite well already. WDYT?
I like it, it’s just that I’m a bit concerned it might confuse users to see this code. Maybe we can somehow mark it clearly as “housekeeping, nothing to see here, move along”?

-- 
Sent from my phone, please excuse brevity and erroneous auto-correct.

> On 21. Oct 2020, at 14:21, Stefan Appelhoff <notifications@github.com> wrote:
> 
> ﻿
> But how would we generate a plot like you propose without adding additional code to the examples?
> 
> without additional code, we'd have to generate images "behind the scenes" during the doc build process, and then link them in the sphinx-gallery config ... or add lots of assets (which I wouldn't like)
> 
> however, I think that my proposal with it's 4 added lines would work quite well already. WDYT?
> 
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub, or unsubscribe.

@sappelhoff It seems we might not need this after all. I believe this will do what we want:
https://sphinx-gallery.github.io/stable/configuration.html#providing-a-figure-for-the-thumbnail-image
Yes, this is also what I had in mind with this part of what I said:

> without additional code, we'd have to generate images "behind the scenes" during the doc build process, and then link them in the sphinx-gallery config ... or add lots of assets (which I wouldn't like)

I'd be fine with generating a few plots during the build process, and linking them as thumbnails. Including them as static files (assets) would be too much baggage IMHO.

Still, for *producing* some of these thumbnails, I think a thumbnail like this one would be nice: 

![image](https://user-images.githubusercontent.com/9084751/96767874-6a79cf00-13dd-11eb-9d25-c8a7ea17cee6.png)


it just looks BIDSy

Resolving patch:
diff --git a/mne_bids/path.py b/mne_bids/path.py
--- a/mne_bids/path.py
+++ b/mne_bids/path.py
@@ -5,6 +5,7 @@
 import glob
 import os
 import re
+from io import StringIO
 import shutil as sh
 from collections import OrderedDict
 from copy import deepcopy
@@ -780,10 +781,29 @@ def _check_non_sub_ses_entity(bids_path):
     return False
 
 
-def print_dir_tree(folder, max_depth=None):
-    """Recursively print dir tree starting from `folder` up to `max_depth`."""
+def print_dir_tree(folder, max_depth=None, return_str=False):
+    """Recursively print a directory tree.
+
+    Parameters
+    ----------
+    folder : str | pathlib.Path
+        The folder for which to print the directory tree.
+    max_depth : int
+        The maximum depth into which to descend recursively for printing
+        the directory tree.
+    return_str : bool
+        If ``True``, return the directory tree as a str instead of
+        printing it.
+
+    Returns
+    -------
+    str | None
+        If `return_str` is ``True``, the directory tree is returned as a
+        str. Else, ``None`` is returned and the directory tree is printed.
+    """
     if not op.exists(folder):
         raise ValueError('Directory does not exist: {}'.format(folder))
+
     msg = '`max_depth` must be a positive integer or None'
     if not isinstance(max_depth, (int, type(None))):
         raise ValueError(msg)
@@ -792,6 +812,12 @@ def print_dir_tree(folder, max_depth=None):
     if max_depth < 0:
         raise ValueError(msg)
 
+    if not isinstance(return_str, bool):
+        raise ValueError('`return_str` must be either True or False.')
+    outfile = None
+    if return_str is True:
+        outfile = StringIO()
+
     # Use max_depth same as the -L param in the unix `tree` command
     max_depth += 1
 
@@ -814,15 +840,20 @@ def print_dir_tree(folder, max_depth=None):
         # Only print if this is up to the depth we asked
         if branchlen <= max_depth:
             if branchlen <= 1:
-                print('|{}'.format(op.basename(root) + os.sep))
+                print('|{}'.format(op.basename(root) + os.sep), file=outfile)
             else:
                 print('|{} {}'.format((branchlen - 1) * '---',
-                                      op.basename(root) + os.sep))
+                                      op.basename(root) + os.sep),
+                      file=outfile)
 
             # Only print files if we are NOT yet up to max_depth or beyond
             if branchlen < max_depth:
                 for file in files:
-                    print('|{} {}'.format(branchlen * '---', file))
+                    print('|{} {}'.format(branchlen * '---', file),
+                          file=outfile)
+
+    if outfile is not None:
+        return outfile.getvalue()
 
 
 def _parse_ext(raw_fname, verbose=False):


Test patch:
diff --git a/mne_bids/tests/test_path.py b/mne_bids/tests/test_path.py
--- a/mne_bids/tests/test_path.py
+++ b/mne_bids/tests/test_path.py
@@ -156,6 +156,17 @@ def test_print_dir_tree(capsys):
     # test if pathlib.Path object
     print_dir_tree(Path(test_dir))
 
+    # test returning a string
+    out = print_dir_tree(test_dir, return_str=True, max_depth=1)
+    assert isinstance(out, str)
+    assert '|--- test_utils.py' in out.split('\n')
+    assert '|--- __pycache__{}'.format(os.sep) in out.split('\n')
+    assert '.pyc' not in out
+
+    # errors if return_str not a bool
+    with pytest.raises(ValueError, match='must be either True or False.'):
+        print_dir_tree(test_dir, return_str='yes')
+
 
 def test_make_folders():
     """Test that folders are created and named properly."""