Repository: mne-tools/mne-python

Issue:
make_report example segfault
related to #5893.

This is what I get on my machine: 

``` shell
~/code/mne-python report_segfault*
(mne) ❯ ipython examples/visualization/make_report.py         
Fatal Python error: Segmentation fault

Thread 0x00007f6211648700 (most recent call first):
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/threading.py", line 295 in wait
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/threading.py", line 551 in wait
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/IPython/core/history.py", line 829 in run
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/IPython/core/history.py", line 58 in needs_sqlite
  File "<decorator-gen-24>", line 2 in run
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/threading.py", line 916 in _bootstrap_inner
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/threading.py", line 884 in _bootstrap

Current thread 0x00007f621490a740 (most recent call first):
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/tvtk/tvtk_base.py", line 595 in _wrap_call
  File "tvtk_classes/open_gl_render_window.py", line 79 in get_pixel_data
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/mayavi/tools/figure.py", line 348 in screenshot
  File "/home/sik/code/mne-python/mne/report.py", line 150 in _iterate_trans_views
  File "/home/sik/code/mne-python/mne/report.py", line 1965 in _render_trans
  File "/home/sik/code/mne-python/mne/report.py", line 298 in _iterate_files
  File "/home/sik/code/mne-python/mne/report.py", line 1470 in <genexpr>
  File "/home/sik/code/mne-python/mne/report.py", line 1470 in parse_folder
  File "/home/sik/code/mne-python/mne/utils/_logging.py", line 74 in verbose
  File "<string>", line 2 in parse_folder
  File "/home/sik/code/mne-python/examples/visualization/make_report.py", line 30 in <module>
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/IPython/utils/py3compat.py", line 188 in execfile
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2683 in safe_execfile
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/IPython/core/shellapp.py", line 323 in _exec_file
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/IPython/core/shellapp.py", line 391 in _run_cmd_line_code
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/IPython/core/shellapp.py", line 271 in init_code
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/IPython/terminal/ipapp.py", line 323 in initialize
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/traitlets/config/application.py", line 87 in catch_config_error
  File "<decorator-gen-112>", line 2 in initialize
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/traitlets/config/application.py", line 657 in launch_instance
  File "/home/sik/miniconda3/envs/mne/lib/python3.6/site-packages/IPython/__init__.py", line 125 in start_ipython
  File "/home/sik/miniconda3/envs/mne/bin/ipython", line 11 in <module>
zsh: segmentation fault (core dumped)  ipython examples/visualization/make_report.py

```

let's see what travis has to say


Discussion:
# [Codecov](https://codecov.io/gh/mne-tools/mne-python/pull/5894?src=pr&el=h1) Report
> Merging [#5894](https://codecov.io/gh/mne-tools/mne-python/pull/5894?src=pr&el=desc) into [master](https://codecov.io/gh/mne-tools/mne-python/commit/5093d0061500787c9c8a617d8ac7f4020afb460c?src=pr&el=desc) will **decrease** coverage by `2.48%`.
> The diff coverage is `11.11%`.


```diff
@@            Coverage Diff             @@
##           master    #5894      +/-   ##
==========================================
- Coverage   89.31%   86.82%   -2.49%     
==========================================
  Files         405      396       -9     
  Lines       74271    71878    -2393     
  Branches    12297    12071     -226     
==========================================
- Hits        66335    62411    -3924     
- Misses       5072     6586    +1514     
- Partials     2864     2881      +17
```



slightly different but same result here:

```shell
(mne) stefanappelhoff@arc-lin-004309:~/Desktop/bids/mne-python$ ipython examples/visualization/make_report.py 
Embedding : jquery-1.10.2.min.js
Embedding : jquery-ui.min.js
Embedding : bootstrap.min.js
Embedding : jquery-ui.min.css
Embedding : bootstrap.min.css
Iterating over 28 potential files (this may take some time)
Rendering : /home/stefanappelhoff/mne_data/MNE-sample-data/MEG/sample/all-trans.fif
Using outer_skin.surf for head surface.
Getting helmet for system 306m
Segmentation fault (core dumped)

```
There was a commit one month ago to Mayavi that might fix it:

https://github.com/enthought/mayavi/pull/771

Can you see if updating Mayavi fixes the problem?
I think I found a fix, PR coming hopefully soon

Resolving patch:
diff --git a/doc/conf.py b/doc/conf.py
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -316,19 +316,27 @@
 
 examples_dirs = ['../examples', '../tutorials']
 gallery_dirs = ['auto_examples', 'auto_tutorials']
-
+os.environ['_MNE_BUILDING_DOC'] = 'true'
 try:
     mlab = mne.utils._import_mlab()
     # Do not pop up any mayavi windows while running the
     # examples. These are very annoying since they steal the focus.
     mlab.options.offscreen = True
-    scrapers = ('matplotlib', 'mayavi')
 except Exception:
     scrapers = ('matplotlib',)
+    report_scraper = None
 else:
     # Let's do the same thing we do in tests: reraise traits exceptions
     from traits.api import push_exception_handler
     push_exception_handler(reraise_exceptions=True)
+    report_scraper = mne.report._ReportScraper()
+    scrapers = ('matplotlib', 'mayavi', report_scraper)
+
+
+def setup(app):
+    if report_scraper is not None:
+        report_scraper.app = app
+        app.connect('build-finished', report_scraper.copyfiles)
 
 
 class Resetter(object):
diff --git a/examples/visualization/make_report.py b/examples/visualization/plot_make_report.py
similarity index 82%
rename from examples/visualization/make_report.py
rename to examples/visualization/plot_make_report.py
--- a/examples/visualization/make_report.py
+++ b/examples/visualization/plot_make_report.py
@@ -26,8 +26,9 @@
 # Do standard folder parsing (this can take a couple of minutes):
 
 report = Report(image_format='png', subjects_dir=subjects_dir,
-                info_fname=evoked_fname, subject='sample', raw_psd=True)
-report.parse_folder(meg_path)
+                info_fname=evoked_fname, subject='sample',
+                raw_psd=False)  # use False for speed here
+report.parse_folder(meg_path, on_error='ignore', mri_decim=10)
 
 ###############################################################################
 # Add a custom section with an evoked slider:
