55import datetime
66import os
77import nbformat
8- import nbconvert .preprocessors
8+ from nbconvert import HTMLExporter
9+ from nbconvert .preprocessors import ExecutePreprocessor
910from multiprocessing import Pool
1011import sys
1112
12- from typing import Iterable , Optional
13+ from typing import Iterable , List , Optional
1314from functools import total_ordering
1415
1516have_pdf_kit = False
@@ -245,6 +246,36 @@ def convert_notebook_file_to_py(
245246 f .write (py_source )
246247
247248
249+ class OurExecutor (ExecutePreprocessor ):
250+ """Catch exception in notebook processing"""
251+ def __init__ (self , ** kw ):
252+ """Initialize the preprocessor."""
253+ ExecutePreprocessor .__init__ (self , ** kw )
254+ self .caught_exception = None
255+
256+ def preprocess_cell (self , cell , resources , index ):
257+ """
258+ Override if you want to apply some preprocessing to each cell.
259+ Must return modified cell and resource dictionary.
260+
261+ Parameters
262+ ----------
263+ cell : NotebookNode cell
264+ Notebook cell being processed
265+ resources : dictionary
266+ Additional resources used in the conversion process. Allows
267+ preprocessors to pass variables into the Jinja engine.
268+ index : int
269+ Index of the cell being processed
270+ """
271+ if self .caught_exception is None :
272+ try :
273+ return ExecutePreprocessor .preprocess_cell (self , cell , resources , index )
274+ except Exception as ex :
275+ self .caught_exception = ex
276+ return cell , self .resources
277+
278+
248279# https://nbconvert.readthedocs.io/en/latest/execute_api.html
249280# https://nbconvert.readthedocs.io/en/latest/nbconvert_library.html
250281# HTML element we are trying to delete:
@@ -329,13 +360,13 @@ def render_as_html(
329360 f'start render_as_html "{ notebook_file_name } " { exec_note } { datetime .datetime .now ()} '
330361 )
331362 if kernel_name is not None :
332- ep = nbconvert . preprocessors . ExecutePreprocessor (
363+ ep = OurExecutor (
333364 timeout = timeout , kernel_name = kernel_name
334365 )
335366 else :
336- ep = nbconvert . preprocessors . ExecutePreprocessor (timeout = timeout )
367+ ep = OurExecutor (timeout = timeout )
337368 nb_res , nb_resources = ep .preprocess (nb )
338- html_exporter = nbconvert . HTMLExporter (exclude_input = exclude_input )
369+ html_exporter = HTMLExporter (exclude_input = exclude_input )
339370 html_body , html_resources = html_exporter .from_notebook_node (nb_res )
340371 if exclude_input and (prompt_strip_regexp is not None ):
341372 # strip output prompts
@@ -350,6 +381,7 @@ def render_as_html(
350381 assert have_pdf_kit
351382 pdf_name = html_name .removesuffix ('.html' ) + '.pdf'
352383 pdfkit .from_string (html_body , pdf_name )
384+ caught = ep .caught_exception
353385 except Exception as e :
354386 caught = e
355387 if caught is not None :
@@ -456,25 +488,26 @@ def __repr__(self) -> str:
456488 return self .__str__ ()
457489
458490
459- def job_fn (arg : JTask ) -> None :
491+ def job_fn (arg : JTask ):
460492 """
461- Function to run a JTask job
493+ Function to run a JTask job. Exceptions pass through
462494 """
463495 assert isinstance (arg , JTask )
464496 # render notebook
465- arg .render_as_html ()
497+ return arg .render_as_html ()
466498
467499
468- def job_fn_eat_exception (arg : JTask ) -> None :
500+ def job_fn_eat_exception (arg : JTask ):
469501 """
470- Function to run a JTask job, eating any exception
502+ Function to run a JTask job, catching any exception and returning it as a value
471503 """
472504 assert isinstance (arg , JTask )
473505 # render notebook
474506 try :
475- arg .render_as_html ()
507+ return arg .render_as_html ()
476508 except Exception as e :
477509 print (f"{ arg } caught { e } " )
510+ return (arg , e )
478511
479512
480513def run_pool (
@@ -483,7 +516,7 @@ def run_pool(
483516 njobs : int = 4 ,
484517 verbose : bool = True ,
485518 stop_on_error : bool = True ,
486- ) -> None :
519+ ) -> List :
487520 """
488521 Run a pool of tasks.
489522
@@ -506,7 +539,7 @@ def run_pool(
506539 # https://stackoverflow.com/a/25791961/6901725
507540 with Pool (njobs ) as pool :
508541 try :
509- list (pool .imap_unordered (job_fn , tasks )) # list is forcing iteration over tasks for side-effects
542+ res = list (pool .imap_unordered (job_fn , tasks )) # list is forcing iteration over tasks for side-effects
510543 except Exception :
511544 if verbose :
512545 sys .stdout .flush ()
@@ -521,4 +554,5 @@ def run_pool(
521554 else :
522555 # simple way, but doesn't exit until all jobs succeed or fail
523556 with Pool (njobs ) as pool :
524- pool .map (job_fn_eat_exception , tasks )
557+ res = list (pool .map (job_fn_eat_exception , tasks ))
558+ return res
0 commit comments