Skip to content

Commit b52a82d

Browse files
MSealwillingc
andauthored
Execute preprocess cells (#1380)
* Added execute preprocess cells Co-authored-by: Carol Willing <[email protected]>
1 parent 4e3f935 commit b52a82d

File tree

2 files changed

+114
-10
lines changed

2 files changed

+114
-10
lines changed

nbconvert/preprocessors/execute.py

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
# Copyright (c) IPython Development Team.
55
# Distributed under the terms of the Modified BSD License.
6+
from typing import Optional
7+
from nbformat import NotebookNode
68
from nbclient import NotebookClient, execute as _execute
9+
from nbclient.util import run_sync
710
# Backwards compatability for imported name
811
from nbclient.exceptions import CellExecutionError
912

@@ -31,12 +34,27 @@ def __init__(self, **kw):
3134
Preprocessor.__init__(self, nb=nb, **kw)
3235
NotebookClient.__init__(self, nb, **kw)
3336

37+
def _check_assign_resources(self, resources):
38+
if resources or not hasattr(self, 'resources'):
39+
self.resources = resources
40+
3441
def preprocess(self, nb, resources=None, km=None):
3542
"""
3643
Preprocess notebook executing each code cell.
3744
3845
The input argument *nb* is modified in-place.
3946
47+
Note that this function recalls NotebookClient.__init__, which may look wrong.
48+
However since the preprocess call acts line an init on exeuction state it's expected.
49+
Therefore, we need to capture it here again to properly reset because traitlet
50+
assignments are not passed. There is a risk if traitlets apply any side effects for
51+
dual init.
52+
The risk should be manageable, and this approach minimizes side-effects relative
53+
to other alternatives.
54+
55+
One alternative but rejected implementation would be to copy the client's init internals
56+
which has already gotten out of sync with nbclient 0.5 release before nbcovnert 6.0 released.
57+
4058
Parameters
4159
----------
4260
nb : NotebookNode
@@ -56,11 +74,73 @@ def preprocess(self, nb, resources=None, km=None):
5674
resources : dictionary
5775
Additional resources used in the conversion process.
5876
"""
59-
# Copied from NotebookClient init :/
60-
self.nb = nb
61-
self.km = km
62-
if resources:
63-
self.resources = resources
64-
self.reset_execution_trackers()
77+
NotebookClient.__init__(self, nb, km)
78+
self._check_assign_resources(resources)
6579
self.execute()
66-
return nb, resources
80+
return self.nb, self.resources
81+
82+
async def async_execute_cell(
83+
self,
84+
cell: NotebookNode,
85+
cell_index: int,
86+
execution_count: Optional[int] = None,
87+
store_history: bool = False) -> NotebookNode:
88+
"""
89+
Executes a single code cell.
90+
91+
Overwrites NotebookClient's version of this method to allow for preprocess_cell calls.
92+
93+
Parameters
94+
----------
95+
cell : nbformat.NotebookNode
96+
The cell which is currently being processed.
97+
cell_index : int
98+
The position of the cell within the notebook object.
99+
execution_count : int
100+
The execution count to be assigned to the cell (default: Use kernel response)
101+
store_history : bool
102+
Determines if history should be stored in the kernel (default: False).
103+
Specific to ipython kernels, which can store command histories.
104+
105+
Returns
106+
-------
107+
output : dict
108+
The execution output payload (or None for no output).
109+
110+
Raises
111+
------
112+
CellExecutionError
113+
If execution failed and should raise an exception, this will be raised
114+
with defaults about the failure.
115+
116+
Returns
117+
-------
118+
cell : NotebookNode
119+
The cell which was just processed.
120+
"""
121+
# Copied and intercepted to allow for custom preprocess_cell contracts to be fullfilled
122+
self.store_history = store_history
123+
cell, resources = self.preprocess_cell(cell, self.resources, cell_index)
124+
if execution_count:
125+
cell['execution_count'] = execution_count
126+
return cell, resources
127+
128+
def preprocess_cell(self, cell, resources, index, **kwargs):
129+
"""
130+
Override if you want to apply some preprocessing to each cell.
131+
Must return modified cell and resource dictionary.
132+
133+
Parameters
134+
----------
135+
cell : NotebookNode cell
136+
Notebook cell being processed
137+
resources : dictionary
138+
Additional resources used in the conversion process. Allows
139+
preprocessors to pass variables into the Jinja engine.
140+
index : int
141+
Index of the cell being processed
142+
"""
143+
self._check_assign_resources(resources)
144+
# Because nbclient is an async library, we need to wrap the parent async call to generate a syncronous version.
145+
cell = run_sync(NotebookClient.async_execute_cell)(self, cell, index, store_history=self.store_history)
146+
return cell, self.resources

nbconvert/preprocessors/tests/test_execute.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import pytest
1212
import nbformat
1313

14+
from copy import deepcopy
15+
1416
from ..execute import ExecutePreprocessor, executenb
1517

1618

@@ -58,19 +60,41 @@ def test_basic_execution():
5860
fname = os.path.join(os.path.dirname(__file__), 'files', 'HelloWorld.ipynb')
5961
with open(fname) as f:
6062
input_nb = nbformat.read(f, 4)
61-
output_nb, _ = preprocessor.preprocess(input_nb)
63+
output_nb, _ = preprocessor.preprocess(deepcopy(input_nb))
6264
assert_notebooks_equal(input_nb, output_nb)
6365

66+
6467
def test_executenb():
6568
fname = os.path.join(os.path.dirname(__file__), 'files', 'HelloWorld.ipynb')
6669
with open(fname) as f:
6770
input_nb = nbformat.read(f, 4)
6871
with pytest.warns(FutureWarning):
69-
output_nb = executenb(input_nb)
72+
output_nb = executenb(deepcopy(input_nb))
7073
assert_notebooks_equal(input_nb, output_nb)
7174

75+
7276
def test_populate_language_info():
7377
preprocessor = ExecutePreprocessor(kernel_name="python")
7478
nb = nbformat.v4.new_notebook() # Certainly has no language_info.
75-
nb, _ = preprocessor.preprocess(nb, resources={})
79+
preprocessor.preprocess(nb, resources={})
80+
# Should mutate input
7681
assert 'language_info' in nb.metadata # See that a basic attribute is filled in
82+
83+
84+
def test_preprocess_cell():
85+
class CellReplacer(ExecutePreprocessor):
86+
def preprocess_cell(self, cell, resources, index, **kwargs):
87+
cell.source = "print('Ignored')"
88+
return super().preprocess_cell(cell, resources, index, **kwargs)
89+
90+
preprocessor = CellReplacer()
91+
fname = os.path.join(os.path.dirname(__file__), 'files', 'HelloWorld.ipynb')
92+
with open(fname) as f:
93+
input_nb = nbformat.read(f, 4)
94+
output_nb, _ = preprocessor.preprocess(deepcopy(input_nb))
95+
expected_nb = deepcopy(input_nb)
96+
for cell in expected_nb.cells:
97+
cell.source = "print('Ignored')"
98+
for output in cell.outputs:
99+
output.text = 'Ignored\n'
100+
assert_notebooks_equal(expected_nb, output_nb)

0 commit comments

Comments
 (0)