Repository: sphinx-gallery/sphinx-gallery

Issue:
Another place to replace "######" separators by "# %%" 
I think that the text at the URL below should also be replaced to favor "# %%"
https://sphinx-gallery.github.io/stable/syntax.html#embed-rst-in-your-example-python-files

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

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

```diff
@@            Coverage Diff             @@
##           master     #624      +/-   ##
==========================================
- Coverage   97.78%   97.71%   -0.07%     
==========================================
  Files          31       31              
  Lines        3157     3157              
==========================================
- Hits         3087     3085       -2     
- Misses         70       72       +2
```


| [Impacted Files](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/624?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sphinx\_gallery/docs\_resolv.py](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/624/diff?src=pr&el=tree#diff-c3BoaW54X2dhbGxlcnkvZG9jc19yZXNvbHYucHk=) | `90.28% <0%> (-0.81%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/624?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/624?src=pr&el=footer). Last update [29c3246...4da61b6](https://codecov.io/gh/sphinx-gallery/sphinx-gallery/pull/624?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-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/doc/syntax.rst b/doc/syntax.rst
index 4ba243d..7a1fdfd 100644
--- a/doc/syntax.rst
+++ b/doc/syntax.rst
@@ -58,7 +58,7 @@ rST in the built gallery examples. For example::
   myvariable = 2
   print("my variable is {}".format(myvariable))
 
-  ###############################################################################
+  # %%
   # This is a section header
   # ------------------------
   #
@@ -94,7 +94,7 @@ functionality::
   This example doesn't do much, it just makes a simple plot
   """
 
-  #%%
+  # %%
   # This is a section header
   # ------------------------
   # This is the first section!
@@ -108,7 +108,7 @@ functionality::
   # This is the end of the 'code block' (if using an above IDE). All code within
   # this block can be easily executed all at once.
 
-  #%%
+  # %%
   # This is another section header
   # ------------------------------
   #
diff --git a/examples/no_output/plot_raise.py b/examples/no_output/plot_raise.py
index bc83048..bec1607 100644
--- a/examples/no_output/plot_raise.py
+++ b/examples/no_output/plot_raise.py
@@ -21,18 +21,18 @@ import matplotlib.pyplot as plt
 
 plt.pcolormesh(np.random.randn(100, 100))
 
-###############################################################################
+# %%
 # This next block will raise a NameError
 
 iae
 
-###############################################################################
+# %%
 # Sphinx gallery will stop executing the remaining code blocks after
 # the exception has occurred in the example script. Nevertheless the
 # html will still render all the example annotated text and
 # code blocks, but no output will be shown.
 
-###############################################################################
+# %%
 # Here is another error raising block but will not be executed
 
 plt.plot('Strings are not a valid argument for the plot function')
diff --git a/examples/plot_1_exp.py b/examples/plot_1_exp.py
index 87c7bbd..3872067 100644
--- a/examples/plot_1_exp.py
+++ b/examples/plot_1_exp.py
@@ -7,7 +7,7 @@ This example demonstrates how to import a local module and how images are
 stacked when two plots are created in one code block. The variable ``N`` from
 the example 'Local module' (file ``local_module.py``) is imported in the code
 below. Further, note that when there is only one code block in an example, the
-output appears before the code block. 
+output appears before the code block.
 """
 
 # Code source: Óscar Nájera
diff --git a/examples/plot_3_capture_repr.py b/examples/plot_3_capture_repr.py
index dbad8ce..d5bc710 100644
--- a/examples/plot_3_capture_repr.py
+++ b/examples/plot_3_capture_repr.py
@@ -31,10 +31,10 @@ b   # this is an expression
 # is the first 'representation' method in the ``capture_repr`` tuple. As this
 # method does not exist for ``b``, Sphinx-Gallery moves on and tries to capture
 # the ``__repr__`` method, which is second in the tuple. This does exist for
-# ``b`` so it is captured and the output is seen above. 
-# 
+# ``b`` so it is captured and the output is seen above.
+#
 # A pandas dataframe is used in the code block below to provide an example of
-# an expression with a ``_repr_html_`` method. 
+# an expression with a ``_repr_html_`` method.
 
 # example 3
 import pandas as pd
@@ -59,10 +59,10 @@ a + b
 # string ``'Hello world'`` is thus captured. A 'representation' of the last
 # expression is also captured. Again, since this expression ``a + b`` does not
 # have a ``_repr_html_`` method, the ``__repr__`` method is captured.
-# 
+#
 # Matplotlib output
 # ##################
-# 
+#
 # Matplotlib function calls generally return a Matplotlib object as well as
 # outputting the figure. For code blocks where the last statement is a
 # Matplotlib expression, a 'representation' of the object will be captured, as
@@ -107,7 +107,7 @@ plt.show()
 # captured for example 2 as ``b`` does not have a ``_repr_html_``.
 # You can change the 'representations' in the ``capture_repr`` tuple to finely
 # tune what is captured in your example ``.py`` files.
-# 
+#
 # To only capture data directed to standard output you can set ``capture_repr``
 # to be an empty tuple: ``capture_repr: ()``. With this setting, only data
 # directed to standard output is captured. For the examples above, output would
diff --git a/examples/plot_4b_provide_thumbnail.py b/examples/plot_4b_provide_thumbnail.py
index 8e2b6f0..d84ffde 100644
--- a/examples/plot_4b_provide_thumbnail.py
+++ b/examples/plot_4b_provide_thumbnail.py
@@ -13,13 +13,13 @@ import numpy as np
 import matplotlib.pyplot as plt
 # sphinx_gallery_thumbnail_path = '_static/demo.png'
 
-###############################################################################
+# %%
 
 x = np.linspace(0, 4*np.pi, 301)
 y1 = np.sin(x)
 y2 = np.cos(x)
 
-###############################################################################
+# %%
 # Plot 1
 # ------
 
@@ -29,7 +29,7 @@ plt.plot(x, y2, label='cos')
 plt.legend()
 plt.show()
 
-###############################################################################
+# %%
 # Plot 2
 # ------
 
diff --git a/examples/plot_5_unicode_everywhere.py b/examples/plot_5_unicode_everywhere.py
index eab3b2b..38b4f0c 100644
--- a/examples/plot_5_unicode_everywhere.py
+++ b/examples/plot_5_unicode_everywhere.py
@@ -35,6 +35,6 @@ print('Std out capture 😎')
 # To avoid matplotlib text output
 plt.show()
 
-###########################################################################
+# %%
 # Debug fonts
 print(plt.rcParams)
diff --git a/examples/plot_6_function_identifier.py b/examples/plot_6_function_identifier.py
index 2baa333..3800f73 100644
--- a/examples/plot_6_function_identifier.py
+++ b/examples/plot_6_function_identifier.py
@@ -22,7 +22,7 @@ figheight = len(names) + .5
 
 fontsize = 12.5
 
-###############################################################################
+# %%
 # Sphinx-Gallery examines both the executed code itself, as well as the
 # documentation blocks (such as this one, or the top-level one),
 # to find backreferences. This means that by writing :obj:`numpy.sin`
diff --git a/sphinx_gallery/gen_gallery.py b/sphinx_gallery/gen_gallery.py
index a47be82..c8a80eb 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': '',
@@ -197,7 +198,11 @@ def _complete_gallery_conf(sphinx_gallery_conf, src_dir, plot_gallery,
     if (not isinstance(first_cell, str)) and (first_cell is not None):
         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))
     # 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..f6ef862 100644
