Repository: sphinx-gallery/sphinx-gallery

Issue:
last_notebook_cell
Could a `last_notebook_cell` option be added much like the `first_notebook_cell` option for adding snippets that we want executed at the end of the downloaded notebook? This would be used to perhaps report on the user's environment with a tool like [`scooby`](https://github.com/banesullivan/scooby):

```py
sphinx_gallery_conf = {
    ...
    'last_notebook_cell': ("# Report on the environment specs\n"
                            "import scooby\n",
                            "scooby.Report()",)
}
```

With something like this, a user could download the example, run it, and encounter an error then immediately share the environment report... I imagine this will be useful for PyVista (`pv.Report`) and `mne-python` (`mne.sys_info()`)

Comments:

Patch:
diff --git a/doc/configuration.rst b/doc/configuration.rst
index 584e854..e7568c8 100644
--- a/doc/configuration.rst
+++ b/doc/configuration.rst
@@ -37,10 +37,10 @@ file:
 - ``min_reported_time`` (:ref:`min_reported_time`)
 - ``show_memory`` (:ref:`show_memory`)
 - ``binder`` (:ref:`binder_links`)
-- ``first_notebook_cell`` (:ref:`first_notebook_cell`)
+- ``first_notebook_cell`` and ``last_notebook_cell`` (:ref:`own_notebook_cell`)
 - ``junit`` (:ref:`junit_xml`)
 - ``log_level`` (:ref:`log_level`)
-- ``capture_repr``, ``ignore_repr_types`` (:ref:`capture_repr`)
+- ``capture_repr`` and ``ignore_repr_types`` (:ref:`capture_repr`)
 
 Some options can also be set or overridden on a file-by-file basis:
 
@@ -545,22 +545,26 @@ To remove the comment from the rendered example set the option::
         'remove_config_comments': True,
     }
 
-.. _first_notebook_cell:
+.. _own_notebook_cell:
 
-Add your own first notebook cell
-================================
+Add your own first and last notebook cell
+=========================================
 
-Sphinx-Gallery adds an extra cell to the beginning of every generated notebook.
-This is often for adding code that is required to run properly in the notebook,
-but not in a ``.py`` file. By default, this text is
+Sphinx-Gallery allows you to add your own first and/or last cell to *every*
+generated notebook. Adding a first cell can be useful for including code that
+is required to run properly in the notebook, but not in a ``.py`` file. By
+default, the following first cell is added to each notebook::
 
 .. code-block:: ipython
 
    %matplotlib inline
 
+Adding a last cell can be useful for performing a desired action such as
+reporting on the user's evironment. By default no last cell is added.
+
 You can choose whatever text you like by modifying the ``first_notebook_cell``
-configuration parameter. For example, the gallery of this documentation
-displays a comment along-side each the code shown above.
+and ``last_notebook_cell`` configuration parameters. For example, the gallery
+of this documentation adds the following first cell::
 
 .. code-block:: ipython
 
@@ -577,8 +581,19 @@ Which is achieved by the following configuration::
                                 "%matplotlib inline")
     }
 
-If the value of ``first_notebook_cell`` is set to ``None``, then no extra first
-cell will be added to the notebook.
+A last cell may be added similarly by setting the ``last_notebook_cell``
+parameter::
+
+    sphinx_gallery_conf = {
+        ...
+        'first_notebook_cell': ("# This cell is added by sphinx-gallery\n"
+                                "# It can be customized to whatever you like\n"
+                                "%matplotlib inline"),
+        'last_notebook_cell': "# This is the last cell",
+    }
+
+If the value of ``first_notebook_cell`` or ``last_notebook_cell`` is set to
+``None``, then no extra first or last cell will be added to the notebook.
 
 
 .. _junit_xml:
