Skip to content

Commit

Permalink
BUG: Fix serialization with Sphinx 7.3 (#1289)
Browse files Browse the repository at this point in the history
Co-authored-by: Lucy Liu <[email protected]>
  • Loading branch information
larsoner and lucyleeow authored Apr 24, 2024
1 parent 0b243bf commit f0e716e
Show file tree
Hide file tree
Showing 24 changed files with 722 additions and 448 deletions.
9 changes: 7 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
command: |
pip install --upgrade --only-binary ":all:" pip setuptools
pip install --upgrade --only-binary ":all:" \
numpy matplotlib seaborn statsmodels pillow joblib sphinx pytest traits pyvista memory_profiler "ipython!=8.7.0" plotly graphviz "docutils>=0.18" imageio pydata-sphinx-theme \
numpy matplotlib seaborn statsmodels pillow joblib "sphinx!=7.3.2,!=7.3.3,!=7.3.4,!=7.3.5,!=7.3.6" pytest traits pyvista memory_profiler "ipython!=8.7.0" plotly graphviz "docutils>=0.18" imageio pydata-sphinx-theme \
"jupyterlite-sphinx>=0.8.0,<0.9.0" "jupyterlite-pyodide-kernel<0.1.0" libarchive-c
pip uninstall -yq vtk # pyvista installs vtk above
pip install --upgrade --only-binary ":all" --extra-index-url https://wheels.vtk.org vtk-osmesa
Expand Down Expand Up @@ -66,7 +66,12 @@ jobs:
- attach_workspace:
at: ~/
- bash_env
- run: sphinx-build doc doc/_build/html -nW --keep-going -b html
- run: sphinx-build doc doc/_build/html -nW --keep-going -b html 2>&1 | tee sphinx_log.txt
- run:
name: Check sphinx log for warnings (which are treated as errors)
when: always
command: |
! grep "^.* WARNING: .*$" sphinx_log.txt
- store_artifacts:
path: doc/_build/html/
destination: html
Expand Down
2 changes: 2 additions & 0 deletions .github/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ if [ "$SPHINX_VERSION" == "dev" ]; then
PIP_DEPENDENCIES="--upgrade --pre https://api.github.com/repos/sphinx-doc/sphinx/zipball/master --default-timeout=60 --extra-index-url 'https://pypi.anaconda.org/scientific-python-nightly-wheels/simple' $PIP_DEPENDENCIES"
elif [ "$SPHINX_VERSION" != "default" ]; then
PIP_DEPENDENCIES="sphinx==${SPHINX_VERSION}.* $PIP_DEPENDENCIES"
else
PIP_DEPENDENCIES="sphinx!=7.3.2,!=7.3.3,!=7.3.4,!=7.3.5,!=7.3.6 $PIP_DEPENDENCIES"
fi

set -x
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ repos:
- id: ruff-format
exclude: plot_syntaxerror
- id: ruff
args: ["--fix"]

- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
Expand Down
11 changes: 11 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
=========

v0.16.0
-------
Sphinx 7.3.0 and above changed caching and serialization checks. Now instead of passing
instantiated classes like ``ResetArgv()``, classes like ``FileNameSortKey``, or
callables like ``notebook_modification_function`` in ``sphinx_gallery_conf``,
you should pass fully qualified name strings to classes or callables. If you change
to using name strings, you can simply use a function as the use of classes to ensure
a stable ``__repr__`` would be redundant.

See :ref:`importing_callables` for details.

v0.15.0
-------

Expand Down
3 changes: 2 additions & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
sphinx >= 4, != 5.2.0
# see https://github.com/sphinx-doc/sphinx/issues/12299
sphinx>=4,!= 5.2.0,!=7.3.2,!=7.3.3,!=7.3.4,!=7.3.5,!=7.3.6
pydata-sphinx-theme
pytest
pytest-coverage
Expand Down
168 changes: 75 additions & 93 deletions doc/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,45 +216,43 @@ It uses an approach similar to what
:func:`sphinx_gallery.scrapers.matplotlib_scraper` does under the hood, which
use the helper function :func:`sphinx_gallery.scrapers.figure_rst` to
create the standardized reST. If your package will be used to write an image
file to disk (e.g., PNG or JPEG), we recommend you use a similar approach,
but via a class so that the ``__repr__`` can remain stable across Sphinx runs::
file to disk (e.g., PNG or JPEG), we recommend you use a similar approach::

class MyModuleScraper():
def __repr__(self):
return 'MyModuleScraper'
def my_module_scraper(block, block_vars, gallery_conf):
import mymodule
# We use a list to collect references to image names
image_names = list()
# The `image_path_iterator` is created by Sphinx-Gallery, it will yield
# a path to a file name that adheres to Sphinx-Gallery naming convention.
image_path_iterator = block_vars['image_path_iterator']

def __call__(self, block, block_vars, gallery_conf):
import mymodule
# We use a list to collect references to image names
image_names = list()
# The `image_path_iterator` is created by Sphinx-Gallery, it will yield
# a path to a file name that adheres to Sphinx-Gallery naming convention.
image_path_iterator = block_vars['image_path_iterator']
# Define a list of our already-created figure objects.
list_of_my_figures = mymodule.get_figures()

# Define a list of our already-created figure objects.
list_of_my_figures = mymodule.get_figures()
# Iterate through figure objects, save to disk, and keep track of paths.
for fig, image_path in zip(list_of_my_figures, image_path_iterator):
fig.save_png(image_path)
image_names.append(image_path)

# Iterate through figure objects, save to disk, and keep track of paths.
for fig, image_path in zip(list_of_my_figures, image_path_iterator):
fig.save_png(image_path)
image_names.append(image_path)
# Close all references to figures so they aren't used later.
mymodule.close('all')

# Close all references to figures so they aren't used later.
mymodule.close('all')
# Use the `figure_rst` helper function to generate the reST for this
# code block's figures. Alternatively you can define your own reST.
return figure_rst(image_names, gallery_conf['src_dir'])

# Use the `figure_rst` helper function to generate the reST for this
# code block's figures. Alternatively you can define your own reST.
return figure_rst(image_names, gallery_conf['src_dir'])

This code would be defined either in your ``conf.py`` file, or as a module that
you import into your ``conf.py`` file. The configuration needed to use this
scraper would look like::
This code could be defined either in your ``conf.py`` file, or as a module that
you import into your ``conf.py`` file (see :ref:`importing_callables`).
The configuration needed to use this scraper would look like::

sphinx_gallery_conf = {
...
'image_scrapers': ('matplotlib', MyModuleScraper()),
'image_scrapers': ('matplotlib', "my_module._scraper.my_module_scraper"),
}

Where Sphinx-Gallery will parse the string ``"my_module._scraper.my_module_scraper"``
to import the callable function.

Example 2: detecting image files on disk
----------------------------------------

Expand All @@ -264,67 +262,55 @@ the reST needed to embed them in the documentation. Note that the example script
will still need to be executed to scrape the files, but the images
don't need to be produced during the execution.

We'll use a callable class in this case, and assume it is defined within your
package in a module called ``scraper``. Here is the scraper code::
We assume the function is defined within your
package in a module called ``_scraper``. Here is the scraper code::

from glob import glob
import shutil
import os
from sphinx_gallery.scrapers import figure_rst

class PNGScraper(object):
def __init__(self):
self.seen = set()

def __repr__(self):
return 'PNGScraper'

def __call__(self, block, block_vars, gallery_conf):
# Find all PNG files in the directory of this example.
path_current_example = os.path.dirname(block_vars['src_file'])
pngs = sorted(glob(os.path.join(path_current_example, '*.png')))

# Iterate through PNGs, copy them to the sphinx-gallery output directory
image_names = list()
image_path_iterator = block_vars['image_path_iterator']
for png in pngs:
if png not in self.seen:
self.seen |= set(png)
this_image_path = image_path_iterator.next()
image_names.append(this_image_path)
shutil.move(png, this_image_path)
# Use the `figure_rst` helper function to generate reST for image files
return figure_rst(image_names, gallery_conf['src_dir'])

def png_scraper(block, block_vars, gallery_conf):
# Find all PNG files in the directory of this example.
path_current_example = os.path.dirname(block_vars['src_file'])
pngs = sorted(glob(os.path.join(path_current_example, '*.png')))

# Iterate through PNGs, copy them to the Sphinx-Gallery output directory
image_names = list()
image_path_iterator = block_vars['image_path_iterator']
seen = set()
for png in pngs:
if png not in seen:
seen |= set(png)
this_image_path = image_path_iterator.next()
image_names.append(this_image_path)
shutil.move(png, this_image_path)
# Use the `figure_rst` helper function to generate reST for image files
return figure_rst(image_names, gallery_conf['src_dir'])

Then, in our ``conf.py`` file, we include the following code::

from mymodule import PNGScraper

sphinx_gallery_conf = {
...
'image_scrapers': ('matplotlib', PNGScraper()),
'image_scrapers': ('matplotlib', 'my_module._scraper.png_scraper'),
}

Example 3: matplotlib with SVG format
-------------------------------------
The :func:`sphinx_gallery.scrapers.matplotlib_scraper` supports ``**kwargs``
to pass to :meth:`matplotlib.figure.Figure.savefig`, one of which is the
``format`` argument. See :ref:`custom_scraper` for supported formats.
To use SVG, you can do::
To use SVG you can define the following function and ensure it is
:ref:`importable <importing_callables>`::

from sphinx_gallery.scrapers import matplotlib_scraper
def matplotlib_svg_scraper(*args, **kwargs):
return matplotlib_scraper(*args, format='svg', **kwargs)

class matplotlib_svg_scraper(object):
def __repr__(self):
return self.__class__.__name__

def __call__(self, *args, **kwargs):
return matplotlib_scraper(*args, format='svg', **kwargs)
Then in your ``conf.py``::

sphinx_gallery_conf = {
...
'image_scrapers': (matplotlib_svg_scraper(),),
'image_scrapers': ("sphinxext.matplotlib_svg_scraper",),
...
}

Expand All @@ -335,35 +321,31 @@ writing a customized scraper class or function.

Example 4: Mayavi scraper
-------------------------
Historically, sphinx-gallery supported scraping Mayavi figures as well as
Historically, Sphinx-Gallery supported scraping Mayavi figures as well as
matplotlib figures. However, due to the complexity of maintaining the scraper,
support was deprecated in version 0.12.0. To continue using a Mayavi scraping,
consider using something like the following::

from sphinx_gallery.scrapers import figure_rst

class MayaviScraper():
def __repr__(self):
return 'MyModuleScraper'

def __call__(self, block, block_vars, gallery_conf):
from mayavi import mlab
image_path_iterator = block_vars['image_path_iterator']
image_paths = list()
e = mlab.get_engine()
for scene, image_path in zip(e.scenes, image_path_iterator):
try:
mlab.savefig(image_path, figure=scene)
except Exception:
mlab.close(all=True)
raise
# make sure the image is not too large
scale_image(image_path, image_path, 850, 999)
if 'images' in gallery_conf['compress_images']:
optipng(image_path, gallery_conf['compress_images_args'])
image_paths.append(image_path)
mlab.close(all=True)
return figure_rst(image_paths, gallery_conf['src_dir'])
def mayavi_scraper(self, block, block_vars, gallery_conf):
from mayavi import mlab
image_path_iterator = block_vars['image_path_iterator']
image_paths = list()
e = mlab.get_engine()
for scene, image_path in zip(e.scenes, image_path_iterator):
try:
mlab.savefig(image_path, figure=scene)
except Exception:
mlab.close(all=True)
raise
# make sure the image is not too large
scale_image(image_path, image_path, 850, 999)
if 'images' in gallery_conf['compress_images']:
optipng(image_path, gallery_conf['compress_images_args'])
image_paths.append(image_path)
mlab.close(all=True)
return figure_rst(image_paths, gallery_conf['src_dir'])

Integrate custom scrapers with Sphinx-Gallery
---------------------------------------------
Expand Down Expand Up @@ -421,14 +403,15 @@ For example, to reset matplotlib to always use the ``ggplot`` style, you could d
style.use('ggplot')

Any custom functions can be defined (or imported) in ``conf.py`` and given to
the ``reset_modules`` configuration key. To add the function defined above::
the ``reset_modules`` configuration key. To add the function defined above (assuming
you've make it :ref:`importable <importing_callables>`)::

sphinx_gallery_conf = {
...
'reset_modules': (reset_mpl, 'seaborn'),
'reset_modules': ("sphinxext.reset_mpl", "seaborn"),
}

In the config above ``'seaborn'`` refers to the native seaborn resetting
In the config above ``"seaborn"`` refers to the native seaborn resetting
function (see :ref:`reset_modules`).

.. note:: Using resetters such as ``reset_mpl`` that deviate from the
Expand All @@ -441,7 +424,6 @@ after an example, a function signature with three parameters can be used, where
the third parameter is required to be named ``when``::

def reset_mpl(gallery_conf, fname, when):

import matplotlib as mpl
mpl.rcParams['lines.linewidth'] = 2
if when == 'after' and fname=='dashed_lines':
Expand Down
Loading

0 comments on commit f0e716e

Please sign in to comment.