Skip to content

Commit

Permalink
Merge pull request #4 from gabalafou/notebook-header-directive
Browse files Browse the repository at this point in the history
  • Loading branch information
agriyakhetarpal authored Jan 15, 2025
2 parents 2964686 + e6970f3 commit a8f287c
Show file tree
Hide file tree
Showing 14 changed files with 367 additions and 240 deletions.
82 changes: 82 additions & 0 deletions doc/source/_static/myst-nb.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* MyST-NB
This stylesheet targets elements output by MyST-NB that represent notebook
cells.
In some cases these rules override MyST-NB. In some cases they override PyData
Sphinx Theme or Sphinx. And in some cases they do not override existing styling
but add new styling. */

/* Set up a few variables for this stylesheet */
.cell,
.pywt-handcoded-cell-output {
--pywt-cell-input-border-left-width: .2rem;

/* This matches the padding applied to <pre> elements by PyData Sphinx Theme */
--pywt-code-block-padding: 1rem;

/* override mystnb */
--mystnb-source-border-radius: .25rem; /* match PyData Sphinx Theme */
}

.cell .cell_input::before {
content: "In";
border-bottom: var(--mystnb-source-border-width) solid var(--pst-color-border);
font-weight: var(--pst-font-weight-caption);

/* Left-aligns the text in this box and the one that follows it */
padding-left: var(--pywt-code-block-padding);
}

/* Cannot use `.cell .cell_input` selector because the stylesheet from MyST-NB
uses `div.cell div.cell_input` and we want to override those rules */
div.cell div.cell_input {
background-color: inherit;
border-color: var(--pst-color-border);
border-left-width: var(--pywt-cell-input-border-left-width);
background-clip: padding-box;
overflow: hidden;
}

.cell .cell_output,
.pywt-handcoded-cell-output {
border: var(--mystnb-source-border-width) solid var(--pst-color-surface);
border-radius: var(--mystnb-source-border-radius);
background-clip: padding-box;
overflow: hidden;
}

.cell .cell_output::before,
.pywt-handcoded-cell-output::before {
content: "Out";
display: block;
font-weight: var(--pst-font-weight-caption);

/* Left-aligns the text in this box and the one that follows it */
padding-left: var(--pywt-code-block-padding);
}

.cell .cell_output .output {
background-color: inherit;
border: none;
margin-top: 0;
}

.cell .cell_output,
/* must prefix the following selector with `div.` to override Sphinx margin rule on div[class*=highlight-] */
div.pywt-handcoded-cell-output {
/* Left-align the text in the output with the text in the input */
margin-left: calc(var(--pywt-cell-input-border-left-width) - var(--mystnb-source-border-width));
}

.cell .cell_output .output,
.cell .cell_input pre,
.cell .cell_output pre,
.pywt-handcoded-cell-output .highlight,
.pywt-handcoded-cell-output pre {
border-radius: 0;
}

.pywt-handcoded-cell-output pre {
border: none; /* MyST-NB sets border to none for <pre> tags inside div.cell */
}
44 changes: 27 additions & 17 deletions doc/source/_static/pywavelets.css
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
/* Custom CSS rules for the interactive documentation button */

.try_examples_button {
color: white;
background-color: #0054a6;
border: none;
padding: 5px 10px;
border-radius: 10px;
margin-bottom: 5px;
box-shadow: 0 2px 5px rgba(108,108,108,0.2);
font-weight: bold;
font-size: small;
color: white;
background-color: #0054a6;
border: none;
padding: 5px 10px;
border-radius: 10px;
margin-bottom: 5px;
box-shadow: 0 2px 5px rgba(108,108,108,0.2);
font-weight: bold;
font-size: small;
}

.try_examples_button:hover {
background-color: #0066cc;
transform: scale(1.02);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
cursor: pointer;
background-color: #0066cc;
transform: scale(1.02);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
cursor: pointer;
}

.try_examples_button_container {
display: flex;
justify-content: flex-start;
gap: 10px;
margin-bottom: 20px;
display: flex;
justify-content: flex-start;
gap: 10px;
margin-bottom: 20px;
}

/*
Admonitions on this site are styled with some top margin. This makes sense when
the admonition appears within the flow of the article. But when it is the very
first child of an article, its top margin gets added to the article's top
padding, resulting in too much whitespace.
*/
.admonition.pywt-margin-top-0 {
margin-top: 0;
}
59 changes: 54 additions & 5 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
# serve to show the default.

import datetime
import importlib.metadata
import os
import re
from pathlib import Path

import jinja2.filters
import numpy as np

import pywt
Expand All @@ -40,15 +39,63 @@ def preprocess_notebooks(app: Sphinx, *args, **kwargs):

