12
12
from sphinx .addnodes import download_reference
13
13
from sphinx .transforms import SphinxTransform
14
14
from sphinx .environment .collectors .asset import ImageCollector
15
+ from sphinx .errors import ExtensionError
15
16
16
17
import ipywidgets .embed
17
18
import nbconvert
@@ -27,6 +28,51 @@ def csv_option(s):
27
28
return [p .strip () for p in s .split ("," )] if s else []
28
29
29
30
31
+ def load_content (cell , location , logger ):
32
+ if cell .arguments :
33
+ # As per 'sphinx.directives.code.LiteralInclude'
34
+ env = cell .state .document .settings .env
35
+ rel_filename , filename = env .relfn2path (cell .arguments [0 ])
36
+ env .note_dependency (rel_filename )
37
+ if cell .content :
38
+ logger .warning (
39
+ 'Ignoring inline code in Jupyter cell included from "{}"' .format (
40
+ rel_filename
41
+ ),
42
+ location = location ,
43
+ )
44
+ try :
45
+ with Path (filename ).open () as f :
46
+ content = [line .rstrip () for line in f .readlines ()]
47
+ except (IOError , OSError ):
48
+ raise IOError ("File {} not found or reading it failed" .format (filename ))
49
+ else :
50
+ cell .assert_has_content ()
51
+ content = cell .content
52
+ return content
53
+
54
+
55
+ def get_highlights (cell , content , location , logger ):
56
+ # The code fragment is taken from CodeBlock directive almost unchanged:
57
+ # https://github.com/sphinx-doc/sphinx/blob/0319faf8f1503453b6ce19020819a8cf44e39f13/sphinx/directives/code.py#L134-L148
58
+
59
+ emphasize_linespec = cell .options .get ("emphasize-lines" )
60
+ if emphasize_linespec :
61
+ nlines = len (content )
62
+ hl_lines = parselinenos (emphasize_linespec , nlines )
63
+ if any (i >= nlines for i in hl_lines ):
64
+ logger .warning (
65
+ "Line number spec is out of range(1-{}): {}" .format (
66
+ nlines , emphasize_linespec
67
+ ),
68
+ location = location ,
69
+ )
70
+ hl_lines = [i + 1 for i in hl_lines if i < nlines ]
71
+ else :
72
+ hl_lines = []
73
+ return hl_lines
74
+
75
+
30
76
class JupyterCell (Directive ):
31
77
"""Define a code cell to be later executed in a Jupyter kernel.
32
78
@@ -89,50 +135,16 @@ def run(self):
89
135
90
136
location = self .state_machine .get_source_and_line (self .lineno )
91
137
92
- if self .arguments :
93
- # As per 'sphinx.directives.code.LiteralInclude'
94
- env = self .state .document .settings .env
95
- rel_filename , filename = env .relfn2path (self .arguments [0 ])
96
- env .note_dependency (rel_filename )
97
- if self .content :
98
- logger .warning (
99
- 'Ignoring inline code in Jupyter cell included from "{}"' .format (
100
- rel_filename
101
- ),
102
- location = location ,
103
- )
104
- try :
105
- with Path (filename ).open () as f :
106
- content = [line .rstrip () for line in f .readlines ()]
107
- except (IOError , OSError ):
108
- raise IOError ("File {} not found or reading it failed" .format (filename ))
109
- else :
110
- self .assert_has_content ()
111
- content = self .content
112
-
113
- # The code fragment is taken from CodeBlock directive almost unchanged:
114
- # https://github.com/sphinx-doc/sphinx/blob/0319faf8f1503453b6ce19020819a8cf44e39f13/sphinx/directives/code.py#L134-L148
115
-
116
- emphasize_linespec = self .options .get ("emphasize-lines" )
117
- if emphasize_linespec :
118
- try :
119
- nlines = len (content )
120
- hl_lines = parselinenos (emphasize_linespec , nlines )
121
- if any (i >= nlines for i in hl_lines ):
122
- logger .warning (
123
- "Line number spec is out of range(1-{}): {}" .format (
124
- nlines , emphasize_linespec
125
- ),
126
- location = location ,
127
- )
128
- hl_lines = [i + 1 for i in hl_lines if i < nlines ]
129
- except ValueError as err :
130
- return [self .state .document .reporter .warning (err , line = self .lineno )]
131
- else :
132
- hl_lines = []
138
+ content = load_content (self , location , logger )
139
+
140
+ try :
141
+ hl_lines = get_highlights (self , content , location , logger )
142
+ except ValueError as err :
143
+ return [self .state .document .reporter .warning (err , line = self .lineno )]
133
144
134
145
# A top-level placeholder for our cell
135
146
cell_node = JupyterCellNode (
147
+ execute = True ,
136
148
hide_code = ("hide-code" in self .options ),
137
149
hide_output = ("hide-output" in self .options ),
138
150
code_below = ("code-below" in self .options ),
@@ -152,6 +164,135 @@ def run(self):
152
164
cell_node += cell_input
153
165
return [cell_node ]
154
166
167
+ class CellInput (Directive ):
168
+ """Define a code cell to be included verbatim but not executed.
169
+
170
+ Arguments
171
+ ---------
172
+ filename : str (optional)
173
+ If provided, a path to a file containing code.
174
+
175
+ Options
176
+ -------
177
+ linenos : bool
178
+ If provided, the code will be shown with line numbering.
179
+ lineno-start: nonnegative int
180
+ If provided, the code will be show with line numbering beginning from
181
+ specified line.
182
+ emphasize-lines : comma separated list of line numbers
183
+ If provided, the specified lines will be highlighted.
184
+
185
+ Content
186
+ -------
187
+ code : str
188
+ A code cell.
189
+ """
190
+
191
+ required_arguments = 0
192
+ optional_arguments = 1
193
+ final_argument_whitespace = True
194
+ has_content = True
195
+
196
+ option_spec = {
197
+ "linenos" : directives .flag ,
198
+ "lineno-start" : directives .nonnegative_int ,
199
+ "emphasize-lines" : directives .unchanged_required ,
200
+ }
201
+
202
+ def run (self ):
203
+ # This only works lazily because the logger is inited by Sphinx
204
+ from . import logger
205
+
206
+ location = self .state_machine .get_source_and_line (self .lineno )
207
+
208
+ content = load_content (self , location , logger )
209
+
210
+ try :
211
+ hl_lines = get_highlights (self , content , location , logger )
212
+ except ValueError as err :
213
+ return [self .state .document .reporter .warning (err , line = self .lineno )]
214
+
215
+ # A top-level placeholder for our cell
216
+ cell_node = JupyterCellNode (
217
+ execute = False ,
218
+ hide_code = False ,
219
+ hide_output = True ,
220
+ code_below = False ,
221
+ emphasize_lines = hl_lines ,
222
+ raises = False ,
223
+ stderr = False ,
224
+ classes = ["jupyter_cell" ],
225
+ )
226
+
227
+ # Add the input section of the cell, we'll add output when jupyter-execute cells are run
228
+ cell_input = CellInputNode (classes = ["cell_input" ])
229
+ cell_input += docutils .nodes .literal_block (
230
+ text = "\n " .join (content ),
231
+ linenos = ("linenos" in self .options ),
232
+ linenostart = (self .options .get ("lineno-start" )),
233
+ )
234
+ cell_node += cell_input
235
+ return [cell_node ]
236
+
237
+ class CellOutput (Directive ):
238
+ """Define an output cell to be included verbatim.
239
+
240
+ Arguments
241
+ ---------
242
+ filename : str (optional)
243
+ If provided, a path to a file containing output.
244
+
245
+ Content
246
+ -------
247
+ code : str
248
+ An output cell.
249
+ """
250
+
251
+ required_arguments = 0
252
+ optional_arguments = 1
253
+ final_argument_whitespace = True
254
+ has_content = True
255
+
256
+ option_spec = {}
257
+
258
+ def run (self ):
259
+ # This only works lazily because the logger is inited by Sphinx
260
+ from . import logger
261
+
262
+ location = self .state_machine .get_source_and_line (self .lineno )
263
+
264
+ content = load_content (self , location , logger )
265
+
266
+ # A top-level placeholder for our cell
267
+ cell_node = JupyterCellNode (
268
+ execute = False ,
269
+ hide_code = True ,
270
+ hide_output = False ,
271
+ code_below = False ,
272
+ emphasize_lines = [],
273
+ raises = False ,
274
+ stderr = False ,
275
+ )
276
+
277
+ # Add a blank input and the given output to the cell
278
+ cell_input = CellInputNode (classes = ["cell_input" ])
279
+ cell_input += docutils .nodes .literal_block (
280
+ text = "" ,
281
+ linenos = False ,
282
+ linenostart = None ,
283
+ )
284
+ cell_node += cell_input
285
+ content_str = "\n " .join (content )
286
+ cell_output = CellOutputNode (classes = ["cell_output" ])
287
+ cell_output += docutils .nodes .literal_block (
288
+ text = content_str ,
289
+ rawsource = content_str ,
290
+ language = "none" ,
291
+ classes = ["output" , "stream" ],
292
+ )
293
+ cell_node += cell_output
294
+ return [cell_node ]
295
+
155
296
156
297
class JupyterCellNode (docutils .nodes .container ):
157
298
"""Inserted into doctree whever a JupyterCell directive is encountered.
@@ -433,6 +574,39 @@ def get_widgets(notebook):
433
574
return None
434
575
435
576
577
+ class CombineCellInputOutput (SphinxTransform ):
578
+ """Merge nodes from CellOutput with the preceding CellInput node."""
579
+
580
+ default_priority = 120
581
+
582
+ def apply (self ):
583
+ moved_outputs = set ()
584
+
585
+ for cell_node in self .document .traverse (JupyterCellNode ):
586
+ if cell_node .attributes ["execute" ] == False :
587
+ if cell_node .attributes ["hide_code" ] == False :
588
+ # Cell came from jupyter-input
589
+ sibling = cell_node .next_node (descend = False , siblings = True )
590
+ if (
591
+ isinstance (sibling , JupyterCellNode )
592
+ and sibling .attributes ["execute" ] == False
593
+ and sibling .attributes ["hide_code" ] == True
594
+ ):
595
+ # Sibling came from jupyter-output, so we merge
596
+ cell_node += sibling .children [1 ]
597
+ cell_node .attributes ["hide_output" ] = False
598
+ moved_outputs .update ({sibling })
599
+ else :
600
+ # Call came from jupyter-output
601
+ if cell_node not in moved_outputs :
602
+ raise ExtensionError (
603
+ "Found a jupyter-output node without a preceding jupyter-input"
604
+ )
605
+
606
+ for output_node in moved_outputs :
607
+ output_node .replace_self ([])
608
+
609
+
436
610
class CellOutputsToNodes (SphinxTransform ):
437
611
"""Use the builder context to transform a CellOutputNode into Sphinx nodes."""
438
612
0 commit comments