Repository: sphinx-gallery/sphinx-gallery

Issue:
Improve binder button instructions
I just gave a shot at using the binder instructions and had a few thoughts for improvements:

1. It's not clear what some of the configuration options are used for (e.g. username/org/branch are used *only* to create the binder links)
2. Specifically, the 'dependencies' option was a bit confusing especially if you're *already* using a `binder/` folder.
3. We should update to use the new binder badge (![](http://mybinder.org/badge_logo.svg))
4. List of paths to dependencies isn't clear that it's relative to doc (maybe it should be relative to root). Maybe this should just be "optional" in case people already set up their repo w/ the necessary dependencies.
5. There's a bug if `gallery_dirs` is a string and not a list (it iterates through the string, not the list)
6. Unclear what the "prefix" part of the config does, need to make sure there isn't a bug + clarify in docs
7. notebooks_dir is unclear if "site" refers to the built HTML site (which it does) vs. the root of the repository
8. A lot of the assumptions above are that you're not building your site with something like readthedocs, but instead are posting the whole HTML on a branch (using something like gh-pages). Need to see if there's a way to make this compatible with readthedocs

keeping this list here for future ref!

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/439?src=pr&el=h1) Report
> Merging [#439](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/439?src=pr&el=desc) into [master](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/commit/29d317b1674a08fc36a4e2ad35e183cdce8f1e39?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/439/graphs/tree.svg?width=650&token=K4eytvhwU5&height=150&src=pr)](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/439?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master     #439   +/-   ##
=======================================
  Coverage   95.52%   95.52%           
=======================================
  Files          29       29           
  Lines        2279     2279           
=======================================
  Hits         2177     2177           
  Misses        102      102
```


| [Impacted Files](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/439?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sphinx\_gallery/binder.py](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/439/diff?src=pr&el=tree#diff-c3BoaW54X2dhbGxlcnkvYmluZGVyLnB5) | `95.95% <ø> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/439?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/439?src=pr&el=footer). Last update [29d317b...aa56c08](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/439?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

choldgraf: here's the updated instructions: https://665-25860190-gh.circle-artifacts.com/0/rtd_html/configuration.html#binder-links
choldgraf: see latest push - how does that look?
choldgraf: thx for the push, I'll try to get to the other comments soon when I have a moment
choldgraf: ok I think your comments are addressed @larsoner 
GaelVaroquaux: I'm overall +1 for merge, aside from the fact that I am uneasy that it is not clear to my that backward compatibility is maintained.

If it's not, we need at least to state it clearly in the changelog.

Patch:
diff --git a/.gitignore b/.gitignore
index 1536321..81c1b2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,3 +71,6 @@ target/
 
 # VisualStudioCode
 .history
+
+# Jupyter notebooks
+.ipynb_checkpoints
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index ace7a37..25cbcc7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,12 +1,10 @@
 language: python
+
+dist: xenial
 sudo: false
-addons:
-  apt:
-    packages:
-    - xvfb # For mayavi headless
 
-virtualenv:
-        system_site_packages: true
+services:
+  - xvfb # For mayavi headless
 
 matrix:
   include:
@@ -36,9 +34,22 @@ matrix:
       env: DISTRIB="conda" PYTHON_VERSION="2.7" LOCALE=C
     - os: linux
       env: DISTRIB="conda" PYTHON_VERSION="3.6" LOCALE=C
+    - os: linux
+      env: DISTRIB="conda" PYTHON_VERSION="3.7" LOCALE=C
     - os: linux
       env: DISTRIB="conda" PYTHON_VERSION="3.6" SPHINX_VERSION="dev"
       if: type = cron
+    - python: "nightly"
+      env: PYTHON_VERSION="nightly"
+      os: linux
+      addons:
+        apt:
+          packages:
+            libblas-dev
+            liblapack-dev
+            gfortran
+  allow_failures:
+    - python: "nightly"
 
 before_install:
   # Make sure that things work even if the locale is set to C (which
@@ -54,16 +65,8 @@ before_install:
 install:
   - source continuous_integration/travis/install.sh
 
-# To test the mayavi environment follow the instructions at
-# https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI
-before_script:
-  - "export DISPLAY=:99.0"
-  - "sh -e /etc/init.d/xvfb start"
-  - sleep 3 # give xvfb some time to start
-
 script:
   - bash continuous_integration/travis/test_script.sh
 
 after_success:
-  - pip install codecov
-  - codecov
+  - bash continuous_integration/travis/after_success.sh 
diff --git a/CHANGES.rst b/CHANGES.rst
index 1f864ab..348fc08 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -18,6 +18,10 @@ Developer changes
   document which includes the prose, code & output of the example which is
   the directly saved to file including the annotations of binder badges,
   download buttons and timing statistics.
+* Binder link config changes. The configuration value for the BinderHub has
+  been changed from ``url`` to ``binderhub_url`` in order to make it more
+  explicit. The old configuration key (``url``) will be deprecated in
+  version v0.4.0)
 
 
 v0.2.0
diff --git a/continuous_integration/travis/after_success.sh b/continuous_integration/travis/after_success.sh
new file mode 100644
index 0000000..6481548
--- /dev/null
+++ b/continuous_integration/travis/after_success.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+# This script is meant to be called by the "after_success" step defined in
+# .travis.yml. See https://docs.travis-ci.com/ for more details.
+
+# License: 3-clause BSD
+
+if [[ "$PYTHON_VERSION" != "nightly" ]]; then
+    pip install codecov
+    codecov
+fi
diff --git a/continuous_integration/travis/install.sh b/continuous_integration/travis/install.sh
index 2426d47..ea87ba4 100755
--- a/continuous_integration/travis/install.sh
+++ b/continuous_integration/travis/install.sh
@@ -32,6 +32,12 @@ if [ "$DISTRIB" == "conda" ]; then
     else
         pip install "https://api.github.com/repos/sphinx-doc/sphinx/zipball/master"
     fi
+elif [ "$PYTHON_VERSION" == "nightly" ]; then
+    # Python nightly requires to use the virtual env provided by travis.
+    pip install numpy scipy
+    pip install -r requirements.txt | cat
+    pip install seaborn sphinx==1.5.5 pytest "six>=1.10.0" pytest-cov sphinx_rtd_theme
+
 elif [ "$DISTRIB" == "ubuntu" ]; then
     # Use a separate virtual environment than the one provided by
     # Travis because it contains numpy and we want to use numpy
@@ -39,6 +45,7 @@ elif [ "$DISTRIB" == "ubuntu" ]; then
     deactivate
     virtualenv --system-site-packages testvenv
     source testvenv/bin/activate
+    pip install --upgrade pip setuptools wheel pyopenssl
     pip install -U requests[security]  # ensure SSL certificate works
     pip install "tornado<5"
     # The pipe just gets rid of the progress bars
diff --git a/doc/advanced.rst b/doc/advanced.rst
index ad0102e..db10e86 100644
--- a/doc/advanced.rst
+++ b/doc/advanced.rst
@@ -59,3 +59,173 @@ its reference
 ``:ref:`sphx_glr_auto_examples_plot_gallery_version.py```. The image
 it generated has the name ``sphx_glr_plot_gallery_version_thumb.png``
 and its thumbnail is ``sphx_glr_plot_gallery_version_thumb.png``
+
+.. _custom_scraper:
+
+Writing a custom image scraper
+==============================
+
+.. warning:: The API for custom scrapers is currently experimental.
+
+By default, Sphinx-gallery supports image scrapers for Matplotlib
+(:func:`sphinx_gallery.scrapers.matplotlib_scraper`) and Mayavi
+(:func:`sphinx_gallery.scrapers.mayavi_scraper`). You can also write a custom
+scraper for other python packages. This section describes how to do so.
+
+Image scrapers are functions (or callable class instances) that do two things:
+
+1. Collect a list of images created in the latest execution of code.
+2. Write these images to disk
+3. Return rST that embeds these figures in the built documentation.
+
+The function should take the following inputs (in this order): ``block``,
+``block_vars``, and ``gallery_conf``. It should return a string containing the
+rST for embedding this figure in the documentation.
+See the :func:`sphinx_gallery.scrapers.matplotlib_scraper` for
+a description of the inputs/outputs.
+
+This function will be called once for each code block of your examples.
+
+Example one - a Matplotlib and Mayavi-style scraper
+---------------------------------------------------
+
+For example, we will show sample code for a scraper for a hypothetical package.
+It uses an approach similar to what :func:`sphinx_gallery.scrapers.matplotlib_scraper`
+and :func:`sphinx_gallery.scrapers.mayavi_scraper` do under the hood, which
+use the helper function :func:`sphinx_gallery.scrapers.figure_rst` to
+create the standardized rST. If your package will be used to write an image file
+to disk (e.g., PNG or JPEG), we recommend you use a similar approach.
+
+.. code-block:: python
+
+   def my_module_scraper(block, block_vars, gallery_conf)
+       import mymodule
+       # We use a list to collect references to image names
+       image_names = list()
+       # The `image_path_iterator` is created by Sphinx-gallery, it will yield
+       # a path to a file name that adheres to Sphinx-gallery naming convention.
+       image_path_iterator = block_vars['image_path_iterator']
+
+       # Define a list of our already-created figure objects.
+       list_of_my_figures = mymodule.get_figures()
+
+       # Iterate through figure objects, save to disk, and keep track of paths.
+       for fig, image_path in zip(list_of_my_figures, image_path_iterator):
+           fig.save_png(image_path)
+           image_names.append(image_path)
+
+       # Close all references to figures so they aren't used later.
+       mymodule.close('all')
+
+       # Use the `figure_rst` helper function to generate the rST for this
+       # code block's figures. Alternatively you can define your own rST.
+       return figure_rst(image_names, gallery_conf['src_dir'])
+
+This code would be defined either in your ``conf.py`` file, or as a module that
+you import into your ``conf.py`` file. The configuration needed to use this
+scraper would look like::
+
+    sphinx_gallery_conf = {
+        ...
+        'image_scrapers': ('matplotlib', my_module_scraper),
+    }
+
+Example two - detecting image files on disk
+-------------------------------------------
+
+Here's another example that assumes that images have *already been written to
+disk*. In this case we won't *generate* any image files, we'll only generate
+the rST needed to embed them in the documentation.
+
+We'll use a callable class in this case, and assume it is defined within your
+package in a module called ``scraper``. Here is the scraper code:
+
+.. code-block:: python
+
+   from glob import glob
+   import shutil
+   import os
+   from sphinx_gallery.gen_rst import figure_rst
+
+   class PNGScraper(object):
+       def __init__(self):
+           self.seen = set()
+
+       def __call__(self, block, block_vars, gallery_conf):
+           # Find all PNG files in the directory of this example.
+           path_current_example = os.path.dirname(block_vars['src_file'])
+           pngs = sorted(glob(os.path.join(os.getcwd(), '*.png'))
+
+           # Iterate through PNGs, copy them to the sphinx-gallery output directory
+           image_names = list()
+           image_path_iterator = block_vars['image_path_iterator']
+           for png in pngs:
+               if png not in seen:
+                   seen |= set(png)
+                   this_image_path = image_path_iterator.next()
+                   image_names.append(this_image_path)
+                   shutil.move(png, this_image_path)
+           # Use the `figure_rst` helper function to generate rST for image files
+           return figure_rst(image_names, gallery_conf['src_dir'])
+
+
+Then, in our ``conf.py`` file, we include the following code:
+
+.. code-block:: python
+
+   from mymodule import PNGScraper
+   my_scraper_instance = PNGScraper()
+
+   sphinx_gallery_conf = {
+       ...
+       'image_scrapers': ('matplotlib', my_scraper_instance),
+   }
+
+Contributing scrapers back to Sphinx-gallery
+--------------------------------------------
+
+If you've developed a custom scraper for Sphinx-gallery that would be useful
+to the broader community, we encourage you to contribute it to the list of
+natively-supported scrapers located in
+`the scrapers module <https://github.com/sphinx-gallery/sphinx-gallery/blob/master/sphinx_gallery/scrapers.py>`_.
+We welcome PRs!
+
+.. _custom_reset:
+
+Defining resetting behavior for custom visualization libraries
+--------------------------------------------------------------
+
+Sphinx-gallery natively supports resetting ``matplotlib`` and ``seaborn``.
+However, if you'd like to support resetting for other libraries (or would like
+to modify the resetting behavior for a natively-supported library), you can
+add a custom function to the resetting tuple defined in ``conf.py``.
+
+The function takes two variables: a dictionary called ``gallery_conf`` (which is
+your Sphinx-gallery configuration) and a string called ``fname`` (which is the
+file name of the currently-executed Python script). These generally don't need
+to be used in order to perform whatever resetting behavior you want, but must
+be included in the function definition for compatibility reasons.
+
+For example, to reset matplotlib to always use the ``ggplot`` style, you could do:
+
+
+.. code-block:: python
+
+   def reset_mpl(gallery_conf, fname):
+       from matplotlib import style
+       style.use('ggplot')
+
+Any custom functions can be defined (or imported) in ``conf.py`` and given to
+the ``reset_modules`` configuration key. For the function defined above:
+
+.. code-block:: python
+
+   sphinx_gallery_conf = {
+       ...
+       'reset_modules': (reset_mpl, 'seaborn'),
+   }
+
+.. note:: Using resetters such as ``reset_mpl`` that deviate from the
+          standard behavior that users will experience when manually running
+          examples themselves is discouraged due to the inconsistency
+          that results between the rendered examples and local outputs.
diff --git a/doc/conf.py b/doc/conf.py
index 7a2f18a..ad9e0c4 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -350,8 +350,8 @@ sphinx_gallery_conf = {
     'min_reported_time': min_reported_time,
     'binder': {'org': 'sphinx-gallery',
                'repo': 'sphinx-gallery.github.io',
-               'url': 'https://mybinder.org',
                'branch': 'master',
+               'binderhub_url': 'https://mybinder.org',
                'dependencies': './binder/requirements.txt',
                'notebooks_dir': 'notebooks',
                'use_jupyter_lab': True,
diff --git a/doc/configuration.rst b/doc/configuration.rst
index ec299bc..790e441 100644
--- a/doc/configuration.rst
+++ b/doc/configuration.rst
@@ -428,6 +428,8 @@ Which is achieved by the following configuration::
                           "# It can be customized to whatever you like\n"
                           "%matplotlib inline")
 
+If the value of ``first_notebook_cell`` is set to ``None``, then no extra first
+cell will be added to the notebook.
 
 .. _disable_all_scripts_download:
 
@@ -493,36 +495,60 @@ dictionary following the pattern below::
          # Required keys
          'org': '<github_org>',
          'repo': '<github_repo>',
-         'url': '<binder_url>',  # Any URL of a binder server. Must be full URL (e.g. https://mybinder.org).
-         'branch': '<branch-for-documentation>',  # Can be any branch, tag, or commit hash. Use a branch that hosts your docs.
+         'ref': '<ref-for-documentation>',  # Can be any branch, tag, or commit hash. Use a branch that hosts your docs.
+         'binderhub_url': '<binder_url>',  # Any URL of a binderhub deployment. Must be full URL (e.g. https://mybinder.org).
          'dependencies': '<list_of_paths_to_dependency_files>',
          # Optional keys
-         'filepath_prefix': '<prefix>' # A prefix to append to any filepaths in Binder links.
-         'notebooks_dir': '<notebooks-directory-name>' # Jupyter notebooks for Binder will be copied to this directory (relative to site root).
+         'filepath_prefix': '<prefix>' # A prefix to prepend to any filepaths in Binder links.
+         'notebooks_dir': '<notebooks-directory-name>' # Jupyter notebooks for Binder will be copied to this directory (relative to built documentation root).
          'use_jupyter_lab': <bool> # Whether Binder links should start Jupyter Lab instead of the Jupyter Notebook interface.
          }
     }
 
