Skip to content

Commit d86f4e7

Browse files
jupyter-input uses jupyter-execute formatting
1 parent 99faab7 commit d86f4e7

File tree

6 files changed

+293
-43
lines changed

6 files changed

+293
-43
lines changed

doc/source/index.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,43 @@ produces:
305305

306306
print("hello, world!", file=sys.stderr)
307307

308+
Advanced usage: manually forming Jupyter cells
309+
----------------------------------------------
310+
311+
When showing code samples that are computationally expensive, access restricted resources, or have non-deterministic output, it can be preferable to not have them run every time you build. You can simply embed input code without executing it using the ``jupyter-input`` directive expected output with ``jupyter-output``::
312+
313+
.. jupyter-input::
314+
:linenos:
315+
316+
import time
317+
318+
def slow_print(str):
319+
time.sleep(4000) # Simulate an expensive process
320+
print(str)
321+
322+
slow_print("hello, world!")
323+
324+
.. jupyter-output::
325+
326+
hello, world!
327+
328+
produces:
329+
330+
.. jupyter-input::
331+
:linenos:
332+
333+
import time
334+
335+
def slow_print(str):
336+
time.sleep(4000) # Simulate an expensive process
337+
print(str)
338+
339+
slow_print("hello, world!")
340+
341+
.. jupyter-output::
342+
343+
hello, world!
344+
308345
Controlling the execution environment
309346
-------------------------------------
310347
The execution environment can be controlled by using the ``jupyter-kernel`` directive. This directive takes