--- a/sphinx_gallery/notebook.py
+++ b/sphinx_gallery/notebook.py
@@ -111,11 +111,14 @@ def jupyter_notebook(script_blocks, gallery_conf):
     gallery_conf : dict
         The sphinx-gallery configuration dictionary.
     """
-    first_cell = gallery_conf.get("first_notebook_cell", "%matplotlib inline")
+    first_cell = gallery_conf["first_notebook_cell"]
+    last_cell = gallery_conf["last_notebook_cell"]
     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
 
diff --git a/tutorials/plot_parse.py b/tutorials/plot_parse.py
index a4ad511..802c346 100644
--- a/tutorials/plot_parse.py
+++ b/tutorials/plot_parse.py
@@ -26,21 +26,22 @@ import matplotlib.pyplot as plt
 
 # %%
 # Block splitters allow you alternate between code and text blocks **and**
-# separate sequential blocks of code (above) and text (below). Note that
-# separated text blocks only shows as a new lines between text, in the rendered
-# output.
+# separate sequential blocks of code (above) and text (below).
 
 ##############################################################################
-# A line of ``#``'s also works for separating blocks (above).
+# A line of ``#``'s also works for separating blocks. The above line of ``#``'s
+# separates the text block above from this text block. Notice however, that
+# separated text blocks only shows as a new lines between text, in the rendered
+# output.
 
 def dummy():
     """This should not be part of a 'text' block'"""
 