@@ -44,7 +45,7 @@
                                     show=False))
     plt.close(figs[-1])
 report.add_slider_to_section(figs, times, 'Evoked Response',
-                             image_format='svg')
+                             image_format='png')  # can also use 'svg'
 
 # to save report
-# report.save('foobar.html', True)
+report.save('my_report.html', overwrite=True)
diff --git a/mne/report.py b/mne/report.py
--- a/mne/report.py
+++ b/mne/report.py
@@ -13,9 +13,11 @@
 import fnmatch
 import re
 import codecs
+from shutil import copyfile
 import time
 from glob import glob
 import warnings
+import webbrowser
 import numpy as np
 
 from . import read_evokeds, read_events, pick_types, read_cov
@@ -145,14 +147,17 @@ def _iterate_trans_views(function, **kwargs):
     """Auxiliary function to iterate over views in trans fig."""
     import matplotlib.pyplot as plt
     from mayavi import mlab, core
+    from pyface.api import GUI
     fig = function(**kwargs)
-
+    gui = GUI()
+    gui.process_events()
     assert isinstance(fig, core.scene.Scene)
 
     views = [(90, 90), (0, 90), (0, -90)]
     fig2, axes = plt.subplots(1, len(views))
     for view, ax in zip(views, axes):
         mlab.view(view[0], view[1])
+        gui.process_events()
         if fig.scene is not None:
             im = mlab.screenshot(figure=fig)
         else:  # Testing mode
