Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# GitHub syntax highlighting
pixi.lock linguist-language=YAML linguist-generated=true
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/build/*
.ipynb_checkpoints
/.pixi/*

# The following are boilerplate. It is best practice to handle this generically
# in ~/.gitignore, but including it here is helpful for beginners.
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Executable Tutorials

A repository demonstrating one way to manage and distribute interactive tutorials.
See the [Guide to Contributing](https://scentific-python.org/executable-tutorials/contributing.html)
for a good overview of what this is like to use.

## Demo Links

- **[Tutorials (a.k.a. recipes)](https://github.com/scientific-python/executable-tutorials/tree/notebooks/docs/recipes)** are written in MyST Markdown.
- They are published in **[Jupyter notebook](https://github.com/scientific-python/executable-tutorials/tree/notebooks/docs/recipes)** format, on the `notebooks` branch of this repo, which can be used in Colab, Binder, etc.
- Executed examples are **[published](https://scientific-python.github.io/exeuctable-tutorials/)** on a GitHub Pages site.
- **[Jupyter Lite (beta)](https://scientific-python.github.io/executable-tutorials/jupyterlite/lab/index.html)** (works only on the basic executable example so far, missing dependencies for others)
- **[Binder](https://mybinder.org/v2/gh/scientific-python/executable-tutorials/notebooks)**


## Goals

- Make content easy to explore and try in a variety of modes:
- interactive and non-interactive
- local and cloud-based
- Jupyter and not-Jupyter
- Document an accessible development workflow, so that non-experts can contribute.
- Keep the infrastructure as simple as possible.

## To Do

- Test notebook execution _of changed recipes only_ in CI on PR.
- Set up devcontainer.
- Add example with additional dependencies.

## Prior Art

Examples that this is drawing from:

- https://github.com/Caltech-IPAC/irsa-tutorials
- https://github.com/MotherDuck-Open-Source/sql-tutorial
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
extensions = ["myst_nb", "sphinx_copybutton"]

html_theme = 'sphinx_book_theme'
html_title = 'Executable Tutorials'
html_theme_options = {
"repository_url": "https://github.com/scientific-python/executable-tutorials",
"path_to_docs": "docs",
"repository_branch": "main",
"use_repository_button": True,
"use_issues_button": True,
"use_edit_page_button": True,
}
nb_execution_mode = "auto"
163 changes: 163 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Guide to Contributing

First, clone this repository.

With HTTPS:

```sh
git clone https://github.com/scientific-python/executable-tutorials
```

With SSH:
```sh
git clone git@github.com:scientific-python/executable-tutorials
```

## Overview

Each "recipe" is a directory under `docs/recipes/`. It may contain one or more
Markdown (`.md`) files with a mixture of narrative text and code. Each recipe
directory may also contain supporting data files, scripts, illustrations,
solutions to exercises, etc.

```none
$ tree docs/
docs/
├── conf.py
├── contributing.md
├── index.md
├── recipes
│   ├── executable
│   │   ├── basics.md
│   ├── matplotlib
│   │   ├── interactive_mpl.md
│   │   └── static_mpl.md
│   └── static
│   └── static.md
...
```

Some of these Markdown files include a header that describes how to convert
them into Jupyter notebooks and execute the code in them. This is described in
more detail below.

## Setup

We use the [pixi](https://pixi.sh/latest/#installation) package manager to
create an environment with dependencies from conda-forge and PyPI. We recommend
installing pixi. Then just:

```sh
pixi install
```

But if you have a strong preference for a different package manager, peek
inside `pixi.toml` to find the list of dependencies and install them however
you wish.

## Build

```sh
pixi run build
```

```{note}
Later, if the build results get into a broken state, use `pixi run clean`
to start fresh.
```

This creates:
- Executed notebooks, generated from eligible Markdown files, under `build/juptyer_execute/`
- HTML under `build/html/`

Open `build/html/index.html` in a web browser.

## Develop

```
pixi run jupyter lab
```

In the file browser, locate one of the examples under `docs/recipes/`. Double
click to open.

Files like `docs/recipes/static/static.md` are plain Markdown files. Files like
`docs/recipes/executable/basics.md` have a YAML header which enables the
documentation build system to convert them to Jupyter notebooks and execute
them:

```yaml
---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.16.4
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---
```

One option is to simply edit these files in any text editor.

Another is to use Jupyter Lab, which can open these files _as notebooks_
enabling you the interactively edit and execute them, saving the changes to
Markdown.

![Context Menu: Open With... Jupytext Notebook](./_static/images/open-with-jupytext-notebook.png)

Note that the cell _outputs_ are not saved to disk. This is a feature, not a
bug: the outputs are build products, and they do not belong in version control.
During the build process, the Markdown document is converted to a Jupyter
notebook format and executed.

## Test

The script `test.sh` runs files with executable code from top to bottom and
prints any unexpected tracebacks.

```
pixi run ./test.sh docs/recipes/executable/basics.md
```

`````{note}

Sometimes examples are _expected_ to raise an exception. These can be marked up
with a "tag" like so. With that tag, the build and test procedures will pass
over the exception.

````markdown
```{code-cell} ipython3
:tags: [raises-exception]

1 / 0
```
````
`````

To test _all_ examples, run:

```
pixi run ./test.sh --all
```

(Above, the script automatically discovers all Markdown files which have that
YAML header marking them as executable.)

## Deploy

Once changes are merged to the `main` branch, the GitHub Actions [Publish workflow][] will:
- Publish HTML to this site.
- Uploaded the executed ipynb notebooks as a GitHub Artifact (useful for debugging)
as an Artifact associated with the workflow run.
- Push to [the `notebooks` branch of this repository][notebooks-branch] a version
where all Markdown Jupytext files have been converted to `.ipynb` format, using
`pixi run ./convert-all ipynb`. This is suitable for use in environments
where users need to be handed Jupyter Notebook files directly, such as Google
Colab.

[notebooks-branch]: https://github.com/scientific-python/executable-tutorials/tree/notebooks/docs/recipes
[Publish workflow]: https://github.com/scientific-python/executable-tutorials/actions/workflows/cd.yml
8 changes: 8 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Recipes

```{toctree}
:maxdepth: 1
:glob:
recipes/**/*
contributing
```
36 changes: 36 additions & 0 deletions docs/recipes/executable/basics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.16.4
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---

# Executable Code

## Basics

```{code-cell} ipython3
1 + 3
```

```{code-cell} ipython3
a = 8**2
```

```{code-cell} ipython3
a
```

This cell has an expected error:

```{code-cell} ipython3
:tags: [raises-exception]

