Generate markdown documents from notebook-backed templates
Project description
Codoc
Generate markdown documents from .template.md files backed by Jupyter notebooks, nobook .py files, and scripts.
Codoc extracts referenced cells and blocks, inserts them into markdown templates, and can validate notebook content by executing the source notebooks first.
Installation
Install from PyPI:
pip install codoc
For development:
git clone git@github.com:alexeygrigorev/codoc.git
cd codoc
uv sync --dev
The main commands are codoc, codoc-watch, and nbedit.
Editors
Codoc includes a bundled VS Code extension in vscode-ext/.
It provides:
- inline previews for
@@code,@@code-output, and@@code-figure - navigation from template directives to source cells and blocks
- inline editing for notebook, nobook, and script-backed sources
Build or package it from the repo:
cd vscode-ext
npm install
npm run compile
npm run package
Run it in debug mode from VS Code:
cd codoc
code .
Then:
- Open the
vscode-ext/folder in the workspace - Run
npm installandnpm run compileinsidevscode-ext/ - Open the Run and Debug panel in VS Code
- Start an Extension Development Host with
F5 - In the new window, open any
.template.mdfile to load the custom editor
The launch configuration is in .vscode/launch.json.
If you edit the extension source, rebuild with npm run compile:dev in vscode-ext/, then stop and restart the Extension Development Host.
Usage
Generate a single template
uv run python -m codoc path/to/file.template.md
This creates path/to/file.md (removing .template from the filename).
Generate all templates in a directory
uv run python -m codoc path/to/folder
Recursively finds all *.template.md files and generates corresponding .md files.
Options
--timeout SECONDS- Timeout for each cell during validation (default: 30)--kernel NAME- Jupyter kernel name to use (default: python3)-o, --output PATH- Output file path (single file only)-v, --verbose- Enable verbose output
uv run python -m codoc templates/ --timeout 60 -v
Watch mode
Watch for template file changes and automatically regenerate on edit:
uv run codoc-watch [path]
The watcher waits for a grace period (default 1.5s) after the last edit before triggering, so it doesn't run while you're actively typing.
Options:
-g, --grace-period SECONDS- Wait time after last change (default: 1.5)--timeout SECONDS- Timeout for each cell during validation (default: 30)--kernel NAME- Jupyter kernel name to use (default: python3)-v, --verbose- Enable verbose output
# Watch current directory
uv run codoc-watch
# Watch specific path
uv run codoc-watch 01-foundation/02-rag/
# Verbose mode with longer grace period
uv run codoc-watch -v -g 2.0
Notebook Editor
The notebook editor provides convenient CLI commands for manipulating Jupyter notebooks without editing JSON directly.
Common Mistakes
Wrong: Using -- after cell_id (confusing with other CLI conventions)
cat << 'EOF' | nbedit nb.ipynb update my-cell --
code here
EOF
Correct: Use --code - for stdin
cat << 'EOF' | nbedit nb.ipynb update my-cell --code -
code here
EOF
The -- separator used by some tools (like git) is NOT valid in nbedit. Always use --code followed by the code content or - for stdin.
Indexing Note
Cell indices use 1-based indexing (1, 2, 3...) instead of 0-based. This matches how humans count and is more intuitive when working with line numbers in editors. If you're used to 0-based indexing from programming, just add 1 when referring to cell positions.
Batch Mode (Recommended)
For most workflows, use batch mode to execute multiple commands at once. Show comprehensive batch mode help:
uv run nbedit --batch-help
Create a batch file with commands starting with @>>:
uv run nbedit notebook.ipynb --batch batch-file.txt
Use - for stdin:
cat batch.txt | uv run nbedit notebook.ipynb --batch -
The notebook is automatically created if it doesn't exist, and saved after all commands complete.
Comments
Lines starting with -- (double dash followed by a space) are treated as comments and ignored.
-- This is a comment
-- So is this
@>> add --id my-cell
print("hello")
Batch Commands
add [--id ID]- Add a code cell with optional @cell_idadd --markdown- Add a markdown cellinsert-after INDEX|ID [--id ID]- Insert after index or cell_idupdate INDEX|ID- Update cell by index or IDdelete INDEX|ID- Delete cell by index or IDrename ID --new-id NEW- Rename a cell IDadd-id INDEX ID- Add @cell_id to cell at indexmove ID AFTER_ID- Move cell after another cell
Batch Examples
Creating a new notebook:
@>> add --id setting-up
from openai import OpenAI
client = OpenAI()
@>> add --id first-request
messages = [{"role": "user", "content": "hello"}]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
print(response.choices[0].message.content)
Updating an existing cell by ID:
@>> update my-cell --code
def new_function():
return "updated"
Deleting a cell by index (1-based):
@>> delete 3
Renaming a cell ID:
@>> rename old-id --new-id new-id
Moving a cell after another:
@>> move cell-to-move target-cell
Common Scenarios
Create a notebook from scratch:
@>> add --id imports
import pandas as pd
import numpy as np
@>> add --id load-data
df = pd.read_csv("data.csv")
print(df.head())
@>> add --id process
df["total"] = df["quantity"] * df["price"]
print(df.describe())
Add a cell after an existing one:
@>> insert-after load-data --id visualize
import matplotlib.pyplot as plt
df.plot(x="date", y="total")
plt.show()
Single Command Mode
For quick operations, use single commands. The notebook is auto-created if it doesn't exist.
List cells (to check state before editing):
uv run nbedit notebook.ipynb list
uv run nbedit notebook.ipynb list --with-output --limit 20
uv run nbedit notebook.ipynb list --line-numbers
The --line-numbers (or -n) flag shows 1-based line numbers for code cells. Use this to identify line ranges for the lines= parameter in @@code directives.
Add a code cell with an optional @cell_id marker:
uv run nbedit notebook.ipynb add --code "print('hello')" --id my-cell
Add a markdown cell:
uv run nbedit notebook.ipynb add --markdown "# Heading"
Read code from stdin:
echo "print('hello')" | uv run nbedit notebook.ipynb add --code -
Insert after a cell by ID:
uv run nbedit notebook.ipynb insert-after existing-id --code "new code" --id new-id
Update by cell ID:
uv run nbedit notebook.ipynb update cell-id --code "updated code"
Delete by index or ID:
uv run nbedit notebook.ipynb delete 3
uv run nbedit notebook.ipynb delete cell-id
Rename cell ID:
uv run nbedit notebook.ipynb rename old-id --new-id new-id
Add @cell_id marker to existing cell (1-based index):
uv run nbedit notebook.ipynb add-id 3 my-new-id
Move cell after another:
uv run nbedit notebook.ipynb move cell-id after-id
Remove all @cell_id markers:
uv run nbedit notebook.ipynb remove-ids
Template Syntax
Frontmatter
Define notebook and script references in YAML frontmatter:
---
notebooks:
- id: openai-basics
path: ../../notebooks/02-openai-api.ipynb
execute: true
- id: doc-agent
path: ../../notebooks/03-documentation-agent.ipynb
execute: false
- id: plain-py
path: ../../notebooks/04-agents.py
execute: true
scripts:
- id: test
path: test_agent.py
---
Notebook fields:
id- Identifier for this notebook (used in directives)path- Path to the notebook file (relative to template file). This can point to either.ipynbor nobook-style.pyfiles.execute- Whether to execute the notebook during generation (default:true)
Script fields:
id- Identifier for this script (used in directives)path- Path to the script file (relative to template file)
Set execute: false for notebooks that:
- Make external API calls
- Have long execution times
- Access resources not available during generation
- Already have nobook outputs checked into sibling
.out.pyfiles
IDs must be unique across both notebooks and scripts in the same template.
Directives
Use @@code to include cell source code (from notebooks) or block source code (from scripts):
@@code openai-basics:create-client
@@code test:setup
Use @@code-output to include cell output (notebooks only):
@@code-output openai-basics:create-client
@@code-output and @@code-figure are not supported for scripts (scripts have no execution output).
To limit the number of output lines (useful for long outputs):
@@code-output openai-basics:create-client limit-lines=5
This shows only the first 5 lines, followed by ... if the output exceeds the limit.
You can also limit by character count:
@@code-output openai-basics:create-client limit-chars=100
This shows only the first 100 characters, followed by ... if the output exceeds the limit.
Both limits can be used together (lines are applied first, then characters):
@@code-output openai-basics:create-client limit-lines=5 limit-chars=100
To extract specific lines from a cell (useful for showing a method from a class):
@@code openai-basics:my-class lines=2-4
This extracts only lines 2 through 4 (1-based, inclusive). Use lines=3 for a single line.
To remove leading spaces from each line (useful for dedenting class methods):
@@code openai-basics:my-class lines=2-4 strip-spaces=4
This removes up to 4 leading spaces from each extracted line. Empty lines are unaffected.
The workflow: run nbedit notebook.ipynb list --line-numbers to see numbered code, then use lines= and strip-spaces= in your template.
Notebook Cells
Mark cells in Jupyter notebooks with # @cell_id=:
# @cell_id=create-client
from openai import OpenAI
client = OpenAI()
Nobook Blocks
Codoc also supports nobook sources under notebooks: frontmatter entries. Use # @block=name markers in a plain .py file:
# @block=create-client
from openai import OpenAI
client = OpenAI()
# @block=run-request
response = client.responses.create(
model="gpt-4.1-mini",
input="hello"
)
print(response.output_text)
Then reference the blocks exactly like notebook cells:
@@code plain-py:create-client
@@code-output plain-py:run-request
If execute: false, Codoc reads output from a sibling .out.py file generated by nobook.
Script Blocks
Mark blocks in script files with # @block=name and # @end:
# @block=setup
from openai import OpenAI
client = OpenAI()
# @end
# code outside blocks is ignored
# @block=make-request
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hello"}]
)
# @end
The code fence language is automatically set to python for .py files.
Example
Template (02-openai-api.template.md):
---
notebooks:
- id: openai-basics
path: ../../notebooks/02-openai-api.ipynb
execute: true
---
# Using the OpenAI API
Let's start by creating a client.
@@code openai-basics:create-client
Now we can make a request:
@@code openai-basics:make-request
The response looks like this:
@@code-output openai-basics:make-request
Notebook (02-openai-api.ipynb) cell:
# @cell_id=create-client
from openai import OpenAI
client = OpenAI()
Generated (02-openai-api.md):
# Using the OpenAI API
Let's start by creating a client.
```python
from openai import OpenAI
client = OpenAI()
Now we can make a request:
messages = [{"role": "user", "content": "tell me a joke"}]
response = client.chat.completions.create(...)
The response looks like this:
ChatCompletionMessage(content="Why did the chicken...", role='assistant')
## Script Example
**Template (`02-testing.template.md`):**
```markdown
---
scripts:
- id: test
path: test_agent.py
---
# Testing the Agent
Set up the test fixtures:
@@code test:setup
Run the actual test:
@@code test:test-function
Script (test_agent.py):
# @block=setup
import pytest
from agent import Agent
# @end
# @block=test-function
def test_agent_responds():
agent = Agent()
result = agent.run("hello")
assert "hello" in result.lower()
# @end
Generated (02-testing.md):
# Testing the Agent
Set up the test fixtures:
```python
import pytest
from agent import Agent
Run the actual test:
def test_agent_responds():
agent = Agent()
result = agent.run("hello")
assert "hello" in result.lower()
## Development
### Run tests
```bash
uv run pytest
Create test fixtures
uv run python tests/create_fixtures.py
This creates the test notebook fixtures in tests/fixtures/notebooks/ by executing them and saving with outputs.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file codoc-0.2.0.tar.gz.
File metadata
- Download URL: codoc-0.2.0.tar.gz
- Upload date:
- Size: 246.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.13.12 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
206b87e650155ab9f641374fc01b3abf5a92b70e48fb17d412274580634b3b17
|
|
| MD5 |
6c7f1f8c574f949c1e3d260324587561
|
|
| BLAKE2b-256 |
2a3b1ec833b166e815783e6f3117734b2f3448012c2bca4f60afc86d7406aac5
|
File details
Details for the file codoc-0.2.0-py3-none-any.whl.
File metadata
- Download URL: codoc-0.2.0-py3-none-any.whl
- Upload date:
- Size: 41.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.13.12 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ee41ea879d12b7f4baeb9374cc988a8a80c463e8f10373af3d54385e03568de
|
|
| MD5 |
240a6197ef17efe2a23de0a2362a3df0
|
|
| BLAKE2b-256 |
d5c7b86bcce42189336708950cb249d188c167ce91e4aed097056c89b82535f6
|