@@ -1359,7 +1364,7 @@ def _render_one_axis(self, slices_iter, name, global_id, cmap,
     @verbose
     def _init_render(self, verbose=None):
         """Initialize the renderer."""
-        inc_fnames = ['jquery-1.10.2.min.js', 'jquery-ui.min.js',
+        inc_fnames = ['jquery.js', 'jquery-ui.min.js',
                       'bootstrap.min.js', 'jquery-ui.min.css',
                       'bootstrap.min.css']
 
@@ -1375,7 +1380,6 @@ def _init_render(self, verbose=None):
             elif inc_fname.endswith('.css'):
                 include.append(u'<style type="text/css">' +
                                file_content + u'</style>')
-
         self.include = ''.join(include)
 
     @verbose
@@ -1598,8 +1602,8 @@ def save(self, fname=None, open_browser=True, overwrite=False):
                     self.html.pop(0)
                     self.html.pop()
 
-        if open_browser and not is_hdf5:
-            import webbrowser
+        building_doc = os.getenv('_MNE_BUILDING_DOC', '').lower() == 'true'
+        if open_browser and not is_hdf5 and not building_doc:
             webbrowser.open_new_tab('file://' + fname)
 
         self.fname = fname
@@ -2058,3 +2062,64 @@ def _fix_global_ids(html):
         html = re.sub('id="###"', 'id="%s"' % global_id, html, count=1)
         global_id += 1
     return html
+
+
+###############################################################################
+# Scraper for sphinx-gallery
+
+_SCRAPER_TEXT = '''
+.. only:: builder_html
+
+    .. container:: row
+
+        .. rubric:: The `HTML document <{0}>`__ written by :meth:`mne.Report.save`:
+
+        .. raw:: html
+
+            <iframe class="sg_report" sandbox="allow-scripts" src="{0}"></iframe>
+
+'''  # noqa: E501
+# Adapted from fa-file-code
+_FA_FILE_CODE = '<svg class="sg_report" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="#dec" d="M149.9 349.1l-.2-.2-32.8-28.9 32.8-28.9c3.6-3.2 4-8.8.8-12.4l-.2-.2-17.4-18.6c-3.4-3.6-9-3.7-12.4-.4l-57.7 54.1c-3.7 3.5-3.7 9.4 0 12.8l57.7 54.1c1.6 1.5 3.8 2.4 6 2.4 2.4 0 4.8-1 6.4-2.8l17.4-18.6c3.3-3.5 3.1-9.1-.4-12.4zm220-251.2L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM256 51.9l76.1 76.1H256zM336 464H48V48h160v104c0 13.3 10.7 24 24 24h104zM209.6 214c-4.7-1.4-9.5 1.3-10.9 6L144 408.1c-1.4 4.7 1.3 9.6 6 10.9l24.4 7.1c4.7 1.4 9.6-1.4 10.9-6L240 231.9c1.4-4.7-1.3-9.6-6-10.9zm24.5 76.9l.2.2 32.8 28.9-32.8 28.9c-3.6 3.2-4 8.8-.8 12.4l.2.2 17.4 18.6c3.3 3.5 8.9 3.7 12.4.4l57.7-54.1c3.7-3.5 3.7-9.4 0-12.8l-57.7-54.1c-3.5-3.3-9.1-3.2-12.4.4l-17.4 18.6c-3.3 3.5-3.1 9.1.4 12.4z" class=""></path></svg>'  # noqa: E501
+
+
+class _ReportScraper(object):
+    """Scrape Report outputs.
+
+    Only works properly if conf.py is configured properly and the file
+    is written to the same directory as the example script.
+    """
+
+    def __init__(self):
+        self.app = None
+        self.files = dict()
+
+    def __repr__(self):
+        return '<ReportScraper>'
+
+    def __call__(self, block, block_vars, gallery_conf):
+        for report in block_vars['example_globals'].values():
+            if (isinstance(report, Report) and hasattr(report, 'fname') and
+                    report.fname.endswith('.html') and
+                    gallery_conf['builder_name'] == 'html'):
+                # Thumbnail
+                image_path_iterator = block_vars['image_path_iterator']
+                img_fname = next(image_path_iterator)
+                img_fname = img_fname.replace('.png', '.svg')
+                with open(img_fname, 'w') as fid:
+                    fid.write(_FA_FILE_CODE)
+                # copy HTML file
+                html_fname = op.basename(report.fname)
+                out_fname = op.join(
+                    self.app.builder.outdir,
+                    op.relpath(op.dirname(block_vars['target_file']),
+                               self.app.builder.srcdir), html_fname)
+                self.files[report.fname] = out_fname
+                # embed links/iframe
+                data = _SCRAPER_TEXT.format(html_fname)
+                return data
+        return ''
+
+    def copyfiles(self, *args, **kwargs):
+        for key, value in self.files.items():
+            copyfile(key, value)


Test patch:
diff --git a/mne/tests/test_report.py b/mne/tests/test_report.py
--- a/mne/tests/test_report.py
+++ b/mne/tests/test_report.py
@@ -18,8 +18,8 @@
 from mne import Epochs, read_events, read_evokeds
 from mne.io import read_raw_fif
 from mne.datasets import testing
-from mne.report import Report, open_report
-from mne.utils import (_TempDir, requires_mayavi, requires_nibabel,
+from mne.report import Report, open_report, _ReportScraper
+from mne.utils import (_TempDir, requires_mayavi, requires_nibabel, Bunch,
                        run_tests_if_main, traits_test, requires_h5py)
 from mne.viz import plot_alignment
 
@@ -403,4 +403,45 @@ def test_add_or_replace():
     assert r.html[3] == old_r.html[3]
 
 
+def test_scraper(tmpdir):
+    """Test report scraping."""
+    r = Report()
+    fig1, fig2 = _get_example_figures()
+    r.add_figs_to_section(fig1, 'a', 'mysection')
+    r.add_figs_to_section(fig2, 'b', 'mysection')
+    # Mock a Sphinx + sphinx_gallery config
+    app = Bunch(builder=Bunch(srcdir=str(tmpdir),
+                              outdir=op.join(str(tmpdir), '_build', 'html')))
+    scraper = _ReportScraper()
+    scraper.app = app
+    gallery_conf = dict(src_dir=app.builder.srcdir, builder_name='html')
+    img_fname = op.join(app.builder.srcdir, 'auto_examples', 'images',
+                        'sg_img.png')
+    target_file = op.join(app.builder.srcdir, 'auto_examples', 'sg.py')
+    os.makedirs(op.dirname(img_fname))
+    os.makedirs(app.builder.outdir)
+    block_vars = dict(image_path_iterator=(img for img in [img_fname]),
+                      example_globals=dict(a=1), target_file=target_file)
+    # Nothing yet
+    block = None
+    rst = scraper(block, block_vars, gallery_conf)
+    assert rst == ''
+    # Still nothing
+    block_vars['example_globals']['r'] = r
+    rst = scraper(block, block_vars, gallery_conf)
+    # Once it's saved, add it
+    assert rst == ''
+    fname = op.join(str(tmpdir), 'my_html.html')
+    r.save(fname, open_browser=False)
+    rst = scraper(block, block_vars, gallery_conf)
+    out_html = op.join(app.builder.outdir, 'auto_examples', 'my_html.html')
+    assert not op.isfile(out_html)
+    os.makedirs(op.join(app.builder.outdir, 'auto_examples'))
+    scraper.copyfiles()
+    assert op.isfile(out_html)
+    assert rst.count('"') == 6
+    assert "<iframe" in rst
+    assert op.isfile(img_fname.replace('png', 'svg'))
+
+
 run_tests_if_main()