1 / 0
```
65 changes: 65 additions & 0 deletions docs/recipes/matplotlib/interactive_mpl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.16.4
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---

# Interactive Matplotlib Figures

Specify the interactive Jupyter matplotlib backend, backed by ipympl. This must be run first, before any figures are created.

```{code-cell} ipython3
%matplotlib widget
```

Creating a new figure display an interactive canvas in Jupyter Lab.

If we do nothing else, this will display a snapshot of the currently-blank canvas in the rendered HTML documentation. To avoid that, we edit the Markdown file so that the cell below has the tag `hide-output`. This places the figure in an expandable box, hidden by fault. Alternatively, we could use `remove-output` to fully remove it.

````markdown
```{code-cell} ipython3
:tags: [hide-output]
import matplotlib.pyplot as plt


plt.plot([1,2,3])
```
````

```{code-cell} ipython3
:tags: [hide-output]

import matplotlib.pyplot as plt

plt.figure()
```

Plotting data to an existing figure updates the original interactive canvas in Jupyter Lab. Users can scroll up to pan and zoom.

To show an updated snapshot in the rendered HTML documentation, we should call `plt.gcf()` to display the current Figure.

```{caution}
If you re-render the canvas---such as by displaying `plt.gcf().canvas`---that will cause the cached snapshot of the figures above to update to show the latest version of the figure, ruining the sequential narrative in the rendered HTML documentation.

This is due to a detail of the matplotlib--Jupyter interaction. Just know to use `plt.gcf()` to safely show snapshots.
```

```{code-cell} ipython3
plt.plot([1, 2, 3, 4])
```

```{code-cell} ipython3
plt.gcf()
```

```{code-cell} ipython3
plt.plot([1, 4, 9, 16])
plt.gcf()
```
35 changes: 35 additions & 0 deletions docs/recipes/matplotlib/static_mpl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.16.4
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---

# Static Matplotlib Figures

Specify the static Jupyter matplotlib integration. This must be run first, before any figures are created.

This disables any interactivity, displaying only a PNG image of the figure in the Jupyter Lab view.

```{code-cell} ipython3
%matplotlib inline
```

```{code-cell} ipython3
import matplotlib.pyplot as plt

plt.plot([1,2,3,4])
```


```{code-cell} ipython3
import matplotlib.pyplot as plt

plt.plot([1,4,9,16])
```
Binary file added docs/recipes/static/nsls2-aerial.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions docs/recipes/static/static.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Static Code

This is a vanilla (CommonMark) Markdown document with text and code cells.
The code won't be executed; it is meant to be read or copy/pasted.

```python
class Thing:
def __init__(self, stuff):
self.stuff = stuff

thing = Thing(3)
```

Here is an image that is bundled with the recipe.

![Aerial photo on NSLS-II at Brookhaven National Laboratory](./nsls2-aerial.jpg)
Loading