Skip to content

Commit 722f3ad

Browse files
committed
Initial commit
0 parents  commit 722f3ad

File tree

8 files changed

+190
-0
lines changed

8 files changed

+190
-0
lines changed

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Jupyter Notebook Splitter
2+
3+
This tool splits a Jupyter Notebook into Sub-Notebooks depending on cell metadata. It converts a *Master* Notebook into a *Teacher* Notebook and a *Student* Notebook; or into a *Slides* Notebook, a *Tasks* Notebook, and a *Solutions* Notebook.
4+
5+
## Installation
6+
7+
Please, for now, download the script and place it into the folder where you want to split Notebooks in.
8+
9+
## Usage
10+
11+
### Overview
12+
13+
**TL;DR: See `./notebook-splitter.py --help`.**
14+
15+
1. Add [cell metadata](https://ipython.org/ipython-doc/3/notebook/nbformat.html#cell-metadata) to your Jupyter Notebook: Add an `exercise` key (default, can be changed) to the metadata (JSON); give it values (*tags*) on which to create Sub-Notebooks
16+
17+
```json
18+
{
19+
"exercise": "task"
20+
}
21+
// another cell
22+
{
23+
"exercise": "solution"
24+
}
25+
```
26+
27+
2. Use `--keep` and `--remove` flags of the Notebook Splitter to keep and remove cells with according *tags*; export it to the respective Notebook:
28+
29+
```bash
30+
./notebook-splitter.py input.ipynb --keep task --remove solution -o tasks.ipynb
31+
./notebook-splitter.py input.ipynb --keep solution --remove task -o solutions.ipynb
32+
./notebook-splitter.py input.ipynb --remove task --remove solution -o slides.ipynb
33+
```
34+
35+
### Examples in Action
36+
37+
See the `examples` directory in this repository.
38+
39+
### Options
40+
41+
* **Repeated Parameters**: `--keep` and `--remove` parameters on the command line of the script can be given multiple times: `--keep task --keep onlytask --remove solution`
42+
* **Remove *All***: As a special parameter value, `--remove all` will remove *all* cells except those for which a `--keep` value is specified (*`--keep all`* is the default)
43+
* **Stdin/Stdout**: If no output file is given with `-o`/`--output`, the resulting Notebook will be printed to `stdout`; if no input file as a parameter is given, the input Notebook will be read from `stdin` (good for Linux-like daisy-chaining of tools)
44+
* **Change *Basekey***: In the above example, the cell meta data key of discrimination is `exercise` which is the default. With `--basekey`, this can be changed.
45+
46+
### Limitations
47+
48+
The values to the `--keep` and `--remove` parameters create sets of values to keep and remove. One could implement this tool probably quite cleverly with set operations (with the added complication of the `--remove all` ). If you can, feel free to file a merge request!

examples/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.ipynb_checkpoints

examples/Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
NB_MASTER = Notebook-Master.ipynb
2+
3+
NB_EMPTY = Notebook-Empty.ipynb
4+
NB_TASK = Notebook-Task.ipynb
5+
NB_SOLUTION = Notebook-Solution.ipynb
6+
7+
.PHONY: all clean
8+
all: $(NB_SOLUTION) $(NB_TASK) $(NB_EMPTY)
9+
10+
$(NB_TASK): $(NB_MASTER) Makefile
11+
../notebook-splitter.py $< --keep task --remove solution -o $@
12+
13+
$(NB_SOLUTION): $(NB_MASTER) Makefile
14+
../notebook-splitter.py $< --keep solution --remove task -o $@
15+
16+
$(NB_EMPTY): $(NB_MASTER) Makefile
17+
../notebook-splitter.py $< --keep even-empty --remove all -o $@
18+
19+
clean:
20+
rm $(NB_SOLUTION) $(NB_TASK) $(NB_EMPTY)

examples/Notebook-Empty.ipynb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"cells": [{"cell_type": "markdown", "metadata": {"exercise": "even-empty"}, "source": ["This cell is included in every Sub-Notebook, even the [*Empty* Notebook](Notebook-Empty.ipynb)"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.2"}}, "nbformat": 4, "nbformat_minor": 2}

examples/Notebook-Master.ipynb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Notebook Splitter Demo"
8+
]
9+
},
10+
{
11+
"cell_type": "markdown",
12+
"metadata": {
13+
"exercise": "even-empty"
14+
},
15+
"source": [
16+
"This cell is included in every Sub-Notebook, even the [*Empty* Notebook](Notebook-Empty.ipynb)"
17+
]
18+
},
19+
{
20+
"cell_type": "markdown",
21+
"metadata": {
22+
"exercise": "task"
23+
},
24+
"source": [
25+
"This cell is included in the [*Task* Notebook](Notebook-Task.ipynb), but not in the *Solution* Notebook"
26+
]
27+
},
28+
{
29+
"cell_type": "markdown",
30+
"metadata": {
31+
"exercise": "solution"
32+
},
33+
"source": [
34+
"This cell is included in the [*Solution* Notebook](Notebook-Solution.ipynb), but not in the *Task* Notebook"
35+
]
36+
}
37+
],
38+
"metadata": {
39+
"kernelspec": {
40+
"display_name": "Python 3",
41+
"language": "python",
42+
"name": "python3"
43+
},
44+
"language_info": {
45+
"codemirror_mode": {
46+
"name": "ipython",
47+
"version": 3
48+
},
49+
"file_extension": ".py",
50+
"mimetype": "text/x-python",
51+
"name": "python",
52+
"nbconvert_exporter": "python",
53+
"pygments_lexer": "ipython3",
54+
"version": "3.7.2"
55+
}
56+
},
57+
"nbformat": 4,
58+
"nbformat_minor": 2
59+
}

