29
29
logger = logging .getLogger (__name__ )
30
30
31
31
32
- def blank_nb (kernel_name ):
33
- try :
34
- spec = get_kernel_spec (kernel_name )
35
- except NoSuchKernel as e :
36
- raise ExtensionError ('Unable to find kernel' , orig_exc = e )
37
- return nbformat .v4 .new_notebook (metadata = {
38
- 'kernelspec' : {
39
- 'display_name' : spec .display_name ,
40
- 'language' : spec .language ,
41
- 'name' : kernel_name ,
42
- }
43
- })
44
-
45
-
46
- def split_on (pred , it ):
47
- """Split an iterator wherever a predicate is True."""
48
-
49
- counter = 0
50
-
51
- def count (x ):
52
- nonlocal counter
53
- if pred (x ):
54
- counter += 1
55
- return counter
56
-
57
- # Return iterable of lists to ensure that we don't lose our
58
- # place in the iterator
59
- return (list (x ) for _ , x in groupby (it , count ))
60
-
61
-
62
- class Cell (docutils .nodes .container ):
63
- """Container for input/output from Jupyter kernel"""
64
- pass
65
-
66
- class KernelNode (docutils .nodes .Element ):
67
- """Dummy node for signaling a new kernel"""
68
- pass
69
-
32
+ ### Directives and their associated doctree nodes
70
33
71
34
class JupyterKernel (Directive ):
35
+ """Specify a new Jupyter Kernel.
36
+
37
+ Arguments
38
+ ---------
39
+ kernel_name : str (optional)
40
+ The name of the kernel in which to execute future Jupyter cells, as
41
+ reported by executing 'jupyter kernelspec list' on the command line.
42
+
43
+ Options
44
+ -------
45
+ id : str
46
+ An identifier for *this kernel instance*. Used to name any output
47
+ files generated when executing the Jupyter cells (e.g. images
48
+ produced by cells, or a script containing the cell inputs).
49
+
50
+ Content
51
+ -------
52
+ None
53
+ """
72
54
73
55
optional_arguments = 1
74
56
final_argument_whitespace = False
@@ -79,15 +61,53 @@ class JupyterKernel(Directive):
79
61
}
80
62
81
63
def run (self ):
82
- kernel_name = self .arguments [0 ] if self .arguments else ''
83
- return [KernelNode (
64
+ return [JupyterKernelNode (
65
+ kernel_name = self .arguments [0 ] if self .arguments else '' ,
66
+ kernel_id = self .options .get ('id' , '' ),
67
+ )]
68
+
69
+
70
+ class JupyterKernelNode (docutils .nodes .Element ):
71
+ """Inserted into doctree whenever a JupyterKernel directive is encountered.
72
+
73
+ Used as a marker to signal that the following JupyterCellNodes (until the
74
+ next, if any, JupyterKernelNode) should be executed in a separate kernel.
75
+ """
76
+
77
+ def __init__ (self , kernel_name , kernel_id ):
78
+ super ().__init__ (
84
79
'' ,
85
80
kernel_name = kernel_name .strip (),
86
- kernel_id = self . options . get ( 'id' , '' ) .strip (),
87
- )]
81
+ kernel_id = kernel_id .strip (),
82
+ )
88
83
89
84
90
85
class JupyterCell (Directive ):
86
+ """Define a code cell to be later executed in a Jupyter kernel.
87
+
88
+ The content of the directive is the code to execute. Code is not
89
+ executed when the directive is parsed, but later during a doctree
90
+ transformation.
91
+
92
+ Arguments
93
+ ---------
94
+ filename : str (optional)
95
+ If provided, a path to a file containing code.
96
+
97
+ Options
98
+ -------
99
+ hide-code : bool
100
+ If provided, the code will not be displayed in the output.
101
+ hide-output : bool
102
+ If provided, the cell output will not be displayed in the output.
103
+ code-below : bool
104
+ If provided, the code will be shown below the cell output.
105
+
106
+ Content
107
+ -------
108
+ code : str
109
+ A code cell.
110
+ """
91
111
92
112
required_arguments = 0
93
113
optional_arguments = 1
@@ -122,17 +142,98 @@ def run(self):
122
142
self .assert_has_content ()
123
143
content = self .content
124
144
125
- # Cell only contains the input for now; we will execute the cell
126
- # and insert the output when the whole document has been parsed.
127
- return [Cell ('' ,
145
+ return [JupyterCellNode (content , self .options )]
146
+
147
+
148
+ class JupyterCellNode (docutils .nodes .container ):
149
+ """Inserted into doctree whever a JupyterKernel directive is encountered.
150
+
151
+ Used as a marker to signal that the following JupyterCellNodes (until the
152
+ next, if any, JupyterKernelNode) should be executed in a separate kernel.
153
+ """
154
+
155
+ def __init__ (self , source_lines , options ):
156
+ return super ().__init__ (
157
+ '' ,
128
158
docutils .nodes .literal_block (
129
- text = '\n ' .join (content ),
159
+ text = '\n ' .join (source_lines ),
130
160
),
131
- hide_code = ('hide-code' in self .options ),
132
- hide_output = ('hide-output' in self .options ),
133
- code_below = ('code-below' in self .options ),
134
- )]
161
+ hide_code = ('hide-code' in options ),
162
+ hide_output = ('hide-output' in options ),
163
+ code_below = ('code-below' in options ),
164
+ )
165
+
166
+
167
+ ### Doctree transformations
135
168
169
+ class ExecuteJupyterCells (SphinxTransform ):
170
+ """Execute code cells in Jupyter kernels.
171
+
172
+ Traverses the doctree to find JupyterKernel and JupyterCell nodes,
173
+ then executes the code in the JupyterCell nodes in sequence, starting
174
+ a new kernel every time a JupyterKernel node is encountered. The output
175
+ from each code cell is inserted into the doctree.
176
+ """
177
+ default_priority = 180 # An early transform, idk
178
+
179
+ def apply (self ):
180
+ doctree = self .document
181
+ doc_relpath = os .path .dirname (self .env .docname ) # relative to src dir
182
+ docname = os .path .basename (self .env .docname )
183
+ default_kernel = self .config .jupyter_execute_default_kernel
184
+ default_names = default_notebook_names (docname )
185
+
186
+ # Check if we have anything to execute.
187
+ if not doctree .traverse (JupyterCellNode ):
188
+ return
189
+
190
+ logger .info ('executing {}' .format (docname ))
191
+ output_dir = os .path .join (output_directory (self .env ), doc_relpath )
192
+
193
+ # Start new notebook whenever a JupyterKernelNode is encountered
194
+ jupyter_nodes = (JupyterCellNode , JupyterKernelNode )
195
+ nodes_by_notebook = split_on (
196
+ lambda n : isinstance (n , JupyterKernelNode ),
197
+ doctree .traverse (lambda n : isinstance (n , jupyter_nodes ))
198
+ )
199
+
200
+ for first , * nodes in nodes_by_notebook :
201
+ if isinstance (first , JupyterKernelNode ):
202
+ kernel_name = first ['kernel_name' ] or default_kernel
203
+ file_name = first ['kernel_id' ] or next (default_names )
204
+ else :
205
+ nodes = (first , * nodes )
206
+ kernel_name = default_kernel
207
+ file_name = next (default_names )
208
+
209
+ notebook = execute_cells (
210
+ kernel_name ,
211
+ [nbformat .v4 .new_code_cell (node .astext ()) for node in nodes ],
212
+ self .config .jupyter_execute_kwargs ,
213
+ )
214
+
215
+ # Highlight the code cells now that we know what language they are
216
+ for node in nodes :
217
+ source = node .children [0 ]
218
+ lexer = notebook .metadata .language_info .pygments_lexer
219
+ source .attributes ['language' ] = lexer
220
+
221
+ # Write certain cell outputs (e.g. images) to separate files, and
222
+ # modify the metadata of the associated cells in 'notebook' to
223
+ # include the path to the output file.
224
+ write_notebook_output (notebook , output_dir , file_name )
225
+
226
+ # Add doctree nodes for cell outputs.
227
+ for node , cell in zip (nodes , notebook .cells ):
228
+ output_nodes = cell_output_to_nodes (
229
+ cell ,
230
+ self .config .jupyter_execute_data_priority ,
231
+ sphinx_abs_dir (self .env )
232
+ )
233
+ attach_outputs (output_nodes , node )
234
+
235
+
236
+ ### Roles
136
237
137
238
def jupyter_download_role (name , rawtext , text , lineno , inliner ):
138
239
_ , filetype = name .split (':' )
@@ -147,11 +248,43 @@ def jupyter_download_role(name, rawtext, text, lineno, inliner):
147
248
return [node ], []
148
249
149
250
251
+ ### Utilities
252
+
253
+ def blank_nb (kernel_name ):
254
+ try :
255
+ spec = get_kernel_spec (kernel_name )
256
+ except NoSuchKernel as e :
257
+ raise ExtensionError ('Unable to find kernel' , orig_exc = e )
258
+ return nbformat .v4 .new_notebook (metadata = {
259
+ 'kernelspec' : {
260
+ 'display_name' : spec .display_name ,
261
+ 'language' : spec .language ,
262
+ 'name' : kernel_name ,
263
+ }
264
+ })
265
+
266
+
267
+ def split_on (pred , it ):
268
+ """Split an iterator wherever a predicate is True."""
269
+
270
+ counter = 0
271
+
272
+ def count (x ):
273
+ nonlocal counter
274
+ if pred (x ):
275
+ counter += 1
276
+ return counter
277
+
278
+ # Return iterable of lists to ensure that we don't lose our
279
+ # place in the iterator
280
+ return (list (x ) for _ , x in groupby (it , count ))
281
+
282
+
150
283
def cell_output_to_nodes (cell , data_priority , dir ):
151
284
"""Convert a jupyter cell with outputs and filenames to doctree nodes.
152
285
153
286
Parameters
154
- ==========
287
+ ----------
155
288
cell : jupyter cell
156
289
data_priority : list of mime types
157
290
Which media types to prioritize.
@@ -304,63 +437,6 @@ def sphinx_abs_dir(env):
304
437
)
305
438
306
439
307
- class ExecuteJupyterCells (SphinxTransform ):
308
- default_priority = 180 # An early transform, idk
309
-
310
- def apply (self ):
311
- doctree = self .document
312
- doc_relpath = os .path .dirname (self .env .docname ) # relative to src dir
313
- docname = os .path .basename (self .env .docname )
314
- default_kernel = self .config .jupyter_execute_default_kernel
315
- default_names = default_notebook_names (docname )
316
-
317
- # Check if we have anything to execute.
318
- if not doctree .traverse (Cell ):
319
- return
320
-
321
- logger .info ('executing {}' .format (docname ))
322
- output_dir = os .path .join (output_directory (self .env ), doc_relpath )
323
-
324
- # Start new notebook whenever a KernelNode is encountered
325
- nodes_by_notebook = split_on (
326
- lambda n : isinstance (n , KernelNode ),
327
- doctree .traverse (lambda n : isinstance (n , (Cell , KernelNode )))
328
- )
329
-
330
- for first , * nodes in nodes_by_notebook :
331
- if isinstance (first , KernelNode ):
332
- kernel_name = first ['kernel_name' ] or default_kernel
333
- file_name = first ['kernel_id' ] or next (default_names )
334
- else :
335
- nodes = (first , * nodes )
336
- kernel_name = default_kernel
337
- file_name = next (default_names )
338
-
339
- notebook = execute_cells (
340
- kernel_name ,
341
- [nbformat .v4 .new_code_cell (node .astext ()) for node in nodes ],
342
- self .config .jupyter_execute_kwargs ,
343
- )
344
-
345
- for node in nodes :
346
- source = node .children [0 ]
347
- lexer = notebook .metadata .language_info .pygments_lexer
348
- source .attributes ['language' ] = lexer
349
-
350
- # Modifies 'notebook' in-place, adding metadata specifying the
351
- # filenames of the saved outputs.
352
- write_notebook_output (notebook , output_dir , file_name )
353
- # Add doctree nodes for cell output; images reference the filenames
354
- # we just wrote to; sphinx copies these when writing outputs.
355
- for node , cell in zip (nodes , notebook .cells ):
356
- output_nodes = cell_output_to_nodes (
357
- cell ,
358
- self .config .jupyter_execute_data_priority ,
359
- sphinx_abs_dir (self .env )
360
- )
361
- attach_outputs (output_nodes , node )
362
-
363
-
364
440
def setup (app ):
365
441
# Configuration
366
442
app .add_config_value (
@@ -386,13 +462,13 @@ def setup(app):
386
462
'env' ,
387
463
)
388
464
389
- # KernelNode is just a doctree marker for the ExecuteJupyterCells
390
- # transform, so we don't actually render it.
465
+ # JupyterKernelNode is just a doctree marker for the
466
+ # ExecuteJupyterCells transform, so we don't actually render it.
391
467
def skip (self , node ):
392
468
raise docutils .nodes .SkipNode
393
469
394
470
app .add_node (
395
- KernelNode ,
471
+ JupyterKernelNode ,
396
472
html = (skip , None ),
397
473
latex = (skip , None ),
398
474
textinfo = (skip , None ),
@@ -401,13 +477,15 @@ def skip(self, node):
401
477
)
402
478
403
479
480
+ # JupyterCellNode is a container that holds the input and
481
+ # any output, so we render it as a container.
404
482
render_container = (
405
483
lambda self , node : self .visit_container (node ),
406
484
lambda self , node : self .depart_container (node ),
407
485
)
408
486
409
487
app .add_node (
410
- Cell ,
488
+ JupyterCellNode ,
411
489
html = render_container ,
412
490
latex = render_container ,
413
491
textinfo = render_container ,
0 commit comments