diff --git a/sphinx_gallery/gen_gallery.py b/sphinx_gallery/gen_gallery.py
index a47be82..758c9ab 100644
--- a/sphinx_gallery/gen_gallery.py
+++ b/sphinx_gallery/gen_gallery.py
@@ -63,6 +63,7 @@ DEFAULT_GALLERY_CONF = {
     'image_scrapers': ('matplotlib',),
     'reset_modules': ('matplotlib', 'seaborn'),
     'first_notebook_cell': '%matplotlib inline',
+    'last_notebook_cell': None,
     'remove_config_comments': False,
     'show_memory': False,
     'junit': '',
@@ -198,6 +199,12 @@ def _complete_gallery_conf(sphinx_gallery_conf, src_dir, plot_gallery,
         raise ValueError("The 'first_notebook_cell' parameter must be type str"
                          "or None, found type %s" % type(first_cell))
     gallery_conf['first_notebook_cell'] = first_cell
+    # Ensure the last cell text is a string if we have it
+    last_cell = gallery_conf.get("last_notebook_cell")
+    if (not isinstance(last_cell, str)) and (last_cell is not None):
+        raise ValueError("The 'last_notebook_cell' parameter must be type str"
+                         "or None, found type %s" % type(last_cell))
+    gallery_conf['last_notebook_cell'] = last_cell
     # Make it easy to know which builder we're in
     gallery_conf['builder_name'] = builder_name
     gallery_conf['titles'] = {}
diff --git a/sphinx_gallery/notebook.py b/sphinx_gallery/notebook.py
index 036da5a..7cfad6e 100644
--- a/sphinx_gallery/notebook.py
+++ b/sphinx_gallery/notebook.py
@@ -112,10 +112,13 @@ def jupyter_notebook(script_blocks, gallery_conf):
         The sphinx-gallery configuration dictionary.
     """
     first_cell = gallery_conf.get("first_notebook_cell", "%matplotlib inline")
+    last_cell = gallery_conf.get("last_notebook_cell", None)
     work_notebook = jupyter_notebook_skeleton()
     if first_cell is not None:
         add_code_cell(work_notebook, first_cell)
     fill_notebook(work_notebook, script_blocks)
+    if last_cell is not None:
+        add_code_cell(work_notebook, last_cell)
 
     return work_notebook
 


Test patch:
diff --git a/sphinx_gallery/tests/test_gen_gallery.py b/sphinx_gallery/tests/test_gen_gallery.py
index c50d752..2e636c5 100644
--- a/sphinx_gallery/tests/test_gen_gallery.py
+++ b/sphinx_gallery/tests/test_gen_gallery.py
@@ -336,6 +336,17 @@ def test_first_notebook_cell_config(sphinx_app_wrapper):
         parse_config(sphinx_app_wrapper.create_sphinx_app())
 
 
+@pytest.mark.conf_file(content="""
+sphinx_gallery_conf = {
+    'last_notebook_cell': 2,
+}""")
+def test_last_notebook_cell_config(sphinx_app_wrapper):
+    from sphinx_gallery.gen_gallery import parse_config
+    # First cell must be str
+    with pytest.raises(ValueError):
+        parse_config(sphinx_app_wrapper.create_sphinx_app())
+
+
 @pytest.mark.conf_file(content="""
 sphinx_gallery_conf = {
     'backreferences_dir': False,
diff --git a/sphinx_gallery/tests/test_notebook.py b/sphinx_gallery/tests/test_notebook.py
index e8c364d..73b8a14 100644
--- a/sphinx_gallery/tests/test_notebook.py
+++ b/sphinx_gallery/tests/test_notebook.py
@@ -101,6 +101,19 @@ def test_jupyter_notebook(gallery_conf):
     cell_src = example_nb.get('cells')[0]['source'][0]
     assert cell_src.startswith('\nAlternating text and code')
 
+    # Test custom last cell text
+    test_text = '# testing last cell'
+    gallery_conf['last_notebook_cell'] = test_text
+    example_nb = jupyter_notebook(blocks, gallery_conf)
+    assert example_nb.get('cells')[-1]['source'][0] == test_text
+
+    # Test empty first cell text
+    test_text = None
+    gallery_conf['last_notebook_cell'] = test_text
+    example_nb = jupyter_notebook(blocks, gallery_conf)
+    cell_src = example_nb.get('cells')[-1]['source'][0]
+    assert cell_src.startswith("Last text block.\n\nThat's all folks !")
+
 ###############################################################################
 # Notebook shell utility