Skip to content

Commit 9fd867f

Browse files
committed
Fix default thumbnail implementation, some documentation changes
1 parent 590a127 commit 9fd867f

File tree

4 files changed

+137
-106
lines changed

4 files changed

+137
-106
lines changed

doc/gallery/default-thumbnail.ipynb

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@
1313
"cell_type": "markdown",
1414
"metadata": {},
1515
"source": [
16-
"# Default Thumbnails\n",
16+
"# Default Thumbnail\n",
1717
"\n",
18-
"By default, a notebook with an image output will use the last of these as its thumbnail. Without an image output, a placeholder will be used. See [a notebook with no thumbnail](no-thumbnail.ipynb) for an example.\n",
18+
"By default,\n",
19+
"the last image output of a notebook will be used as its thumbnail.\n",
20+
"Without an image output, a placeholder will be used.\n",
21+
"See [a notebook with no thumbnail](no-thumbnail.ipynb) for an example.\n",
1922
"\n",
20-
"However, if a thumbnail is explicitly assigned by [Using Cell Metadata to Select a Thumbnail](cell-metadata.ipynb), [Using a Cell Tag to Select a Thumbnail](cell-tag.ipynb) or [Specifying Thumbnails in `conf.py`](thumbnail-from-conf-py.ipynb), these methods will take precedence: cell tags and metadata are higher priority than in `conf.py`."
23+
"However, if a thumbnail is explicitly assigned by\n",
24+
"[Using Cell Metadata to Select a Thumbnail](cell-metadata.ipynb),\n",
25+
"[Using a Cell Tag to Select a Thumbnail](cell-tag.ipynb) or\n",
26+
"[Specifying a Thumbnail File](thumbnail-from-conf-py.ipynb),\n",
27+
"these methods will take precedence."
2128
]
2229
},
2330
{
@@ -45,14 +52,15 @@
4552
"source": [
4653
"fig, ax = plt.subplots(figsize=[6, 3])\n",
4754
"x = np.linspace(-5, 5, 50)\n",
48-
"ax.plot(x, np.sinc(x))"
55+
"ax.plot(x, np.sinc(x));"
4956
]
5057
},
5158
{
5259
"cell_type": "markdown",
5360
"metadata": {},
5461
"source": [
55-
"But the next cell is the last containing an image in the notebook, so it will be used as the thumbnail."
62+
"But the next cell is the last containing an image in the notebook,\n",
63+
"so its last image output will be used as the thumbnail."
5664
]
5765
},
5866
{
@@ -61,23 +69,32 @@
6169
"metadata": {},
6270
"outputs": [],
6371
"source": [
72+
"display(fig)\n",
6473
"fig, ax = plt.subplots(figsize=[6, 3])\n",
6574
"x = np.linspace(-5, 5, 50)\n",
66-
"ax.plot(x, -np.sinc(x), color='red')"
75+
"ax.plot(x, -np.sinc(x), color='red');"
6776
]
6877
}
6978
],
7079
"metadata": {
7180
"kernelspec": {
72-
"display_name": "Python 3",
81+
"display_name": "Python 3 (ipykernel)",
7382
"language": "python",
7483
"name": "python3"
7584
},
7685
"language_info": {
77-
"name": "python"
78-
},
79-
"orig_nbformat": 4
86+
"codemirror_mode": {
87+
"name": "ipython",
88+
"version": 3
89+
},
90+
"file_extension": ".py",
91+
"mimetype": "text/x-python",
92+
"name": "python",
93+
"nbconvert_exporter": "python",
94+
"pygments_lexer": "ipython3",
95+
"version": "3.11.2"
96+
}
8097
},
8198
"nbformat": 4,
82-
"nbformat_minor": 2
99+
"nbformat_minor": 4
83100
}

