sphinx-fplotlib-gallery 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ # pixi environments
2
+ .pixi/*
3
+ !.pixi/config.toml
4
+
5
+ # build artifacts
6
+ *build/
7
+ *.mod
8
+ *.o
9
+ *.a
10
+ *.exe
11
+
12
+ # generated plots
13
+ tests/out/
14
+ tests/refs/
15
+ demo.svg
16
+ fplotlib_show.svg
17
+ *.svg
18
+
19
+ # python
20
+ __pycache__/
21
+ *.pyc
22
+
23
+ # sphinx / sphinx-gallery
24
+ docs/_build/
25
+ docs/auto_gallery/
26
+ docs/sg_execution_times.rst
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fortran-lang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.5
2
+ Name: sphinx-fplotlib-gallery
3
+ Version: 0.1.0
4
+ Summary: Sphinx-Gallery support for Fortran examples that plot with fplotlib
5
+ Project-URL: Homepage, https://github.com/fortran-lang/fplotlib
6
+ Project-URL: Documentation, https://fortran-lang.github.io/fplotlib/
7
+ Project-URL: Source, https://github.com/fortran-lang/fplotlib/tree/main/sphinx-fplotlib-gallery
8
+ Project-URL: Issues, https://github.com/fortran-lang/fplotlib/issues
9
+ Author: fplotlib maintainers
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: fortran,fplotlib,fpm,plotting,sphinx,sphinx-gallery
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: Sphinx :: Extension
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Programming Language :: Fortran
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Topic :: Documentation :: Sphinx
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: sphinx-gallery>=0.17
22
+ Requires-Dist: sphinx>=7
23
+ Description-Content-Type: text/markdown
24
+
25
+ # sphinx-fplotlib-gallery
26
+
27
+ A [Sphinx](https://www.sphinx-doc.org) extension that lets
28
+ [Sphinx-Gallery](https://sphinx-gallery.github.io) build, run and show Fortran
29
+ examples that draw with [fplotlib](https://github.com/fortran-lang/fplotlib).
30
+
31
+ Each example is a Fortran program. The extension builds all of them with
32
+ [fpm](https://fpm.fortran-lang.org), runs each one in a scratch directory, and
33
+ embeds every picture it saved under the code block that saved it, the same way
34
+ Sphinx-Gallery shows matplotlib figures.
35
+
36
+ ## Install
37
+
38
+ ```sh
39
+ pip install sphinx-fplotlib-gallery
40
+ ```
41
+
42
+ Building the gallery also needs `fpm` and a Fortran compiler on `PATH`.
43
+ Without them, pass `-D plot_gallery=0` to `sphinx-build`: the examples are then
44
+ rendered and highlighted but not run.
45
+
46
+ ## Configure
47
+
48
+ In `conf.py`:
49
+
50
+ ```python
51
+ extensions = [
52
+ "sphinx_gallery.gen_gallery",
53
+ "fplotlib_gallery",
54
+ ]
55
+
56
+ sphinx_gallery_conf = {
57
+ "examples_dirs": ["gallery"], # where the .f90 examples live
58
+ "gallery_dirs": ["auto_gallery"], # where the generated pages go
59
+ "filename_pattern": r"[\/]plot_", # which examples to run
60
+ "image_scrapers": ("fplotlib_gallery.fplotlib_scraper",),
61
+ "reset_modules": (),
62
+ "inspect_global_variables": False,
63
+ }
64
+ ```
65
+
66
+ The Fortran suffixes are registered with Sphinx-Gallery automatically.
67
+
68
+ ### Where fplotlib comes from
69
+
70
+ `fplotlib_gallery_root` is the fpm package the documentation belongs to. It
71
+ defaults to the parent of the Sphinx source directory.
72
+
73
+ - **Documenting fplotlib itself:** the examples build against the checkout.
74
+ - **Documenting another fpm package:** the examples depend on that package by
75
+ path, and on fplotlib too unless the package already depends on it.
76
+ - **No `fpm.toml` there:** the examples depend on fplotlib alone.
77
+
78
+ In the last two cases fplotlib comes from `fplotlib_gallery_fplotlib`, an fpm
79
+ dependency spec written as a TOML inline value. It defaults to the `main`
80
+ branch on GitHub. Pin it for reproducible documentation:
81
+
82
+ ```python
83
+ fplotlib_gallery_fplotlib = '{ git = "https://github.com/fortran-lang/fplotlib", tag = "v0.1.0" }'
84
+ ```
85
+
86
+ ### Other settings
87
+
88
+ | Setting | Default | Meaning |
89
+ | --- | --- | --- |
90
+ | `fplotlib_gallery_dependencies` | `{}` | Extra fpm dependencies, as `{name: TOML value}`, e.g. `{"stdlib": '"*"'}` |
91
+ | `fplotlib_gallery_compiler` | fpm's choice | Value passed to `fpm --compiler` |
92
+ | `fplotlib_gallery_fpm_flags` | autodetected | Value passed to `fpm --flag` |
93
+ | `fplotlib_gallery_fpm` | `"fpm"` | The fpm executable |
94
+ | `fplotlib_gallery_build_dir` | `<srcdir>/_build/fplotlib-gallery` | Where the generated fpm project is built |
95
+ | `fplotlib_gallery_timeout` | `600` | Seconds allowed per fpm call and per example |
96
+ | `fplotlib_gallery_save_functions` | `["savefig", "save_animation"]` | Calls whose filename ties a picture to a code block |
97
+
98
+ ## Write an example
99
+
100
+ Name the file to match `filename_pattern`, for example `plot_sine.f90`. Start it
101
+ with a `!` comment block holding the page title and introduction. Later prose
102
+ starts with `! %%` and splits the code into blocks.
103
+
104
+ ```fortran
105
+ ! Sine wave
106
+ ! =========
107
+ !
108
+ ! A first plot.
109
+ program plot_sine
110
+ use fplotlib
111
+ implicit none
112
+ integer :: i
113
+ real(8) :: x(100)
114
+ x = [(0.1d0 * i, i = 0, 99)]
115
+
116
+ ! %%
117
+ ! Each picture appears under the block that saved it.
118
+
119
+ call plot(x, sin(x), "b-")
120
+ call savefig("sine.svg")
121
+ end program plot_sine
122
+ ```
123
+
124
+ Every format fplotlib writes works, including animated GIF from
125
+ `save_animation`. Each example runs in an empty directory of its own.
126
+
127
+ ## Troubleshooting
128
+
129
+ On Windows, fpm can fail with `Cannot open file '...o.digest'` when the docs
130
+ live in a deep directory, because its build paths exceed the 260-character
131
+ limit. Point `fplotlib_gallery_build_dir` at a short path such as
132
+ `C:/tmp/fplotlib-gallery`, or enable long paths in Windows.
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,112 @@
1
+ # sphinx-fplotlib-gallery
2
+
3
+ A [Sphinx](https://www.sphinx-doc.org) extension that lets
4
+ [Sphinx-Gallery](https://sphinx-gallery.github.io) build, run and show Fortran
5
+ examples that draw with [fplotlib](https://github.com/fortran-lang/fplotlib).
6
+
7
+ Each example is a Fortran program. The extension builds all of them with
8
+ [fpm](https://fpm.fortran-lang.org), runs each one in a scratch directory, and
9
+ embeds every picture it saved under the code block that saved it, the same way
10
+ Sphinx-Gallery shows matplotlib figures.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ pip install sphinx-fplotlib-gallery
16
+ ```
17
+
18
+ Building the gallery also needs `fpm` and a Fortran compiler on `PATH`.
19
+ Without them, pass `-D plot_gallery=0` to `sphinx-build`: the examples are then
20
+ rendered and highlighted but not run.
21
+
22
+ ## Configure
23
+
24
+ In `conf.py`:
25
+
26
+ ```python
27
+ extensions = [
28
+ "sphinx_gallery.gen_gallery",
29
+ "fplotlib_gallery",
30
+ ]
31
+
32
+ sphinx_gallery_conf = {
33
+ "examples_dirs": ["gallery"], # where the .f90 examples live
34
+ "gallery_dirs": ["auto_gallery"], # where the generated pages go
35
+ "filename_pattern": r"[\/]plot_", # which examples to run
36
+ "image_scrapers": ("fplotlib_gallery.fplotlib_scraper",),
37
+ "reset_modules": (),
38
+ "inspect_global_variables": False,
39
+ }
40
+ ```
41
+
42
+ The Fortran suffixes are registered with Sphinx-Gallery automatically.
43
+
44
+ ### Where fplotlib comes from
45
+
46
+ `fplotlib_gallery_root` is the fpm package the documentation belongs to. It
47
+ defaults to the parent of the Sphinx source directory.
48
+
49
+ - **Documenting fplotlib itself:** the examples build against the checkout.
50
+ - **Documenting another fpm package:** the examples depend on that package by
51
+ path, and on fplotlib too unless the package already depends on it.
52
+ - **No `fpm.toml` there:** the examples depend on fplotlib alone.
53
+
54
+ In the last two cases fplotlib comes from `fplotlib_gallery_fplotlib`, an fpm
55
+ dependency spec written as a TOML inline value. It defaults to the `main`
56
+ branch on GitHub. Pin it for reproducible documentation:
57
+
58
+ ```python
59
+ fplotlib_gallery_fplotlib = '{ git = "https://github.com/fortran-lang/fplotlib", tag = "v0.1.0" }'
60
+ ```
61
+
62
+ ### Other settings
63
+
64
+ | Setting | Default | Meaning |
65
+ | --- | --- | --- |
66
+ | `fplotlib_gallery_dependencies` | `{}` | Extra fpm dependencies, as `{name: TOML value}`, e.g. `{"stdlib": '"*"'}` |
67
+ | `fplotlib_gallery_compiler` | fpm's choice | Value passed to `fpm --compiler` |
68
+ | `fplotlib_gallery_fpm_flags` | autodetected | Value passed to `fpm --flag` |
69
+ | `fplotlib_gallery_fpm` | `"fpm"` | The fpm executable |
70
+ | `fplotlib_gallery_build_dir` | `<srcdir>/_build/fplotlib-gallery` | Where the generated fpm project is built |
71
+ | `fplotlib_gallery_timeout` | `600` | Seconds allowed per fpm call and per example |
72
+ | `fplotlib_gallery_save_functions` | `["savefig", "save_animation"]` | Calls whose filename ties a picture to a code block |
73
+
74
+ ## Write an example
75
+
76
+ Name the file to match `filename_pattern`, for example `plot_sine.f90`. Start it
77
+ with a `!` comment block holding the page title and introduction. Later prose
78
+ starts with `! %%` and splits the code into blocks.
79
+
80
+ ```fortran
81
+ ! Sine wave
82
+ ! =========
83
+ !
84
+ ! A first plot.
85
+ program plot_sine
86
+ use fplotlib
87
+ implicit none
88
+ integer :: i
89
+ real(8) :: x(100)
90
+ x = [(0.1d0 * i, i = 0, 99)]
91
+
92
+ ! %%
93
+ ! Each picture appears under the block that saved it.
94
+
95
+ call plot(x, sin(x), "b-")
96
+ call savefig("sine.svg")
97
+ end program plot_sine
98
+ ```
99
+
100
+ Every format fplotlib writes works, including animated GIF from
101
+ `save_animation`. Each example runs in an empty directory of its own.
102
+
103
+ ## Troubleshooting
104
+
105
+ On Windows, fpm can fail with `Cannot open file '...o.digest'` when the docs
106
+ live in a deep directory, because its build paths exceed the 260-character
107
+ limit. Point `fplotlib_gallery_build_dir` at a short path such as
108
+ `C:/tmp/fplotlib-gallery`, or enable long paths in Windows.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sphinx-fplotlib-gallery"
7
+ dynamic = ["version"]
8
+ description = "Sphinx-Gallery support for Fortran examples that plot with fplotlib"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "fplotlib maintainers" }]
14
+ keywords = ["sphinx", "sphinx-gallery", "fortran", "fpm", "fplotlib", "plotting"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: Sphinx :: Extension",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ "Programming Language :: Fortran",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Documentation :: Sphinx",
23
+ ]
24
+ dependencies = [
25
+ "sphinx>=7",
26
+ # The extension replaces Sphinx-Gallery's executor for Fortran sources and
27
+ # relies on its block parser and scraper API.
28
+ "sphinx-gallery>=0.17",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/fortran-lang/fplotlib"
33
+ Documentation = "https://fortran-lang.github.io/fplotlib/"
34
+ Source = "https://github.com/fortran-lang/fplotlib/tree/main/sphinx-fplotlib-gallery"
35
+ Issues = "https://github.com/fortran-lang/fplotlib/issues"
36
+
37
+ [tool.hatch.version]
38
+ path = "src/fplotlib_gallery/__init__.py"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/fplotlib_gallery"]
@@ -0,0 +1,803 @@
1
+ """Sphinx-Gallery support for Fortran examples that draw with fplotlib.
2
+
3
+ Sphinx-Gallery splits and highlights non-Python examples out of the box -- its
4
+ ``BlockParser`` already recognises Fortran comments, so a ``.f90`` file whose
5
+ header is a ``!`` comment block becomes a gallery page with a title, prose and
6
+ syntax-highlighted code. What it does not do is *run* them: every code block is
7
+ handed to :func:`compile`, because upstream only ever executes Python.
8
+
9
+ A Fortran example is not a script anyway. It is a single compilation unit that
10
+ has to be built and run as a whole, so this extension replaces Sphinx-Gallery's
11
+ per-block executor for Fortran sources with one that
12
+
13
+ 1. generates a throw-away fpm project that depends on fplotlib (and on the
14
+ project the documentation belongs to, when that is an fpm package), plus
15
+ whatever ``fplotlib_gallery_dependencies`` adds, and whose examples are the
16
+ gallery sources,
17
+ 2. builds it once per documentation build,
18
+ 3. runs each example in a private working directory, and
19
+ 4. hands the pictures it wrote to :func:`fplotlib_scraper`, which moves them
20
+ into Sphinx-Gallery's image directory and emits the ``image-sg`` directives.
21
+
22
+ Pictures are attributed to the code block that saved them: the scraper reads the
23
+ ``savefig`` and ``save_animation`` calls out of the block's own source, so a
24
+ figure appears under the code that produced it, exactly as it does for
25
+ matplotlib examples. Any picture the example wrote without a literal filename
26
+ -- a name assembled at run time, say -- is attached to the last code block,
27
+ along with everything the program printed.
28
+
29
+ Because any format fplotlib writes is just a file on disk, SVG and animated GIF
30
+ need no special handling: Sphinx-Gallery already embeds and thumbnails both.
31
+
32
+ Usage in ``conf.py``::
33
+
34
+ extensions = [..., "sphinx_gallery.gen_gallery", "fplotlib_gallery"]
35
+
36
+ sphinx_gallery_conf = {
37
+ "examples_dirs": "gallery",
38
+ "gallery_dirs": "auto_gallery",
39
+ "image_scrapers": ("fplotlib_gallery.fplotlib_scraper",),
40
+ "reset_modules": (),
41
+ }
42
+
43
+ ``example_extensions`` and ``filetype_parsers`` are filled in automatically for
44
+ the Fortran suffixes; everything else is ordinary Sphinx-Gallery configuration.
45
+
46
+ Where fplotlib comes from
47
+ -------------------------
48
+
49
+ ``fplotlib_gallery_root`` names the fpm package the documentation belongs to.
50
+ It defaults to the parent of the Sphinx source directory, which is right for the
51
+ usual ``<project>/docs`` layout.
52
+
53
+ * If that package *is* fplotlib, the examples are built against the checkout,
54
+ so the gallery always shows the code being documented.
55
+ * If it is another fpm package, the examples depend on it by path, and on
56
+ fplotlib too unless the package already declares fplotlib itself.
57
+ * If there is no ``fpm.toml`` there at all, the examples depend on fplotlib
58
+ alone.
59
+
60
+ ``fplotlib_gallery_fplotlib`` is the fpm dependency spec used for fplotlib in
61
+ the last two cases, as a TOML inline value. It defaults to the ``main`` branch
62
+ of https://github.com/fortran-lang/fplotlib; pin a tag or revision for
63
+ reproducible docs, or set it to ``""`` to never add fplotlib.
64
+
65
+ Settings
66
+ --------
67
+
68
+ ``fplotlib_gallery_root``
69
+ The fpm package the documentation belongs to (see above).
70
+ ``fplotlib_gallery_fplotlib``
71
+ The fplotlib dependency spec (see above).
72
+ ``fplotlib_gallery_fpm``
73
+ The fpm executable. Default ``"fpm"``.
74
+ ``fplotlib_gallery_compiler``
75
+ ``--compiler`` passed to fpm. Default: fpm's own choice.
76
+ ``fplotlib_gallery_fpm_flags``
77
+ Extra ``--flag`` string; ``None`` to autodetect. The
78
+ ``FPLOTLIB_GALLERY_FPM_FLAGS`` environment variable overrides the default.
79
+ ``fplotlib_gallery_dependencies``
80
+ Extra fpm dependencies of the examples, as ``{name: TOML value}``.
81
+ ``fplotlib_gallery_build_dir``
82
+ Where the runner project lives. Default ``<srcdir>/_build/fplotlib-gallery``.
83
+ ``fplotlib_gallery_timeout``
84
+ Seconds allowed per fpm call and per example. Default 600.
85
+ ``fplotlib_gallery_save_functions``
86
+ Calls whose literal filename argument attributes a picture to a code block.
87
+ Default ``["savefig", "save_animation"]``.
88
+
89
+ Writing an example
90
+ ------------------
91
+
92
+ Drop a Fortran program into the gallery directory, named to match
93
+ ``filename_pattern`` (``plot_*.f90`` by default). Its first lines are a ``!``
94
+ comment block holding the page's reST: a title, its underline, and as much
95
+ prose as it deserves. Sphinx-Gallery's parser ends a text block at the first
96
+ blank line, so keep the header contiguous and write ``!`` on its own for the
97
+ blank lines inside it.
98
+
99
+ Later prose starts with ``! %%`` and continues on ``!`` lines, which also splits
100
+ the code either side of it into separate blocks::
101
+
102
+ ! %%
103
+ ! Now the interesting part.
104
+
105
+ call plot(x, y, "b-")
106
+ call savefig("picture.svg")
107
+
108
+ Nothing else is required: ``savefig("name.svg")`` puts that picture under this
109
+ block, and any format fplotlib writes will do.
110
+
111
+ Two things to know when building animations. Frames have to come out the same
112
+ size, so keep anything that changes the layout out of the frame loop, and
113
+ ``save_animation`` only writes GIF.
114
+
115
+ Finally, the example runs in an empty scratch directory of its own, so it may
116
+ write whatever it likes and should refer to its own files by plain relative
117
+ names. ``sphinx-build -D plot_gallery=0`` renders the pages without a Fortran
118
+ toolchain: the examples are still parsed and highlighted, just not run.
119
+
120
+ A note on the timings
121
+ ---------------------
122
+
123
+ The seconds Sphinx-Gallery prints for an example -- on its page and in
124
+ ``sg_execution_times`` -- are the time the *program* took, plus the scraping of
125
+ its pictures. Compiling is not included. One fpm call builds the library and
126
+ every example together, so there is no honest way to split that cost per
127
+ example: charging it to whichever example ran first would make that one look
128
+ slow and would swing by a factor of five with the state of fpm's cache. The
129
+ build is timed as a whole and logged instead, as ``[fplotlib] fpm finished in
130
+ ...``.
131
+ """
132
+
133
+ from __future__ import annotations
134
+
135
+ import os
136
+ import re
137
+ import shutil
138
+ import subprocess
139
+ from dataclasses import dataclass, field
140
+ from pathlib import Path, PurePath
141
+ from textwrap import indent
142
+ from time import time
143
+
144
+ from sphinx.errors import ExtensionError
145
+ from sphinx.util import logging
146
+ from sphinx.util.console import blue, bold, red
147
+
148
+ logger = logging.getLogger(__name__)
149
+
150
+ __version__ = "0.1.0"
151
+
152
+ #: Source suffixes treated as Fortran gallery examples (compared lower-cased).
153
+ FORTRAN_SUFFIXES = (".f90", ".f95", ".f03", ".f08", ".f")
154
+
155
+ #: Picture formats Sphinx-Gallery knows how to embed and thumbnail. Kept in
156
+ #: step with ``sphinx_gallery.scrapers._KNOWN_IMG_EXTS``.
157
+ IMAGE_SUFFIXES = (".png", ".svg", ".jpg", ".gif", ".webp")
158
+
159
+ #: Sphinx-Gallery's own warning namespace, so a ``suppress_warnings`` entry that
160
+ #: already covers the gallery keeps covering these too.
161
+ _WARNING_TYPE = "sphx_glr"
162
+
163
+ _EXE_SUFFIX = ".exe" if os.name == "nt" else ""
164
+
165
+ #: fplotlib's fpm dependency spec when the documented package is not fplotlib.
166
+ DEFAULT_FPLOTLIB = '{ git = "https://github.com/fortran-lang/fplotlib", branch = "main" }'
167
+
168
+ _NAME = re.compile(r"""^\s*name\s*=\s*(['"])(?P<name>[^'"]+)\1""", re.MULTILINE)
169
+ _DEPENDS_ON_FPLOTLIB = re.compile(
170
+ r"^\s*(?:fplotlib\s*=|\[\s*dependencies\s*\.\s*fplotlib\s*\])", re.MULTILINE
171
+ )
172
+
173
+ _ANSI = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
174
+
175
+ _MANIFEST = """\
176
+ # Generated by the fplotlib_gallery Sphinx extension -- do not edit by hand.
177
+ #
178
+ # A throw-away project whose examples are the gallery sources and whose
179
+ # dependencies are fplotlib, the package these docs are built from, and those
180
+ # fplotlib_gallery_dependencies names.
181
+ name = "fplotlib-gallery"
182
+ version = "0.0.0"
183
+
184
+ [build]
185
+ auto-executables = false
186
+ auto-examples = false
187
+ auto-tests = false
188
+
189
+ [dependencies]
190
+ {dependencies}{examples}"""
191
+
192
+ # Every example gets a source directory to itself: fpm compiles all the sources
193
+ # under an example's `source-dir`, so sharing one would make each example fail
194
+ # to build whenever any other example does.
195
+ _MANIFEST_EXAMPLE = """
196
+ [[example]]
197
+ name = "{name}"
198
+ source-dir = "example/{name}"
199
+ main = "{name}.f90"
200
+ """
201
+
202
+
203
+ def _read_manifest(path: Path) -> tuple[str, bool]:
204
+ """Return the package name in fpm manifest `path`, and whether it declares
205
+ fplotlib as a dependency.
206
+
207
+ A regex rather than a TOML parser: ``tomllib`` is Python 3.11+, and the
208
+ first ``name =`` of an fpm manifest is the package's by convention.
209
+ """
210
+ text = path.read_text(encoding="utf-8")
211
+ match = _NAME.search(text)
212
+ if match is None:
213
+ raise ExtensionError(f"fplotlib_gallery: no package name in {path}.")
214
+ return match.group("name"), bool(_DEPENDS_ON_FPLOTLIB.search(text))
215
+
216
+
217
+ class FortranExampleError(RuntimeError):
218
+ """An example could not be built, or did not run to completion."""
219
+
220
+
221
+ @dataclass
222
+ class _Example:
223
+ """A gallery source and the fpm example name it is built under."""
224
+
225
+ name: str
226
+ source: Path
227
+
228
+
229
+ @dataclass
230
+ class _Run:
231
+ """What one example produced: its output and the pictures it wrote."""
232
+
233
+ stdout: str
234
+ #: Base name -> path in the working directory, for filename lookups.
235
+ by_name: dict[str, Path]
236
+ #: Every picture in the order it was written, for the trailing sweep.
237
+ in_order: list[Path]
238
+ #: Seconds the program itself took, excluding everything fpm did.
239
+ duration: float
240
+ #: Pictures already emitted, so no two blocks show the same figure.
241
+ claimed: set[Path] = field(default_factory=set)
242
+
243
+
244
+ def _default_flags(compiler: str | None) -> str:
245
+ """Return extra fpm flags for `compiler`, or an empty string.
246
+
247
+ A conda-forge gfortran on Windows links against DLLs that anything else on
248
+ ``PATH`` can shadow, and the example then dies with
249
+ ``STATUS_ENTRYPOINT_NOT_FOUND`` before it draws anything. Linking the
250
+ runtime statically costs nothing for examples this small and makes the build
251
+ independent of whatever else happens to be installed.
252
+ """
253
+ env = os.environ.get("FPLOTLIB_GALLERY_FPM_FLAGS")
254
+ if env is not None:
255
+ return env
256
+ is_gfortran = compiler is None or "gfortran" in Path(compiler).name.lower()
257
+ if is_gfortran and os.name == "nt":
258
+ return "-static-libgfortran -static-libgcc"
259
+ return ""
260
+
261
+
262
+ class FortranGallery:
263
+ """Builds and runs the Fortran examples of one Sphinx project."""
264
+
265
+ def __init__(self, app) -> None:
266
+ config = app.config
267
+ self.srcdir = Path(app.srcdir)
268
+ self.fpm = config.fplotlib_gallery_fpm
269
+ self.compiler = config.fplotlib_gallery_compiler
270
+ self.timeout = config.fplotlib_gallery_timeout
271
+ self.dependencies = dict(config.fplotlib_gallery_dependencies or {})
272
+ flags = config.fplotlib_gallery_fpm_flags
273
+ self.flags = _default_flags(self.compiler) if flags is None else flags
274
+
275
+ root = config.fplotlib_gallery_root
276
+ explicit_root = bool(root)
277
+ self.root = Path(root) if root else self.srcdir.parent
278
+ if not self.root.is_absolute():
279
+ self.root = (self.srcdir / self.root).resolve()
280
+ manifest = self.root / "fpm.toml"
281
+ #: The documented fpm package, or None when the gallery stands alone.
282
+ self.package: str | None = None
283
+ package_uses_fplotlib = False
284
+ if manifest.is_file():
285
+ self.package, package_uses_fplotlib = _read_manifest(manifest)
286
+ elif explicit_root:
287
+ raise ExtensionError(
288
+ f"fplotlib_gallery: no fpm.toml under {self.root}. Point "
289
+ f"'fplotlib_gallery_root' in conf.py at an fpm package."
290
+ )
291
+ if self.package == "fplotlib" or package_uses_fplotlib:
292
+ self.fplotlib_spec = ""
293
+ else:
294
+ spec = config.fplotlib_gallery_fplotlib
295
+ self.fplotlib_spec = DEFAULT_FPLOTLIB if spec is None else spec
296
+
297
+ # Deliberately not under the output directory -- that is published as-is
298
+ # -- but kept between builds all the same, so fpm keeps its cache and
299
+ # rebuilding unchanged examples costs a second or two.
300
+ build_dir = config.fplotlib_gallery_build_dir
301
+ if build_dir is None:
302
+ build_dir = self.srcdir / "_build" / "fplotlib-gallery"
303
+ self.build_dir = Path(build_dir)
304
+ if not self.build_dir.is_absolute():
305
+ self.build_dir = self.srcdir / self.build_dir
306
+
307
+ self._examples: dict[Path, _Example] | None = None
308
+ self._runs: dict[Path, _Run] = {}
309
+ self._prepared = False
310
+ self._batch_built = False
311
+ self._exe_dir: Path | None = None
312
+ self._gallery_conf = config.sphinx_gallery_conf
313
+
314
+ # -- discovery ---------------------------------------------------------
315
+
316
+ @property
317
+ def examples(self) -> dict[Path, _Example]:
318
+ """Every Fortran example under the configured ``examples_dirs``."""
319
+ if self._examples is None:
320
+ self._examples = self._discover()
321
+ return self._examples
322
+
323
+ def _discover(self) -> dict[Path, _Example]:
324
+ dirs = self._gallery_conf.get("examples_dirs", [])
325
+ if isinstance(dirs, (str, Path)):
326
+ dirs = [dirs]
327
+ found: dict[Path, _Example] = {}
328
+ names: dict[str, Path] = {}
329
+ for entry in dirs:
330
+ base = Path(entry)
331
+ if not base.is_absolute():
332
+ base = self.srcdir / base
333
+ base = base.resolve()
334
+ if not base.is_dir():
335
+ continue
336
+ for source in sorted(base.rglob("*")):
337
+ if source.suffix.lower() not in FORTRAN_SUFFIXES:
338
+ continue
339
+ source = source.resolve()
340
+ stem = source.relative_to(base).with_suffix("").as_posix()
341
+ name = re.sub(r"[^0-9A-Za-z_]", "_", stem)
342
+ clash = names.get(name)
343
+ if clash is not None and clash != source:
344
+ raise ExtensionError(
345
+ f"fplotlib_gallery: {source} and {clash} both map to the "
346
+ f"fpm example name '{name}'. Rename one of them."
347
+ )
348
+ names[name] = source
349
+ found[source] = _Example(name=name, source=source)
350
+ return found
351
+
352
+ # -- building ----------------------------------------------------------
353
+
354
+ def _fpm(self, *args: str, example: str | None = None):
355
+ command = [self.fpm, *args]
356
+ if example is not None:
357
+ command += ["--example", example]
358
+ if self.compiler:
359
+ command += ["--compiler", self.compiler]
360
+ if self.flags:
361
+ command += ["--flag", self.flags]
362
+ logger.debug("[fplotlib_gallery] %s (in %s)", " ".join(command), self.build_dir)
363
+ try:
364
+ return subprocess.run(
365
+ command,
366
+ cwd=self.build_dir,
367
+ capture_output=True,
368
+ text=True,
369
+ errors="replace",
370
+ timeout=self.timeout,
371
+ )
372
+ except FileNotFoundError as exc:
373
+ raise ExtensionError(
374
+ f"fplotlib_gallery: could not run '{self.fpm}'. Install the Fortran "
375
+ f"package manager (https://fpm.fortran-lang.org) or point "
376
+ f"'fplotlib_gallery_fpm' at it."
377
+ ) from exc
378
+ except subprocess.TimeoutExpired as exc:
379
+ raise FortranExampleError(
380
+ f"'{' '.join(command)}' did not finish within {self.timeout}s "
381
+ f"(fplotlib_gallery_timeout)."
382
+ ) from exc
383
+
384
+ def _prepare(self) -> None:
385
+ """Write the runner project and build every example, once per run."""
386
+ if self._prepared:
387
+ return
388
+ self._prepared = True
389
+ examples = self.examples
390
+ if not examples:
391
+ return
392
+
393
+ source_dir = self.build_dir / "example"
394
+ source_dir.mkdir(parents=True, exist_ok=True)
395
+
396
+ # Copy only what changed, so fpm's per-file cache stays warm.
397
+ wanted = set()
398
+ for example in examples.values():
399
+ target = source_dir / example.name / f"{example.name}.f90"
400
+ wanted.add(example.name)
401
+ target.parent.mkdir(exist_ok=True)
402
+ text = example.source.read_text(encoding="utf-8")
403
+ if not target.is_file() or target.read_text(encoding="utf-8") != text:
404
+ target.write_text(text, encoding="utf-8")
405
+ for stale in source_dir.iterdir():
406
+ if stale.name in wanted:
407
+ continue
408
+ if stale.is_dir():
409
+ shutil.rmtree(stale, ignore_errors=True)
410
+ else:
411
+ stale.unlink()
412
+
413
+ self._write_manifest([example.name for example in examples.values()])
414
+
415
+ count = len(examples)
416
+ plural = "s" if count != 1 else ""
417
+ logger.info(
418
+ bold("[fplotlib] ") + f"building {count} Fortran example{plural} with fpm"
419
+ )
420
+ # One build for the whole gallery, which is the fast path: the library is
421
+ # compiled once and every example links against it. A failure is not
422
+ # fatal, because fpm builds every target it knows about before running
423
+ # any of them -- so one example that does not compile would otherwise be
424
+ # reported against all of them. _executable() then retries the examples
425
+ # one at a time, and only the broken one is reported as failing.
426
+ started = time()
427
+ process = self._fpm("build")
428
+ self._batch_built = process.returncode == 0
429
+ # Reported here because it is not in any example's time: it is shared,
430
+ # and charging it to whichever example happened to run first would say
431
+ # more about the state of fpm's cache than about the example.
432
+ logger.info(
433
+ bold("[fplotlib] ") + f"fpm finished in {time() - started:.1f}s"
434
+ )
435
+ if not self._batch_built:
436
+ logger.debug(
437
+ "[fplotlib_gallery] the batch build failed; falling back to "
438
+ "building each example on its own:\n%s",
439
+ _clean(process.stdout, process.stderr),
440
+ )
441
+
442
+ def _write_manifest(self, names) -> None:
443
+ """Write the runner manifest declaring exactly `names` as examples."""
444
+ dependencies: dict[str, str] = {}
445
+ if self.package is not None:
446
+ relative_root = Path(os.path.relpath(self.root, self.build_dir)).as_posix()
447
+ dependencies[self.package] = f'{{ path = "{relative_root}" }}'
448
+ if self.fplotlib_spec:
449
+ dependencies["fplotlib"] = self.fplotlib_spec
450
+ dependencies.update(self.dependencies)
451
+ manifest = _MANIFEST.format(
452
+ dependencies="".join(
453
+ f"{name} = {spec}\n" for name, spec in dependencies.items()
454
+ ),
455
+ examples="".join(_MANIFEST_EXAMPLE.format(name=name) for name in names),
456
+ )
457
+ manifest_path = self.build_dir / "fpm.toml"
458
+ if (
459
+ not manifest_path.is_file()
460
+ or manifest_path.read_text(encoding="utf-8") != manifest
461
+ ):
462
+ manifest_path.write_text(manifest, encoding="utf-8")
463
+
464
+ def _executable(self, example: _Example) -> Path:
465
+ """Return the binary fpm built for `example`."""
466
+ if self._batch_built and self._exe_dir is not None:
467
+ candidate = self._exe_dir / f"{example.name}{_EXE_SUFFIX}"
468
+ if candidate.is_file():
469
+ return candidate
470
+ if not self._batch_built:
471
+ # Narrow the manifest to this one example so that a sibling that does
472
+ # not compile cannot fail it too. fpm keys its cache on the sources,
473
+ # not the manifest, so the library stays built and this costs a
474
+ # recompile of the example alone.
475
+ self._write_manifest([example.name])
476
+ # `--runner echo` makes fpm print the path it would execute, which beats
477
+ # guessing at the name of its hashed build directory.
478
+ process = self._fpm("run", "--runner", "echo", example=example.name)
479
+ if process.returncode == 0:
480
+ for line in reversed(process.stdout.splitlines()):
481
+ candidate = self.build_dir / line.strip().strip('"')
482
+ if candidate.is_file():
483
+ self._exe_dir = candidate.parent
484
+ return candidate
485
+ raise FortranExampleError(
486
+ f"fpm could not build '{example.name}':\n\n"
487
+ + indent(_clean(process.stdout, process.stderr), " ")
488
+ )
489
+
490
+ # -- running -----------------------------------------------------------
491
+
492
+ def run(self, src_file) -> _Run:
493
+ """Build if needed, then run `src_file` and collect its pictures."""
494
+ source = Path(src_file).resolve()
495
+ if source in self._runs:
496
+ return self._runs[source]
497
+
498
+ self._prepare()
499
+ example = self.examples.get(source)
500
+ if example is None:
501
+ raise FortranExampleError(
502
+ f"{source} is not under any of the gallery 'examples_dirs'."
503
+ )
504
+
505
+ # A private, empty directory per example: whatever is in it afterwards is
506
+ # exactly what the program drew.
507
+ work_dir = self.build_dir / "run" / example.name
508
+ if work_dir.exists():
509
+ shutil.rmtree(work_dir)
510
+ work_dir.mkdir(parents=True)
511
+
512
+ executable = self._executable(example)
513
+ # Timed around the program alone. Compiling is deliberately left out:
514
+ # one fpm call builds the whole gallery, so whichever example ran first
515
+ # would otherwise be charged for the library and for all of its
516
+ # siblings, and the figure would swing with the state of fpm's cache.
517
+ started = time()
518
+ try:
519
+ process = subprocess.run(
520
+ [str(executable)],
521
+ cwd=work_dir,
522
+ capture_output=True,
523
+ text=True,
524
+ errors="replace",
525
+ timeout=self.timeout,
526
+ )
527
+ except subprocess.TimeoutExpired as exc:
528
+ raise FortranExampleError(
529
+ f"{example.name} did not finish within {self.timeout}s "
530
+ f"(fplotlib_gallery_timeout)."
531
+ ) from exc
532
+ duration = time() - started
533
+ if process.returncode != 0:
534
+ raise FortranExampleError(
535
+ f"{example.name} exited with code {process.returncode}:\n\n"
536
+ + indent(_clean(process.stdout, process.stderr), " ")
537
+ )
538
+
539
+ pictures = [
540
+ path
541
+ for path in work_dir.rglob("*")
542
+ if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES
543
+ ]
544
+ pictures.sort(key=lambda path: (path.stat().st_mtime_ns, path.name))
545
+ by_name: dict[str, Path] = {}
546
+ for path in pictures:
547
+ by_name.setdefault(path.name, path)
548
+
549
+ run = _Run(
550
+ stdout=_clean(process.stdout, process.stderr),
551
+ by_name=by_name,
552
+ in_order=pictures,
553
+ duration=duration,
554
+ )
555
+ self._runs[source] = run
556
+ return run
557
+
558
+
559
+ def _clean(*streams: str) -> str:
560
+ """Join captured streams, drop ANSI colouring and trailing blank lines."""
561
+ text = "\n".join(stream for stream in streams if stream and stream.strip())
562
+ return _ANSI.sub("", text.expandtabs()).rstrip()
563
+
564
+
565
+ # -- the scraper -----------------------------------------------------------
566
+
567
+
568
+ def _compile_save_call(names) -> re.Pattern:
569
+ """Build the regex matching ``call savefig("...")`` and friends."""
570
+ alternatives = "|".join(re.escape(name) for name in names)
571
+ return re.compile(
572
+ rf"""\b(?:{alternatives})\s*\(\s*(?:filename\s*=\s*)?"""
573
+ r"""(?P<quote>['"])(?P<path>[^'"]+)(?P=quote)""",
574
+ re.IGNORECASE,
575
+ )
576
+
577
+
578
+ #: Replaced from ``fplotlib_gallery_save_functions`` when the config is read.
579
+ _SAVE_CALL = _compile_save_call(["savefig", "save_animation"])
580
+
581
+
582
+ def _saved_in(content: str) -> list[str]:
583
+ """Return the base names a code block saves, in source order."""
584
+ names: list[str] = []
585
+ for match in _SAVE_CALL.finditer(content):
586
+ name = PurePath(match.group("path").replace("\\", "/")).name
587
+ if name and name not in names:
588
+ names.append(name)
589
+ return names
590
+
591
+
592
+ def fplotlib_scraper(block, block_vars, gallery_conf) -> str:
593
+ """Sphinx-Gallery image scraper for fplotlib's Fortran examples.
594
+
595
+ Returns the reST for the pictures belonging to `block`: the ones it saved
596
+ under a literal filename, plus -- on the last code block -- anything the
597
+ program wrote that no earlier block claimed. Returns an empty string for
598
+ Python examples, so it is safe to list alongside the matplotlib scraper.
599
+ """
600
+ run = block_vars.get("fplotlib_run")
601
+ if run is None:
602
+ return ""
603
+
604
+ selected: list[Path] = []
605
+ for name in _saved_in(block.content):
606
+ path = run.by_name.get(name)
607
+ if path is not None and path not in run.claimed:
608
+ run.claimed.add(path)
609
+ selected.append(path)
610
+ if block_vars.get("fplotlib_last_block"):
611
+ for path in run.in_order:
612
+ if path not in run.claimed:
613
+ run.claimed.add(path)
614
+ selected.append(path)
615
+ if not selected:
616
+ return ""
617
+
618
+ from sphinx_gallery.scrapers import figure_rst
619
+
620
+ image_paths = block_vars["image_path_iterator"]
621
+ written: list[str] = []
622
+ for path in selected:
623
+ # The iterator hands out '..._001.png'; keep its numbering but the real
624
+ # suffix. Sphinx-Gallery finds the file again by probing the known
625
+ # extensions, so the mismatch with the template is expected.
626
+ target = Path(next(image_paths)).with_suffix(path.suffix.lower())
627
+ target.parent.mkdir(parents=True, exist_ok=True)
628
+ # A picture whose format changed between builds would otherwise leave a
629
+ # stale sibling that the probing picks up in preference to this one.
630
+ for suffix in IMAGE_SUFFIXES:
631
+ stale = target.with_suffix(suffix)
632
+ if stale != target and stale.is_file():
633
+ stale.unlink()
634
+ shutil.copyfile(path, target)
635
+ written.append(str(target))
636
+
637
+ return figure_rst(written, gallery_conf["src_dir"])
638
+
639
+
640
+ # -- replacing Sphinx-Gallery's executor -----------------------------------
641
+
642
+ _GALLERY: FortranGallery | None = None
643
+ _ORIGINAL_EXECUTE_SCRIPT = None
644
+
645
+
646
+ def _is_fortran(src_file) -> bool:
647
+ return Path(src_file).suffix.lower() in FORTRAN_SUFFIXES
648
+
649
+
650
+ def _execute_script(script_blocks, script_vars, gallery_conf, file_conf):
651
+ """Stand in for ``gen_rst.execute_script`` on Fortran examples."""
652
+ if not _is_fortran(script_vars.get("src_file", "")) or _GALLERY is None:
653
+ return _ORIGINAL_EXECUTE_SCRIPT(
654
+ script_blocks, script_vars, gallery_conf, file_conf
655
+ )
656
+
657
+ from sphinx_gallery.gen_rst import CODE_OUTPUT, get_md5sum
658
+ from sphinx_gallery.scrapers import save_figures
659
+
660
+ # Bookkeeping the rest of Sphinx-Gallery expects of an executor.
661
+ script_vars["memory_delta"] = 0.0
662
+ script_vars.setdefault("example_globals", {})
663
+
664
+ output_blocks = [""] * len(script_blocks)
665
+ code_blocks = [i for i, block in enumerate(script_blocks) if block.type == "code"]
666
+ if not script_vars["execute_script"] or not code_blocks:
667
+ return output_blocks, 0.0
668
+
669
+ try:
670
+ run = _GALLERY.run(script_vars["src_file"])
671
+ except FortranExampleError as exc:
672
+ output_blocks[code_blocks[-1]] = _report_failure(exc, script_vars, gallery_conf)
673
+ # An example that never ran costs nothing, as upstream reports for one
674
+ # that was not executed. What a failure did cost was compiling, and
675
+ # that is reported against the gallery rather than against one example.
676
+ return output_blocks, 0.0
677
+
678
+ script_vars["fplotlib_run"] = run
679
+ last = code_blocks[-1]
680
+ started = time()
681
+ for index in code_blocks:
682
+ script_vars["fplotlib_last_block"] = index == last
683
+ images_rst = save_figures(script_blocks[index], script_vars, gallery_conf)
684
+ captured = ""
685
+ if index == last and run.stdout.strip():
686
+ captured = CODE_OUTPUT.format(indent(run.stdout, " " * 4))
687
+ output_blocks[index] = f"\n{images_rst}\n\n{captured}\n\n\n"
688
+ # The program plus the scraping it caused: the work that is this example's
689
+ # own. Building is excluded; see FortranGallery.run.
690
+ elapsed = run.duration + (time() - started)
691
+
692
+ # Mirrors the upstream executor: the md5 lets the next build skip this
693
+ # example, so it is only written once the example has actually run.
694
+ with open(script_vars["target_file"] + ".md5", "w") as handle:
695
+ handle.write(get_md5sum(script_vars["target_file"], mode="t"))
696
+ script_vars["passing"] = True
697
+ return output_blocks, elapsed
698
+
699
+
700
+ def _report_failure(exc: FortranExampleError, script_vars, gallery_conf) -> str:
701
+ """Log a failed example and render its diagnostics, as upstream does.
702
+
703
+ Deliberately not routed through ``gen_rst.handle_exception``: that formats a
704
+ Python traceback, and what is worth showing here is the compiler's or the
705
+ program's own output.
706
+ """
707
+ from sphinx_gallery.gen_gallery import _expected_failing_examples
708
+ from sphinx_gallery.gen_rst import codestr2rst
709
+
710
+ message = str(exc)
711
+ src_file = str(script_vars["src_file"])
712
+ relative = os.path.relpath(src_file, gallery_conf["src_dir"])
713
+ expected = _expected_failing_examples(gallery_conf)
714
+ if {src_file, str(Path(src_file).resolve())} & expected:
715
+ logger.info(
716
+ "\n%s expectedly failed to execute correctly:\n\n%s",
717
+ bold(blue(relative)),
718
+ blue(indent(message, " ")),
719
+ )
720
+ else:
721
+ logger.warning(
722
+ "\n%s unexpectedly failed to execute correctly:\n\n%s",
723
+ bold(red(relative)),
724
+ red(indent(message, " ")),
725
+ type=_WARNING_TYPE,
726
+ subtype="example_error",
727
+ )
728
+
729
+ script_vars["formatted_exception"] = message
730
+ script_vars["execute_script"] = False
731
+ if gallery_conf["abort_on_example_error"]:
732
+ raise ExtensionError(f"{relative} failed to run:\n\n{indent(message, ' ')}")
733
+ return (
734
+ "\n.. rst-class:: sphx-glr-script-out\n\n"
735
+ + codestr2rst(message, lang="none")
736
+ + "\n\n"
737
+ )
738
+
739
+
740
+ def _patch_sphinx_gallery() -> None:
741
+ """Route Fortran examples through our executor, once."""
742
+ global _ORIGINAL_EXECUTE_SCRIPT
743
+ if _ORIGINAL_EXECUTE_SCRIPT is not None:
744
+ return
745
+ from sphinx_gallery import gen_rst
746
+
747
+ _ORIGINAL_EXECUTE_SCRIPT = gen_rst.execute_script
748
+ gen_rst.execute_script = _execute_script
749
+
750
+
751
+ def _on_config_inited(app, config) -> None:
752
+ global _GALLERY, _SAVE_CALL
753
+
754
+ gallery_conf = getattr(config, "sphinx_gallery_conf", None)
755
+ if gallery_conf is None:
756
+ raise ExtensionError(
757
+ "fplotlib_gallery needs 'sphinx_gallery.gen_gallery' in 'extensions' "
758
+ "and a 'sphinx_gallery_conf' setting."
759
+ )
760
+ if gallery_conf.get("parallel"):
761
+ raise ExtensionError(
762
+ "fplotlib_gallery cannot run examples in parallel; remove 'parallel' "
763
+ "from sphinx_gallery_conf."
764
+ )
765
+
766
+ # Teach Sphinx-Gallery about Fortran so that conf.py does not have to.
767
+ extensions = set(gallery_conf.get("example_extensions", {".py"}))
768
+ parsers = dict(gallery_conf.get("filetype_parsers", {}))
769
+ for suffix in FORTRAN_SUFFIXES:
770
+ extensions.add(suffix)
771
+ parsers.setdefault(suffix, "Fortran")
772
+ gallery_conf["example_extensions"] = extensions
773
+ gallery_conf["filetype_parsers"] = parsers
774
+
775
+ _SAVE_CALL = _compile_save_call(config.fplotlib_gallery_save_functions)
776
+ _GALLERY = FortranGallery(app)
777
+ _patch_sphinx_gallery()
778
+
779
+
780
+ def setup(app):
781
+ """Register the extension."""
782
+ app.setup_extension("sphinx_gallery.gen_gallery")
783
+
784
+ app.add_config_value("fplotlib_gallery_root", None, "env")
785
+ app.add_config_value("fplotlib_gallery_fplotlib", None, "env")
786
+ app.add_config_value("fplotlib_gallery_fpm", "fpm", "env")
787
+ app.add_config_value("fplotlib_gallery_build_dir", None, "env")
788
+ app.add_config_value("fplotlib_gallery_compiler", None, "env")
789
+ app.add_config_value("fplotlib_gallery_fpm_flags", None, "env")
790
+ app.add_config_value("fplotlib_gallery_timeout", 600, "env")
791
+ app.add_config_value("fplotlib_gallery_dependencies", {}, "env")
792
+ app.add_config_value(
793
+ "fplotlib_gallery_save_functions", ["savefig", "save_animation"], "env"
794
+ )
795
+
796
+ app.connect("config-inited", _on_config_inited)
797
+
798
+ return {
799
+ "version": __version__,
800
+ # Examples are built and run by one fpm project in this process.
801
+ "parallel_read_safe": False,
802
+ "parallel_write_safe": True,
803
+ }