Skip to content

Commit d4f3854

Browse files
DOC: Add interactive notebooks to pages in the "Usage Examples" section (#741)
1 parent ba9cf07 commit d4f3854

26 files changed

+1846
-1331
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pip-log.txt
2424
doc/_build/
2525
doc/build/
2626
.jupyterlite.doit.db
27+
/doc/jupyter_execute/
28+
doc/source/regression/*.ipynb
2729

2830
# Editors
2931
.idea

doc/source/_static/myst-nb.css

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/* MyST-NB
2+
3+
This stylesheet targets elements output by MyST-NB that represent notebook
4+
cells.
5+
6+
In some cases these rules override MyST-NB. In some cases they override PyData
7+
Sphinx Theme or Sphinx. And in some cases they do not override existing styling
8+
but add new styling. */
9+
10+
/* Set up a few variables for this stylesheet */
11+
.cell,
12+
.pywt-handcoded-cell-output {
13+
--pywt-cell-input-border-left-width: .2rem;
14+
15+
/* This matches the padding applied to <pre> elements by PyData Sphinx Theme */
16+
--pywt-code-block-padding: 1rem;
17+
18+
/* override mystnb */
19+
--mystnb-source-border-radius: .25rem; /* match PyData Sphinx Theme */
20+
}
21+
22+
.cell .cell_input::before {
23+
content: "In";
24+
border-bottom: var(--mystnb-source-border-width) solid var(--pst-color-border);
25+
font-weight: var(--pst-font-weight-caption);
26+
27+
/* Left-aligns the text in this box and the one that follows it */
28+
padding-left: var(--pywt-code-block-padding);
29+
}
30+
31+
/* Cannot use `.cell .cell_input` selector because the stylesheet from MyST-NB
32+
uses `div.cell div.cell_input` and we want to override those rules */
33+
div.cell div.cell_input {
34+
background-color: inherit;
35+
border-color: var(--pst-color-border);
36+
border-left-width: var(--pywt-cell-input-border-left-width);
37+
background-clip: padding-box;
38+
overflow: hidden;
39+
}
40+
41+
.cell .cell_output,
42+
.pywt-handcoded-cell-output {
43+
border: var(--mystnb-source-border-width) solid var(--pst-color-surface);
44+
border-radius: var(--mystnb-source-border-radius);
45+
background-clip: padding-box;
46+
overflow: hidden;
47+
}
48+
49+
.cell .cell_output::before,
50+
.pywt-handcoded-cell-output::before {
51+
content: "Out";
52+
display: block;
53+
font-weight: var(--pst-font-weight-caption);
54+
55+
/* Left-aligns the text in this box and the one that follows it */
56+
padding-left: var(--pywt-code-block-padding);
57+
}
58+
59+
.cell .cell_output .output {
60+
background-color: inherit;
61+
border: none;
62+
margin-top: 0;
63+
}
64+
65+
.cell .cell_output,
66+
/* must prefix the following selector with `div.` to override Sphinx margin rule on div[class*=highlight-] */
67+
div.pywt-handcoded-cell-output {
68+
/* Left-align the text in the output with the text in the input */
69+
margin-left: calc(var(--pywt-cell-input-border-left-width) - var(--mystnb-source-border-width));
70+
}
71+
72+
.cell .cell_output .output,
73+
.cell .cell_input pre,
74+
.cell .cell_output pre,
75+
.pywt-handcoded-cell-output .highlight,
76+
.pywt-handcoded-cell-output pre {
77+
border-radius: 0;
78+
}
79+
80+
.pywt-handcoded-cell-output pre {
81+
border: none; /* MyST-NB sets border to none for <pre> tags inside div.cell */
82+
}

doc/source/_static/pywavelets.css

+27-17
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
/* Custom CSS rules for the interactive documentation button */
22

33
.try_examples_button {
4-
color: white;
5-
background-color: #0054a6;
6-
border: none;
7-
padding: 5px 10px;
8-
border-radius: 10px;
9-
margin-bottom: 5px;
10-
box-shadow: 0 2px 5px rgba(108,108,108,0.2);
11-
font-weight: bold;
12-
font-size: small;
4+
color: white;
5+
background-color: #0054a6;
6+
border: none;
7+
padding: 5px 10px;
8+
border-radius: 10px;
9+
margin-bottom: 5px;
10+
box-shadow: 0 2px 5px rgba(108,108,108,0.2);
11+
font-weight: bold;
12+
font-size: small;
1313
}
1414

1515
.try_examples_button:hover {
16-
background-color: #0066cc;
17-
transform: scale(1.02);
18-
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
19-
cursor: pointer;
16+
background-color: #0066cc;
17+
transform: scale(1.02);
18+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
19+
cursor: pointer;
2020
}
2121

2222
.try_examples_button_container {
23-
display: flex;
24-
justify-content: flex-start;
25-
gap: 10px;
26-
margin-bottom: 20px;
23+
display: flex;
24+
justify-content: flex-start;
25+
gap: 10px;
26+
margin-bottom: 20px;
27+
}
28+
29+
/*
30+
Admonitions on this site are styled with some top margin. This makes sense when
31+
the admonition appears within the flow of the article. But when it is the very
32+
first child of an article, its top margin gets added to the article's top
33+
padding, resulting in too much whitespace.
34+
*/
35+
.admonition.pywt-margin-top-0 {
36+
margin-top: 0;
2737
}

doc/source/conf.py

+111-7
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
# serve to show the default.
1212

1313
import datetime
14-
import importlib.metadata
14+
import os
15+
import re
16+
from pathlib import Path
1517

16-
import jinja2.filters
1718
import numpy as np
1819

1920
import pywt
@@ -25,6 +26,76 @@
2526
except TypeError:
2627
pass
2728

29+
from sphinx.application import Sphinx
30+
31+
HERE = Path(__file__).parent
32+
33+
34+
def preprocess_notebooks(app: Sphinx, *args, **kwargs):
35+
"""Preprocess Markdown notebooks to convert them to IPyNB format."""
36+
37+
import jupytext
38+
import nbformat
39+
40+
print("Converting Markdown files to IPyNB...")
41+
for path in (HERE / "regression").glob("*.md"):
42+
if any(path.match(pattern) for pattern in exclude_patterns):
43+
continue
44+
nb = jupytext.read(str(path))
45+
46+
# In .md to .ipynb conversion, do not include any cells that have the
47+
# jupyterlite_sphinx_strip tag
48+
nb.cells = [
49+
cell for cell in nb.cells if "jupyterlite_sphinx_strip" not in cell.metadata.get("tags", [])
50+
]
51+
52+
ipynb_path = path.with_suffix(".ipynb")
53+
with open(ipynb_path, "w") as f:
54+
nbformat.write(nb, f)
55+
print(f"Converted {path} to {ipynb_path}")
56+
57+
58+
# Should match {{ parent_docname }} or {{parent_docname}}
59+
parent_docname_substitution_re = re.compile(r"{{\s*parent_docname\s*}}")
60+
61+
def sub_parent_docname_in_header(
62+
app: Sphinx, relative_path: Path, parent_docname: str, content: list[str]
63+
):
64+
"""Fill in the name of the document in the header.
65+
66+
When regression/header.md is read via the include directive, replace
67+
{{ parent_docname }} with the name of the parent document that included
68+
header.md.
69+
70+
Note: parent_docname does not include the file extension.
71+
72+
Here is a simplified example of how this works.
73+
74+
Contents of header.md:
75+
76+
{download}`Download {{ parent_docname }}.md <{{ parent_docname }}.md>`
77+
78+
Contents of foobar.md:
79+
80+
```{include} header.md
81+
```
82+
83+
After this function and others are run...
84+
85+
Contents of foobar.md:
86+
87+
{download}`Download foobar.md <foobar.md>`
88+
"""
89+
if not relative_path.match("regression/header.md"):
90+
return
91+
92+
for i, value in enumerate(content):
93+
content[i] = re.sub(parent_docname_substitution_re, parent_docname, value)
94+
95+
96+
def setup(app):
97+
app.connect("config-inited", preprocess_notebooks)
98+
app.connect("include-read", sub_parent_docname_in_header)
2899

29100
# If extensions (or modules to document with autodoc) are in another directory,
30101
# add these directories to sys.path here. If the directory is relative to the
@@ -38,6 +109,7 @@
38109
extensions = [
39110
'jupyterlite_sphinx',
40111
'matplotlib.sphinxext.plot_directive',
112+
'myst_nb',
41113
'numpydoc',
42114
'sphinx.ext.autodoc',
43115
'sphinx.ext.autosummary',
@@ -47,15 +119,19 @@
47119
'sphinx.ext.mathjax',
48120
'sphinx.ext.todo',
49121
'sphinx_copybutton',
122+
'sphinx_design',
50123
'sphinx_togglebutton',
51-
52124
]
53125

54126
# Add any paths that contain templates here, relative to this directory.
55127
templates_path = ['_templates']
56128

57129
# The suffix of source filenames.
58-
source_suffix = '.rst'
130+
source_suffix = {
131+
'.rst': 'restructuredtext',
132+
'.md': 'myst-nb',
133+
'ipynb': None, # do not parse IPyNB files
134+
}
59135

60136
# The encoding of source files.
61137
source_encoding = 'utf-8'
@@ -141,8 +217,8 @@
141217
"show_prev_next": True,
142218
"footer_start": ["copyright", "sphinx-version"],
143219
"footer_end": ["theme-version"],
144-
"pygment_light_style": "a11y-high-contrast-light",
145-
"pygment_dark_style": "a11y-high-contrast-dark",
220+
"pygments_light_style": "a11y-high-contrast-light",
221+
"pygments_dark_style": "a11y-high-contrast-dark",
146222
"icon_links": [
147223
{
148224
"name": "Discussion group on Google Groups",
@@ -200,6 +276,7 @@
200276
# _static directory.
201277
html_css_files = [
202278
"pywavelets.css",
279+
"myst-nb.css"
203280
]
204281

205282
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
@@ -278,7 +355,12 @@
278355

279356
# List of patterns, relative to source directory, that match files and
280357
# directories to ignore when looking for source files.
281-
exclude_patterns = ['substitutions.rst', ]
358+
exclude_patterns = [
359+
'substitutions.rst',
360+
'regression/header.md',
361+
'regression/README.md',
362+
'regression/*.ipynb' # exclude IPyNB files from the build
363+
]
282364

283365
# numpydoc_show_class_members = False
284366
numpydoc_class_members_toctree = False
@@ -310,3 +392,25 @@
310392
Shall you encounter any issues, please feel free to report them on the
311393
[PyWavelets issue tracker](https://github.com/PyWavelets/pywt/issues)."""
312394
)
395+
396+
jupyterlite_silence = True
397+
strip_tagged_cells = True
398+
399+
# -- Options for MyST-NB and Markdown-based content --------------------------
400+
401+
os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
402+
403+
# https://myst-nb.readthedocs.io/en/latest/configuration.html
404+
nb_execution_mode = 'auto'
405+
nb_execution_timeout = 60
406+
nb_execution_allow_errors = False
407+
nb_execution_raise_on_error = True
408+
nb_render_markdown_format = "myst"
409+
nb_remove_code_source = False
410+
nb_remove_code_outputs = False
411+
412+
myst_enable_extensions = [
413+
'amsmath',
414+
'colon_fence',
415+
'dollarmath',
416+
]

doc/source/regression/README.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Regression folder
2+
3+
This folder contains various useful examples illustrating how to use and how not
4+
to use PyWavelets.
5+
6+
The examples are written in the [MyST markdown notebook
7+
format](https://myst-nb.readthedocs.io/en/v0.13.2/use/markdown.html). This
8+
allows each .md file to function simultaneously as documentation that can be fed
9+
into Sphinx and as a source file that can be converted to the Jupyter notebook
10+
format (.ipynb), which can then be opened in notebook applications such as
11+
JupyterLab. For this reason, each example page in this folder includes a header template
12+
that adds a blurb to the top of each page about how the page can be
13+
run or downloaded as a Jupyter notebook.
14+
15+
There are a few shortcomings to this approach of generating the code cell outputs in
16+
the documentation pages at build time rather than hand editing them into the
17+
document source file. One is that we can no longer compare the generated outputs
18+
with the expected outputs as we used to do with doctest. Another is that we
19+
lose some control over how we want the outputs to appear, unless we use a workaround.
20+
21+
Here is the workaround we created. First we tell MyST-NB to remove the generated
22+
cell output from the documentation page by adding the `remove-output` tag to the
23+
`code-cell` directive in the markdown file. Then we hand code the output in a
24+
`code-block` directive, not to be confused with `code-cell`! The `code-cell`
25+
directive says "I am notebook code cell input, run me!" The `code-block`
26+
directive says, "I am just a block of code for documentation purposes, don't run
27+
me!" To the code block, we add the `.pywt-handcoded-cell-output` class so that
28+
we can style it to look the same as other cell outputs on the same HTML page.
29+
Finally, we tag the handcoded output with `jupyterlite_sphinx_strip` so that we
30+
can exclude it when converting from .md to .ipynb. That way only generated
31+
output appears in the .ipynb notebook.
32+
33+
To recap:
34+
35+
- We use the `remove-output` tag to remove the **generated** code cell output
36+
during .md to .html conversion (this conversion is done by MyST-NB).
37+
- We use the `jupyterlite_sphinx_strip` tag to remove the **handcoded** output
38+
during .md to .ipynb conversion (this conversion is done by Jupytext).
39+
40+
Example markdown:
41+
42+
```{code-cell}
43+
:tags: [raises-exception, remove-output]
44+
1 / 0
45+
```
46+
47+
+++ {"tags" ["jupyterlite_sphinx_strip"]}
48+
49+
```{code-block} python
50+
:class: pywt-handcoded-cell-output
51+
Traceback (most recent call last):
52+
...
53+
ZeroDivisionError: division by zero
54+
```

0 commit comments

Comments
 (0)