doc/gallery/gallery-with-nested-documents.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@
7070
"Only links and the first section title are scanned,\n",
7171
"everything else is ignored.\n",
7272
"\n",
73+
"* [Last Image Is Used by Default](default-thumbnail.ipynb)\n",
7374
"* [Using a Cell Tag to Select a Thumbnail](cell-tag.ipynb)\n",
74-
"* [Using Cell Metadata to Select a Thumbnail](cell-metadata.ipynb)\n",
75+
"* [Using Cell Metadata to Select a Thumbnail and Provide a Tooltip](cell-metadata.ipynb)\n",
7576
"* [Choosing from Multiple Outputs](multiple-outputs.ipynb)\n",
76-
"* [Default Thumbnails](default-thumbnail.ipynb)\n",
7777
"* [No Thumbnail Available](no-thumbnail.ipynb)\n",
7878
"* [Specifying a Thumbnail File](thumbnail-from-conf-py.ipynb)\n",
7979
"\n",
@@ -99,7 +99,7 @@
9999
"name": "python",
100100
"nbconvert_exporter": "python",
101101
"pygments_lexer": "ipython3",
102-
"version": "3.11.1"
102+
"version": "3.11.2"
103103
}
104104
},
105105
"nbformat": 4,

doc/gallery/thumbnail-from-conf-py.ipynb

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
"source": [
1616
"# Specifying Thumbnails in `conf.py`\n",
1717
"\n",
18-
"This notebook doesn't contain any thumbnail metadata.\n",
19-
"\n",
20-
"But in the file [conf.py](../conf.py),\n",
18+
"This notebook doesn't contain a `nbsphinx-thumbnail`\n",
19+
"[cell tag](cell-tag.ipynb) nor\n",
20+
"[cell metadata](cell-metadata.ipynb).\n",
21+
"Instead, in the file [conf.py](../conf.py),\n",
2122
"a thumbnail is specified (via the\n",
2223
"[nbsphinx_thumbnails](../configuration.ipynb#nbsphinx_thumbnails)\n",
2324
"option),\n",
@@ -48,15 +49,6 @@
4849
"we are creating an image file here:"
4950
]
5051
},
51-
{
52-
"cell_type": "code",
53-
"execution_count": null,
54-
"metadata": {},
55-
"outputs": [],
56-
"source": [
57-
"%matplotlib agg"
58-
]
59-
},
6052
{
6153
"cell_type": "code",
6254
"execution_count": null,
@@ -74,7 +66,8 @@
7466
"source": [
7567
"fig, ax = plt.subplots()\n",
7668
"ax.plot([4, 8, 15, 16, 23, 42])\n",
77-
"fig.savefig('a-local-file.png')"
69+
"fig.savefig('a-local-file.png')\n",
70+
"plt.close() # avoid plotting the figure"
7871
]
7972
},
8073
{
@@ -97,6 +90,25 @@
9790
"\n",
9891
"Please note that the notebook name does *not* contain the `.ipynb` suffix."
9992
]
93+
},
94+
{
95+
"cell_type": "markdown",
96+
"metadata": {},
97+
"source": [
98+
"Note that the following plot is *not* used as a thumbnail\n",
99+
"because the `nbsphinx_thumbnails` setting overrides\n",
100+
"[the default behavior](default-thumbnail.ipynb)."
101+
]
102+
},
103+
{
104+
"cell_type": "code",
105+
"execution_count": null,
106+
"metadata": {},
107+
"outputs": [],
108+
"source": [
109+
"fig, ax = plt.subplots(figsize=[6, 3])\n",
110+
"ax.plot([4, 9, 7, 20, 6, 33, 13, 23, 16, 62, 8], 'r:');"
111+
]
100112
}
101113
],
102114
"metadata": {
@@ -115,7 +127,7 @@
115127
"name": "python",
116128
"nbconvert_exporter": "python",
117129
"pygments_lexer": "ipython3",
118-
"version": "3.11.1"
130+
"version": "3.11.2"
119131
}
120132
},
121133
"nbformat": 4,

src/nbsphinx/__init__.py

Lines changed: 80 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
'text/plain',
6969
)
7070

