Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quantum-kittens
GitHub Repository: quantum-kittens/platypus
Path: blob/main/scripts/content_checks/nb_autorun.py
3855 views
1
import re
2
import sys
3
import time
4
import nbformat
5
import nbconvert
6
from datetime import datetime
7
from tools import parse_args, style, indent
8
9
10
class ExecutePreprocessor(nbconvert.preprocessors.ExecutePreprocessor):
11
"""Need custom preprocessor to skip `uses-hardware` cells"""
12
def preprocess_cell(self, cell, resources, cell_index):
13
if hasattr(cell.metadata, 'tags'):
14
if 'uses-hardware' in cell.metadata.tags:
15
# Skip execution
16
return cell, resources
17
return super().preprocess_cell(cell, resources, cell_index)
18
19
20
def timestr():
21
"""Get current time (for reporting in terminal)"""
22
timestr = f"[{datetime.now().time().strftime('%H:%M')}]"
23
return style('faint', timestr)
24
25
26
def contains_code_cells(notebook):
27
for cell in notebook.cells:
28
if cell.cell_type == 'code':
29
return True
30
return False
31
32
33
def format_message_terminal(msg):
34
"""Formats error messages nicely for the terminal"""
35
outstr = style(msg['severity'], msg['name'])
36
outstr += f": {msg['description']}"
37
if 'code' in msg:
38
outstr += "\nError occurred as result of the following code:\n"
39
outstr += indent(msg['code'])
40
return outstr
41
42
43
def get_warnings(cell):
44
"""Returns any warning messages from a cell's output"""
45
warning_messages = []
46
for output in cell.outputs:
47
if hasattr(output, 'name') and output.name == 'stderr':
48
try: # Try to identify warning type
49
warning_name = re.search(r'(?<=\s)([A-Z][a-z0-9]+)+(?=:)',
50
output.text)[0]
51
description = re.split(warning_name,
52
output.text,
53
maxsplit=1)[1].strip(' :')
54
except TypeError:
55
warning_name = 'Warning'
56
description = output.text
57
58
warning_messages.append({'name': warning_name,
59
'severity': 'warning',
60
'description': description,
61
'full_output': output.text})
62
return warning_messages
63
64
65
66
def run_notebook(filepath, write=False, fail_on_warning=False):
67
"""Attempts to run a notebook and return any error / warning messages.
68
Args:
69
filepath (Path): Path to the notebook
70
write (bool): Whether to write the updated outputs to the file.
71
Returns:
72
bool: True if notebook executed without error, False otherwise.
73
(Note: will not write if there are any errors during execution.)
74
list: List of dicts containing error / warning message information.
75
"""
76
execution_success = True
77
messages = [] # To collect error / warning messages
78
79
with open(filepath) as f:
80
notebook = nbformat.read(f, as_version=4)
81
82
if not contains_code_cells(notebook):
83
# Avoid creating new kernel for no reason
84
return True, messages
85
86
# Clear outputs
87
processor = nbconvert.preprocessors.ClearOutputPreprocessor()
88
processor.preprocess(notebook,
89
{'metadata': {'path': filepath.parents[0]}})
90
91
# Execute notebook
92
processor = ExecutePreprocessor(timeout=None)
93
try:
94
processor.preprocess(notebook,
95
{'metadata': {'path': filepath.parents[0]}})
96
except Exception as err:
97
err_msg = {'name': err.ename,
98
'severity': 'error',
99
'description': err.evalue,
100
'code': err.traceback.split('------------------')[1],
101
'full_output': err.traceback
102
}
103
messages.append(err_msg)
104
execution_success = False
105
106
# Search output for warning messages (can't work out how to get the kernel
107
# to report these)
108
for cell in notebook.cells:
109
if cell.cell_type != 'code':
110
continue
111
112
ignore_warning_tag = (hasattr(cell.metadata, 'tags')
113
and 'ignore-warning' in cell.metadata.tags)
114
115
warning_messages = get_warnings(cell)
116
if not ignore_warning_tag:
117
messages += warning_messages
118
if fail_on_warning and (messages!=[]):
119
execution_success = False
120
121
# Clean up unused tags if warning disappears
122
if ignore_warning_tag and (warning_messages == []):
123
cell.metadata.tags.remove('ignore-warning')
124
125
# Remove useless execution metadata
126
for cell in notebook.cells:
127
if 'execution' in cell.metadata:
128
del cell.metadata['execution']
129
130
if execution_success and write:
131
with open(filepath, 'w', encoding='utf-8') as f:
132
nbformat.write(notebook, f)
133
134
return execution_success, messages
135
136
137
if __name__ == '__main__':
138
# usage: python nb_autorun.py --write --fail-on-warning notebook1.ipynb path/to/notebook2.ipynb
139
switches, filepaths = parse_args(sys.argv)
140
141
write, fail_on_warning = False, False
142
for switch in switches:
143
if switch == '--write':
144
write = True
145
if switch == '--fail-on-warning':
146
fail_on_warning = True
147
148
log = {'t0': time.time(),
149
'total_time': 0,
150
'total_files': 0,
151
'broken_files': 0
152
}
153
154
# Start executing notebooks
155
print('\n\033[?25l', end="") # hide cursor
156
for path in filepaths:
157
log['total_files'] += 1
158
print('-', timestr(), path, end=' ', flush=True)
159
160
success, messages = run_notebook(path, write, fail_on_warning)
161
if success:
162
print("\r" + style('success', '✔'))
163
else:
164
log['broken_files'] += 1
165
print("\r" + style('error', '✖'))
166
167
if messages:
168
message_strings = [format_message_terminal(m) for m in messages]
169
print(indent('\n'.join(message_strings)))
170
171
print('\033[?25h', end='') # un-hide cursor
172
173
# Display output and exit
174
log['total_time'] = time.time()-log['t0']
175
print(f"Finished in {log['total_time']:.2f} seconds\n")
176
if log['broken_files'] > 0:
177
print(f"Found problems in {log['broken_files']}/{log['total_files']} "
178
"notebooks, see output above for more info.\n")
179
if fail_on_warning:
180
print("If this test failed due to a new warning that is out of "
181
"scope of\nthis PR, please make a new issue describing the "
182
"warning, and add\nan `ignore-warning` tag to any problem "
183
"cells so your PR can pass\nthis test.\n")
184
sys.exit(1)
185
sys.exit(0)
186
187