-Note that ``branch:`` should be the branch on which your documentation is hosted.
-If you host your documentation on GitHub, this is usually ``gh-pages`` or ``master``.
+If a Sphinx-Gallery configuration for Binder is discovered, the following extra things will happen:
+
+1. The dependency files specified in ``dependencies`` will be copied to a ``binder/`` folder in your built documentation.
+2. The built Jupyter Notebooks from the documentation will be copied to a folder called ``<notebooks_dir/>`` at the root of
+   your built documentation (they will follow the same folder hierarchy within the notebooks directory folder.
+2. The rST output of each Sphinx-Gallery example will now have a ``launch binder`` button in it.
+4. That button will point to a binder link with the following structure::
+
+       <binderhub_url>/v2/gh/<org>/<repo>/<ref>?filepath=<filepath_prefix>/<notebooks_dir>/path/to/notebook.ipynb
+
+Below is a more complete explanation of each field.
+
+org (type: string)
+  The GitHub organization where your documentation is stored.
+repo (type: string)
+  The GitHub repository where your documentation is stored.
+ref (type: string)
+  A reference to the version of your repository where your documentation exists.
+  For example, if your built documentation is stored on a ``gh-pages`` branch, then this field
+  should be set to ``gh-pages``.
+binderhub_url (type: string)
+  The full URL to a BinderHub deployment where you want your examples to run. One
+  public BinderHub deployment is at ``https://mybinder.org``, though if you (and your users) have access to
+  another, this can be configured with this field.
+dependencies (type: list)
+  A list of paths (relative to ``conf.py``) to dependency files that Binder uses to infer the environment needed
+  to run your examples. For example, a ``requirements.txt`` file. These will be copied into a folder
+  called ``binder/`` in your built documentation folder. For a list of all the possible dependency files
+  you can use, see `the Binder configuration documentation <https://mybinder.readthedocs.io/en/latest/config_files.html#config-files>`_.
+filepath_prefix (type: string | None, default: ``None``)
+  A prefix to append to the filepath in the Binder links. You should use this if you will store your built
+  documentation in a sub-folder of a repository, instead of in the root.
+notebooks_dir (type: string, default: ``notebooks``)
+  The name of a folder where the built Jupyter notebooks will be copied. This ensures that all the notebooks are
+  in one place (though they retain their folder hierarchy) in case you'd like users to browse multiple notebook examples in one session.
+use_jupyter_lab (type: bool, default: ``False``)
+  Whether the default interface activated by the Binder link will be for
+  Jupyter Lab or the classic Jupyter Notebook interface.
 
 Each generated Jupyter Notebook will be copied to the folder
 specified in ``notebooks_dir``. This will be a subfolder of the sphinx output
 directory and included with your site build.
 Binder links will point to these notebooks.
 
-.. important::
-
-   ``dependencies`` should be a list of paths to Binder configuration files that
-   define the environment needed to run the examples. For example, a
-   ``requirements.txt`` file. These will be copied to
-   the documentation branch specified in ``branch:``. When a user clicks your
-   Binder link, these files will be used to create the environment.
-   For more information on what files you can use, see `preparing your
-   repository <https://mybinder.readthedocs.io/en/latest/using.html#preparing-a-repository-for-binder>`_
-   in the `Binder documentation <https://docs.mybinder.org>`__ for more information on
-   what build files are supported.
-
 See the Sphinx-Gallery `Sphinx configuration file <https://github.com/sphinx-gallery/sphinx-gallery/blob/master/doc/conf.py>`_
 for an example that uses the `public Binder server <http://mybinder.org>`_.
 
@@ -564,9 +590,14 @@ The highest precedence is always given to the `-D` flag of the
 Image scrapers
 ==============
 
-By default, at the end of each code block Sphinx-gallery will detect new
-:mod:`matplotlib.pyplot` figures and turn them into images. This behavior is
-equivalent to the default of::
+Image scrapers are plugins that allow Sphinx-gallery to detect images produced
+during execution of your examples, and then embed them into documentation.
+Scrapers can be activated by appending scraper names to the ``image_scrapers``
+field of your Sphinx-gallery configuration (see below). There are currently
+two native image scrapers in Sphinx-gallery (Matplotlib and Mayavi).
+
+By default, Sphinx-gallery will only detect new :mod:`matplotlib.pyplot`
+figures. This behavior is equivalent to the default of::
 
     sphinx_gallery_conf = {
         ...
@@ -574,12 +605,14 @@ equivalent to the default of::
     }
 
 Built-in support is also provided for finding :mod:`Mayavi <mayavi.mlab>`
-figures. To enable this feature, you can do::
+figures. Enable this feature with the following configuration::
 
-    sphinx_gallery_conf = {
-        ...
-        'image_scrapers': ('matplotlib', 'mayavi'),
-    }
+.. code-block:: python
+
+   sphinx_gallery_conf = {
+       ...
+       'image_scrapers': ('matplotlib', 'mayavi'),
+   }
 
 .. note:: The parameter ``find_mayavi_figures`` which can also be used to
           extract Mayavi figures is **deprecated** in version 0.2+,
@@ -588,95 +621,43 @@ figures. To enable this feature, you can do::
 Custom scrapers
 ^^^^^^^^^^^^^^^
 
-.. note:: The API for custom scrapers is currently experimental.
-
-You can also add your own custom function (or callable class instance)
-to this list. See :func:`sphinx_gallery.scrapers.matplotlib_scraper` for
-a description of the interface. Here is pseudocode for what this function
-and :func:`sphinx_gallery.scrapers.mayavi_scraper` do under the hood, which
-uses :func:`sphinx_gallery.scrapers.figure_rst` to create standardized RST::
-
-    def mod_scraper(block, block_vars, gallery_conf)
-        import mymod
-        image_names = list()
-        image_path_iterator = block_vars['image_path_iterator']
-        for fig, image_path in zip(mymod.get_figures(), image_path_iterator):
-            fig.save_png(image_path)
-            image_names.append(image_path)
-        mymod.close('all')
-        return figure_rst(image_names, gallery_conf['src_dir'])
-
-
-For example, a naive class to scrape any new PNG outputs in the
-current directory could do, e.g.::
-
-    import glob
-    import shutil
-    from sphinx_gallery.gen_rst import figure_rst
-
-
-    class PNGScraper(object):
-        def __init__(self):
-            self.seen = set()
-
-        def __call__(self, block, block_vars, gallery_conf):
-            pngs = sorted(glob.glob(os.path.join(os.getcwd(), '*.png')))
-            image_names = list()
-            image_path_iterator = block_vars['image_path_iterator']
-            for png in pngs:
-                if png not in seen:
-                    seen |= set(png)
-                    image_names.append(image_path_iterator.next())
-                    shutil.copyfile(png, image_names[-1])
-            return figure_rst(image_names, gallery_conf['src_dir'])
-
-
-    sphinx_gallery_conf = {
-        ...
-        'image_scrapers': ('matplotlib', PNGScraper()),
-    }
-
-If you have an idea for a scraper that is likely to be useful to others
-(e.g., a fully functional image-file-detector, or one for another visualization
-libarry), feel free to contribute it on GitHub!
+It is possible to write custom scrapers for images generated by packages
+outside of those supported natively in Sphinx-gallery. This is accomplished
+by writing your own Python function to define how to detect and retrieve
+images produced by an arbitrary package. **For instructions on how to accomplish
+this, see :ref:`custom_scrapers`**.
 
+If you've developed a custom scraper for Sphinx-gallery that would be useful
+to the broader community, we encourage you to contribute it to the list of
+natively-supported scrapers located in
+`the scrapers module <https://github.com/sphinx-gallery/sphinx-gallery/blob/master/sphinx_gallery/scrapers.py>`_.
+We welcome PRs!
 
 .. _reset_modules:
 
 Resetting modules
 =================
 
-By default, at the end of each file ``matplotlib`` is reset using
-:func:`matplotlib.pyplot.rcdefaults` and ``seaborn`` is reset by trying
-to unload the module from ``sys.modules``. This is equivalent to the
-entries::
+Often you wish to "reset" the behavior of your visualization packages in order
+to ensure that any changes made to plotting behavior in one example do not
+propagate to the other examples.
+
+By default, at the end of executing each example file, Sphinx-gallery will
+reset ``matplotlib`` (by using :func:`matplotlib.pyplot.rcdefaults`) and ``seaborn``
+(by trying to unload the module from ``sys.modules``). This is equivalent to the
+following configuration::
 
     sphinx_gallery_conf = {
         ...
         'reset_modules': ('matplotlib', 'seaborn'),
     }
 
-You can also add your own custom function to this tuple.
-The function takes two variables, the ``gallery_conf`` dict and the
-``fname``, which on the first call is the directory name being processed, and
-then the names of all files within that directory.
-For example, to reset matplotlib to always use the ``ggplot`` style, you could do::
-
-
-    def reset_mpl(gallery_conf, fname):
-        from matplotlib import style
-        style.use('ggplot')
-
+Currently, Sphinx-gallery natively supports resetting ``matplotlib`` and
+``seaborn``. However, you can also add your own custom function to
+this tuple in order to define resetting behavior for other visualization libraries.
 
-    sphinx_gallery_conf = {
-        ...
-        'reset_modules': (reset_mpl,),
-    }
+To do so, follow the instructions in :ref:`custom_reset`.
 
-.. note:: Using resetters such as ``reset_mpl`` that deviate from
-          standard behaviors that users will experience when manually running
-          examples themselves is discouraged due to the inconsistency
-          that results between the rendered examples and local outputs.
 
 Dealing with failing Gallery example scripts
 ============================================
diff --git a/sphinx_gallery/binder.py b/sphinx_gallery/binder.py
index 64c52df..64056b8 100644
--- a/sphinx_gallery/binder.py
+++ b/sphinx_gallery/binder.py
@@ -64,8 +64,8 @@ def gen_binder_url(fpath, binder_conf, gallery_conf):
     path_link = path_link.replace(os.path.sep, '/')
 
     # Create the URL
-    binder_url = binder_conf['url']
-    binder_url = '/'.join([binder_conf['url'],
+    binder_url = binder_conf['binderhub_url']
+    binder_url = '/'.join([binder_conf['binderhub_url'],
                            'v2', 'gh',
                            binder_conf['org'],
                            binder_conf['repo'],
@@ -88,7 +88,7 @@ def gen_binder_rst(fpath, binder_conf, gallery_conf):
     binder_conf: dict | None
         If a dictionary it must have the following keys:
 
-        'url': The URL of the BinderHub instance that's running a Binder
+        'binderhub_url': The URL of the BinderHub instance that's running a Binder
             service.
         'org': The GitHub organization to which the documentation will be
             pushed.
@@ -110,7 +110,7 @@ def gen_binder_rst(fpath, binder_conf, gallery_conf):
     rst = (
         "\n"
         "  .. container:: binder-badge\n\n"
-        "    .. image:: https://static.mybinder.org/badge.svg\n"
+        "    .. image:: https://mybinder.org/badge_logo.svg\n"
         "      :target: {}\n"
         "      :width: 150 px\n").format(binder_url)
     return rst
@@ -138,10 +138,16 @@ def copy_binder_files(app, exception):
 def _copy_binder_reqs(app, binder_conf):
     """Copy Binder requirements files to a "binder" folder in the docs."""
     path_reqs = binder_conf.get('dependencies')
+    for path in path_reqs:
+        if not os.path.exists(os.path.join(app.srcdir, path)):
+            raise ValueError(("Couldn't find the Binder requirements file: {}, "
+                              "did you specify the path correctly?".format(path)))
 
     binder_folder = os.path.join(app.outdir, 'binder')
     if not os.path.isdir(binder_folder):
         os.makedirs(binder_folder)
+
+    # Copy over the requirements to the output directory
     for path in path_reqs:
         shutil.copy(os.path.join(app.srcdir, path), binder_folder)
 
@@ -178,6 +184,9 @@ def _copy_binder_notebooks(app):
     shutil.rmtree(notebooks_dir, ignore_errors=True)
     os.makedirs(notebooks_dir)
 
+    if not isinstance(gallery_dirs, (list, tuple)):
+        gallery_dirs = [gallery_dirs]
+
     iterator = sphinx_compatibility.status_iterator(
         gallery_dirs, 'copying binder notebooks...', length=len(gallery_dirs))
 
@@ -195,9 +204,16 @@ def check_binder_conf(binder_conf):
         raise ValueError('`binder_conf` must be a dictionary or None.')
     if len(binder_conf) == 0:
         return binder_conf
+    
+    if binder_conf.get('url') and not binder_conf.get('binderhub_url'):
+        logger.warning(
+            'Found old BinderHub URL keyword ("url"). Please update your '
+            'configuration to use the new keyword ("binderhub_url"). "url" will be '
+            'deprecated in sphinx-gallery v0.4')
+        binder_conf['binderhub_url'] = binderhub_conf.get('url')
 
     # Ensure all fields are populated
-    req_values = ['url', 'org', 'repo', 'branch', 'dependencies']
+    req_values = ['binderhub_url', 'org', 'repo', 'branch', 'dependencies']
     optional_values = ['filepath_prefix', 'notebooks_dir', 'use_jupyter_lab']
     missing_values = []
     for val in req_values:
@@ -213,10 +229,10 @@ def check_binder_conf(binder_conf):
             raise ValueError("Unknown Binder config key: {}".format(key))
 
     # Ensure we have http in the URL
-    if not any(binder_conf['url'].startswith(ii)
+    if not any(binder_conf['binderhub_url'].startswith(ii)
                for ii in ['http://', 'https://']):
         raise ValueError('did not supply a valid url, '
-                         'gave url: {}'.format(binder_conf['url']))
+                         'gave binderhub_url: {}'.format(binder_conf['binderhub_url']))
 
     # Ensure we have at least one dependency file
     # Need at least one of these two files
diff --git a/sphinx_gallery/gen_gallery.py b/sphinx_gallery/gen_gallery.py
index af418b0..8aed7be 100644
--- a/sphinx_gallery/gen_gallery.py
+++ b/sphinx_gallery/gen_gallery.py
@@ -186,9 +186,9 @@ def _complete_gallery_conf(sphinx_gallery_conf, src_dir, plot_gallery,
 
     # Ensure the first cell text is a string if we have it
     first_cell = gallery_conf.get("first_notebook_cell")
-    if not isinstance(first_cell, basestring):
+    if (not isinstance(first_cell, basestring)) and (first_cell is not None):
         raise ValueError("The 'first_notebook_cell' parameter must be type str"
-                         "found type %s" % type(first_cell))
+                         "or None, found type %s" % type(first_cell))
     gallery_conf['first_notebook_cell'] = first_cell
     return gallery_conf
 
diff --git a/sphinx_gallery/notebook.py b/sphinx_gallery/notebook.py
index 79472b2..947109d 100644
--- a/sphinx_gallery/notebook.py
+++ b/sphinx_gallery/notebook.py
@@ -113,7 +113,8 @@ def jupyter_notebook(script_blocks, gallery_conf):
     """
     first_cell = gallery_conf.get("first_notebook_cell", "%matplotlib inline")
     work_notebook = jupyter_notebook_skeleton()
-    add_code_cell(work_notebook, first_cell)
+    if first_cell is not None:
+        add_code_cell(work_notebook, first_cell)
     fill_notebook(work_notebook, script_blocks)
 
     return work_notebook


Test patch:
diff --git a/sphinx_gallery/tests/test_binder.py b/sphinx_gallery/tests/test_binder.py
index efca66b..1023cc9 100644
--- a/sphinx_gallery/tests/test_binder.py
+++ b/sphinx_gallery/tests/test_binder.py
@@ -5,24 +5,23 @@
 Testing the binder badge functionality
 """
 from __future__ import division, absolute_import, print_function
-import os
-import tempfile
-import sys
+
 from copy import deepcopy
 
 import pytest
 
-from sphinx_gallery.binder import gen_binder_url, check_binder_conf
+from sphinx_gallery.binder import (gen_binder_url, check_binder_conf,
+                                   _copy_binder_reqs)
 
 
 def test_binder():
     """Testing binder URL generation and checks."""
     file_path = 'blahblah/mydir/myfile.py'
-    conf_base = {'url': 'http://test1.com', 'org': 'org',
+    conf_base = {'binderhub_url': 'http://test1.com', 'org': 'org',
                  'repo': 'repo', 'branch': 'branch',
                  'dependencies': '../requirements.txt'}
     conf_base = check_binder_conf(conf_base)
-    gallery_conf_base = {'gallery_dirs': ['mydir'], 'src_dir': 'blahblah'}
+    gallery_conf_base = {'gallery_dirs': 'mydir', 'src_dir': 'blahblah'}
 
     url = gen_binder_url(file_path, conf_base, gallery_conf_base)
     expected = ('http://test1.com/v2/gh/org/repo/'
@@ -42,38 +41,48 @@ def test_binder():
     conf1.pop('filepath_prefix')
 
     # URL must have http
-    with pytest.raises(ValueError) as excinfo:
-        conf2 = deepcopy(conf1)
-        conf2['url'] = 'test1.com'
+    conf2 = deepcopy(conf1)
+    conf2['binderhub_url'] = 'test1.com'
+    with pytest.raises(ValueError, match='did not supply a valid url'):
         url = check_binder_conf(conf2)
 
-    excinfo.match(r'did not supply a valid url')
-
     # Assert missing params
     for key in conf1.keys():
         if key == 'notebooks_dir':
             continue
         conf3 = deepcopy(conf1)
         conf3.pop(key)
-        with pytest.raises(ValueError) as excinfo:
+        with pytest.raises(ValueError, match='binder_conf is missing values'):
             url = check_binder_conf(conf3)
-        excinfo.match(r"binder_conf is missing values for")
 
     # Dependencies file
     dependency_file_tests = ['requirements_not.txt', 'doc-requirements.txt']
     for ifile in dependency_file_tests:
-        with pytest.raises(ValueError) as excinfo:
-            conf3 = deepcopy(conf1)
-            conf3['dependencies'] = ifile
+        conf3 = deepcopy(conf1)
+        conf3['dependencies'] = ifile
+        with pytest.raises(ValueError,
+                           match=r"Did not find one of `requirements.txt` "
+                           "or `environment.yml`"):
             url = check_binder_conf(conf3)
-        excinfo.match(r"Did not find one of `requirements.txt` "
-                      "or `environment.yml`")
 
-    with pytest.raises(ValueError) as excinfo:
-        conf6 = deepcopy(conf1)
-        conf6['dependencies'] = {'test': 'test'}
+    conf6 = deepcopy(conf1)
+    conf6['dependencies'] = {'test': 'test'}
+    with pytest.raises(ValueError, match='`dependencies` value should be a '
+                       'list of strings'):
         url = check_binder_conf(conf6)
-    excinfo.match(r"`dependencies` value should be a list of strings")
+
+    # Missing requirements file should raise an error
+    conf7 = deepcopy(conf1)
+    conf7['dependencies'] = ['requirements.txt', 'this_doesntexist.txt']
+    # Hack to test the case when dependencies point to a non-existing file
+    # w/o needing to do a full build
+
+    def apptmp():
+        pass
+    apptmp.srcdir = '/'
+    with pytest.raises(ValueError, match="Couldn't find the Binder "
+                       "requirements file"):
+        url = _copy_binder_reqs(apptmp, conf7)
 
     # Check returns the correct object
     conf4 = check_binder_conf({})
@@ -82,11 +91,10 @@ def test_binder():
         assert iconf == {}
 
     # Assert extra unkonwn params
-    with pytest.raises(ValueError) as excinfo:
-        conf7 = deepcopy(conf1)
-        conf7['foo'] = 'blah'
+    conf7 = deepcopy(conf1)
+    conf7['foo'] = 'blah'
+    with pytest.raises(ValueError, match='Unknown Binder config key'):
         url = check_binder_conf(conf7)
-    excinfo.match(r"Unknown Binder config key")
 
     # Assert using lab correctly changes URL
     conf_lab = deepcopy(conf_base)
diff --git a/sphinx_gallery/tests/test_gen_gallery.py b/sphinx_gallery/tests/test_gen_gallery.py
index da78e91..b67f1f4 100644
--- a/sphinx_gallery/tests/test_gen_gallery.py
+++ b/sphinx_gallery/tests/test_gen_gallery.py
@@ -310,7 +310,7 @@ sphinx_gallery_conf = {
     'backreferences_dir' : os.path.join('modules', 'gen'),
     'examples_dirs': 'src',
     'gallery_dirs': ['ex'],
-    'binder': {'url': 'http://test1.com', 'org': 'org',
+    'binder': {'binderhub_url': 'http://test1.com', 'org': 'org',
                'repo': 'repo', 'branch': 'branch',
                'notebooks_dir': 'ntbk_folder',
                'dependencies': 'requirements.txt'}
diff --git a/sphinx_gallery/tests/test_gen_rst.py b/sphinx_gallery/tests/test_gen_rst.py
index 1252443..c9d97e3 100644
--- a/sphinx_gallery/tests/test_gen_rst.py
+++ b/sphinx_gallery/tests/test_gen_rst.py
@@ -288,7 +288,7 @@ def test_rst_example(gallery_conf):
 
     gallery_conf.update(binder={'org': 'sphinx-gallery',
                                 'repo': 'sphinx-gallery.github.io',
-                                'url': 'https://mybinder.org',
+                                'binderhub_url': 'https://mybinder.org',
                                 'branch': 'master',
                                 'dependencies': './binder/requirements.txt',
                                 'notebooks_dir': 'notebooks',
diff --git a/sphinx_gallery/tests/test_notebook.py b/sphinx_gallery/tests/test_notebook.py
index bd7a95d..0d5b9f0 100644
--- a/sphinx_gallery/tests/test_notebook.py
+++ b/sphinx_gallery/tests/test_notebook.py
@@ -100,6 +100,12 @@ def test_jupyter_notebook(gallery_conf):
     example_nb = jupyter_notebook(blocks, gallery_conf)
     assert example_nb.get('cells')[0]['source'][0] == test_text
 
+    # Test empty first cell text
+    test_text = None
+    gallery_conf['first_notebook_cell'] = test_text
+    example_nb = jupyter_notebook(blocks, gallery_conf)
+    assert example_nb.get('cells')[0]['source'][0].startswith('\nThe Header Docstring')
+
 ###############################################################################
 # Notebook shell utility