Skip to content

Commit 4aa1da4

Browse files
authored
feat: add ability to run commands after a cell (#94)
* add post-command * bump isort to fix Poetry configuration is invalid * lints * format post command in error message * handle masked exceptions * s/post_command/post_cell_execute/ * s/post command/post cell execution
1 parent 8a92468 commit 4aa1da4

6 files changed

+200
-17
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ repos:
1919
- id: black
2020
files: '(src|tests).*py'
2121
- repo: https://github.com/PyCQA/isort
22-
rev: 5.6.4
22+
rev: 5.12.0
2323
hooks:
2424
- id: isort
2525
args: ["-m", "3", "--tc"]

src/nbmake/nb_run.py

+29-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pathlib import Path
2-
from typing import Any, Dict, Optional
2+
from typing import Any, Dict, List, Optional
33

44
import nbformat
55
from nbclient.client import (
@@ -79,20 +79,34 @@ async def apply_mocks(
7979
if execute_reply["content"]["ename"] == "ModuleNotFoundError":
8080
if self.find_import_errors:
8181
raise CellImportError()
82-
83-
if c.kc is None:
84-
raise Exception("there is no kernelclient")
85-
mocks: Dict[str, Any] = (
86-
cell.get("metadata", {}).get("nbmake", {}).get("mock", {})
87-
)
88-
for v in mocks:
89-
if isinstance(mocks[v], str):
90-
out = await c.kc.execute_interactive(f"{v} = '{mocks[v]}'")
91-
else:
92-
out = await c.kc.execute_interactive(f"{v} = {mocks[v]}")
93-
94-
if out["content"]["status"] != "ok":
95-
raise Exception(f"Failed to apply mock {v}\n\n{str(out)}")
82+
else:
83+
if c.kc is None:
84+
raise Exception("there is no kernelclient")
85+
mocks: Dict[str, Any] = (
86+
cell.get("metadata", {}).get("nbmake", {}).get("mock", {})
87+
)
88+
for v in mocks:
89+
if isinstance(mocks[v], str):
90+
out = await c.kc.execute_interactive(f"{v} = '{mocks[v]}'")
91+
else:
92+
out = await c.kc.execute_interactive(f"{v} = {mocks[v]}")
93+
94+
if out["content"]["status"] != "ok":
95+
raise Exception(f"Failed to apply mock {v}\n\n{str(out)}")
96+
97+
post_cell_execute: List[str] = (
98+
cell.get("metadata", {})
99+
.get("nbmake", {})
100+
.get("post_cell_execute", [])
101+
)
102+
if post_cell_execute:
103+
pce = "\n".join(post_cell_execute)
104+
out = await c.kc.execute_interactive(pce)
105+
106+
if out["content"]["status"] != "ok":
107+
raise Exception(
108+
f"Post cell execution failed:\n{pce}\n\n{str(out)}"
109+
)
96110

97111
c.on_cell_executed = apply_mocks
98112

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {
7+
"nbmake": {
8+
"post_cell_execute": [
9+
"y = 3",
10+
"z = x+y"
11+
]
12+
}
13+
},
14+
"outputs": [],
15+
"source": [
16+
"x = 1\n",
17+
"y = 2\n",
18+
"z = 0\n",
19+
"# this cell has a post_cell_execute that assigns y and z"
20+
]
21+
},
22+
{
23+
"cell_type": "code",
24+
"execution_count": null,
25+
"metadata": {},
26+
"outputs": [],
27+
"source": [
28+
"assert y == 3\n",
29+
"assert z == 4"
30+
]
31+
}
32+
],
33+
"metadata": {
34+
"kernelspec": {
35+
"display_name": ".venv",
36+
"language": "python",
37+
"name": "python3"
38+
},
39+
"language_info": {
40+
"name": "python",
41+
"version": "3.10.5"
42+
},
43+
"vscode": {
44+
"interpreter": {
45+
"hash": "b62bdd1a52024cb952ae76a82719d36933eb1b9c8ddb26512ead942ee2142676"
46+
}
47+
}
48+
},
49+
"nbformat": 4,
50+
"nbformat_minor": 2
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {
7+
"nbmake": {
8+
"post_cell_execute": [
9+
"raise Exception('boom!')"
10+
]
11+
}
12+
},
13+
"outputs": [],
14+
"source": [
15+
"\"This cell has a post cell execution that fails\""
16+
]
17+
}
18+
],
19+
"metadata": {
20+
"kernelspec": {
21+
"display_name": ".venv",
22+
"language": "python",
23+
"name": "python3"
24+
},
25+
"language_info": {
26+
"codemirror_mode": {
27+
"name": "ipython",
28+
"version": 3
29+
},
30+
"file_extension": ".py",
31+
"mimetype": "text/x-python",
32+
"name": "python",
33+
"nbconvert_exporter": "python",
34+
"pygments_lexer": "ipython3",
35+
"version": "3.10.5"
36+
},
37+
"vscode": {
38+
"interpreter": {
39+
"hash": "b62bdd1a52024cb952ae76a82719d36933eb1b9c8ddb26512ead942ee2142676"
40+
}
41+
}
42+
},
43+
"nbformat": 4,
44+
"nbformat_minor": 2
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {
7+
"nbmake": {
8+
"post_cell_execute": [
9+
"raise Exception('boom!')"
10+
]
11+
}
12+
},
13+
"outputs": [],
14+
"source": [
15+
"raise Exception(\"bang!\")\n",
16+
"# this cell has a post_cell_execute that also raises an exception"
17+
]
18+
}
19+
],
20+
"metadata": {
21+
"kernelspec": {
22+
"display_name": ".venv",
23+
"language": "python",
24+
"name": "python3"
25+
},
26+
"language_info": {
27+
"codemirror_mode": {
28+
"name": "ipython",
29+
"version": 3
30+
},
31+
"file_extension": ".py",
32+
"mimetype": "text/x-python",
33+
"name": "python",
34+
"nbconvert_exporter": "python",
35+
"pygments_lexer": "ipython3",
36+
"version": "3.10.5"
37+
},
38+
"vscode": {
39+
"interpreter": {
40+
"hash": "b62bdd1a52024cb952ae76a82719d36933eb1b9c8ddb26512ead942ee2142676"
41+
}
42+
}
43+
},
44+
"nbformat": 4,
45+
"nbformat_minor": 2
46+
}

