1
1
"""Script to execute and pre-process example notebooks"""
2
2
3
+ import re
3
4
from typing import Tuple , List , Final
4
5
from zipfile import ZIP_DEFLATED , ZipFile
5
6
from pathlib import Path
10
11
import sys
11
12
import tarfile
12
13
from functools import partial
14
+ import traceback
13
15
14
16
import nbformat
15
17
from nbconvert .preprocessors .execute import ExecutePreprocessor
30
32
)
31
33
from cookbook .github import download_dir , get_tag_matching_installed_version
32
34
from cookbook .globals_ import *
33
- from cookbook .utils import set_env
35
+ from cookbook .utils import set_env , to_result , in_regexes
36
+
37
+
38
+ class NotebookExceptionError (ValueError ):
39
+ def __init__ (self , src : str , exc : Exception ):
40
+ self .src : str = str (src )
41
+ self .exc : Exception = exc
42
+ self .tb : str = "" .join (traceback .format_exception (exc , chain = False ))
34
43
35
44
36
45
def needed_files (notebook_path : Path ) -> List [Tuple [Path , Path ]]:
@@ -208,7 +217,8 @@ def execute_notebook(
208
217
try :
209
218
executor .preprocess (nb , {"metadata" : {"path" : src .parent }})
210
219
except Exception as e :
211
- raise ValueError (f"Exception encountered while executing { src_rel } " )
220
+ print ("Failed to execute" , src .relative_to (SRC_IPYNB_ROOT ))
221
+ raise NotebookExceptionError (str (src_rel ), e )
212
222
213
223
# Store the tag used to execute the notebook in metadata
214
224
set_metadata (nb , "src_repo_tag" , tag )
@@ -230,7 +240,7 @@ def execute_notebook(
230
240
EXEC_IPYNB_ROOT / thumbnail_path .relative_to (SRC_IPYNB_ROOT ),
231
241
)
232
242
233
- print ("Done executing " , src .relative_to (SRC_IPYNB_ROOT ))
243
+ print ("Successfully executed " , src .relative_to (SRC_IPYNB_ROOT ))
234
244
235
245
236
246
def delay_iterator (iterator , seconds = 1.0 ):
@@ -260,6 +270,8 @@ def main(
260
270
do_exec = True ,
261
271
prefix : Path | None = None ,
262
272
processes : int | None = None ,
273
+ failed_notebooks_log : Path | None = None ,
274
+ allow_failures : bool = False ,
263
275
):
264
276
print ("Working in" , Path ().resolve ())
265
277
@@ -287,7 +299,6 @@ def main(
287
299
for notebook in find_notebooks (dst_path )
288
300
if str (notebook .relative_to (SRC_IPYNB_ROOT )) not in SKIP_NOTEBOOKS
289
301
)
290
- print (notebooks , SKIP_NOTEBOOKS )
291
302
292
303
# Create Colab and downloadable versions of the notebooks
293
304
if do_proc :
@@ -299,20 +310,63 @@ def main(
299
310
create_download (notebook )
300
311
301
312
# Execute notebooks in parallel for rendering as HTML
313
+ execution_failed = False
302
314
if do_exec :
303
315
shutil .rmtree (EXEC_IPYNB_ROOT , ignore_errors = True )
304
316
# Context manager ensures the pool is correctly terminated if there's
305
317
# an exception
306
318
with Pool (processes = processes ) as pool :
307
319
# Wait a second between launching subprocesses
308
320
# Workaround https://github.com/jupyter/nbconvert/issues/1066
309
- _ = [
310
- * pool .imap_unordered (
311
- partial (execute_notebook , cache_branch = cache_branch ),
321
+ exec_results = [
322
+ * pool .imap (
323
+ to_result (
324
+ partial (execute_notebook , cache_branch = cache_branch ),
325
+ NotebookExceptionError ,
326
+ ),
312
327
delay_iterator (notebooks ),
313
328
)
314
329
]
315
330
331
+ exceptions : list [NotebookExceptionError ] = [
332
+ result for result in exec_results if isinstance (result , Exception )
333
+ ]
334
+ ignored_exceptions = [
335
+ exc for exc in exceptions if in_regexes (exc .src , OPTIONAL_NOTEBOOKS )
336
+ ]
337
+
338
+ if exceptions :
339
+ for exception in exceptions :
340
+ print (
341
+ "-" * 80
342
+ + "\n "
343
+ + f"{ exception .src } failed. Traceback:\n \n { exception .tb } "
344
+ )
345
+ if not in_regexes (exception .src , OPTIONAL_NOTEBOOKS ):
346
+ execution_failed = True
347
+ print (f"The following { len (exceptions )} /{ len (notebooks )} notebooks failed:" )
348
+ for exception in exceptions :
349
+ print (" " , exception .src )
350
+ print ("For tracebacks, see above." )
351
+
352
+ if failed_notebooks_log is not None :
353
+ print (f"Writing log to { failed_notebooks_log .absolute ()} " )
354
+ failed_notebooks_log .write_text (
355
+ json .dumps (
356
+ {
357
+ "n_successful" : len (notebooks ) - len (exceptions ),
358
+ "n_total" : len (notebooks ),
359
+ "n_ignored" : len (ignored_exceptions ),
360
+ "failed" : [
361
+ exc .src
362
+ for exc in exceptions
363
+ if exc not in ignored_exceptions
364
+ ],
365
+ "ignored" : [exc .src for exc in ignored_exceptions ],
366
+ }
367
+ )
368
+ )
369
+
316
370
if isinstance (prefix , Path ):
317
371
prefix .mkdir (parents = True , exist_ok = True )
318
372
@@ -327,6 +381,9 @@ def main(
327
381
prefix / directory .relative_to (OPENFF_DOCS_ROOT ),
328
382
)
329
383
384
+ if execution_failed :
385
+ exit (1 )
386
+
330
387
331
388
if __name__ == "__main__" :
332
389
import sys , os
@@ -365,10 +422,41 @@ def main(
365
422
"Specify cache branch in a single argument: `--cache-branch=<branch>`"
366
423
)
367
424
425
+ # --log-failures is the path to store a list of failing notebooks in
426
+ failed_notebooks_log = None
427
+ for arg in sys .argv :
428
+ if arg .startswith ("--log-failures=" ):
429
+ failed_notebooks_log = Path (arg [15 :])
430
+ if "--log-failures" in sys .argv :
431
+ raise ValueError (
432
+ "Specify path to log file in a single argument: `--log-failures=<path>`"
433
+ )
434
+
435
+ # if --allow-failures is True, do not exit with error code 1 if a
436
+ # notebook fails
437
+ allow_failures = "false"
438
+ for arg in sys .argv :
439
+ if arg .startswith ("--allow-failures=" ):
440
+ allow_failures = arg [17 :].lower ()
441
+ if allow_failures in ["true" , "1" , "y" , "yes" , "t" ]:
442
+ allow_failures = True
443
+ elif allow_failures in ["false" , "0" , "n" , "no" , "false" ]:
444
+ allow_failures = False
445
+ else :
446
+ raise ValueError (
447
+ f"Didn't understand value of --allow-failures { allow_failures } ; try `true` or `false`"
448
+ )
449
+ if "--log-failures" in sys .argv :
450
+ raise ValueError (
451
+ "Specify value in a single argument: `--allow-failures=true` or `--allow-failures=false`"
452
+ )
453
+
368
454
main (
369
455
cache_branch = cache_branch ,
370
456
do_proc = not "--skip-proc" in sys .argv ,
371
457
do_exec = not "--skip-exec" in sys .argv ,
372
458
prefix = prefix ,
373
459
processes = processes ,
460
+ failed_notebooks_log = failed_notebooks_log ,
461
+ allow_failures = allow_failures ,
374
462
)
0 commit comments