Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notebook header #4

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions doc/source/_static/myst-nb.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* 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. */

/*
Undo the border that MyST-NB puts on the input cell
*/
div.cell div.cell_input {
border: none;
}

/*
Undo border radius on code cells (this styling actually comes from Sphinx, not MyST-NB)
*/
div.cell pre,
div.cell div.highlight,
div.pywt-handcoded-cell-output,
div.pywt-handcoded-cell-output pre,
div.pywt-handcoded-cell-output div.highlight {
border-radius: 0;
}

/*
Add border to output container, remove background color.
*/
div.cell div.cell_output,
div.pywt-handcoded-cell-output {
background-color: var(--pst-color-background);
border-radius: 0;
border-color: var(--pst-color-border-muted);
border-style: solid;
border-width: 1px;
border-top-style: double;
border-top-width: medium;
}
div.cell div.cell_output div.output,
div.cell div.cell_output pre,
div.pywt-handcoded-cell-output pre {
background-color: transparent;
border: none;
}

/*
Remove extra whitespace
*/
div.cell div.cell_output,
div.cell div.cell_output > :first-child {
margin-top: 0;
}
div.pywt-handcoded-cell-output {
margin-top: -1em;
}
gabalafou marked this conversation as resolved.
Show resolved Hide resolved
49 changes: 32 additions & 17 deletions doc/source/_static/pywavelets.css
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
/* 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.

We use "tip" admonitions at the top of each doc in the regression/ directory.
This rule removes the top margin for tip admonitions when they occur as the very
first child of some container. This will not affect non-first-child tip
admonitions.
*/
.admonition.tip:first-child {
margin-top: 0;
}
53 changes: 51 additions & 2 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 path.match("regression/header.md"):
continue
nb = jupytext.read(str(path))

# In .md to .ipynd 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", [])
]

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we can drop the entire functionality and use jupyterlite-sphinx to do this natively; should I add those changes here for you to rebase your branch over them, or do you wish to do so yourself?

Copy link
Author

@gabalafou gabalafou Dec 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, at what point does the JupyterLite Sphinx extension strip the tagged cells? Does it happen at render time in the Lite app or does it happen when converting .md to .ipynb?

I think that the handcoded outputs should be stripped in either case, whether loading the notebook in Lite or downloading it as a .ipynb file.

It is perhaps a mistake, though, to use the jupyterlite_sphinx_strip tag here to remove the cell during markdown to ipynb conversion. Perhaps I should do a repo-wide search of "jupyterlite_sphinx_strip" and replace it with something like "pywt-remove-from-ipynb"... what do you think?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, at what point does the JupyterLite Sphinx extension strip the tagged cells? Does it happen at render time in the Lite app or does it happen when converting .md to .ipynb?

It happens on neither of those – the stripping does happen at build time, but after we have converted .md to .ipynb. Essentially, we convert a notebook first (if we need to), strip it, and then pass it along for JupyterLite to render.

It is perhaps a mistake, though, to use the jupyterlite_sphinx_strip tag here to remove the cell during markdown to ipynb conversion. Perhaps I should do a repo-wide search of "jupyterlite_sphinx_strip" and replace it with something like "pywt-remove-from-ipynb"... what do you think?

I think it should be fine to stay with jupyterlite_sphinx_strip, since we indeed designed it for this highly-specific use case in mind. Is there something different you had in mind, or maybe I misunderstood something?

That said, if we were to use jupyterlite-sphinx here, we would need to think about how to get the IPyNB file for downloads. The reason why it works as of now is because we convert the .md file to .ipynb in place, and store it in the same folder, but jupyterlite-sphinx does not do so and it directly stores the converted notebook to the _contents/ directory, which means that getting its location will be slightly tricky. We would need to get the "Download" button in the JupyterLite interface itself and it won't be available in Sphinx, which I implemented via the overrides.json file and the "Download button" JupyterLab extension (please see scipy/scipy#22161 for an example). How should we go ahead with this?

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,7 @@ def setup(app):
# directories to ignore when looking for source files.
exclude_patterns = [
'substitutions.rst',
'regression/header.md',
'regression/*.ipynb' # exclude IPyNB files from the build
]

Expand Down
59 changes: 37 additions & 22 deletions doc/source/regression/dwt-idwt.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,7 @@ mystnb:

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