tests/test_nb_run.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import os
21
from pathlib import Path
32

3+
import pytest
44
from nbformat import write
55
from nbformat.v4 import new_code_cell, new_notebook, new_output
66
from pytest import Pytester
@@ -122,6 +122,33 @@ def test_when_mock_then_succeeds(self, testdir2: Never):
122122
res: NotebookResult = run.execute()
123123
assert res.error == None
124124

125+
def test_when_post_cell_execute_then_succeeds(self, testdir2: Never):
126+
nb = Path(__file__).parent / "resources" / "post_cell_execute.ipynb"
127+
run = NotebookRun(nb, 300)
128+
res: NotebookResult = run.execute()
129+
assert res.error == None
130+
131+
def test_when_post_cell_execute_then_command_fails(self, testdir2: Never):
132+
nb = Path(__file__).parent / "resources" / "post_cell_execute_error.ipynb"
133+
run = NotebookRun(nb, 300)
134+
with pytest.raises(Exception) as exc_info:
135+
run.execute()
136+
137+
assert exc_info != None
138+
assert "boom!" in exc_info.value.args[0]
139+
140+
def test_when_post_cell_execute_then_cell_fails(self, testdir2: Never):
141+
nb = (
142+
Path(__file__).parent / "resources" / "post_cell_execute_masked_error.ipynb"
143+
)
144+
run = NotebookRun(nb, 300)
145+
res: NotebookResult = run.execute()
146+
147+
# make sure the cell exception (bang!) is raised and not masked
148+
# by the post cell execution exception (boom!)
149+
assert res.error != None
150+
assert "bang!" in res.error.summary
151+
125152
def test_when_magic_error_then_fails(self, testdir2: Never):
126153
nb = Path(__file__).parent / "resources" / "magic_error.ipynb"
127154
run = NotebookRun(nb, 300)

0 commit comments

Comments
 (0)