10000 Add support for capturing svg, png, and jpeg and _repr_mimebundle_ · sphinx-gallery/sphinx-gallery@1d282e5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1d282e5

Browse files
committed
Add support for capturing svg, png, and jpeg and _repr_mimebundle_
1 parent 1e825be commit 1d282e5

18 files changed

+285
-29
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
command: |
4141
pip install --progress-bar off --user --upgrade --only-binary ":all:" pip setuptools
4242
pip install --progress-bar off --user --upgrade --only-binary ":all:" numpy matplotlib "pyqt5!=5.15.2,!=5.15.3,!=5.15.8" vtk
43-
pip install --progress-bar off --user --upgrade seaborn statsmodels pillow joblib sphinx pytest traits pyvista memory_profiler "ipython!=8.7.0" plotly graphviz "docutils>=0.18" imageio
43+
pip install --progress-bar off --user --upgrade seaborn statsmodels pillow joblib sphinx pytest traits pyvista memory_profiler "ipython!=8.7.0" plotly graphviz "docutils>=0.18" imageio sphinxcontrib-svg2pdfconverter[CairoSVG]
4444
pip install --progress-bar off "jupyterlite-sphinx>=0.8.0,<0.9.0" "jupyterlite-pyodide-kernel<0.1.0" libarchive-c
4545
pip install --progress-bar off --user --upgrade --pre pydata-sphinx-theme
4646
- save_cache:

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ absl-py
1616
graphviz
1717
packaging
1818
jupyterlite-sphinx
19+
sphinxcontrib-svg2pdfconverter[CairoSVG]

doc/advanced.rst

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,16 @@ Write a custom image scraper
143143
By default, Sphinx-Gallery supports image scraping for Matplotlib
144144
(:func:`~sphinx_gallery.scrapers.matplotlib_scraper`). If you wish to capture
145145
output from other python packages, first determine if the object you wish to
146-
capture has a ``_repr_html_`` method. If so, you can use the configuration
147-
``capture_repr`` (:ref:`capture_repr`) to control the display of the object,
148-
without the need to write a custom scraper. This configuration allows capture
149-
of the raw html output, in a process similar to other html-based displays such
150-
as `jupyter <https://jupyter.org/>`_. If the first option does not work,
151-
this section describes how to write a custom scraper.
146+
capture has any of the other supported capture methods: ``_repr_html_``,
147+
``_repr_png_``, ``_repr_jpeg_``, and ``_repr_svg_``. If so, you can use the
148+
configuration ``capture_repr`` (:ref:`capture_repr`) to control the display of
149+
the object, without the need to write a custom scraper. This configuration allows
150+
capture of the raw html/png/jpeg/svg output, in a process similar to other enriched
151+
displays such as `jupyter <https://jupyter.org/>`_. If the object supports
152+
``_repr_mimebundle_``, adding, e.g., ``_repr_svg_`` to ``capture_repr`` will also
153+
look for SVG in the returned MIME-bundle.
154+
155+
If the first option does not work, this section describes how to write a custom scraper.
152156

153157
Image scrapers are functions (or callable class instances) that do the following
154158
things:

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ def setup(app):
327327
'sklearn': ('https://scikit-learn.org/stable', None),
328328
'sphinx': ('https://www.sphinx-doc.org/en/master', None),
329329
'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None),
330+
'ipython': ('https://ipython.readthedocs.io/en/stable/', None),
330331
}
331332

332333
examples_dirs = ['../examples', '../tutorials']

doc/configuration.rst

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,8 +1856,21 @@ are:
18561856
* ``__str__`` - returns a string containing a nicely printable representation
18571857
of an object. This is what is used when you ``print()`` an object or pass it
18581858
to ``format()``.
1859-
* ``_repr_html_`` - returns a HTML version of the object. This method is only
1860-
present in some objects, for example, pandas dataframes.
1859+
* ``_repr_html_`` - returns an HTML version of the object.
1860+
* ``_repr_png_`` - returns a PNG version of the object.
1861+
* ``_repr_jpeg_`` - returns a JPEG version of the object.
1862+
* ``_repr_svg_`` - returns an SVG version of the object.
1863+
1864+
Note that the last four methods are only available for some objects. For example,
1865+
Pandas dataframes, SymPy expressions, and GraphViz graphs, support one or more of
1866+
these formats.
1867+
1868+
.. note::
1869+
1870+
Some objects support :py:meth:`~MyObject._repr_mimebundle_`, which is the preferred
1871+
way to access enriched representations. By specifying, e.g., ``_repr_svg_``,
1872+
Sphinx-Gallery will first look for an SVG in the MIME bundle.
1873+
If not, it will call ``_repr_svg_`` if available.
18611874