jupyter_sphinx/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from .ast import (
1414
JupyterCell,
1515
JupyterCellNode,
16+
InputCell,
1617
CellInputNode,
18+
OutputCell,
1719
CellOutputNode,
1820
CellOutputBundleNode,
1921
JupyterKernelNode,
@@ -267,6 +269,8 @@ def setup(app):
267269

268270
app.add_directive("jupyter-execute", JupyterCell)
269271
app.add_directive("jupyter-kernel", JupyterKernel)
272+
app.add_directive("jupyter-input", InputCell)
273+
app.add_directive("jupyter-output", OutputCell)
270274
app.add_directive("thebe-button", ThebeButton)
271275
app.add_role("jupyter-download:notebook", JupyterDownloadRole())
272276
app.add_role("jupyter-download:nb", JupyterDownloadRole())

jupyter_sphinx/ast.py

Lines changed: 182 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,51 @@ def csv_option(s):
2727
return [p.strip() for p in s.split(",")] if s else []
2828

2929

30+
def load_content(cell, location, logger):
31+
if cell.arguments:
32+
# As per 'sphinx.directives.code.LiteralInclude'
33+
env = cell.state.document.settings.env
34+
rel_filename, filename = env.relfn2path(cell.arguments[0])
35+
env.note_dependency(rel_filename)
36+
if cell.content:
37+
logger.warning(
38+
'Ignoring inline code in Jupyter cell included from "{}"'.format(
39+
rel_filename
40+
),
41+
location=location,
42+
)
43+
try:
44+
with Path(filename).open() as f:
45+
content = [line.rstrip() for line in f.readlines()]
46+
except (IOError, OSError):
47+
raise IOError("File {} not found or reading it failed".format(filename))
48+
else:
49+
cell.assert_has_content()
50+
content = cell.content
51+
return content
52+
53+
54+
def get_highlights(cell, content, location, logger):
55+
# The code fragment is taken from CodeBlock directive almost unchanged:
56+
# https://github.com/sphinx-doc/sphinx/blob/0319faf8f1503453b6ce19020819a8cf44e39f13/sphinx/directives/code.py#L134-L148
57+
58+
emphasize_linespec = cell.options.get("emphasize-lines")
59+
if emphasize_linespec:
60+
nlines = len(content)
61+
hl_lines = parselinenos(emphasize_linespec, nlines)
62+
if any(i >= nlines for i in hl_lines):
63+
logger.warning(
64+
"Line number spec is out of range(1-{}): {}".format(
65+
nlines, emphasize_linespec
66+
),
67+
location=location,
68+
)
69+
hl_lines = [i + 1 for i in hl_lines if i < nlines]
70+
else:
71+
hl_lines = []
72+
return hl_lines
73+
74+
3075
class JupyterCell(Directive):
3176
"""Define a code cell to be later executed in a Jupyter kernel.
3277
@@ -89,50 +134,16 @@ def run(self):
89134

90135
location = self.state_machine.get_source_and_line(self.lineno)
91136

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 = []
137+
content = load_content(self, location, logger)
138+
139+
try:
140+
hl_lines = get_highlights(self, content, location, logger)
141+
except ValueError as err:
142+
return [self.state.document.reporter.warning(err, line=self.lineno)]
133143

134144
# A top-level placeholder for our cell
135145
cell_node = JupyterCellNode(
146+
execute=True,
136147
hide_code=("hide-code" in self.options),
137148
hide_output=("hide-output" in self.options),
138149
code_below=("code-below" in self.options),
@@ -152,6 +163,136 @@ def run(self):
152163
cell_node += cell_input
153164
return [cell_node]
154165

166+
class InputCell(Directive):
167+
"""Define a code cell to be included verbatim but not executed.
168+
169+
Arguments
170+
---------
171+
filename : str (optional)
172+
If provided, a path to a file containing code.
173+
174+
Options
175+
-------
176+
linenos : bool
177+
If provided, the code will be shown with line numbering.
178+
lineno-start: nonnegative int
179+
If provided, the code will be show with line numbering beginning from
180+
specified line.
181+
emphasize-lines : comma separated list of line numbers
182+
If provided, the specified lines will be highlighted.
183+
184+
Content
185+
-------
186+
code : str
187+
A code cell.
188+
"""
189+
190+
required_arguments = 0
191+
optional_arguments = 1
192+
final_argument_whitespace = True
193+
has_content = True
194+
195+
option_spec = {
196+
"linenos": directives.flag,
197+
"lineno-start": directives.nonnegative_int,
198+
"emphasize-lines": directives.unchanged_required,
199+
}
200+
201+
def run(self):
202+
# This only works lazily because the logger is inited by Sphinx
203+
from . import logger
204+
205+
location = self.state_machine.get_source_and_line(self.lineno)
206+
207+
content = load_content(self, location, logger)
208+
209+
try:
210+
hl_lines = get_highlights(self, content, location, logger)
211+
except ValueError as err:
212+
return [self.state.document.reporter.warning(err, line=self.lineno)]
213+
214+
# A top-level placeholder for our cell
215+
cell_node = JupyterCellNode(
216+
execute=False,
217+
hide_code=False,
218+
hide_output=True,
219+
code_below=False,
220+
emphasize_lines=hl_lines,
221+
raises=False,
222+
stderr=False,
223+
classes=["jupyter_cell"],
224+
)
225+
226+
# Add the input section of the cell, we'll add output when jupyter-execute cells are run
227+
cell_input = CellInputNode(classes=["cell_input"])
228+
cell_input += docutils.nodes.literal_block(
229+
text="\n".join(content),
230+
linenos=("linenos" in self.options),
231+
linenostart=(self.options.get("lineno-start")),
232+
)
233+
cell_node += cell_input
234+
return [cell_node]
235+
236+
class OutputCell(Directive):
237+
"""Define an output cell to be included verbatim.
238+
239+
Arguments
240+
---------
241+
filename : str (optional)
242+
If provided, a path to a file containing output.
243+
244+
Content
245+
-------
246+
code : str
247+
An output cell.
248+
"""
249+
250+
required_arguments = 0
251+
optional_arguments = 1
252+
final_argument_whitespace = True
253+
has_content = True
254+
255+
option_spec = {}
256+
257+
def run(self):
258+
# This only works lazily because the logger is inited by Sphinx
259+
from . import logger
260+
261+
location = self.state_machine.get_source_and_line(self.lineno)
262+
263+
content = load_content(self, location, logger)
264+
265+
# A top-level placeholder for our cell
266+
cell_node = JupyterCellNode(
267+
execute=False,
268+
hide_code=True,
269+
hide_output=False,
270+
code_below=False,
271+
emphasize_lines=[],
272+
raises=False,
273+
stderr=False,
274+
classes=["jupyter_cell"],
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+
155296

156297
class JupyterCellNode(docutils.nodes.container):
157298
"""Inserted into doctree whever a JupyterCell directive is encountered.

jupyter_sphinx/execute.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def apply(self):
149149

150150
notebook = execute_cells(
151151
kernel_name,
152-
[nbformat.v4.new_code_cell(node.astext()) for node in nodes],
152+
[nbformat.v4.new_code_cell(node.astext() if node["execute"] else "") for node in nodes],
153153
self.config.jupyter_execute_kwargs,
154154
)
155155

@@ -185,6 +185,15 @@ def apply(self):
185185
"Cell printed to stderr:\n{}".format(stderr[0]["text"])
186186
)
187187

188+
for node, cell in zip(nodes, notebook.cells):
189+
if not node["execute"]:
190+
cell.source = node.children[0].astext()
191+
if len(node.children) == 2:
192+
output = nbformat.v4.new_output("stream")
193+
output.text = node.children[1].astext()
194+
cell.outputs = [output]
195+
node.children.pop()
196+
188197
try:
189198
lexer = notebook.metadata.language_info.pygments_lexer
190199
except AttributeError:

jupyter_sphinx/thebelab.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Inserting interactive links with Thebelab."""
2-
import os
32
import json
43
import docutils
54
from docutils.parsers.rst import Directive

0 commit comments

Comments
 (0)