# nbtools


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Path safety

Every path-taking tool below first calls
[`_resolve_safe`](https://drscotthawley.github.io/slmn/nbtools.html#_resolve_safe).
It’s a no-op by default, but if `SLMN_SANDBOX_ROOTS` is set
(colon-separated directories) it refuses any path that resolves outside
them – a guardrail for when these tools are driven by an agent.

## Reading & searching

[`read_nb`](https://drscotthawley.github.io/slmn/nbtools.html#read_nb)
and
[`grep_nb`](https://drscotthawley.github.io/slmn/nbtools.html#grep_nb)
are the read side.
[`read_nb`](https://drscotthawley.github.io/slmn/nbtools.html#read_nb)
with no arguments lists every cell’s id and a one-line preview – the
usual way to get your bearings before a targeted read or edit, since
every other tool here addresses cells *by id*.

------------------------------------------------------------------------

<a
href="https://github.com/drscotthawley/slmn/blob/main/slmn/nbtools.py#L27"
target="_blank" style="float:right; font-size:smaller">source</a>

### read_nb

``` python
def read_nb(
    path:str, # path to the .ipynb file
    cell_ids:list=None, # if given, only these cells' full source (ids may be partial/substring matches)
    full:bool=False, # if True (and no cell_ids given), print full source of every cell
)->str:
```

*Read cell source(s) from a Jupyter notebook. With no cell_ids and
full=False, lists every cell’s id + a source preview (one line each) –
good for getting your bearings before a targeted read.*

------------------------------------------------------------------------

<a
href="https://github.com/drscotthawley/slmn/blob/main/slmn/nbtools.py#L46"
target="_blank" style="float:right; font-size:smaller">source</a>

### grep_nb

``` python
def grep_nb(
    path:str, # path to the .ipynb file
    pattern:str, # regex pattern to search for, per line, within each cell's source
    context:int=0, # lines of context before/after each match to include
    line_numbers:bool=False, # prefix each shown line with its (0-based) line number within the cell
)->str:
```

*Grep cell sources in a Jupyter notebook. Returns the matching cells (id
header + matching/context lines); empty string if nothing matched.*

## Editing cells

Two ways to change an existing cell, both addressed by id:
[`edit_nb`](https://drscotthawley.github.io/slmn/nbtools.html#edit_nb)
does a find/replace within a cell’s source (like a code editor’s
replace), while
[`patch_nb_cell`](https://drscotthawley.github.io/slmn/nbtools.html#patch_nb_cell)
overwrites a cell’s source wholesale. All writes use
`json.dump(..., indent=1)` to match Jupyter’s own formatting, so a
one-cell change stays a one-cell git diff.

------------------------------------------------------------------------

<a
href="https://github.com/drscotthawley/slmn/blob/main/slmn/nbtools.py#L77"
target="_blank" style="float:right; font-size:smaller">source</a>

### edit_nb

``` python
def edit_nb(
    path:str, # path to the .ipynb file
    cell_id:str, # cell id, or a prefix of one
    old_str:str, # text to find in the cell's source (first occurrence)
    new_str:str, # replacement text
)->str:
```

*Replace the first occurrence of old_str with new_str in one cell’s
source, addressed by id-prefix. Falls back to a
trailing-whitespace-insensitive match if the exact text isn’t found.
Raises ValueError if the cell or the text can’t be found.*

------------------------------------------------------------------------

<a
href="https://github.com/drscotthawley/slmn/blob/main/slmn/nbtools.py#L100"
target="_blank" style="float:right; font-size:smaller">source</a>

### patch_nb_cell

``` python
def patch_nb_cell(
    path:str, # path to the .ipynb file
    cell_id:str, # exact cell id
    new_source:str, # the cell's complete new source, replacing the old source entirely
)->str:
```

*Replace a cell’s entire source (exact id match, not a prefix – use this
when you want to overwrite a cell wholesale rather than patch part of
it; see edit_nb for a targeted string replacement). Raises ValueError if
the cell isn’t found.*

## Adding cells

[`add_nb_cell`](https://drscotthawley.github.io/slmn/nbtools.html#add_nb_cell)
is the single-cell primitive;
[`insert_cells`](https://drscotthawley.github.io/slmn/nbtools.html#insert_cells)
is a thin loop over it for bulk inserts.
[`add_nb_cell`](https://drscotthawley.github.io/slmn/nbtools.html#add_nb_cell)
– not the caller – owns the `#| export` / `#| eval: false` directives
for code cells: pass plain source plus the `export`/`eval` flags and it
regenerates them idempotently, while leaving other directives
(`#| default_exp`, `#| hide`) alone. (This very notebook edit was made
with these tools.)

------------------------------------------------------------------------

<a
href="https://github.com/drscotthawley/slmn/blob/main/slmn/nbtools.py#L115"
target="_blank" style="float:right; font-size:smaller">source</a>

### insert_cells

``` python
def insert_cells(
    path:str, # path to the .ipynb file
    anchor_id:str, # exact cell id to insert after
    sources:list, # one or more new cells' source, inserted in order right after the anchor -- plain source, no '#| ...' directives (see add_nb_cell)
    export:bool=True, # forwarded to add_nb_cell for every inserted cell -- see there
    eval:bool=True, # forwarded to add_nb_cell for every inserted cell -- see there
)->str:
```

*Insert one or more new code cells immediately after the cell with id
`anchor_id`. A thin bulk convenience wrapper over add_nb_cell (the
single-cell primitive), so the actual nbformat plumbing – including the
‘\#| export’/‘\#| eval: false’ directive handling – lives in one place.
Raises ValueError if the anchor isn’t found.*

------------------------------------------------------------------------

<a
href="https://github.com/drscotthawley/slmn/blob/main/slmn/nbtools.py#L138"
target="_blank" style="float:right; font-size:smaller">source</a>

### add_nb_cell

``` python
def add_nb_cell(
    path:str, # path to the .ipynb file
    source:str, # the new cell's source text. Don't include '#| export'/'#| eval: false' -- they're regenerated from the flags below (any you do include are stripped first, so it stays idempotent). OTHER directives like '#| default_exp' or '#| hide' ARE preserved, so pass those literally.
    cell_type:str='code', # nbformat cell type: 'code', 'markdown', or 'raw'
    after_id:str=None, # id of the cell to insert immediately after; None appends at the end (in cell order, NOT a raw file append)
    cell_id:str=None, # if given, force the new cell's id (e.g. to match an intended nbdev '# %%' marker); default lets nbformat assign one
    export:bool=True, # code cells only: prepend '#| export' so nbdev exports this cell into the module. Ignored for markdown/raw cells.
    eval:bool=True, # code cells only: if False, prepend '#| eval: false' so nbdev skips executing this cell during tests/docs (e.g. a live-server/network demo). Ignored for markdown/raw cells.
)->str:
```

*Add a new cell to a Jupyter notebook and return its id. Appends to the
end by default, or inserts right after `after_id`. `cell_type` is an
nbformat type (‘code’/‘markdown’/‘raw’) – note that boopiter-style
note/prompt/assistant cells are all ‘markdown’ at the nbformat level.
This tool – not the caller – owns the ‘\#| export’/‘\#| eval: false’
directives for code cells: pass plain source and use `export`/`eval`
(any of those two you include literally get stripped and regenerated, so
it’s idempotent; other directives like ‘\#| default_exp’/‘\#| hide’ are
left as-is). Reads/writes the file as plain JSON (indent=1) like the
other nbtools here, so a one-cell add stays a one-cell git diff rather
than reserializing the whole notebook. Raises ValueError on an unknown
`cell_type`, or an `after_id` that isn’t present.*

## Writing docs in bulk

[`write_nb_docs`](https://drscotthawley.github.io/slmn/nbtools.html#write_nb_docs)
is the batch form of the two tools above, for documenting a notebook in
one shot: give it `notes` (markdown section headings to insert after
named cells) and/or `patches` (cells to rewrite), and it validates every
id *before* writing anything – so a single wrong id fails cleanly
instead of leaving a half-documented notebook. It’s the reusable version
of this very notebook’s doc pass; point it at another project to do the
same there.

------------------------------------------------------------------------

<a
href="https://github.com/drscotthawley/slmn/blob/main/slmn/nbtools.py#L169"
target="_blank" style="float:right; font-size:smaller">source</a>

### write_nb_docs

``` python
def write_nb_docs(
    path:str, # path to the .ipynb file
    notes:list=None, # markdown Note cells to INSERT: a list of [after_id, source] pairs. Each cell is inserted right after the cell whose id equals after_id; order within the list is preserved even when several notes share one anchor.
    patches:list=None, # existing cells to REWRITE: a list of [cell_id, new_source] pairs. Each named cell's entire source is replaced wholesale (like patch_nb_cell).
    cell_type:str='markdown', # nbformat cell type for the inserted `notes` ('markdown' for docs; 'code'/'raw' also accepted)
)->str:
```

*Apply a batch of documentation edits to a notebook in ONE atomic pass:
insert markdown section-Note cells after named anchor cells (`notes`)
and/or rewrite existing cells wholesale (`patches`). Every referenced id
is validated against the notebook FIRST, so a single wrong id raises
before anything is written – no half-applied state (the failure mode of
firing off many add_nb_cell/patch_nb_cell calls in a row, where one bad
id leaves the earlier ones already committed). The whole notebook is
written once with json.dump(indent=1), so it stays a minimal git diff.
This is the reusable form of the ‘add section headings throughout a
notebook, then tidy the index’ workflow – point it at any nbdev notebook
(slmn’s own, or another project like boopiter) to document it. `notes`
source is inserted verbatim (markdown needs no directives); for code
cells that need ‘\#| export’/‘\#| eval’ use add_nb_cell instead.*

## Documenting a whole project

[`write_project_docs`](https://drscotthawley.github.io/slmn/nbtools.html#write_project_docs)
is
[`write_nb_docs`](https://drscotthawley.github.io/slmn/nbtools.html#write_nb_docs)
extended across every notebook in a project: give it
`{notebook_path: {'notes': ..., 'patches': ...}}` and it validates every
cell id in every notebook before writing any of them, then applies each
notebook’s batch. It also folds in the recipe for wiring up an
`llms.txt` file on an nbdev project’s docs site – see its docstring,
which is intentionally long, since it’s the guidance a future
documentation pass (on slmn itself, or another project like boopiter)
should read before touching `llms.txt`.

------------------------------------------------------------------------

<a
href="https://github.com/drscotthawley/slmn/blob/main/slmn/nbtools.py#L205"
target="_blank" style="float:right; font-size:smaller">source</a>

### write_project_docs

``` python
def write_project_docs(
    specs:dict, # {notebook_path: {'notes': [[after_id, source], ...], 'patches': [[cell_id, new_source], ...]}} -- one entry per notebook to touch; either key may be omitted or None
    llms_txt:dict=None, # optional {path: source} of extra plain-text files to write verbatim, e.g. {'nbs/llms.txt': '# proj\n> ...'}
)->str:
```

*Document a whole nbdev project in one call: apply a write_nb_docs batch
to each notebook* named in `specs`, plus optionally write extra
plain-text files (llms_txt). Validates every cell id across every
notebook FIRST, so a bad id in notebook \#5 doesn’t leave \#1-4 already
rewritten – the project-wide version of write_nb_docs’s own atomicity.

This is the level to reach for when repeating slmn’s own documentation
pass (section-Note headings per notebook + a tidied index/README) on
another nbdev project, e.g. boopiter. For the full recipe – including
when to use `notes` (additive, default) vs `patches` (only for
disposable nbdev/quarto boilerplate, never over real human-written
content) and how to wire up an llms.txt file for the project’s docs site
– see the write-project-docs skill:
slmn/skills/write-project-docs/SKILL.md.

## Smoke test

A quick round-trip against a throwaway notebook – insert, read, grep,
edit, patch – so a `nbdev-test` run exercises the whole toolkit end to
end. Not exported.

``` python
# Round-trip smoke test against a throwaway notebook.
import tempfile, os
tmp = tempfile.mktemp(suffix='.ipynb')
_nbf.write(_nbf.v4.new_notebook(cells=[_nbf.v4.new_code_cell('x = 1', id='cell0')]), tmp)

print(insert_cells(tmp, 'cell0', ['y = 2', 'z = 3']))
print(read_nb(tmp))
print(grep_nb(tmp, 'y ='))
print(edit_nb(tmp, 'cell0', 'x = 1', 'x = 100'))
print(patch_nb_cell(tmp, 'cell0', 'x = 999'))

# batch docs: insert two notes after cell0 (order preserved) + rewrite cell0, atomically
print(write_nb_docs(tmp, notes=[['cell0', '# Title'], ['cell0', 'intro paragraph']],
                 patches=[['cell0', 'x = 4242']]))
print(read_nb(tmp, full=True))

# project-level batch docs: same idea, spanning multiple notebooks + an extra plain-text file
tmp2 = tempfile.mktemp(suffix='.ipynb')
_nbf.write(_nbf.v4.new_notebook(cells=[_nbf.v4.new_code_cell('w = 1', id='cell0')]), tmp2)
llms_tmp = tempfile.mktemp(suffix='.txt')
print(write_project_docs({
    tmp: {'patches': [['cell0', 'x = 1']]},
    tmp2: {'notes': [['cell0', '# Other notebook']]},
}, llms_txt={llms_tmp: '# demo\n> summary\n'}))
os.remove(tmp2); os.remove(llms_tmp)

os.remove(tmp)
```