-    #############################################
-    # Comment inside code to remain in code block
+    # %%
+    # This comment inside a code block will remain in the code block
     pass
 
-# this should not be part of a 'text' block
+# this line should not be part of a 'text' block
 
 # %%
 #
@@ -51,14 +52,15 @@ def dummy():
 
 # %%
 # .. warning::
-#     The next kind of comments are not supported and become too hard to escape
-#     so just don't code like this::
+#     The next kind of comments are not supported (notice the line of ``#``'s
+#     and the ``# %%`` start at the margin instead of being indented like
+#     above) and become too hard to escape so just don't use code like this::
 #
 #         def dummy2():
 #             """Function docstring"""
 #         ####################################
 #         # This comment
-#         #%%
+#         # %%
 #         # and this comment inside python indentation
 #         # breaks the block structure and is not
 #         # supported


Test patch:
diff --git a/sphinx_gallery/tests/reference_parse.txt b/sphinx_gallery/tests/reference_parse.txt
index 870df72..a0e5010 100644
--- a/sphinx_gallery/tests/reference_parse.txt
+++ b/sphinx_gallery/tests/reference_parse.txt
@@ -30,54 +30,60 @@
   19),
 ('text',
   'Block splitters allow you alternate between code and text blocks **and**\n'
-  'separate sequential blocks of code (above) and text (below). Note that\n'
+  'separate sequential blocks of code (above) and text (below).\n',
+  28),
+('text',
+  "A line of ``#``'s also works for separating blocks. The above line of "
+  "``#``'s\n"
+  'separates the text block above from this text block. Notice however, that\n'
   'separated text blocks only shows as a new lines between text, in the '
   'rendered\n'
   'output.\n',
-  28),
-('text', "A line of ``#``'s also works for separating blocks (above).\n", 34),
+  32),
 ('code',
   '\n'
   'def dummy():\n'
   '    """This should not be part of a \'text\' block\'"""\n'
   '\n'
-  '    #############################################\n'
-  '    # Comment inside code to remain in code block\n'
+  '    # %%\n'
+  '    # This comment inside a code block will remain in the code block\n'
   '    pass\n'
   '\n'
-  "# this should not be part of a 'text' block\n"
+  "# this line should not be part of a 'text' block\n"
   '\n',
-  35),
+  36),
 ('text',
   '####################################################################\n'
   '\n'
   'The above syntax makes a line cut in Sphinx. Note the space between the '
   'first\n'
   "``#`` and the line of ``#``'s.\n",
-  46),
+  47),
 ('text',
   '.. warning::\n'
-  '    The next kind of comments are not supported and become too hard to '
-  'escape\n'
-  "    so just don't code like this::\n"
+  '    The next kind of comments are not supported (notice the line of '
+  "``#``'s\n"
+  '    and the ``# %%`` start at the margin instead of being indented like\n'
+  "    above) and become too hard to escape so just don't use code like "
+  'this::\n'
   '\n'
   '        def dummy2():\n'
   '            """Function docstring"""\n'
   '        ####################################\n'
   '        # This comment\n'
-  '        #%%\n'
+  '        # %%\n'
   '        # and this comment inside python indentation\n'
   '        # breaks the block structure and is not\n'
   '        # supported\n'
   '            dummy2\n'
   '\n',
-  53),
+  54),
 ('code',
   '\n'
   '"""Free strings are not supported. They will be rendered as a code '
   'block"""\n'
   '\n',
-  67),
+  69),
 ('text',
   'New lines can be included in your text block and the parser\n'
   'is capable of retaining this important whitespace to work with Sphinx.\n'
@@ -87,11 +93,11 @@
   '\n'
   'rST header within text block\n'
   '^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
-  71),
-('code', "\nprint('one')\n\n", 79),
+  73),
+('code', "\nprint('one')\n\n", 81),
 ('code',
   '\n# another way to separate code blocks shown above\nB = 1\n\n',
-  84),
+  86),
 ('text',
   'Last text block.\n'
   '\n'
@@ -100,4 +106,4 @@
   '.. literalinclude:: plot_parse.py\n'
   '\n'
   '\n',
-  89)]
\ No newline at end of file
+  91)]
\ No newline at end of file
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