-
Notifications
You must be signed in to change notification settings - Fork 69
/
notebook_sphinxext.py
181 lines (138 loc) · 5.77 KB
/
notebook_sphinxext.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import os
import shutil
import glob
from docutils import nodes
from docutils.parsers.rst import directives, Directive
from nbconvert.exporters import html, python
from runipy.notebook_runner import NotebookRunner
HERE = os.path.dirname(os.path.abspath(__file__))
IPYTHON_NOTEBOOK_DIR = "%s/../../examples" % HERE
class NotebookDirective(Directive):
"""Insert an evaluated notebook into a document
This uses runipy and nbconvert to transform a path to an unevaluated notebook
into html suitable for embedding in a Sphinx document.
"""
required_arguments = 1
optional_arguments = 1
option_spec = {'skip_exceptions' : directives.flag}
def run(self):
# check if raw html is supported
if not self.state.document.settings.raw_enabled:
raise self.warning('"%s" directive disabled.' % self.name)
# get path to notebook
source_dir = os.path.dirname(
os.path.abspath(self.state.document.current_source))
nb_basename = os.path.basename(self.arguments[0])
rst_file = self.state_machine.document.attributes['source']
rst_dir = os.path.abspath(os.path.dirname(rst_file))
nb_abs_path = os.path.join(IPYTHON_NOTEBOOK_DIR, nb_basename)
# Move files around.
rel_dir = os.path.relpath(rst_dir, setup.confdir)
rel_path = os.path.join(rel_dir, nb_basename)
dest_dir = os.path.join(setup.app.builder.outdir, rel_dir)
dest_path = os.path.join(dest_dir, nb_basename)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
# Copy unevaluated script
try:
shutil.copyfile(nb_abs_path, dest_path)
except IOError:
raise RuntimeError("Unable to copy notebook to build destination. %s -> %s" % (nb_abs_path, dest_path))
dest_path_eval = dest_path.replace('.ipynb', '_evaluated.ipynb')
dest_path_script = dest_path.replace('.ipynb', '.py')
rel_path_eval = nb_basename.replace('.ipynb', '_evaluated.ipynb')
rel_path_script = nb_basename.replace('.ipynb', '.py')
# Create python script vesion
unevaluated_text = nb_to_html(nb_abs_path)
script_text = nb_to_python(nb_abs_path)
f = open(dest_path_script, 'wb')
f.write(script_text.encode('utf8'))
f.close()
skip_exceptions = 'skip_exceptions' in self.options
try:
evaluated_text = evaluate_notebook(nb_abs_path, dest_path_eval,
skip_exceptions=skip_exceptions)
except:
# bail
return []
# Create link to notebook and script files
link_rst = "(" + \
formatted_link(nb_basename) + "; " + \
formatted_link(rel_path_eval) + "; " + \
formatted_link(rel_path_script) + \
")"
self.state_machine.insert_input([link_rst], rst_file)
# create notebook node
attributes = {'format': 'html', 'source': 'nb_path'}
nb_node = notebook_node('', evaluated_text, **attributes)
(nb_node.source, nb_node.line) = \
self.state_machine.get_source_and_line(self.lineno)
# add dependency
self.state.document.settings.record_dependencies.add(nb_abs_path)
# clean up png files left behind by notebooks.
png_files = glob.glob("*.png")
fits_files = glob.glob("*.fits")
h5_files = glob.glob("*.h5")
for file in png_files:
os.remove(file)
return [nb_node]
class notebook_node(nodes.raw):
pass
def nb_to_python(nb_path):
"""convert notebook to python script"""
exporter = python.PythonExporter()
output, resources = exporter.from_filename(nb_path)
return output
def nb_to_html(nb_path):
"""convert notebook to html"""
exporter = html.HTMLExporter(template_file='full')
output, resources = exporter.from_filename(nb_path)
header = output.split('<head>', 1)[1].split('</head>',1)[0]
body = output.split('<body>', 1)[1].split('</body>',1)[0]
# http://imgur.com/eR9bMRH
header = header.replace('<style', '<style scoped="scoped"')
header = header.replace('body {\n overflow: visible;\n padding: 8px;\n}\n', '')
# Filter out styles that conflict with the sphinx theme.
filter_strings = [
'navbar',
'body{',
'alert{',
'uneditable-input{',
'collapse{',
]
filter_strings.extend(['h%s{' % (i+1) for i in range(6)])
header_lines = filter(
lambda x: not any([s in x for s in filter_strings]), header.split('\n'))
header = '\n'.join(header_lines)
# concatenate raw html lines
lines = ['<div class="ipynotebook">']
lines.append(header)
lines.append(body)
lines.append('</div>')
return '\n'.join(lines)
def evaluate_notebook(nb_path, dest_path=None, skip_exceptions=False):
# Create evaluated version and save it to the dest path.
# Always use --pylab so figures appear inline
# perhaps this is questionable?
nb_runner = NotebookRunner(nb_path, pylab=True)
nb_runner.run_notebook(skip_exceptions=skip_exceptions)
if dest_path is None:
dest_path = 'temp_evaluated.ipynb'
nb_runner.save_notebook(dest_path)
ret = nb_to_html(dest_path)
if dest_path is 'temp_evaluated.ipynb':
os.remove(dest_path)
return ret
def formatted_link(path):
return "`%s <%s>`__" % (os.path.basename(path), path)
def visit_notebook_node(self, node):
self.visit_raw(node)
def depart_notebook_node(self, node):
self.depart_raw(node)
def setup(app):
setup.app = app
setup.config = app.config
setup.confdir = app.confdir
app.add_node(notebook_node,
html=(visit_notebook_node, depart_notebook_node))
app.add_directive('notebook', NotebookDirective)