examples/Notebook-Solution.ipynb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Notebook Splitter Demo"]}, {"cell_type": "markdown", "metadata": {"exercise": "even-empty"}, "source": ["This cell is included in every Sub-Notebook, even the [*Empty* Notebook](Notebook-Empty.ipynb)"]}, {"cell_type": "markdown", "metadata": {"exercise": "solution"}, "source": ["This cell is included in the [*Solution* Notebook](Notebook-Solution.ipynb), but not in the *Task* Notebook"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.2"}}, "nbformat": 4, "nbformat_minor": 2}

examples/Notebook-Task.ipynb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Notebook Splitter Demo"]}, {"cell_type": "markdown", "metadata": {"exercise": "even-empty"}, "source": ["This cell is included in every Sub-Notebook, even the [*Empty* Notebook](Notebook-Empty.ipynb)"]}, {"cell_type": "markdown", "metadata": {"exercise": "task"}, "source": ["This cell is included in the [*Task* Notebook](Notebook-Task.ipynb), but not in the *Solution* Notebook"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.2"}}, "nbformat": 4, "nbformat_minor": 2}

notebook-splitter.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python3
2+
# Andreas Herten, 25 February 2019
3+
import json
4+
import copy
5+
import os
6+
import sys
7+
import argparse
8+
import textwrap
9+
10+
11+
def parse(inputfile, keep, remove, basekey):
12+
"""
13+
From inputfile, parse the JSON and remove those cells which have values of the basekey, which are in the list of tags to remove but not in the list of tags to remove.
14+
"""
15+
notebook = json.load(inputfile)
16+
17+
if isinstance(keep, str): keep = [keep]
18+
if isinstance(remove, str): remove = [remove]
19+
20+
notebook_new = copy.deepcopy(notebook)
21+
notebook_new["cells"] = []
22+
23+
for cell in notebook["cells"]:
24+
keepcell = True
25+
for key in remove:
26+
cell_tags = None
27+
if basekey in cell["metadata"]:
28+
cell_tags = cell["metadata"][basekey]
29+
if isinstance(cell_tags, str): cell_tags = [cell_tags]
30+
if cell_tags == None:
31+
if key == "all":
32+
keepcell = False
33+
if cell_tags != None:
34+
if key in cell_tags or key == "all":
35+
for tag in cell_tags:
36+
if tag not in keep:
37+
keepcell = False
38+
if keepcell == True:
39+
notebook_new["cells"].append(cell)
40+
41+
return notebook_new
42+
43+
44+
if __name__ == '__main__':
45+
parser = argparse.ArgumentParser(description=textwrap.dedent("""
46+
Split up Jupyter Notebook to Sub-Notebooks by cell metadata.
47+
48+
Specify tags to keep with `--keep`, specify tags to remove with `--remove`; multiple instances are ok. Special `--remove` tag: all (which removes all cells except those specified with `--keep`).
49+
All tags will be read from BASEKEY, which usually is `exercise`, if you do not specify it differently.
50+
"""), formatter_class=argparse.RawTextHelpFormatter)
51+
parser.add_argument('infile', type=argparse.FileType('r'), nargs='?', default=sys.stdin, help="Input file to parse")
52+
parser.add_argument('--output', "-o", nargs='?', type=argparse.FileType('w'), default=sys.stdout, help="Output file to parse to")
53+
parser.add_argument('--keep', "-k", action="append", help="Keep these tags.")
54+
parser.add_argument('--remove', "-r", action="append", help="Remove these tags. Special: all (remove all except for those of --keep).")
55+
parser.add_argument('--basekey', type=str, help="Basekey to use for discriminating the tags.", default="exercise")
56+
args = parser.parse_args()
57+
notebook = parse(inputfile=args.infile, keep=args.keep, remove=args.remove, basekey=args.basekey)
58+
59+
json.dump(notebook, args.output)

0 commit comments

Comments
 (0)