71-
MIME_TYPE_SUFFIXES = {
71+
THUMBNAIL_MIME_TYPES = {
7272
'image/svg+xml': '.svg',
7373
'image/png': '.png',
7474
'image/jpeg': '.jpg',
@@ -410,106 +410,106 @@ def from_notebook_node(self, nb, resources=None, **kw):
410410
resources['nbsphinx_widgets'] = True
411411

412412
thumbnail = {}
413-
thumbnail_filename = None
414413

415414
def warning(msg, *args):
416415
logger.warning(
417-
'"nbsphinx-thumbnail": ' + msg, *args,
416+
'"nbsphinx-thumbnail" in cell %s: ' + msg, cell_index, *args,
418417
location=resources.get('nbsphinx_docname'),
419418
type='nbsphinx', subtype='thumbnail')
420419
thumbnail['filename'] = _BROKEN_THUMBNAIL
421420

422421
for cell_index, cell in enumerate(nb.cells):
423-
# figure out if this cell is explicitly tagged
424-
# but if it's not, we'll default to the last figure in the notebook
425-
# if one exists
426-
metadata_cell = 'nbsphinx-thumbnail' in cell.metadata
427-
tagged_cell = 'nbsphinx_thubnail' in cell.metadata.get('tags',[])
428-
thumbnail_cell = metadata_cell or tagged_cell
429-
430-
if metadata_cell:
422+
if 'nbsphinx-thumbnail' in cell.metadata:
431423
data = cell.metadata['nbsphinx-thumbnail'].copy()
432-
output_index = data.pop('output-index', -1)
424+
output_index = data.pop('output-index', None)
433425
tooltip = data.pop('tooltip', '')
434-
435426
if data:
436427
warning('Invalid key(s): %s', set(data))
437428
break
438-
else:
439-
output_index = -1
429+
elif 'nbsphinx-thumbnail' in cell.metadata.get('tags', []):
430+
output_index = None
440431
tooltip = ''
441-
442-
if cell.cell_type != 'code':
443-
if thumbnail_cell:
444-
warning('Only allowed in code cells; cell %s has type "%s"',
445-
cell_index, cell.cell_type)
446-
break
447-
432+
else:
448433
continue
449434

450-
if thumbnail and thumbnail_cell:
451-
warning('Only allowed onced per notebook')
435+
if cell.cell_type != 'code':
436+
warning(
437+
'Only allowed in code cells; wrong cell type: "%s"',
438+
cell.cell_type)
452439
break
453440

454-
if not cell.outputs:
455-
if thumbnail_cell:
456-
warning('No outputs in cell %s', cell_index)
457-
break
458-
459-
continue
441+
if thumbnail:
442+
warning('Only allowed once per notebook')
443+
break
460444

461-
if output_index == -1:
445+
if output_index is None:
462446
output_index = len(cell.outputs) - 1
463-
elif output_index >= len(cell.outputs):
464-
warning('Invalid "output-index" in cell %s: %s',
465-
cell_index, output_index)
447+
try:
448+
suffix = _extract_thumbnail(cell, output_index)
449+
except _ExtractThumbnailException as e:
450+
warning(*e.args)
466451
break
467452

468-
out = cell.outputs[output_index]
469-
470-
if out.output_type not in {'display_data', 'execute_result'}:
471-
if thumbnail_cell:
472-
warning('Unsupported output type in cell %s/output %s: "%s"',
473-
cell_index, output_index, out.output_type)
474-
break
475-
476-
continue
477-
478-
for mime_type in DISPLAY_DATA_PRIORITY_HTML:
479-
if mime_type not in out.data or mime_type not in MIME_TYPE_SUFFIXES:
480-
continue
481-
482-
thumbnail_filename = '{}_{}_{}{}'.format(
483-
resources['unique_key'],
484-
cell_index,
485-
output_index,
486-
MIME_TYPE_SUFFIXES[mime_type],
487-
)
488-
break
489-
else:
490-
if thumbnail_cell:
491-
warning('Unsupported MIME type(s) in cell %s/output %s: %s',
492-
cell_index, output_index, set(out.data))
453+
thumbnail['filename'] = '{}_{}_{}{}'.format(
454+
resources['unique_key'],
455+
cell_index,
456+
output_index,
457+
suffix,
458+
)
459+
if tooltip:
460+
thumbnail['tooltip'] = tooltip
461+
462+
if not thumbnail:
463+
# No explicit thumbnails were specified in the notebook.
464+
# Now we are looking for the last output image in the notebook.
465+
for cell_index, cell in reversed(list(enumerate(nb.cells))):
466+
if cell.cell_type == 'code':
467+
for output_index in reversed(range(len(cell.outputs))):
468+
try:
469+
suffix = _extract_thumbnail(cell, output_index)
470+
except _ExtractThumbnailException:
471+
continue
472+
thumbnail['filename'] = '{}_{}_{}{}'.format(
473+
resources['unique_key'],
474+
cell_index,
475+
output_index,
476+
suffix,
477+
)
478+
# NB: we use this as marker for implicit thumbnail:
479+
thumbnail['tooltip'] = None
480+
break
481+
else:
482+
continue
493483
break
494484

495-
continue
485+
if thumbnail:
486+
resources['nbsphinx_thumbnail'] = thumbnail
487+
return rststr, resources
496488

497-
if thumbnail_cell:
498-
thumbnail['filename'] = thumbnail_filename
499-
thumbnail['implicit'] = False
500-
if tooltip:
501-
thumbnail['tooltip'] = tooltip
502489

503-
break
490+
class _ExtractThumbnailException(Exception):
491+
"""Internal exception thrown by _extract_thumbnail()."""
504492

505-
else:
506-
# default to the last figure in the notebook, if it's a valid thumbnail
507-
if thumbnail_filename:
508-
thumbnail['filename'] = thumbnail_filename
509-
thumbnail['implicit'] = True
510493

511-
resources['nbsphinx_thumbnail'] = thumbnail
512-
return rststr, resources
494+
def _extract_thumbnail(cell, output_index):
495+
if not cell.outputs:
496+
raise _ExtractThumbnailException('No outputs')
497+
if output_index not in range(len(cell.outputs)):
498+
raise _ExtractThumbnailException(
499+
'Invalid "output-index": %s', output_index)
500+
out = cell.outputs[output_index]
501+
if out.output_type not in {'display_data', 'execute_result'}:
502+
raise _ExtractThumbnailException(
503+
'Unsupported output type in output %s: "%s"',
504+
output_index, out.output_type)
505+
for mime_type in DISPLAY_DATA_PRIORITY_HTML:
506+
if mime_type not in out.data or mime_type not in THUMBNAIL_MIME_TYPES:
507+
continue
508+
return THUMBNAIL_MIME_TYPES[mime_type]
509+
else:
510+
raise _ExtractThumbnailException(
511+
'Unsupported MIME type(s) in output %s: %s',
512+
output_index, set(out.data))
513513

514514

515515
class NotebookParser(rst.Parser):
@@ -1735,24 +1735,26 @@ def has_wildcard(pattern):
17351735
conf_py_thumbnail = candidate
17361736

17371737
thumbnail = app.env.nbsphinx_thumbnails.get(doc, {})
1738+
# NB: "None" is used as marker for implicit thumbnail:
17381739
tooltip = thumbnail.get('tooltip', '')
17391740
filename = thumbnail.get('filename', '')
1740-
was_implicit_thumbnail = thumbnail.get('implicit', True)
17411741

1742-
# thumbnail priority: broken, explicit in notebook, from conf.py
1743-
# implicit in notebook, default
1742+
# thumbnail priority: broken, explicit in notebook,
1743+
# from conf.py, implicit in notebook, default
17441744
if filename is _BROKEN_THUMBNAIL:
17451745
filename = os.path.join(
17461746
base, '_static', 'nbsphinx-broken-thumbnail.svg')
1747-
elif filename and not was_implicit_thumbnail:
1747+
elif filename and tooltip is not None:
17481748
# thumbnail from tagged cell or metadata
17491749
filename = os.path.join(
17501750
base, app.builder.imagedir, filename)
17511751
elif conf_py_thumbnail:
17521752
# NB: Settings from conf.py can be overwritten in notebook
17531753
filename = os.path.join(base, conf_py_thumbnail)
17541754
elif filename:
1755-
# implicit thumbnail from an image in the notebook
1755+
# implicit thumbnail from the last image in the notebook
1756+
assert tooltip is None
1757+
tooltip = ''
17561758
filename = os.path.join(
17571759
base, app.builder.imagedir, filename)
17581760
else:

0 commit comments

Comments
 (0)