2424import logging
2525from pathlib import Path
2626from tempfile import NamedTemporaryFile
27+ from uuid import uuid4
2728
29+ # Monkey-patch nbclient to handle display_id=None for widget updates.
30+ # This fixes an issue where ipywidgets/tqdm progress bars cause
31+ # "assert display_id is not None" errors in nbclient.
32+ import nbclient .client
2833import papermill
2934import yaml
3035from nbformat .notebooknode import NotebookNode
4045SKIP_NOTEBOOKS_FILE = HERE / "skip_notebooks.yml"
4146SKIP_NOTEBOOKS = set (yaml .safe_load (SKIP_NOTEBOOKS_FILE .read_text ()))
4247
48+ _original_output = nbclient .client .NotebookClient .output
49+
50+
51+ def _patched_output (self , outs , msg , display_id , cell_index ):
52+ """Patched output method that catches assertion errors from widget updates."""
53+ try :
54+ return _original_output (self , outs , msg , display_id , cell_index )
55+ except AssertionError :
56+ # Silently skip messages that cause display_id assertion errors
57+ # (typically from ipywidgets/tqdm progress bar updates)
58+ return None
59+
60+
61+ nbclient .client .NotebookClient .output = _patched_output
62+
4363
4464def setup_logging () -> None :
4565 logging .basicConfig (
@@ -48,13 +68,26 @@ def setup_logging() -> None:
4868 )
4969
5070
71+ def generate_random_id () -> str :
72+ return str (uuid4 ())
73+
74+
75+ def clear_cell_outputs (cells : list ) -> None :
76+ """Clear all outputs from cells to avoid widget state issues with nbclient."""
77+ for cell in cells :
78+ if cell .get ("cell_type" ) == "code" :
79+ cell ["outputs" ] = []
80+ cell ["execution_count" ] = None
81+
82+
5183def inject_mock_code (cells : list ) -> None :
5284 """Inject mock pm.sample code at the start of the notebook."""
85+ clear_cell_outputs (cells )
5386 cells .insert (
5487 0 ,
5588 NotebookNode (
56- id = "mock -injection" ,
57- execution_count = 0 ,
89+ id = f"code -injection- { generate_random_id () } " ,
90+ execution_count = sum ( map ( ord , "Mock pm.sample" )) ,
5891 cell_type = "code" ,
5992 metadata = {"tags" : []},
6093 outputs = [],
@@ -133,6 +166,12 @@ def parse_args() -> argparse.Namespace:
133166 dest = "exclude_patterns" ,
134167 help = "Pattern to exclude from notebook names (can be used multiple times)" ,
135168 )
169+ parser .add_argument (
170+ "--parallel" ,
171+ action = "store_true" ,
172+ default = False ,
173+ help = "Run notebooks in parallel when possible." ,
174+ )
136175 return parser .parse_args ()
137176
138177
@@ -149,7 +188,17 @@ def parse_args() -> argparse.Namespace:
149188 for nb in notebooks :
150189 logging .info (f" - { nb .name } " )
151190
152- for notebook in notebooks :
153- run_notebook (notebook )
191+ if args .parallel :
192+ try :
193+ from joblib import Parallel , delayed
194+ except ImportError as exc :
195+ raise ImportError (
196+ "Parallel execution requires joblib. Install it or run without --parallel."
197+ ) from exc
198+
199+ Parallel (n_jobs = - 1 )(delayed (run_notebook )(notebook ) for notebook in notebooks )
200+ else :
201+ for notebook in notebooks :
202+ run_notebook (notebook )
154203
155204 logging .info ("All notebooks completed successfully!" )
0 commit comments