Skip to content

Commit d2c8e47

Browse files
tsbinnslucyleeowpre-commit-ci[bot]larsoner
authored
[ENH] Add custom thumbnails for failing examples (#1313)
Co-authored-by: Lucy Liu <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson <[email protected]>
1 parent 73b7ec8 commit d2c8e47

File tree

10 files changed

+181
-20
lines changed

10 files changed

+181
-20
lines changed

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ def setup(app):
393393
"within_subsection_order": "FileNameSortKey",
394394
"expected_failing_examples": [
395395
"../examples/no_output/plot_raise.py",
396+
"../examples/no_output/plot_raise_thumbnail.py",
396397
"../examples/no_output/plot_syntaxerror.py",
397398
],
398399
"min_reported_time": min_reported_time,

doc/configuration.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Some options can also be set or overridden on a file-by-file basis:
9999
- ``# sphinx_gallery_line_numbers`` (:ref:`adding_line_numbers`)
100100
- ``# sphinx_gallery_thumbnail_number`` (:ref:`choosing_thumbnail`)
101101
- ``# sphinx_gallery_thumbnail_path`` (:ref:`providing_thumbnail`)
102+
- ``# sphinx_gallery_failing_thumbnail`` (:ref:`failing_thumbnail`)
102103
- ``# sphinx_gallery_dummy_images`` (:ref:`dummy_images`)
103104
- ``# sphinx_gallery_capture_repr`` (:ref:`capture_repr`)
104105

@@ -1318,6 +1319,29 @@ Note that ``sphinx_gallery_thumbnail_number`` overrules
13181319
:ref:`sphx_glr_auto_examples_plot_4b_provide_thumbnail.py` for an example of
13191320
this functionality.
13201321

1322+
.. _failing_thumbnail:
1323+
1324+
Controlling thumbnail behaviour in failing examples
1325+
===================================================
1326+
1327+
By default, expected failing examples will have their thumbnail image as a
1328+
stamp with the word "BROKEN". This behaviour is controlled by
1329+
``sphinx_gallery_failing_thumbnail``, which is by default ``True``. In cases
1330+
where control over the thumbnail image is desired, this should be set to
1331+
``False``. This will return thumbnail behaviour to 'normal', whereby
1332+
thumbnail will be either the first figure created (or the
1333+
:ref:`default thumbnail <custom_default_thumb>` if no figure is created)
1334+
or :ref:`provided thumbnail <providing_thumbnail>`::
1335+
1336+
1337+
# sphinx_gallery_failing_thumbnail = False
1338+
1339+
Compare the thumbnails of
1340+
:ref:`sphx_glr_auto_examples_no_output_plot_raise_thumbnail.py` (where the
1341+
option is ``False``) and :ref:`sphx_glr_auto_examples_no_output_plot_raise.py`
1342+
(where the option is the default ``True``) for an example of this
1343+
functionality.
1344+
13211345

13221346
.. _binder_links:
13231347

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Example that fails to execute (with normal thumbnail behaviour)
3+
===============================================================
4+
5+
By default, examples with code blocks that raise an error will have the broken
6+
image stamp as their gallery thumbnail. However, this may not be desired, e.g.
7+
if only part of the example is expected to fail and it should not look like the
8+
entire example fails.
9+
10+
In these cases, the `sphinx_gallery_failing_thumbnail` variable can be set to
11+
``False``, which will change the thumbnail selection to the default behaviour
12+
as for non-failing examples.
13+
"""
14+
15+
# Code source: Thomas S. Binns
16+
# License: BSD 3 clause
17+
# sphinx_gallery_line_numbers = True
18+
19+
# sphinx_gallery_failing_thumbnail = False
20+
21+
import matplotlib.pyplot as plt
22+
import numpy as np
23+
24+
plt.pcolormesh(np.random.randn(100, 100))
25+
26+
# %%
27+
# This block will raise an AssertionError
28+
29+
assert False

sphinx_gallery/gen_rst.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,9 @@ def save_thumbnail(image_path_template, src_file, script_vars, file_conf, galler
395395
base_image_name = os.path.splitext(os.path.basename(src_file))[0]
396396
thumb_file = os.path.join(thumb_dir, f"sphx_glr_{base_image_name}_thumb.{ext}")
397397

398-
if "formatted_exception" in script_vars:
398+
if "formatted_exception" in script_vars and file_conf.get(
399+
"failing_thumbnail", True
400+
):
399401
img = os.path.join(glr_path_static(), "broken_example.png")
400402
elif os.path.exists(thumbnail_image_path):
401403
img = thumbnail_image_path

sphinx_gallery/tests/test_full.py

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
#
4242
# total number of plot_*.py files in
4343
# (examples + examples_rst_index + examples_with_rst + examples_README_header)
44-
N_EXAMPLES = 15 + 3 + 2 + 1
45-
N_FAILING = 2
44+
N_EXAMPLES = 17 + 3 + 2 + 1
45+
N_FAILING = 4
4646
N_GOOD = N_EXAMPLES - N_FAILING # galleries that run w/o error
4747
# passthroughs and non-executed examples in
4848
# (examples + examples_rst_index + examples_with_rst + examples_README_header)
@@ -193,7 +193,7 @@ def test_junit(sphinx_app, tmp_path):
193193
want = dict(
194194
errors="0",
195195
failures="0",
196-
skipped="2",
196+
skipped="4",
197197
tests=f"{N_EXAMPLES}",
198198
name="sphinx-gallery",
199199
)
@@ -241,18 +241,42 @@ def test_junit(sphinx_app, tmp_path):
241241
with open(junit_file, "rb") as fid:
242242
suite = lxml.etree.fromstring(fid.read())
243243
# this time we only ran the stale files
244-
want.update(failures="2", skipped="1", tests="3")
244+
want.update(failures="2", skipped="3", tests="5")
245245
got = dict(suite.attrib)
246246
del got["time"]
247-
assert len(suite) == 3
248-
assert suite[0].attrib["classname"] == "plot_numpy_matplotlib"
249-
assert suite[0][0].tag == "failure", suite[0].attrib["classname"]
250-
assert suite[0][0].attrib["message"].startswith("RuntimeError: Forcing")
251-
assert suite[1].attrib["classname"] == "plot_scraper_broken"
252-
assert suite[1][0].tag == "skipped", suite[1].attrib["classname"]
253-
assert suite[2].attrib["classname"] == "plot_future_imports_broken"
254-
assert suite[2][0].tag == "failure", suite[2].attrib["classname"]
255-
assert suite[2][0].attrib["message"] == "Passed even though it was marked to fail"
247+
skips_and_fails = [
248+
{
249+
"classname": "plot_failing_example",
250+
"tag": "skipped",
251+
"message": None,
252+
},
253+
{
254+
"classname": "plot_failing_example_thumbnail",
255+
"tag": "skipped",
256+
"message": None,
257+
},
258+
{
259+
"classname": "plot_numpy_matplotlib",
260+
"tag": "failure",
261+
"message": "RuntimeError: Forcing",
262+
},
263+
{
264+
"classname": "plot_scraper_broken",
265+
"tag": "skipped",
266+
"message": None,
267+
},
268+
{
269+
"classname": "plot_future_imports_broken",
270+
"tag": "failure",
271+
"message": "Passed even though it was marked to fail",
272+
},
273+
]
274+
assert len(suite) == len(skips_and_fails)
275+
for this_suite, this_example in zip(suite, skips_and_fails):
276+
assert this_suite.attrib["classname"] == this_example["classname"]
277+
assert this_suite[0].tag == this_example["tag"], this_suite.attrib["classname"]
278+
if this_example["message"] is not None:
279+
assert this_suite[0].attrib["message"].startswith(this_example["message"])
256280
assert got == want
257281

258282

@@ -330,6 +354,47 @@ def test_negative_thumbnail_config(sphinx_app, tmpdir):
330354
assert corr > 0.99
331355

332356

357+
def test_thumbnail_expected_failing_examples(sphinx_app, tmpdir):
358+
"""Test thumbnail behaviour for expected failing examples."""
359+
import numpy as np
360+
361+
# Get the "BROKEN" stamp for the default failing example thumbnail
362+
stamp_fname = op.join(
363+
sphinx_app.srcdir, "_static_nonstandard", "broken_example.png"
364+
)
365+
stamp_fname_scaled = str(tmpdir.join("new.png"))
366+
scale_image(
367+
stamp_fname,
368+
stamp_fname_scaled,
369+
*sphinx_app.config.sphinx_gallery_conf["thumbnail_size"],
370+
)
371+
Image = _get_image()
372+
broken_stamp = np.asarray(Image.open(stamp_fname_scaled))
373+
assert broken_stamp.shape[2] in (3, 4) # optipng can strip the alpha channel
374+
375+
# Get thumbnail from example with failing example thumbnail behaviour
376+
# (i.e. thumbnail should be "BROKEN" stamp)
377+
thumb_fname = op.join(
378+
sphinx_app.outdir, "_images", "sphx_glr_plot_failing_example_thumb.png"
379+
)
380+
thumbnail = np.asarray(Image.open(thumb_fname))
381+
assert broken_stamp.shape[:2] == thumbnail.shape[:2]
382+
corr = np.corrcoef(broken_stamp[..., :3].ravel(), thumbnail[..., :3].ravel())[0, 1]
383+
assert corr > 0.99 # i.e. thumbnail and "BROKEN" stamp are identical
384+
385+
# Get thumbnail from example with default thumbnail behaviour
386+
# (i.e. thumbnail should be the plot from the example, not the "BROKEN" stamp)
387+
thumb_fname = op.join(
388+
sphinx_app.outdir,
389+
"_images",
390+
"sphx_glr_plot_failing_example_thumbnail_thumb.png",
391+
)
392+
thumbnail = np.asarray(Image.open(thumb_fname))
393+
assert broken_stamp.shape[:2] == thumbnail.shape[:2]
394+
corr = np.corrcoef(broken_stamp[..., :3].ravel(), thumbnail[..., :3].ravel())[0, 1]
395+
assert corr < 0.7 # i.e. thumbnail and "BROKEN" stamp are not identical
396+
397+
333398
def test_command_line_args_img(sphinx_app):
334399
generated_examples_dir = op.join(sphinx_app.outdir, "auto_examples")
335400
thumb_fname = "../_images/sphx_glr_plot_command_line_args_thumb.png"
@@ -797,6 +862,8 @@ def test_rebuild(tmpdir_factory, sphinx_app):
797862
"sg_api_usage",
798863
"plot_future_imports_broken",
799864
"plot_scraper_broken",
865+
"plot_failing_example",
866+
"plot_failing_example_thumbnail",
800867
)
801868
_assert_mtimes(generated_rst_0, generated_rst_1, ignore=ignore)
802869

@@ -890,6 +957,8 @@ def _rerun(
890957
#
891958
# - auto_examples/future/plot_future_imports_broken
892959
# - auto_examples/future/sg_execution_times
960+
# - auto_examples/plot_failing_example
961+
# - auto_examples/plot_failing_example_thumbnail
893962
# - auto_examples/plot_scraper_broken
894963
# - auto_examples/sg_execution_times
895964
# - auto_examples_rst_index/sg_execution_times
@@ -904,9 +973,9 @@ def _rerun(
904973
# - auto_examples/index
905974
# - auto_examples/plot_numpy_matplotlib
906975
if how == "modify":
907-
n_ch = "([3-9]|10|11)"
976+
n_ch = "([3-9]|1[0-3])" # 3-13
908977
else:
909-
n_ch = "[1-9]"
978+
n_ch = "([1-9]|1[01])" # 1-11
910979
lines = "\n".join([f"\n{how} != {n_ch}:"] + lines)
911980
want = f".*updating environment:.*[0|1] added, {n_ch} changed, 0 removed.*"
912981
assert re.match(want, status, flags) is not None, lines
@@ -968,6 +1037,8 @@ def _rerun(
9681037
# this one will not change even though it was retried
9691038
"plot_future_imports_broken",
9701039
"plot_scraper_broken",
1040+
"plot_failing_example",
1041+
"plot_failing_example_thumbnail",
9711042
)
9721043
# not reliable on Windows and one Ubuntu run
9731044
bad = sys.platform.startswith("win") or os.getenv("BAD_MTIME", "0") == "1"
@@ -1381,10 +1452,10 @@ def test_recommend_n_examples(sphinx_app):
13811452

13821453
assert '<p class="rubric">Related examples</p>' in html
13831454
assert count == n_examples
1384-
# Check the same 3 related examples are shown
1455+
# Check the same 3 related examples are shown (can change when new examples added)
13851456
assert "sphx-glr-auto-examples-plot-repr-py" in html
1386-
assert "sphx-glr-auto-examples-plot-webp-py" in html
1387-
assert "sphx-glr-auto-examples-plot-numpy-matplotlib-py" in html
1457+
assert "sphx-glr-auto-examples-plot-matplotlib-backend-py" in html
1458+
assert "sphx-glr-auto-examples-plot-second-future-imports-py" in html
13881459

13891460

13901461
def test_sidebar_components_download_links(sphinx_app):

sphinx_gallery/tests/test_gen_rst.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,5 @@ def test_newlines(log_collector_wrap):
12391239
assert tee.newlines == tee.output.newlines
12401240

12411241

1242-
# TODO: test that broken thumbnail does appear when needed
12431242
# TODO: test that examples are executed after a no-plot and produce
12441243
# the correct image in the thumbnail
20.9 KB
Loading

sphinx_gallery/tests/tinybuild/doc/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@
7878
"expected_failing_examples": [
7979
"../examples/future/plot_future_imports_broken.py",
8080
"../examples/plot_scraper_broken.py",
81+
"../examples/plot_failing_example.py",
82+
"../examples/plot_failing_example_thumbnail.py",
8183
],
8284
"show_memory": False,
8385
"compress_images": ("images", "thumbnails"),
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
Failing example test
3+
====================
4+
Test failing thumbnail appears for files in expected_failing_examples.
5+
"""
6+
7+
import matplotlib.pyplot as plt
8+
import numpy as np
9+
10+
plt.pcolormesh(np.random.randn(100, 100))
11+
12+
# %%
13+
# will raise AssertionError
14+
15+
assert False
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
Failing example test with normal thumbnail behaviour
3+
====================================================
4+
Test files in expected_failing_examples run, but normal thumbnail behaviour is
5+
retained when sphinx_gallery_failing_thumbnail = False.
6+
"""
7+
8+
# sphinx_gallery_failing_thumbnail = False
9+
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
13+
plt.pcolormesh(np.random.randn(100, 100))
14+
15+
# %%
16+
# will raise AssertionError
17+
18+
assert False

0 commit comments

Comments
 (0)