From 9fd867f1682326a7e8635aa8f8ec8cb1531e0778 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 10 Mar 2023 23:02:22 +0100 Subject: [PATCH] Fix default thumbnail implementation, some documentation changes --- doc/gallery/default-thumbnail.ipynb | 39 +++-- .../gallery-with-nested-documents.ipynb | 6 +- doc/gallery/thumbnail-from-conf-py.ipynb | 40 +++-- src/nbsphinx/__init__.py | 158 +++++++++--------- 4 files changed, 137 insertions(+), 106 deletions(-) diff --git a/doc/gallery/default-thumbnail.ipynb b/doc/gallery/default-thumbnail.ipynb index dce76017..e3bfb3b7 100644 --- a/doc/gallery/default-thumbnail.ipynb +++ b/doc/gallery/default-thumbnail.ipynb @@ -13,11 +13,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Default Thumbnails\n", + "# Default Thumbnail\n", "\n", - "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", + "By default,\n", + "the last image output of a notebook will be used as its thumbnail.\n", + "Without an image output, a placeholder will be used.\n", + "See [a notebook with no thumbnail](no-thumbnail.ipynb) for an example.\n", "\n", - "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`." + "However, if a thumbnail is explicitly assigned by\n", + "[Using Cell Metadata to Select a Thumbnail](cell-metadata.ipynb),\n", + "[Using a Cell Tag to Select a Thumbnail](cell-tag.ipynb) or\n", + "[Specifying a Thumbnail File](thumbnail-from-conf-py.ipynb),\n", + "these methods will take precedence." ] }, { @@ -45,14 +52,15 @@ "source": [ "fig, ax = plt.subplots(figsize=[6, 3])\n", "x = np.linspace(-5, 5, 50)\n", - "ax.plot(x, np.sinc(x))" + "ax.plot(x, np.sinc(x));" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "But the next cell is the last containing an image in the notebook, so it will be used as the thumbnail." + "But the next cell is the last containing an image in the notebook,\n", + "so its last image output will be used as the thumbnail." ] }, { @@ -61,23 +69,32 @@ "metadata": {}, "outputs": [], "source": [ + "display(fig)\n", "fig, ax = plt.subplots(figsize=[6, 3])\n", "x = np.linspace(-5, 5, 50)\n", - "ax.plot(x, -np.sinc(x), color='red')" + "ax.plot(x, -np.sinc(x), color='red');" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { - "name": "python" - }, - "orig_nbformat": 4 + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/doc/gallery/gallery-with-nested-documents.ipynb b/doc/gallery/gallery-with-nested-documents.ipynb index d67b29f1..10e48e62 100644 --- a/doc/gallery/gallery-with-nested-documents.ipynb +++ b/doc/gallery/gallery-with-nested-documents.ipynb @@ -70,10 +70,10 @@ "Only links and the first section title are scanned,\n", "everything else is ignored.\n", "\n", + "* [Last Image Is Used by Default](default-thumbnail.ipynb)\n", "* [Using a Cell Tag to Select a Thumbnail](cell-tag.ipynb)\n", - "* [Using Cell Metadata to Select a Thumbnail](cell-metadata.ipynb)\n", + "* [Using Cell Metadata to Select a Thumbnail and Provide a Tooltip](cell-metadata.ipynb)\n", "* [Choosing from Multiple Outputs](multiple-outputs.ipynb)\n", - "* [Default Thumbnails](default-thumbnail.ipynb)\n", "* [No Thumbnail Available](no-thumbnail.ipynb)\n", "* [Specifying a Thumbnail File](thumbnail-from-conf-py.ipynb)\n", "\n", @@ -99,7 +99,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/doc/gallery/thumbnail-from-conf-py.ipynb b/doc/gallery/thumbnail-from-conf-py.ipynb index 8020f96e..2deeaa9a 100644 --- a/doc/gallery/thumbnail-from-conf-py.ipynb +++ b/doc/gallery/thumbnail-from-conf-py.ipynb @@ -15,9 +15,10 @@ "source": [ "# Specifying Thumbnails in `conf.py`\n", "\n", - "This notebook doesn't contain any thumbnail metadata.\n", - "\n", - "But in the file [conf.py](../conf.py),\n", + "This notebook doesn't contain a `nbsphinx-thumbnail`\n", + "[cell tag](cell-tag.ipynb) nor\n", + "[cell metadata](cell-metadata.ipynb).\n", + "Instead, in the file [conf.py](../conf.py),\n", "a thumbnail is specified (via the\n", "[nbsphinx_thumbnails](../configuration.ipynb#nbsphinx_thumbnails)\n", "option),\n", @@ -48,15 +49,6 @@ "we are creating an image file here:" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib agg" - ] - }, { "cell_type": "code", "execution_count": null, @@ -74,7 +66,8 @@ "source": [ "fig, ax = plt.subplots()\n", "ax.plot([4, 8, 15, 16, 23, 42])\n", - "fig.savefig('a-local-file.png')" + "fig.savefig('a-local-file.png')\n", + "plt.close() # avoid plotting the figure" ] }, { @@ -97,6 +90,25 @@ "\n", "Please note that the notebook name does *not* contain the `.ipynb` suffix." ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the following plot is *not* used as a thumbnail\n", + "because the `nbsphinx_thumbnails` setting overrides\n", + "[the default behavior](default-thumbnail.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=[6, 3])\n", + "ax.plot([4, 9, 7, 20, 6, 33, 13, 23, 16, 62, 8], 'r:');" + ] } ], "metadata": { @@ -115,7 +127,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/src/nbsphinx/__init__.py b/src/nbsphinx/__init__.py index ee766f4e..795bd8fa 100644 --- a/src/nbsphinx/__init__.py +++ b/src/nbsphinx/__init__.py @@ -68,7 +68,7 @@ 'text/plain', ) -MIME_TYPE_SUFFIXES = { +THUMBNAIL_MIME_TYPES = { 'image/svg+xml': '.svg', 'image/png': '.png', 'image/jpeg': '.jpg', @@ -410,106 +410,106 @@ def from_notebook_node(self, nb, resources=None, **kw): resources['nbsphinx_widgets'] = True thumbnail = {} - thumbnail_filename = None def warning(msg, *args): logger.warning( - '"nbsphinx-thumbnail": ' + msg, *args, + '"nbsphinx-thumbnail" in cell %s: ' + msg, cell_index, *args, location=resources.get('nbsphinx_docname'), type='nbsphinx', subtype='thumbnail') thumbnail['filename'] = _BROKEN_THUMBNAIL for cell_index, cell in enumerate(nb.cells): - # figure out if this cell is explicitly tagged - # but if it's not, we'll default to the last figure in the notebook - # if one exists - metadata_cell = 'nbsphinx-thumbnail' in cell.metadata - tagged_cell = 'nbsphinx_thubnail' in cell.metadata.get('tags',[]) - thumbnail_cell = metadata_cell or tagged_cell - - if metadata_cell: + if 'nbsphinx-thumbnail' in cell.metadata: data = cell.metadata['nbsphinx-thumbnail'].copy() - output_index = data.pop('output-index', -1) + output_index = data.pop('output-index', None) tooltip = data.pop('tooltip', '') - if data: warning('Invalid key(s): %s', set(data)) break - else: - output_index = -1 + elif 'nbsphinx-thumbnail' in cell.metadata.get('tags', []): + output_index = None tooltip = '' - - if cell.cell_type != 'code': - if thumbnail_cell: - warning('Only allowed in code cells; cell %s has type "%s"', - cell_index, cell.cell_type) - break - + else: continue - if thumbnail and thumbnail_cell: - warning('Only allowed onced per notebook') + if cell.cell_type != 'code': + warning( + 'Only allowed in code cells; wrong cell type: "%s"', + cell.cell_type) break - if not cell.outputs: - if thumbnail_cell: - warning('No outputs in cell %s', cell_index) - break - - continue + if thumbnail: + warning('Only allowed once per notebook') + break - if output_index == -1: + if output_index is None: output_index = len(cell.outputs) - 1 - elif output_index >= len(cell.outputs): - warning('Invalid "output-index" in cell %s: %s', - cell_index, output_index) + try: + suffix = _extract_thumbnail(cell, output_index) + except _ExtractThumbnailException as e: + warning(*e.args) break - out = cell.outputs[output_index] - - if out.output_type not in {'display_data', 'execute_result'}: - if thumbnail_cell: - warning('Unsupported output type in cell %s/output %s: "%s"', - cell_index, output_index, out.output_type) - break - - continue - - for mime_type in DISPLAY_DATA_PRIORITY_HTML: - if mime_type not in out.data or mime_type not in MIME_TYPE_SUFFIXES: - continue - - thumbnail_filename = '{}_{}_{}{}'.format( - resources['unique_key'], - cell_index, - output_index, - MIME_TYPE_SUFFIXES[mime_type], - ) - break - else: - if thumbnail_cell: - warning('Unsupported MIME type(s) in cell %s/output %s: %s', - cell_index, output_index, set(out.data)) + thumbnail['filename'] = '{}_{}_{}{}'.format( + resources['unique_key'], + cell_index, + output_index, + suffix, + ) + if tooltip: + thumbnail['tooltip'] = tooltip + + if not thumbnail: + # No explicit thumbnails were specified in the notebook. + # Now we are looking for the last output image in the notebook. + for cell_index, cell in reversed(list(enumerate(nb.cells))): + if cell.cell_type == 'code': + for output_index in reversed(range(len(cell.outputs))): + try: + suffix = _extract_thumbnail(cell, output_index) + except _ExtractThumbnailException: + continue + thumbnail['filename'] = '{}_{}_{}{}'.format( + resources['unique_key'], + cell_index, + output_index, + suffix, + ) + # NB: we use this as marker for implicit thumbnail: + thumbnail['tooltip'] = None + break + else: + continue break - continue + if thumbnail: + resources['nbsphinx_thumbnail'] = thumbnail + return rststr, resources - if thumbnail_cell: - thumbnail['filename'] = thumbnail_filename - thumbnail['implicit'] = False - if tooltip: - thumbnail['tooltip'] = tooltip - break +class _ExtractThumbnailException(Exception): + """Internal exception thrown by _extract_thumbnail().""" - else: - # default to the last figure in the notebook, if it's a valid thumbnail - if thumbnail_filename: - thumbnail['filename'] = thumbnail_filename - thumbnail['implicit'] = True - resources['nbsphinx_thumbnail'] = thumbnail - return rststr, resources +def _extract_thumbnail(cell, output_index): + if not cell.outputs: + raise _ExtractThumbnailException('No outputs') + if output_index not in range(len(cell.outputs)): + raise _ExtractThumbnailException( + 'Invalid "output-index": %s', output_index) + out = cell.outputs[output_index] + if out.output_type not in {'display_data', 'execute_result'}: + raise _ExtractThumbnailException( + 'Unsupported output type in output %s: "%s"', + output_index, out.output_type) + for mime_type in DISPLAY_DATA_PRIORITY_HTML: + if mime_type not in out.data or mime_type not in THUMBNAIL_MIME_TYPES: + continue + return THUMBNAIL_MIME_TYPES[mime_type] + else: + raise _ExtractThumbnailException( + 'Unsupported MIME type(s) in output %s: %s', + output_index, set(out.data)) class NotebookParser(rst.Parser): @@ -1735,16 +1735,16 @@ def has_wildcard(pattern): conf_py_thumbnail = candidate thumbnail = app.env.nbsphinx_thumbnails.get(doc, {}) + # NB: "None" is used as marker for implicit thumbnail: tooltip = thumbnail.get('tooltip', '') filename = thumbnail.get('filename', '') - was_implicit_thumbnail = thumbnail.get('implicit', True) - # thumbnail priority: broken, explicit in notebook, from conf.py - # implicit in notebook, default + # thumbnail priority: broken, explicit in notebook, + # from conf.py, implicit in notebook, default if filename is _BROKEN_THUMBNAIL: filename = os.path.join( base, '_static', 'nbsphinx-broken-thumbnail.svg') - elif filename and not was_implicit_thumbnail: + elif filename and tooltip is not None: # thumbnail from tagged cell or metadata filename = os.path.join( base, app.builder.imagedir, filename) @@ -1752,7 +1752,9 @@ def has_wildcard(pattern): # NB: Settings from conf.py can be overwritten in notebook filename = os.path.join(base, conf_py_thumbnail) elif filename: - # implicit thumbnail from an image in the notebook + # implicit thumbnail from the last image in the notebook + assert tooltip is None + tooltip = '' filename = os.path.join( base, app.builder.imagedir, filename) else: