From 11cc304b06474e029b1abc459296411c390ffc06 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 18 Mar 2024 12:27:39 -0400 Subject: [PATCH] start to work on adding sphinx gallery for docs --- .gitignore | 1 + docs/source/conf.py | 56 ++++++++++ docs/source/index.rst | 1 + examples/README.rst | 2 + examples/desktop/README.rst | 1 + examples/desktop/gridplot/README.rst | 1 + examples/desktop/gridplot/gridplot.py | 4 +- .../desktop/gridplot/gridplot_non_square.py | 1 + fastplotlib/__init__.py | 10 ++ fastplotlib/utils/gallery_scraper.py | 101 ++++++++++++++++++ setup.py | 3 + 11 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 examples/README.rst create mode 100644 examples/desktop/README.rst create mode 100644 examples/desktop/gridplot/README.rst create mode 100644 fastplotlib/utils/gallery_scraper.py diff --git a/.gitignore b/.gitignore index 9857fe4b2..131df05cc 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/source/_gallery # PyBuilder target/ diff --git a/docs/source/conf.py b/docs/source/conf.py index a6a9fd1f6..3d41d4f4e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,6 +4,26 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html import fastplotlib +from sphinx_gallery.sorting import ExplicitOrder +import wgpu.gui.offscreen +import os +import sys + +ROOT_DIR = os.path.abspath(os.path.join(__file__, "..", "..")) +sys.path.insert(0, ROOT_DIR) + +# -- Sphix Gallery Hackz ----------------------------------------------------- +# When building the gallery, render offscreen and don't process +# the event loop while parsing the example + + +def _ignore_offscreen_run(): + wgpu.gui.offscreen.run = lambda: None + + +os.environ["WGPU_FORCE_OFFSCREEN"] = "True" +_ignore_offscreen_run() + # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information @@ -24,6 +44,7 @@ "sphinx_copybutton", "sphinx_design", "nbsphinx", + "sphinx_gallery.gen_gallery" ] autosummary_generate = True @@ -33,6 +54,41 @@ napoleon_custom_sections = ["Features"] +parent_path = os.path.abspath(os.path.join(ROOT_DIR, os.pardir)) + +sphinx_gallery_conf = { + "examples_dirs": f"{parent_path}/examples/desktop", + "gallery_dirs": "_gallery", + "backreferences_dir": "_gallery/backreferences", + # "exclude_implicit_doc": { + # "WgpuRenderer", + # "Resource", + # "WorldObject", + # "Geometry", + # "Material", + # "Controller", + # "Camera", + # "show", + # "Display", + # "Group", + # "Scene", + # "Light", + # }, + "doc_module": ("fastplotlib",), + "image_scrapers": ("fastplotlib",), + # "subsection_order": ExplicitOrder( + # [ + # f"{parent_path}/examples/desktop", + # # "../examples/feature_demo", + # # "../examples/validation", + # # "../examples/other", + # ] + # ), + "remove_config_comments": True, + # Exclude files in 'other' dir from being executed + "filename_pattern": r"^((?![\\/]other[\\/]).)*$", +} + # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/docs/source/index.rst b/docs/source/index.rst index 9dbd30783..fc12e7b8a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,6 +17,7 @@ Welcome to fastplotlib's documentation! :maxdepth: 1 GPU Info + Gallery <_gallery/index.rst> .. toctree:: :maxdepth: 1 diff --git a/examples/README.rst b/examples/README.rst new file mode 100644 index 000000000..09edaa5c6 --- /dev/null +++ b/examples/README.rst @@ -0,0 +1,2 @@ +Examples using fastplotlib +========================== diff --git a/examples/desktop/README.rst b/examples/desktop/README.rst new file mode 100644 index 000000000..d72750943 --- /dev/null +++ b/examples/desktop/README.rst @@ -0,0 +1 @@ +.. rubric:: Desktop Examples \ No newline at end of file diff --git a/examples/desktop/gridplot/README.rst b/examples/desktop/gridplot/README.rst new file mode 100644 index 000000000..7a11d7df6 --- /dev/null +++ b/examples/desktop/gridplot/README.rst @@ -0,0 +1 @@ +.. rubric:: GridPlot Examples diff --git a/examples/desktop/gridplot/gridplot.py b/examples/desktop/gridplot/gridplot.py index 3acf6a8ba..0c078d8db 100644 --- a/examples/desktop/gridplot/gridplot.py +++ b/examples/desktop/gridplot/gridplot.py @@ -1,10 +1,12 @@ """ GridPlot Simple -============ +=============== + Example showing simple 2x2 GridPlot with Standard images from imageio. """ # test_example = true +# sphinx_gallery_fpl_render = True import fastplotlib as fpl import imageio.v3 as iio diff --git a/examples/desktop/gridplot/gridplot_non_square.py b/examples/desktop/gridplot/gridplot_non_square.py index fe43a3c04..20c000455 100644 --- a/examples/desktop/gridplot/gridplot_non_square.py +++ b/examples/desktop/gridplot/gridplot_non_square.py @@ -5,6 +5,7 @@ """ # test_example = true +# sphinx_gallery_fpl_render = True import fastplotlib as fpl import imageio.v3 as iio diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 27545f0ad..e3cf2293f 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -21,6 +21,16 @@ _notebook_print_banner() + +def _get_sg_image_scraper(): + import sphinx_gallery.scrapers + from .utils.gallery_scraper import fpl_scraper + # add webp as supported extension + sphinx_gallery.scrapers._KNOWN_IMG_EXTS += ("webp",) + + return fpl_scraper + + __all__ = [ "Plot", "GridPlot", diff --git a/fastplotlib/utils/gallery_scraper.py b/fastplotlib/utils/gallery_scraper.py new file mode 100644 index 000000000..58109b0ea --- /dev/null +++ b/fastplotlib/utils/gallery_scraper.py @@ -0,0 +1,101 @@ +from pathlib import Path + +import imageio.v3 as iio +from sphinx_gallery.scrapers import figure_rst +from wgpu.gui import WgpuCanvasBase + +from pygfx.renderers import Renderer +from pygfx.utils.show import Display + +# The scraper's default configuration. An example code-block +# may overwrite these values by setting comments of the form +# +# # sphinx_gallery_pygfx_ = +# +# inside the code block. These comments will not be shown in the generated +# gallery example and will be reset after each code block. +default_config = { + "render": False, # if True, render an image + "animate": False, # if True, render a GIF + "target_name": "renderer", # the display to use + # GIF settings + "duration": 3, # how many seconds to record + "loop": 0, # loop forever +} + + +def fpl_scraper(block, block_vars, gallery_conf, **kwargs): + """Scrape pygfx images and animations + Parameters + ---------- + block : tuple + A tuple containing the (label, content, line_number) of the block. + block_vars : dict + Dict of block variables. + gallery_conf : dict + Contains the configuration of Sphinx-Gallery + **kwargs : dict + Additional keyword arguments to pass to + :meth:`~matplotlib.figure.Figure.savefig`, e.g. ``format='svg'``. + The ``format`` kwarg in particular is used to set the file extension + of the output file (currently only 'png', 'jpg', and 'svg' are + supported). + Returns + ------- + rst : str + The ReSTructuredText that will be rendered to HTML containing + the images. This is often produced by :func:`figure_rst`. + """ + + # parse block-level config + scraper_config = default_config.copy() + config_prefix = "# sphinx_gallery_fpl_" + for line in block[1].split("\n"): + if not line.startswith(config_prefix): + continue + + name, value = line[len(config_prefix):].split(" = ") + scraper_config[name] = eval(value) + + if not scraper_config["render"] and not scraper_config["animate"]: + return "" # nothing to do + + target = block_vars["example_globals"][scraper_config["target_name"]] + if isinstance(target, Display): + canvas = target.canvas + elif isinstance(target, Renderer): + canvas = target.target + elif isinstance(target, WgpuCanvasBase): + canvas = target + else: + raise ValueError("`target` must be a Display, Renderer, or Canvas.") + + images = [] + + if scraper_config["render"]: + path_generator = block_vars["image_path_iterator"] + img_path = next(path_generator) + iio.imwrite(img_path, canvas.draw()) + images.append(img_path) + + if scraper_config["animate"]: + frames = [] + + # by default videos are rendered at ~ 30 FPS + n_frames = scraper_config["duration"] * 30 + + for _ in range(n_frames): + frames.append(canvas.draw()) + + path_generator = block_vars["image_path_iterator"] + img_path = Path(next(path_generator)).with_suffix(".webp") + iio.imwrite( + img_path, + frames, + duration=33, + loop=scraper_config["loop"], + lossless=True, + ) + images.append(img_path) + + return figure_rst(images, gallery_conf["src_dir"]) diff --git a/setup.py b/setup.py index 2622b1406..8a8abc071 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,9 @@ "pandoc", "jupyterlab", "sidecar", + "sphinx-gallery", + "imageio", + "matplotlib" ], "notebook": [ "jupyterlab",