```{eval-rst}
.. currentmodule:: pywt

.. dropdown:: 🧑‍🔬 This notebook can be executed online. Click this section to try it out! ✨
:color: success

.. notebooklite:: dwt-idwt.ipynb
:width: 100%
:height: 600px
:prompt: Open notebook

.. dropdown:: Download this notebook
:color: info
:open:

Please use the following links to download this notebook in various formats:

1. :download:`Download IPyNB (IPython Notebook) <dwt-idwt.ipynb>`
2. :download:`Download Markdown Notebook (Jupytext) <dwt-idwt.md>`
```{include} header.md
```

+++
Expand Down Expand Up @@ -172,23 +154,45 @@ Remember that only one argument at a time can be `None`:

```{code-cell}
---
tags: [raises-exception]
tags: [raises-exception, remove-output]
---
print(pywt.idwt(None, None, 'db2', 'symmetric'))
```

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

```{code-block} python
:class: pywt-handcoded-cell-output
Traceback (most recent call last):
agriyakhetarpal marked this conversation as resolved.
Show resolved Hide resolved
...
ValueError: At least one coefficient parameter must be specified.
```

+++

### Coefficients data size in `pywt.idwt`

When doing the `idwt` transform, usually the coefficient arrays
must have the same size.

```{code-cell}
---
tags: [raises-exception]
tags: [raises-exception, remove-output]
---
print(pywt.idwt([1, 2, 3, 4, 5], [1, 2, 3, 4], 'db2', 'symmetric'))
```

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

```{code-block} python
:class: pywt-handcoded-cell-output
Traceback (most recent call last):
...
ValueError: Coefficients arrays must have the same size.
```

+++

Not every coefficient array can be used in `idwt`. In the
following example the `idwt` will fail because the input arrays are
invalid - they couldn't be created as a result of `dwt`, because
Expand All @@ -197,11 +201,22 @@ mode is `4`, not `3`:

```{code-cell}
---
tags: [raises-exception]
tags: [raises-exception, remove-output]
---
pywt.idwt([1,2,4], [4,1,3], 'db4', 'symmetric')
```

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

```{code-block} python
:class: pywt-handcoded-cell-output
Traceback (most recent call last):
...
ValueError: Invalid coefficient arrays length for specified wavelet. Wavelet and mode must be the same as used for decomposition.
```

+++

```{code-cell}
int(pywt.dwt_coeff_len(1, pywt.Wavelet('db4').dec_len, 'symmetric'))
```
20 changes: 1 addition & 19 deletions doc/source/regression/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,7 @@ kernelspec:

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

```{eval-rst}
.. currentmodule:: pywt

.. dropdown:: 🧑‍🔬 This notebook can be executed online. Click this section to try it out! ✨
:color: success

.. notebooklite:: gotchas.ipynb
:width: 100%
:height: 600px
:prompt: Open notebook

.. dropdown:: Download this notebook
:color: info
:open:

Please use the following links to download this notebook in various formats:

1. :download:`Download IPyNB (IPython Notebook) <gotchas.ipynb>`
2. :download:`Download Markdown Notebook (Jupytext) <gotchas.md>`
```{include} header.md
```

+++
Expand Down
13 changes: 13 additions & 0 deletions doc/source/regression/header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
````{tip}

This page can also be run or downloaded as a notebook.

```{jupyterlite} {{ parent_docname }}.ipynb
:new_tab: True
```

Downloads:

1. {download}`Download {{ parent_docname }}.ipynb <{{ parent_docname }}.ipynb>`
2. {download}`Download {{ parent_docname }}.md <{{ parent_docname }}.md>`
````
29 changes: 9 additions & 20 deletions doc/source/regression/modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,7 @@ kernelspec:

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

```{eval-rst}
.. currentmodule:: pywt

.. dropdown:: 🧑‍🔬 This notebook can be executed online. Click this section to try it out! ✨
:color: success

.. notebooklite:: modes.ipynb
:width: 100%
:height: 600px
:prompt: Open notebook

.. dropdown:: Download this notebook
:color: info
:open:

Please use the following links to download this notebook in various formats:

1. :download:`Download IPyNB (IPython Notebook) <modes.ipynb>`
2. :download:`Download Markdown Notebook (Jupytext) <modes.md>`
```{include} header.md
```

+++
Expand Down Expand Up @@ -67,11 +49,18 @@ Therefore, an invalid mode name should raise a `ValueError`:

```{code-cell}
---
tags: [raises-exception]
tags: [raises-exception, remove-output]
---
pywt.dwt([1,2,3,4], 'db2', 'invalid')
```

```{code-block} python
:class: pywt-handcoded-cell-output
Traceback (most recent call last):
...
ValueError: Unknown mode name 'invalid'.
```

You can also refer to modes via the attributes of the `Modes` class:

```{code-cell}
Expand Down
Loading