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.
- sphinx_fplotlib_gallery-0.1.0/.gitignore +26 -0
- sphinx_fplotlib_gallery-0.1.0/LICENSE +21 -0
- sphinx_fplotlib_gallery-0.1.0/PKG-INFO +136 -0
- sphinx_fplotlib_gallery-0.1.0/README.md +112 -0
- sphinx_fplotlib_gallery-0.1.0/pyproject.toml +41 -0
- sphinx_fplotlib_gallery-0.1.0/src/fplotlib_gallery/__init__.py +803 -0
|
@@ -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
|
+
}
|