print("Converting Markdown files to IPyNB...")
for path in (HERE / "regression").glob("*.md"):
if any(path.match(pattern) for pattern in exclude_patterns):
continue
nb = jupytext.read(str(path))

# In .md to .ipynb conversion, do not include any cells that have the
# jupyterlite_sphinx_strip tag
nb.cells = [
cell for cell in nb.cells if "jupyterlite_sphinx_strip" not in cell.metadata.get("tags", [])
]

ipynb_path = path.with_suffix(".ipynb")
with open(ipynb_path, "w") as f:
nbformat.write(nb, f)
print(f"Converted {path} to {ipynb_path}")


# Should match {{ parent_docname }} or {{parent_docname}}
parent_docname_substitution_re = re.compile(r"{{\s*parent_docname\s*}}")

def sub_parent_docname_in_header(
app: Sphinx, relative_path: Path, parent_docname: str, content: list[str]
):
"""Fill in the name of the document in the header.
When regression/header.md is read via the include directive, replace
{{ parent_docname }} with the name of the parent document that included
header.md.
Note: parent_docname does not include the file extension.
Here is a simplified example of how this works.
Contents of header.md:
{download}`Download {{ parent_docname }}.md <{{ parent_docname }}.md>`
Contents of foobar.md:
```{include} header.md
```
After this function and others are run...
Contents of foobar.md:
{download}`Download foobar.md <foobar.md>`
"""
if not relative_path.match("regression/header.md"):
return

for i, value in enumerate(content):
content[i] = re.sub(parent_docname_substitution_re, parent_docname, value)


def setup(app):
app.connect("config-inited", preprocess_notebooks)
app.connect("include-read", sub_parent_docname_in_header)

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
Expand Down Expand Up @@ -229,6 +276,7 @@ def setup(app):
# _static directory.
html_css_files = [
"pywavelets.css",
"myst-nb.css"
]

# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
Expand Down Expand Up @@ -309,6 +357,8 @@ def setup(app):
# directories to ignore when looking for source files.
exclude_patterns = [
'substitutions.rst',
'regression/header.md',
'regression/README.md',
'regression/*.ipynb' # exclude IPyNB files from the build
]

Expand Down Expand Up @@ -350,13 +400,12 @@ def setup(app):

os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"

# https://myst-nb.readthedocs.io/en/latest/configuration.html
nb_execution_mode = 'auto'
nb_execution_timeout = 60
nb_execution_allow_errors = False

nb_execution_raise_on_error = True
nb_render_markdown_format = "myst"
render_markdown_format = "myst"

nb_remove_code_source = False
nb_remove_code_outputs = False

Expand Down
54 changes: 54 additions & 0 deletions doc/source/regression/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Regression folder

This folder contains various useful examples illustrating how to use and how not
to use PyWavelets.

The examples are written in the [MyST markdown notebook
format](https://myst-nb.readthedocs.io/en/v0.13.2/use/markdown.html). This
allows each .md file to function simultaneously as documentation that can be fed
into Sphinx and as a source file that can be converted to the Jupyter notebook
format (.ipynb), which can then be opened in notebook applications such as
JupyterLab. For this reason, each example page in this folder includes a header template
that adds a blurb to the top of each page about how the page can be
run or downloaded as a Jupyter notebook.

There a few shortcomings to this approach of generating the code cell outputs in
the documentation pages at build time rather than hand editing them into the
document source file. One is that we can no longer compare the generated outputs
with the expected outputs as we used to do with doctest. Another is that we
lose some control over how we want the outputs to appear, unless we use a workaround.

Here is the workaround we created. First we tell MyST-NB to remove the generated
cell output from the documentation page by adding the `remove-output` tag to the
`code-cell` directive in the markdown file. Then we hand code the output in a
`code-block` directive, not to be confused with `code-cell`! The `code-cell`
directive says "I am notebook code cell input, run me!" The `code-block`
directive says, "I am just a block of code for documentation purposes, don't run
me!" To the code block, we add the `.pywt-handcoded-cell-output` class so that
we can style it to look the same as other cell outputs on the same HTML page.
Finally, we tag the handcoded output with `jupyterlite_sphinx_strip` so that we
can exclude it when converting from .md to .ipynb. That way only generated
output appears in the .ipynb notebook.

To recap:

- We use the `remove-output` tag to remove the **generated** code cell output
during .md to .html conversion (this conversion is done by MyST-NB).
- We use the `jupyterlite_sphinx_strip` tag to remove the **handcoded** output
during .md to .ipynb conversion (this conversion is done by Jupytext).

Example markdown:

```{code-cell}
:tags: [raises-exception, remove-output]
1 / 0
```

+++ {"tags" ["jupyterlite_sphinx_strip"]}

```{code-block} python
:class: pywt-handcoded-cell-output
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
```
Loading

0 comments on commit a8f287c

Please sign in to comment.