18621875
Output capture can be controlled globally by the ``capture_repr`` configuration
18631876
setting or file-by-file by adding a comment to the example file, which overrides

examples/no_output/just_code.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
This demonstrates an example ``.py`` file that is not executed when gallery is
66
generated (see :ref:`build_pattern`) but nevertheless gets included as an
7-
example. Note that no output is capture as this file is not executed.
7+
example. Note that no output is captured as this file is not executed.
88
"""
99

1010
# Code source: Óscar Nájera

examples/plot_3_capture_repr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
# default ``capture_repr`` setting, ``_repr_html_`` is attempted to be captured
112112
# first. If this method does not exist, the ``__repr__`` method would be
113113
# captured. If the ``__repr__`` also does not exist (unlikely for non-user
114-
# defined objects), nothing would be captured. For example, if the the
114+
# defined objects), nothing would be captured. For example, if the
115115
# configuration was set to ``'capture_repr': ('_repr_html_')`` nothing would be
116116
# captured for example 2 as ``b`` does not have a ``_repr_html_``.
117117
# You can change the 'representations' in the ``capture_repr`` tuple to finely

sphinx_gallery/directives.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class ImageSg(images.Image):
128128
/plot_types/basic/images/sphx_glr_bar_001_2_00x.png 2.00x
129129
:class: sphx-glr-single-img
130130
131-
The resulting html is::
131+
The resulting HTML is::
132132
133133
<img src="sphx_glr_bar_001_hidpi.png"
134134
srcset="_images/sphx_glr_bar_001.png,

sphinx_gallery/gen_gallery.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ def _fill_gallery_conf_defaults(sphinx_gallery_conf, app=None,
171171

172172
# Check capture_repr
173173
capture_repr = gallery_conf['capture_repr']
174-
supported_reprs = ['__repr__', '__str__', '_repr_html_']
174+
supported_reprs = {'__repr__', '__str__', '_repr_html_', '_repr_png_',
175+
'_repr_svg_', '_repr_jpeg_'}
175176
if isinstance(capture_repr, tuple):
176177
for rep in capture_repr:
177178
if rep not in supported_reprs:
@@ -815,7 +816,7 @@ def _make_graph(fname, entries, gallery_conf):
815816

816817

817818
def write_api_entry_usage(app, docname, source):
818-
"""Write an html page describing which API entries are used and unused.
819+
"""Write an HTML page describing which API entries are used and unused.
819820
820821
To document and graph only those API entries that are used by
821822
autodoc, we have to wait for autodoc to finish and hook into the

sphinx_gallery/gen_rst.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@
3030
import sys
3131
import traceback
3232
import codeop
33+
from pathlib import PurePosixPath
3334

3435
from sphinx.errors import ExtensionError
3536
import sphinx.util
3637

3738
from .scrapers import (save_figures, ImagePathIterator, clean_modules,
38-
_find_image_ext)
39+
_find_image_ext, figure_rst)
3940
from .utils import (replace_py_ipynb, scale_image, get_md5sum, _replace_md5,
4041
optipng, status_iterator)
4142
from . import glr_path_static
@@ -726,12 +727,40 @@ def _exec_and_get_memory(compiler, ast_Module, code_ast, gallery_conf,
726727
return is_last_expr, mem_max
727728

728729

730+
# Map _repr_*_ to MIME type present in _repr_mimebundle_
731+
MIME_MAPPING = {'_repr_svg_': 'image/svg+xml',
732+
'_repr_jpeg_': 'image/jpeg',
733+
'_repr_png_': 'image/png',
734+
'_repr_html_': 'text/html'}
735+
MIME_MAPPING_REPR = set(MIME_MAPPING.keys())
736+
737+
729738
def _get_last_repr(capture_repr, ___):
730-
"""Get a repr of the last expression, using first method in 'capture_repr'
731-
available for the last expression."""
739+
"""
740+
Get a repr of the last expression, using first method in 'capture_repr'
741+
available for the last expression.
742+
"""
743+
# First try `_repr_mimebundle_`for those representations that may be there
744+
capture_repr_set = set(capture_repr)
745+
included_mime_types = {MIME_MAPPING[repr] for repr in
746+
capture_repr_set.intersection(MIME_MAPPING_REPR)}
747+
excluded_mime_types = {MIME_MAPPING[repr] for repr in
748+
MIME_MAPPING_REPR.difference(capture_repr_set)}
749+
mimebundle = {}
750+
if included_mime_types and hasattr(___, '_repr_mimebundle_'):
751+
try:
752+
mimebundle = ___._repr_mimebundle_(include=included_mime_types,
753+
exclude=excluded_mime_types)
754+
except Exception:
755+
pass
756+
732757
for meth in capture_repr:
733758
try:
734-
last_repr = getattr(___, meth)()
759+
if meth in MIME_MAPPING and MIME_MAPPING[meth] in mimebundle:
760+
# Already generated in _repr_mimebundle_
761+
last_repr = mimebundle[MIME_MAPPING[meth]]
762+
else:
763+
last_repr = getattr(___, meth)()
735764
# for case when last statement is print()
736765
if last_repr is None or last_repr == 'None':
737766
repr_meth = None
@@ -741,13 +770,13 @@ def _get_last_repr(capture_repr, ___):
741770
last_repr = None
742771
repr_meth = None
743772
else:
744-
if isinstance(last_repr, str):
773+
if isinstance(last_repr, (str, bytes)):
745774
break
746775
return last_repr, repr_meth
747776

748777

749778
def _get_code_output(is_last_expr, example_globals, gallery_conf, logging_tee,
750-
images_rst, file_conf):
779+
images_rst, file_conf, script_vars):
751780
"""Obtain standard output and html output in reST."""
752781
last_repr = None
753782
repr_meth = None
@@ -778,10 +807,20 @@ def _get_code_output(is_last_expr, example_globals, gallery_conf, logging_tee,
778807
captured_std = ansi_escape.sub('', captured_std)
779808

780809
# give html output its own header
810+
captured_html = ''
781811
if repr_meth == '_repr_html_':
782812
captured_html = HTML_HEADER.format(indent(last_repr, ' ' * 4))
783-
else:
784-
captured_html = ''
813+
elif repr_meth in ('_repr_png_', '_repr_jpeg_', '_repr_svg_'):
814+
image_path = next(script_vars['image_path_iterator'])
815+
image_path = PurePosixPath(image_path)
816+
if repr_meth in ('_repr_jpeg_', '_repr_svg_'):
817+
suffix = '.svg' if repr_meth == '_repr_svg_' else ".jpg"
818+
image_path = image_path.with_suffix(suffix)
819+
mode = 'w' if repr_meth == '_repr_svg_' else 'wb'
820+
with open(image_path, mode) as f:
821+
f.write(last_repr)
822+
sg_image = repr_meth != '_repr_svg_'
823+
images_rst += figure_rst([image_path], gallery_conf["src_dir"], sg_image=sg_image)
785824

786825
code_output = f"\n{images_rst}\n\n{captured_std}\n{captured_html}\n\n"
787826
return code_output
@@ -882,7 +921,7 @@ def execute_code_block(compiler, block, example_globals, script_vars,
882921

883922
code_output = _get_code_output(
884923
is_last_expr, example_globals, gallery_conf, logging_tee,
885-
images_rst, file_conf
924+
images_rst, file_conf, script_vars
886925
)
887926
finally:
888927
_reset_cwd_syspath(cwd, sys_path)

0 commit comments

Comments
 (0)
0