from fastcore.ansi import ansi2html
from IPython.display import HTML
from fastcore.test import *
from base64 import b64decode
from io import BytesIO
from PIL import Image
import tempfile
from contextlib import contextmanagershell
ExecutionResult.__repr__
def __repr__():Return repr(self).
ExecutionInfo.__repr__
def __repr__():Return repr(self).
leading_comment_lines
def leading_comment_lines(
lines
):Input cleanup transform: drop leading blank/comment lines when the first code line is a cell magic, which IPython otherwise misparses as a line magic
CaptureShell
def CaptureShell(
path:str | pathlib.Path=None, mpl_format:str='retina', history:bool=False, timeout:Optional=None,
profile:bool=False
):An enhanced, interactive shell for Python.
CaptureShell.run_cell
def run_cell(
raw_cell, # The code (including IPython code such as %magic functions) to run.
store_history:bool=False, # If True, the raw and translated cell will be stored in IPython's
# history. For user code calling back into IPython's machinery, this
# should be set to False.
silent:bool=False, # If True, avoid side-effects, such as implicit displayhooks and
# and logging. silent=True forces store_history=False.
shell_futures:bool=True, # If True, the code will share future statements with the interactive
# shell. It will both be affected by previous __future__ imports, and
# any __future__ imports in the code will affect the shell. If False,
# __future__ imports are not shared in either direction.
cell_id:NoneType=None, # A unique identifier for the cell. This is used in the messaging system
# to match output with execution requests and for tracking cell execution
# history across kernel restarts. In notebook contexts, this is typically
# a UUID generated by the frontend. If None, the kernel may generate an
# internal identifier or proceed without cell tracking capabilities.
stdout:bool=True, stderr:bool=True, display:bool=True, timeout:NoneType=None, verbose:bool=False
):Run a complete IPython cell.
s = CaptureShell(mpl_format='retina')s.run_cell('a=1');Code running inside a cell can itself call run_cell (the %%capture magic does exactly that). Such nested calls behave like plain IPython: they return an ExecutionResult, and their output flows to the enclosing capture context, so %%capture works:
s.run_cell("%%capture c\nprint(1)")
test_eq(s.run_cell("c.stdout").result.result, '1\n')
s.run_cell("r = get_ipython().run_cell('7')")
test_eq(s.run_cell("type(r).__name__").result.result, 'ExecutionResult')IPython requires a cell magic to be the very first line of a cell: even a comment above it makes the %%foo line parse as a line magic named %foo, which fails with a confusing “Line magic function %%foo not found” error. CaptureShell smooths both edges: leading blank and comment lines above a cell magic are dropped (extending IPython’s own leading_empty_lines cleanup), and when real code precedes the magic (which can’t be auto-fixed), the error message explains the actual problem.
o = s.run_cell('# a narration comment\n\n%%capture c2\nprint(2)')
test_eq(o.exception, None)
test_eq(s.run_cell('c2.stdout').result.result, '2\n')o = s.run_cell('a=1\n%%capture c3\nprint(3)')
assert 'must start the cell' in str(o.exception)o = s.run_cell('print(a)')
o{ 'display_objects': [],
'exception': None,
'quiet': False,
'result': result: None; err: None; info: <cell: print(a); id: None>,
'stderr': '',
'stdout': '1\n'}o = s.run_cell('from warnings import warn; warn("1")')
o{ 'display_objects': [],
'exception': None,
'quiet': False,
'result': result: None; err: None; info: <cell: from warnings import warn; warn("1"); id: None>,
'stderr': '<ipython-input-1-a51443ae013a>:1: UserWarning: 1\n'
' from warnings import warn; warn("1")\n',
'stdout': ''}o = s.run_cell('1')
o{ 'display_objects': [],
'exception': None,
'quiet': False,
'result': result: 1; err: None; info: <cell: 1; id: None>,
'stderr': '',
'stdout': ''}o = s.run_cell('from IPython.display import Markdown,display; print(0); display(Markdown("*2*")); Markdown("*1*")')
o{ 'display_objects': [<IPython.utils.capture.RichOutput object>],
'exception': None,
'quiet': False,
'result': result: <IPython.core.display.Markdown object>; err: None; info: <cell: from IPython.display import Markdown,display; print(0); display(Markdown("*2*")); Markdown("*1*"); id: None>,
'stderr': '',
'stdout': '0\n'}o.result.result1
o.display_objects[0]2
o = s.run_cell('1;')
o{ 'display_objects': [],
'exception': None,
'quiet': True,
'result': result: 1; err: None; info: <cell: 1;; id: None>,
'stderr': '',
'stdout': ''}o = s.run_cell('import matplotlib.pyplot as plt; plt.plot([1,2,3])')
o{ 'display_objects': [<IPython.utils.capture.RichOutput object>],
'exception': None,
'quiet': False,
'result': result: [<matplotlib.lines.Line2D object>]; err: None; info: <cell: import matplotlib.pyplot as plt; plt.plot([1,2,3]); id: None>,
'stderr': '',
'stdout': ''}o.result.result[0]o.display_objects[0]
o = s.run_cell('''
import pandas as pd
pd.DataFrame({'A': [1, 2], 'B': [3, 4]})''')
o{ 'display_objects': [],
'exception': None,
'quiet': False,
'result': result: A B
0 1 3
1 2 4; err: None; info: <cell:
import pandas as pd
pd.DataFrame({'A': [1, 2], 'B': [3, 4]}); id: None>,
'stderr': '',
'stdout': ''}o.result.result| A | B | |
|---|---|---|
| 0 | 1 | 3 |
| 1 | 2 | 4 |
o = s.run_cell('1/0')
o{ 'display_objects': [],
'exception': ZeroDivisionError('division by zero'),
'quiet': False,
'result': result: None; err: division by zero; info: <cell: 1/0; id: None>,
'stderr': '',
'stdout': '\x1b[31m---------------------------------------------------------------------------\x1b[39m\n'
'\x1b[31mZeroDivisionError\x1b[39m '
'Traceback (most recent call last)\n'
'\x1b[36mCell\x1b[39m\x1b[36m '
'\x1b[39m\x1b[32mIn[1]\x1b[39m\x1b[32m, line 1\x1b[39m\n'
'\x1b[32m----> \x1b[39m\x1b[32m1\x1b[39m '
'\x1b[30;43m1\x1b[39;49m\x1b[30;43m/\x1b[39;49m\x1b[30;43m0\x1b[39;49m\n'
'\n'
'\x1b[31mZeroDivisionError\x1b[39m: division by zero\n'}Testing errors caught after exec:
o = s.run_cell('import time; time.sleep(2)', timeout=1)
test_eq(type(o['exception']), TimeoutError)Testing errors caught before exec:
o = s.run_cell('print(', timeout=1)
test_eq(isinstance(o['exception'], SyntaxError), True)
o = s.run_cell("def foo():\npass")
test_eq(isinstance(o['exception'], IndentationError), True)
o = s.run_cell("if True:\n\tpass\n pass")
test_eq(isinstance(o['exception'], TabError), True)CaptureShell.load_profile
def load_profile(
name:str='default'
):Load profile name’s extensions and run its startup files (output suppressed), as ipykernel does
profile=True makes a CaptureShell behave like ipykernel at startup: it reads the IPython profile (honoring IPYTHONDIR), applies shell config traits from ipython_config.py and ipython_kernel_config.py, loads their InteractiveShellApp.extensions, and runs the profile’s startup files. (exec_lines/exec_files are not run.)
To demo profile loading we need a temporary IPython profile:
@contextmanager
def _tmp_profile(cfg='', kcfg='', startup=''):
with tempfile.TemporaryDirectory() as td, modified_env(IPYTHONDIR=td):
pd = Path(td)/'profile_default'
(pd/'startup').mkdir(parents=True)
if cfg: (pd/'ipython_config.py').write_text(cfg)
if kcfg: (pd/'ipython_kernel_config.py').write_text(kcfg)
if startup: (pd/'startup'/'00-start.py').write_text(startup)
yield tdStartup files in the profile’s startup directory are run, so their variables appear in the shell’s namespace:
with _tmp_profile(startup="a=7\n"): test_eq(CaptureShell(profile=True).user_ns['a'], 7)Extensions and startup files often print or display things as they load; load_profile suppresses that output so it can’t leak into test runs or captured results (failures still surface as warnings):
with _tmp_profile(startup="b=1\nprint('starting up')\n"):
with capture_output() as cap: sp = CaptureShell(profile=True)
test_eq((cap.stdout, sp.user_ns['b']), ('', 1))Shell config traits from the profile’s config files are applied too:
with _tmp_profile(kcfg="c.InteractiveShell.ast_node_interactivity='all'\n"):
test_eq(CaptureShell(profile=True).ast_node_interactivity, 'all')With the default history=False, the shell’s history database lives in memory, so creating shells never writes a history.sqlite into the profile directory:
with _tmp_profile() as td:
CaptureShell(profile=True)
test_eq((Path(td)/'profile_default'/'history.sqlite').exists(), False)Cells / run
format_exc
def format_exc(
e
):Format exception e as a string list
NbResult
def NbResult(
*args, **kwargs
):Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
CaptureShell.run
def run(
code:str, # Python/IPython code to run
stdout:bool=True, # Capture stdout and save as output?
stderr:bool=True, # Capture stderr and save as output?
timeout:Optional=None, # Shell command will time out after {timeout} seconds
verbose:bool=False, # Show stdout/stderr during execution
):Run code, returning a list of all outputs in Jupyter notebook format
quiet follows IPython’s trailing-semicolon convention for ordinary Python cells. A cell magic’s body is an opaque payload, which may be shell code, markup, or another language. Plain IPython does not let a semicolon at the end of that payload suppress the magic’s result, so CaptureShell only sets quiet for non-cell-magic source.
cs = CaptureShell(mpl_format=None)
cs.register_magic_function(lambda line, cell: 'visible', 'cell', 'visible')
res = cs.run("%%visible\npayload;")
test_eq(res[0]['data']['text/plain'], ["'visible'"])
test_eq(cs.run("1+1;"), [])s = CaptureShell()s.run("print(1)")[{'name': 'stdout', 'output_type': 'stream', 'text': ['1\n']}]
Code can include magics and ! shell commands:
o = s.run("%time 1+1")
o[{'name': 'stdout',
'output_type': 'stream',
'text': ['CPU times: user 1e+03 ns, sys: 1 us, total: 2 us\n',
'Wall time: 2.62 us\n']},
{'data': {'text/plain': ['2']},
'metadata': {},
'output_type': 'execute_result',
'execution_count': None}]
The result of the last successful execution is stored in result:
s.result2
A trailing ; stops the result from being captured:
s.run("1+2;")[]
o = s.run("1/0")
o[{'name': 'stdout',
'output_type': 'stream',
'text': ['\x1b[31m---------------------------------------------------------------------------\x1b[39m\n',
'\x1b[31mZeroDivisionError\x1b[39m Traceback (most recent call last)\n',
'\x1b[36mCell\x1b[39m\x1b[36m \x1b[39m\x1b[32mIn[1]\x1b[39m\x1b[32m, line 1\x1b[39m\n',
'\x1b[32m----> \x1b[39m\x1b[32m1\x1b[39m \x1b[30;43m1\x1b[39;49m\x1b[30;43m/\x1b[39;49m\x1b[30;43m0\x1b[39;49m\n',
'\n',
'\x1b[31mZeroDivisionError\x1b[39m: division by zero\n']},
{'ename': 'ZeroDivisionError',
'evalue': 'division by zero',
'output_type': 'error',
'traceback': ['Traceback (most recent call last):\n',
' File "/Users/jhoward/aai-ws/.venv/lib/python3.13/site-packages/IPython/core/interactiveshell.py", line 3748, in run_code\n exec(code_obj, self.user_global_ns, self.user_ns)\n ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
' File "<ipython-input-1-9e1622b385b6>", line 1, in <module>\n 1/0\n ~^~\n',
'ZeroDivisionError: division by zero\n']}]
This is how IPython formats exceptions internally:
from IPython.core.ultratb import VerboseTBwith warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
formatter = VerboseTB(color_scheme='Linux')try: f()
except Exception as e:
ex = e
print(formatter.text(type(e), e, e.__traceback__))---------------------------------------------------------------------------
NameError Traceback (most recent call last)
File <ipython-input-1-8ef4b5800722>:1
----> 1 try: f()
2 except Exception as e:
3 ex = e
NameError: name 'f' is not defined
s.run("import time; time.sleep(0.1); print('no timeout')", timeout=1)[{'name': 'stdout', 'output_type': 'stream', 'text': ['no timeout\n']}]
o = s.run("import time; time.sleep(1.1)", timeout=1)
o[0]['text'][:2]['\x1b[31m---------------------------------------------------------------------------\x1b[39m\n',
'\x1b[31mTimeoutError\x1b[39m Traceback (most recent call last)\n']
o1 = s.run('from IPython.display import Markdown,display; print(0); print(1); display(Markdown("*2*")); Markdown("*1*")')
o1[{'name': 'stdout', 'output_type': 'stream', 'text': ['0\n', '1\n']},
{'data': {'text/plain': ['<IPython.core.display.Markdown object>'],
'text/markdown': ['*2*']},
'metadata': {},
'output_type': 'display_data'},
{'data': {'text/plain': ['<IPython.core.display.Markdown object>'],
'text/markdown': ['*1*']},
'metadata': {},
'output_type': 'execute_result',
'execution_count': None}]
CaptureShell.run_async
async def run_async(
code:str, # Python/IPython code to run
stdout:bool=True, # Capture stdout and save as output?
stderr:bool=True, # Capture stderr and save as output?
timeout:Optional=None, # Shell command will time out after {timeout} seconds
verbose:bool=False, # Show stdout/stderr during execution
):Call self as a function.
await s.run_async("1+1")[{'data': {'text/plain': ['2']},
'metadata': {},
'output_type': 'execute_result',
'execution_count': None}]
render_outputs
def render_outputs(
outputs, ansi_renderer:function=_strip, include_imgs:bool=True, pygments:bool=False, md_tfm:function=noop,
html_tfm:function=noop
):Call self as a function.
HTML(render_outputs(o))---------------------------------------------------------------------------
TimeoutError Traceback (most recent call last)
Cell In[1], line 1
----> 1 import time; time.sleep(1.1)
File <ipython-input-1-289b30000b65>:7, in run_cell.<locals>.handler(*args)
5 if not timeout: timeout = self.timeout
6 if timeout:
----> 7 def handler(*args): raise TimeoutError()
8 signal.signal(signal.SIGALRM, handler)
9 signal.alarm(timeout)
TimeoutError:
We can use ansi2html to convert from ANSI to HTML for color rendering. You need some css styles for the colors to render properly. Jupyter already has these built in so it’s not neccessary here, but if you plan on using this in another web app you will need to ensure that css styling is included.
HTML(render_outputs(o, ansi2html))---------------------------------------------------------------------------
TimeoutError Traceback (most recent call last)
Cell In[1], line 1
----> 1 import time; time.sleep(1.1)
File <ipython-input-1-289b30000b65>:7, in run_cell.<locals>.handler(*args)
5 if not timeout: timeout = self.timeout
6 if timeout:
----> 7 def handler(*args): raise TimeoutError()
8 signal.signal(signal.SIGALRM, handler)
9 signal.alarm(timeout)
TimeoutError:
Images and matplotlib figures are captured:
res = s.run('''import matplotlib.pyplot as plt
plt.figure(figsize=(2,1))
plt.plot([1,2,4]);''')
HTML(render_outputs(res))If an exception is raised then the exception type, object, and stacktrace are stored in exc:
o = s.run('raise Exception("Oops")')
o[{'name': 'stdout',
'output_type': 'stream',
'text': ['\x1b[31m---------------------------------------------------------------------------\x1b[39m\n',
'\x1b[31mException\x1b[39m Traceback (most recent call last)\n',
'\x1b[36mCell\x1b[39m\x1b[36m \x1b[39m\x1b[32mIn[1]\x1b[39m\x1b[32m, line 1\x1b[39m\n',
'\x1b[32m----> \x1b[39m\x1b[32m1\x1b[39m \x1b[38;5;28;01mraise\x1b[39;00m \x1b[38;5;167;01mException\x1b[39;00m(\x1b[33m"\x1b[39m\x1b[33mOops\x1b[39m\x1b[33m"\x1b[39m)\n',
'\n',
'\x1b[31mException\x1b[39m: Oops\n']},
{'ename': 'Exception',
'evalue': 'Oops',
'output_type': 'error',
'traceback': ['Traceback (most recent call last):\n',
' File "/Users/jhoward/aai-ws/.venv/lib/python3.13/site-packages/IPython/core/interactiveshell.py", line 3748, in run_code\n exec(code_obj, self.user_global_ns, self.user_ns)\n ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
' File "<ipython-input-1-01648acb07bd>", line 1, in <module>\n raise Exception("Oops")\n',
'Exception: Oops\n']}]
s.excException('Oops')
CaptureShell.cell
def cell(
cell, stdout:bool=True, stderr:bool=True, verbose:bool=False
):Run cell, skipping if not code, and store outputs back in cell
clean = Path('../tests/clean.ipynb')
nb = read_nb(clean)
c = nb.cells[1]
c{ 'cell_type': 'code',
'execution_count': None,
'id': 'b123d6d0',
'idx_': 1,
'metadata': {},
'outputs': [],
'source': 'print(1)\n2'}s.cell(c)
c.outputs[{'name': 'stdout', 'output_type': 'stream', 'text': ['1\n']},
{'data': {'text/plain': ['2']},
'metadata': {},
'output_type': 'execute_result',
'execution_count': None}]
Re-running a cell that no longer produces output clears its stale outputs:
c2 = read_nb(clean).cells[1]
s.cell(c2)
assert c2.outputs
c2.source = 'x = 1'
s.cell(c2)
test_eq(c2.outputs, [])find_output
def find_output(
outp, # Output from `run`
ot:str='execute_result', # Output_type to find
):Find first output of type ot in CaptureShell.run output
find_output(c.outputs)['data']{'text/plain': ['2']}find_output(c.outputs, 'stream')['text']['1\n']
out_exec
def out_exec(
outp
):Get data from execution result in outp.
out_exec(c.outputs)'2'
out_stream
def out_stream(
outp
):Get text from stream in outp.
out_stream(c.outputs)'1'
out_error
def out_error(
outp
):Get traceback from error in outp.
NBs
CaptureShell.run_all
def run_all(
nb, # A notebook read with `nbclient` or `read_nb`
exc_stop:bool=False, # Stop on exceptions?
preproc:callable=_false, # Called before each cell is executed
postproc:callable=_false, # Called after each cell is executed
inject_code:str | None=None, # Code to inject into a cell
inject_idx:int=0, # Cell to replace with `inject_code`
verbose:bool=False, # Show stdout/stderr during execution
):Run all cells in nb, stopping at first exception if exc_stop
nb.cells[2].outputs[]
s.run_all(nb)
nb.cells[2].outputs[{'data': {'text/plain': ['<IPython.core.display.Markdown object>'],
'text/markdown': ["This is *bold*. Here's a [link](https://www.fast.ai)."]},
'metadata': {},
'output_type': 'execute_result',
'execution_count': None}]
With exc_stop=False (the default), execution continues after exceptions, and exception details are stored into the appropriate cell’s output:
nb.cells[-1].source'raise Exception("Oopsie!")'
nb.cells[-1].outputs[{'name': 'stdout',
'output_type': 'stream',
'text': ['\x1b[31m---------------------------------------------------------------------------\x1b[39m\n',
'\x1b[31mException\x1b[39m Traceback (most recent call last)\n',
'\x1b[36mCell\x1b[39m\x1b[36m \x1b[39m\x1b[32mIn[1]\x1b[39m\x1b[32m, line 1\x1b[39m\n',
'\x1b[32m----> \x1b[39m\x1b[32m1\x1b[39m \x1b[38;5;28;01mraise\x1b[39;00m \x1b[38;5;167;01mException\x1b[39;00m(\x1b[33m"\x1b[39m\x1b[33mOopsie!\x1b[39m\x1b[33m"\x1b[39m)\n',
'\n',
'\x1b[31mException\x1b[39m: Oopsie!\n']},
{'ename': 'Exception',
'evalue': 'Oopsie!',
'output_type': 'error',
'traceback': ['Traceback (most recent call last):\n',
' File "/Users/jhoward/aai-ws/.venv/lib/python3.13/site-packages/IPython/core/interactiveshell.py", line 3748, in run_code\n exec(code_obj, self.user_global_ns, self.user_ns)\n ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
' File "<ipython-input-1-1c97c1d317ab>", line 1, in <module>\n raise Exception("Oopsie!")\n',
'Exception: Oopsie!\n']}]
With exc_stop=True, exceptions in a cell are raised and no further processing occurs:
try: s.run_all(nb, exc_stop=True)
except Exception as e: print(f"got exception: {e}")got exception: Oopsie!
We can pass a function to preproc to have it run on every cell. It can modify the cell as needed. If the function returns True, then that cell will not be executed. For instance, to skip the cell which raises an exception:
nb = read_nb(clean)
s.run_all(nb, preproc=lambda c: 'raise' in c.source)This cell will contain no output, since it was skipped.
nb.cells[-1].outputs[]
nb.cells[1].outputs[{'name': 'stdout', 'output_type': 'stream', 'text': ['1\n']},
{'data': {'text/plain': ['2']},
'metadata': {},
'output_type': 'execute_result',
'execution_count': None}]
You can also pass a function to postproc to modify a cell after it is executed.
CaptureShell.execute
def execute(
src:str | pathlib.Path, # Notebook path to read from
dest:str | None=None, # Notebook path to write to
exc_stop:bool=False, # Stop on exceptions?
preproc:callable=_false, # Called before each cell is executed
postproc:callable=_false, # Called after each cell is executed
inject_code:str | None=None, # Code to inject into a cell
inject_path:str | pathlib.Path | None=None, # Path to file containing code to inject into a cell
inject_idx:int=0, # Cell to replace with `inject_code`
verbose:bool=False, # Show stdout/stderr during execution
):Execute notebook from src and save with outputs to `dest
This is a shortcut for the combination of read_nb, CaptureShell.run_all, and write_nb.
s = CaptureShell()
try:
s.execute(clean, 'tmp.ipynb')
print(read_nb('tmp.ipynb').cells[1].outputs)
finally: Path('tmp.ipynb').unlink()[{'name': 'stdout', 'output_type': 'stream', 'text': '1\n'}, {'data': {'text/plain': '2'}, 'execution_count': None, 'metadata': {}, 'output_type': 'execute_result'}]
p = Path.home()/'git'/'fastcore'/'nbs'
n = p/'03a_parallel.ipynb'CaptureShell.prettytb
def prettytb(
fname:str | pathlib.Path=None, # filename to print alongside the traceback
):Show a pretty traceback for notebooks, optionally printing fname.
If an error occurs while running a notebook, you can retrieve a pretty version of the error with the prettytb method:
s = CaptureShell()
try: s.execute('../tests/error.ipynb', exc_stop=True)
except: print(s.prettytb())AssertionError in ../tests/error.ipynb:
===========================================================================
While Executing Cell #2:
Traceback (most recent call last):
File "<ipython-input-1-5c812627fe60>", line 2, in <module>
try: s.execute('../tests/error.ipynb', exc_stop=True)
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<ipython-input-1-4b062e10fd76>", line 19, in execute
self.run_all(nb, exc_stop=exc_stop, preproc=preproc, postproc=postproc,
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
inject_code=inject_code, inject_idx=inject_idx, verbose=verbose)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<ipython-input-1-54c4c86f5c38>", line 21, in run_all
if self.exc and exc_stop: raise self.exc from None
^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jhoward/aai-ws/.venv/lib/python3.13/site-packages/IPython/core/interactiveshell.py", line 3748, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<ipython-input-1-b968a57a586e>", line 3, in <module>
foo()
~~~^^
File "/Users/jhoward/aai-ws/execnb/tests/err.py", line 2, in foo
assert 13 == 98
^^^^^^^^
AssertionError
nbopen / nbrun
Downstream kernels (e.g clikernel) need to run cells from a notebook by cell id, e.g to power %open/%run magics.
select_cells
def select_cells(
nb, # A notebook read with `read_nb`
msgid:str=None, # Cell id, or unique prefix, to match
above:bool=False, # Include the matched cell and all cells above it?
below:bool=False, # Include the matched cell and all cells below it?
all:bool=False, # Include all code cells (ignores `msgid`)?
exported:bool=False, # Only cells with `#| export` or `#| exports`?
skip_noeval:bool=False, # Skip `#| eval: false` and `nbdev_export` cells (like `nbdev-test`)?
):Select code cells from nb by cell id or unique prefix
This selects code cells by id prefix, optionally including cells above/below the match, or all cells, and optionally filtering to just those with an nbdev export directive:
nb = read_nb(clean)
codes = [c for c in nb.cells if c.cell_type=='code']
c1 = nb.cells[1]
ci = codes.index(c1)
cid = str(c1.id)
test_eq(select_cells(nb, cid), [c1])
test_eq(select_cells(nb, cid[:4]), [c1])
test_eq(select_cells(nb, cid, above=True), codes[:ci+1])
test_eq(select_cells(nb, cid, below=True), codes[ci:])
test_eq(select_cells(nb, all=True), codes)
c1.source = '#| export\n'+c1.source
test_eq(select_cells(nb, all=True, exported=True), [c1])
c1.source = '#| eval: false\n'+c1.source
test_eq(select_cells(nb, all=True, skip_noeval=True), [c for c in codes if c is not c1])
test_eq(select_cells(nb, all=True), codes)
c2 = next(c for c in codes if c is not c1)
c2.source = 'nbdev_export'+'()'
test_eq(select_cells(nb, all=True, skip_noeval=True), [c for c in codes if c not in (c1,c2)])
c2.id = cid[:4]+'0000'
with expect_fail(Exception, cid[:4]): select_cells(nb, cid[:4])
test_eq(select_cells(nb, cid), [c1])render_outputs is HTML-oriented; downstream kernels print each executed cell’s result to stdout instead, so we use fastcore’s render_text, which renders outputs to concise text.
s = CaptureShell()
o = render_text(s.run('print(1); 2'))
test_is('1' in o and '2' in o, True)
test_eq(render_text(s.run('1;')), '')
test_is('ZeroDivisionError' in render_text(s.run('1/0')), True)Finally, nbopen sets a default notebook for future nbrun calls, and nbrun selects cells and runs them in the order given, printing each cell’s rendered output. The notebook is re-read from disk on every nbrun so file edits are picked up. The default path is stored as a shell attribute (not in user_ns) so a kernel restart clears it naturally, and nbopen raises on a nonexistent path rather than recording it, so a mistyped argument can’t poison later calls that rely on the default.
CaptureShell.nbrun
def nbrun(
*msgids:str, # Cell id prefixes to run, in the order given
fname:str | pathlib.Path=None, # Notebook path (defaults to last `nbopen`)
above:bool=False, # Also run all cells above the match?
below:bool=False, # Also run all cells below the match?
all:bool=False, # Run all code cells?
exported:bool=False, # Only cells with `#| export` or `#| exports`?
skip_noeval:bool=False, # Skip `#| eval: false` and `nbdev_export` cells (like `nbdev-test`)?
):Run cell(s) from a notebook by id prefix, printing rendered outputs
CaptureShell.nbopen
def nbopen(
fname:str | pathlib.Path
):Set the default notebook for nbrun
s = CaptureShell()
s.nbopen(clean)
o = render_text(s.run(f'get_ipython().nbrun("{cid}")'))
test_is('1' in o and '2' in o, True)s = CaptureShell()
s.nbopen(clean)
o = render_text(s.run(f'get_ipython().nbrun("38ab158d", "{cid}")'))
test_is('bold' in o and 'b123d6d0' in o and o.index('bold') < o.index('b123d6d0'), True)
with expect_fail(Exception, 'does_not_exist'): s.nbopen('does_not_exist.ipynb')
test_eq(s._nbrun_fname, Path(clean))def is_sublist(sub, lst): return any(lst[i:i+len(sub)] == sub for i in range(len(lst)-len(sub)+1))s2 = CaptureShell()
r = s2.run_cell('from IPython.display import display,Markdown; display(Markdown("x"))')
r.display_objects, r.stdout([<IPython.utils.capture.RichOutput>], '')
If you pass inject_code to CaptureShell.execute or CaptureShell.run_all, the source of nb.cells[inject_idx] will be replaced with inject_code. By default, the first cell is replaced. For instance consider this notebook:
nb = read_nb('../tests/params.ipynb')
for c in nb.cells: print('- ',c.source)- a=1
- print(a)
We can replace the first cell with a=2 by passing that as inject_code, and the notebook will run with that change:
nb = read_nb('../tests/params.ipynb')
s.run_all(nb, inject_code="a=2")
list(nb.cells)[{'cell_type': 'code',
'execution_count': None,
'id': 'a63ce885',
'metadata': {'time_run': '2026-01-04T20:52:46.502210+00:00'},
'outputs': [],
'source': 'a=2',
'idx_': 0},
{'cell_type': 'code',
'execution_count': None,
'id': 'ea528db5',
'metadata': {'time_run': '2026-01-04T20:52:46.506607+00:00'},
'outputs': [{'name': 'stdout', 'output_type': 'stream', 'text': ['2\n']}],
'source': 'print(a)',
'idx_': 1}]
This can be used with CaptureShell.execute to parameterise runs of models in notebooks. Place any defaults for configuration code needed in the first cell, and then when running execute pass in new parameters as needed in inject_code. To replace only some of the defaults, leave an empty cell as the second cell, and inject code using inject_idx=1 to replace the empty second cell with code that overrides some of the defaults set in the first cell. When using execute you can pass inject_path instead of inject_code to read the injected code from a file.
exec_nb
def exec_nb(
src:str, # Notebook path to read from
dest:str='', # Notebook path to write to
exc_stop:bool=False, # Stop on exceptions?
inject_code:str=None, # Code to inject into a cell
inject_path:str=None, # Path to file containing code to inject into a cell
inject_idx:int=0, # Cell to replace with `inject_code`
verbose:bool=False, # Show stdout/stderr during execution
):Execute notebook from src and save with outputs to dest
This is the command-line version of CaptureShell.execute. Run exec_nb -h from the command line to see how to pass arguments. If you don’t pass dest then the output notebook won’t be saved; this is mainly useful for running tests.
SmartCompleter
def SmartCompleter(
shell, # a pointer to the ipython shell itself. This is needed
# because this completer knows about magic functions, and those can
# only be accessed via the ipython instance.
namespace:NoneType=None, # an optional dict where completions are performed.
jedi:bool=False
):Extension of the completer class with IPython-specific features
cc = SmartCompleter(get_ipython())
def test_set(a,b): return test_eq(set(a), set(b))
class _f:
def __init__(self): self.bar,self.baz,self.room = 0,0,0
foo = _f()
assert set(cc("b")).issuperset(['bool', 'bytes'])
test_set(cc("foo.b"), ['bar', 'baz'])
test_set(cc("x=1; x = foo.b"), ['bar', 'baz'])
test_set(cc("ab"), ['abs'])
test_set(cc("b = ab"), ['abs'])
test_set(cc(""), [])
test_set(cc("foo."), ['bar', 'baz', 'room'])
test_set(cc("nonexistent.b"), [])
test_set(cc("foo.nonexistent.b"), [])
assert set(cc("import ab")).issuperset(['abc'])
test_set(cc("from abc import AB"), ['ABC', 'ABCMeta'])s = CaptureShell()
cc = SmartCompleter(s)
s.run('''def captures(pat, s, n, **kw):
return 1''')
cc('captures(')['n=', 'pat=', 's=']
CaptureShell.complete
def complete(
c
): # The actual text that was completed.Return the completed text and a list of completions.
s = CaptureShell()
s.run('a=1')
s.complete('a.b')['bit_count', 'bit_length']
s.run('import re')
s.complete('re.compile(')['flags=', 'pattern=']