Repository: sphinx-gallery/sphinx-gallery

Issue:
Can sphinx_gallery_thumbnail_number support negative indexes?
Sometimes, the last figure in an example file is the best one. It's a little inconvenient to count the number of figures to choose the last figure.

I'm wondering if it's possible to let `sphinx_gallery_thumbnail_number` support negative indexes? For example, `sphinx_gallery_thumbnail_number = -1` means the last figure.



Discussion:
seisman: > Can you modify some test so that the line is actually tested? Something in `test_gen_rst.py` or even `tinybuild` would work

I'm not sure how to do the tests.

Like other tests in `test_gen_rst.py`, I can create a test rst string containing `# sphinx_gallery_thumbnail_number = -1`, but it's unclear to me how to check if the last figure is used as thumbnail.
lucyleeow: I think the easier option would be adding a test to [`tinybuild`](https://github.com/sphinx-gallery/sphinx-gallery/tree/master/sphinx_gallery/tests/tinybuild). This small doc gets built during testing and [`test_full.py`](https://github.com/sphinx-gallery/sphinx-gallery/blob/master/sphinx_gallery/tests/test_full.py) tests it.

You can use an example which makes >1 plot (e.g., `[plot_matplotlib_alt.py`](https://github.com/sphinx-gallery/sphinx-gallery/blob/master/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_alt.py)) or add your own to `tinybuild/examples` that makes a few plots - add a negative thumbnail number config. Then add a test to `test_full.py` making sure the thumbnail number / thumbnail path is correct. Let me know if that doesn't make sense.

Patch:
diff --git a/continuous_integration/azure/install.sh b/continuous_integration/azure/install.sh
index 06c3709..74598f4 100644
--- a/continuous_integration/azure/install.sh
+++ b/continuous_integration/azure/install.sh
@@ -55,7 +55,7 @@ elif [ "$DISTRIB" == "nightly" ]; then
     # pip install --no-use-pep517 -q https://api.github.com/repos/matplotlib/matplotlib/zipball/master
     #
     # So for now we'll just live without NumPy.
-    pip install -q --upgrade --pre sphinx joblib pytest-cov pygments colorama
+    pip install -q --upgrade --pre sphinx joblib pytest-cov pygments colorama jinja2>=2.3
     pip install -q .
 elif [ "$DISTRIB" == "minimal" ]; then
     python -m pip install --upgrade . pytest pytest-cov coverage
diff --git a/doc/configuration.rst b/doc/configuration.rst
index 35c2002..ea51f97 100644
--- a/doc/configuration.rst
+++ b/doc/configuration.rst
@@ -663,24 +663,24 @@ Add your own first and last notebook cell
 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::
+default, the following first cell is added to each notebook:
 
 .. code-block:: ipython
 
-   %matplotlib inline
+    %matplotlib inline
 
 Adding a last cell can be useful for performing a desired action such as
 reporting on the user's environment. By default no last cell is added.
 
 You can choose whatever text you like by modifying the ``first_notebook_cell``
 and ``last_notebook_cell`` configuration parameters. For example, the gallery
-of this documentation adds the following first cell::
+of this documentation adds the following first cell:
 
 .. code-block:: ipython
 
-  # This cell is added by sphinx-gallery
-  # It can be customized to whatever you like
-  %matplotlib inline
+    # This cell is added by sphinx-gallery
+    # It can be customized to whatever you like
+    %matplotlib inline
 
 Which is achieved by the following configuration::
 
@@ -890,6 +890,12 @@ to use the 2nd figure created as the thumbnail::
 
     # sphinx_gallery_thumbnail_number = 2
 
+You can also use negative numbers, which counts from the last figure.
+For example -1 means using the last figure created in the example
+as the thumbnail::
+
+    # sphinx_gallery_thumbnail_number = -1
+
 The default behavior is ``sphinx_gallery_thumbnail_number = 1``. See
 :ref:`sphx_glr_auto_examples_plot_4_choose_thumbnail.py` for an example
 of this functionality.
@@ -1256,7 +1262,7 @@ Setting gallery thumbnail size
 
 By default Sphinx-Gallery will generate thumbnails at size ``(400, 280)``.
 The thumbnail image will then be scaled to the size specified by
-``'thumbnail_size``, adding pillarboxes or letterboxes as necessary to
+``thumbnail_size``, adding pillarboxes or letterboxes as necessary to
 maintain the original aspect ratio. The default ``thumbnail_size`` is
 ``(400, 280)`` (no scaling) and can be changed via the ``thumbnail_size``
 configuration, e.g.::
@@ -1269,7 +1275,7 @@ configuration, e.g.::
 The gallery uses various CSS classes to display these thumbnails, which
 default to maximum 160x112px. To change this, e.g. to display the images
 at 250x250px, you can modify the default CSS with something like the following
-in your ``gallery.css``` file:
+in your ``gallery.css`` file:
 
 .. code-block:: css
 
diff --git a/sphinx_gallery/gen_rst.py b/sphinx_gallery/gen_rst.py
index 0ed8582..e5966ae 100644
--- a/sphinx_gallery/gen_rst.py
+++ b/sphinx_gallery/gen_rst.py
@@ -256,7 +256,8 @@ def md5sum_is_current(src_file, mode='b'):
     return False
 
 
-def save_thumbnail(image_path_template, src_file, file_conf, gallery_conf):
+def save_thumbnail(image_path_template, src_file, script_vars, file_conf,
+                   gallery_conf):
     """Generate and Save the thumbnail image
 
     Parameters
@@ -265,6 +266,11 @@ def save_thumbnail(image_path_template, src_file, file_conf, gallery_conf):
         holds the template where to save and how to name the image
     src_file : str
         path to source python file
+    script_vars : dict
+        Configuration and run time variables
+    file_conf : dict
+        File-specific settings given in source file comments as:
+        ``# sphinx_gallery_<name> = <value>``
     gallery_conf : dict
         Sphinx-Gallery configuration dictionary
     """
@@ -287,6 +293,9 @@ def save_thumbnail(image_path_template, src_file, file_conf, gallery_conf):
             raise ExtensionError(
                 'sphinx_gallery_thumbnail_number setting is not a number, '
                 'got %r' % (thumbnail_number,))
+        # negative index means counting from the last one
+        if thumbnail_number < 0:
+            thumbnail_number += len(script_vars["image_path_iterator"]) + 1
         image_path = image_path_template.format(thumbnail_number)
     del thumbnail_number, thumbnail_path, image_path_template
     thumbnail_image_path, ext = _find_image_ext(image_path)
@@ -839,6 +848,11 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf,
             for label, content, line_number in script_blocks
         ]
 
+    # Remove final empty block, which can occur after config comments
+    # are removed
+    if script_blocks[-1][1].isspace():
+        script_blocks = script_blocks[:-1]
+
     if executable:
         clean_modules(gallery_conf, fname)
     output_blocks, time_elapsed = execute_script(script_blocks,
@@ -855,7 +869,8 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf,
     save_rst_example(example_rst, target_file, time_elapsed, memory_used,
                      gallery_conf)
 
-    save_thumbnail(image_path_template, src_file, file_conf, gallery_conf)
+    save_thumbnail(image_path_template, src_file, script_vars, file_conf,
+                   gallery_conf)
 
     example_nb = jupyter_notebook(script_blocks, gallery_conf, target_dir)
     ipy_fname = replace_py_ipynb(target_file) + '.new'


Test patch:
diff --git a/sphinx_gallery/tests/test_full.py b/sphinx_gallery/tests/test_full.py
index c5d628e..fc94464 100644
--- a/sphinx_gallery/tests/test_full.py
+++ b/sphinx_gallery/tests/test_full.py
@@ -181,6 +181,29 @@ def test_thumbnail_path(sphinx_app, tmpdir):
     assert corr > 0.99
 
 
+def test_negative_thumbnail_config(sphinx_app, tmpdir):
+    """Test 'sphinx_gallery_thumbnail_number' config works correctly for
+    negative numbers."""
+    import numpy as np
+    # Make sure our thumbnail is the 2nd (last) image
+    fname_orig = op.join(
+        sphinx_app.outdir, '_images',
+        'sphx_glr_plot_matplotlib_alt_002.png')
+    fname_thumb = op.join(
+        sphinx_app.outdir, '_images',
+        'sphx_glr_plot_matplotlib_alt_thumb.png')
+    fname_new = str(tmpdir.join('new.png'))
+    scale_image(fname_orig, fname_new,
+                *sphinx_app.config.sphinx_gallery_conf["thumbnail_size"])
+    Image = _get_image()
+    orig = np.asarray(Image.open(fname_thumb))
+    new = np.asarray(Image.open(fname_new))
+    assert new.shape[:2] == orig.shape[:2]
+    assert new.shape[2] in (3, 4)  # optipng can strip the alpha channel
+    corr = np.corrcoef(new[..., :3].ravel(), orig[..., :3].ravel())[0, 1]
+    assert corr > 0.99
+
+
 def test_command_line_args_img(sphinx_app):
     generated_examples_dir = op.join(sphinx_app.outdir, 'auto_examples')
     thumb_fname = '../_images/sphx_glr_plot_command_line_args_thumb.png'
diff --git a/sphinx_gallery/tests/test_gen_rst.py b/sphinx_gallery/tests/test_gen_rst.py
index dccb334..443faff 100644
--- a/sphinx_gallery/tests/test_gen_rst.py
+++ b/sphinx_gallery/tests/test_gen_rst.py
@@ -451,6 +451,18 @@ def test_remove_config_comments(gallery_conf, req_pil):
     assert '# sphinx_gallery_thumbnail_number = 1' not in rst
 
 
+def test_final_empty_block(gallery_conf, req_pil):
+    """Test empty final block is removed. Empty final block can occur after
+    sole config comment is removed from final block."""
+    CONTENT.extend(
+        ('# %%', '', '# sphinx_gallery_line_numbers = True')
+    )
+    gallery_conf['remove_config_comments'] = True
+    rst = _generate_rst(gallery_conf, 'test.py', CONTENT)
+    want = "RuntimeWarning)\n\n\n.. rst-class:: sphx-glr-timing"
+    assert want in rst
+
+
 def test_download_link_note_only_html(gallery_conf, req_pil):
     """Test html only directive for download_link."""
     rst = _generate_rst(gallery_conf, 'test.py', CONTENT)
diff --git a/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_alt.py b/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_alt.py
index 6617c3c..36c0007 100644
--- a/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_alt.py
+++ b/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_alt.py
@@ -27,6 +27,8 @@ plt.show()
 # %%
 # Several titles.
 
+# sphinx_gallery_thumbnail_number = -1
+
 plt.plot(range(10))
 
 plt.title('Center Title')