Metadata-Version: 2.4
Name: typst_pyexec
Version: 0.0.7
Summary: Reactive Python execution engine for Typst documents
Author: typst_pyexec contributors
License: MIT
License-File: LICENSE
Keywords: jupyter,notebook,python,reactive,typst
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Text Processing :: Markup
Requires-Python: >=3.10
Requires-Dist: black>=24.0
Requires-Dist: ipykernel>=6.0
Requires-Dist: joblib>=1.3
Requires-Dist: jupyter-client>=8.0
Requires-Dist: matplotlib>=3.7
Requires-Dist: nbformat>=5.9
Requires-Dist: pandas>=2.0
Requires-Dist: watchdog>=3.0
Provides-Extra: dev
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest-timeout>=2.1; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# typst_pyexec

typst_pyexec is a reactive Python execution engine for Typst documents.

It executes Python fences inside `.typ` files, captures outputs (stdout, figures, tables), and injects rendered Typst markup into an intermediate file (`*.typst_pyexec.typ`) that can be compiled directly.

## Requirements

- Python >= 3.10
- Typst compiler for `build` and `watch` (default command `typst`)
- Optional: tinymist for faster live preview (`tinymist preview`)

## Core Capabilities

- Reactive dependency graph based on Python AST analysis
- Incremental execution with source-hash cache
- Persistent Jupyter kernel between builds with cold-kernel hydration
- Matplotlib figure export to SVG with PNG fallback
- Subfigure reconstruction via `figure(grid(...))`
- DataFrame HTML rendering to Typst `#table(...)`
- Watch mode with automatic rebuilds and live preview

## Installation

```bash
pip install typst_pyexec
```

For development:

```bash
uv sync --extra dev
```

## CLI Usage

Build once:

```bash
typst_pyexec build document.typ
```

Watch and rebuild on save:

```bash
typst_pyexec watch document.typ
```

Watch with explicit preview backend:

```bash
typst_pyexec watch document.typ --preview-engine auto
```

Clean local state directory:

```bash
typst_pyexec clean
```

Common options:

- `--output-dir <dir>`: write intermediate and state outputs to a custom folder (default: source file directory)
- `--no-cache`: disable cache reads and writes, and execute all cells
- `--jobs <n>`: reserved for future multi-kernel scheduling (`-1` default)
- `--compiler <cmd>`: Typst compiler binary name or path (default `typst`)
- `watch --preview-engine <auto|tinymist|typst|none>`: select live preview backend

### Watch mode behavior

- Watch mode regenerates `*.typst_pyexec.typ` on each save and delegates rendering to a live preview process.
- `--preview-engine auto` tries `tinymist preview` first (if `tinymist` is on PATH), then falls back to `typst watch`.
- `--preview-engine tinymist` prefers `tinymist preview`; if `tinymist` is unavailable it falls back to `typst watch`.
- `--preview-engine typst` always runs `typst watch`.
- `--preview-engine none` disables live preview and only refreshes the intermediate file.

## Block Options (`%|`)

Place options at the top of a Python fence:

````typst
```python
%| echo: false
%| raw: true
%| figure: true
%| plt-lines.linewidth: 0.8
%| plt-axes.linewidth: 0.6
print("hello")
```
````

Supported options:

- `execute` (default `true`): skip execution when `false`
- `refresh` (default `false`): force this cell to run every build
- `echo` (default `true`): show or hide source code
- `raw` (default `true`): show or hide textual runtime output (stdout, tracebacks, `text/plain` bundles)
- `figure` (default `true`): show or hide rendered figures
- `caption`: explicit figure caption override
- `label`: Typst label for cross-references
- `keep-subplots` (default `false`): preserve multi-axis plot as one image
- `keep-colorbar` / `keep-colorbars` (default `true`): keep colorbars attached in the exported figure; set to `false` to drop them
- `subfigure-caption-position` (`top` or `bottom`): position subfigure captions for subplot grids
- `img-*`: passthrough kwargs for Typst `image(...)`
- `fig-*`: passthrough kwargs for the outer/global Typst `figure(...)`
- `subfig-*`: passthrough kwargs for each generated Typst subfigure (`kind: "subfigure"`)
- `grid-*`: passthrough kwargs for Typst `grid(...)`
- `grid-columns`: explicit `columns` value for the grid (overrides inferred default)
- `plt-*`: per-cell matplotlib `rcParams` overrides (key after `plt-` maps to rcParam name)

Example for subplot grids with captions on top and separate outer/inner styling:

````typst
```python
%| keep-subplots: false
%| subfigure-caption-position: top
%| fig-supplement: "Figure"
%| fig-placement: top
%| subfig-supplement: "Figure"
%| subfig-stroke: rgb("#888")
```
````

In this configuration, `fig-*` applies only to the outer/global figure, and `subfig-*` applies to each child subfigure.

Boolean values are case-insensitive and accept: `true/false`, `yes/no`, `on/off`, `1/0`.

`plt-*` values are parsed as JSON first, then Python literals. If parsing fails, the raw string is used.

Default `plt-*` values applied to every executed cell:

- `lines.linewidth: 0.8`
- `axes.linewidth: 0.6`
- `grid.linewidth: 0.2`
- `axes.grid: true`
- `lines.markersize: 4` (scatter points are about 2x smaller in area than matplotlib default)

`plt-*` examples:

- `%| plt-lines.linewidth: 0.8`
- `%| plt-axes.linewidth: 0.6`
- `%| plt-grid.linewidth: 0.4`
- `%| plt-axes.grid: true`

Precedence order for plot style values:

1. Built-in defaults (values above)
2. `%| plt-*` metadata options
3. Any `matplotlib.rcParams[...] = ...` or `plt.rcParams.update(...)` in Python code

This means Python code always wins over `%| plt-*`, and `%| plt-*` wins over defaults.

Behavior notes:

- `cell_id` is internal and generated automatically.
- Python fences can be indented to fit paragraph or layout context; shared left padding is removed before execution.
- In generated `.typst_pyexec.typ`, that original block padding is preserved for both rendered source and rendered outputs.
- Option-only edits (including `plt-*`) do not invalidate cache because cache keys are based on Python source.
- To re-run on option updates, set `refresh: true`.

## Figure and Caption Behavior

- Matplotlib figures are exported only when explicitly shown (`plt.show()` or `fig.show()`).
- For single-axis plots, title or suptitle text is promoted into the Typst caption when `caption` is not set.
- For subplot grids, each axis title becomes a child caption; suptitle becomes the outer caption.
- Use `%| subfigure-caption-position: top` to place subplot captions above each subplot image.
- In subplot grids, `fig-*` affects only the outer/global figure, while `subfig-*` affects each child subfigure.
- Colorbars remain attached to their corresponding plot/subplot during export and are rendered together.
- Exported figures use tight bounding boxes so colorbar tick labels are not clipped when kept.
- With `keep-subplots: true`, multi-axis figures are kept as one image and subplot titles remain inside the image (the first subplot title is not promoted to caption).
- Title and suptitle text are removed from exported images only when they are promoted to captions.
- If SVG export fails and PNG is emitted, renderer auto-resolves PNG paths in final Typst output.

## How It Works

1. Parse Python fences from Typst source
2. Build def/use DAG from AST
3. Compute changed cells from source-hash cache
4. Execute required cells in topological groups
5. Capture stdout, display bundles, and figure artifacts
6. Render Typst fragments per cell
7. Inject into `document.typst_pyexec.typ`
8. Build mode: compile with Typst compiler
9. Watch mode: refresh intermediate file and stream live preview via `tinymist preview` or `typst watch`

## Caching and Execution Model

- Cache keys are SHA-256 hashes of the Python source for each cell.
- Cache entries are stored under `.typst_pyexec/cache`.
- `--no-cache` disables cache reads and writes and forces all executable cells to run.
- `refresh: true` forces a cell to execute every build but does not cascade to dependents.
- When a kernel is cold or reconnected, typst_pyexec replays prerequisite cells to rebuild namespace state.

## Local State

typst_pyexec writes runtime state into `.typst_pyexec/` inside the output directory:

- `kernel_connection.json`: reconnect data for persistent kernel
- `cache/`: JSON entries keyed by SHA-256 of cell source
- `figures/`: SVG/PNG artifacts
- `notebook.ipynb`: synchronized notebook in normal execution mode (original user cells)
- `notebook_export.ipynb`: synchronized notebook in export mode (figure capture preamble/postamble included)

### Notebook Modes

Two notebooks are intentionally generated:

- `notebook.ipynb` preserves the authored Python cells and attached outputs exactly as written (minimal overhead).
- `notebook_export.ipynb` wraps each cell with figure-export helpers that invoke optimized functions.

Both notebooks include a setup cell that initializes the environment:

- Normal mode: sets working directory for relative path consistency
- Export mode: sets working directory and imports matplotlib plus `save_figures_and_metadata`, enabling the export notebook to execute standalone

## Development Quality Gates

Run checks locally:

```bash
uv run ruff check .
uv run black --check typst_pyexec tests
uv run mypy typst_pyexec
uv run pytest
```

## CI/CD (GitHub Actions)

- `CI`: matrix on Ubuntu and Windows, Python 3.10-3.12, with lint + format + type-check + tests + coverage artifact
- `Release`: tag-driven (`v*.*.*`) build and publish workflow for PyPI using trusted publishing

Workflows are in:

- `.github/workflows/ci.yml`
- `.github/workflows/release.yml`

## Publish to PyPI

typst_pyexec is already configured for trusted publishing through GitHub Actions.

Release steps:

1. Bump version in `pyproject.toml` and `typst_pyexec/__init__.py`.
2. Commit and push to main.
3. Create and push a version tag:

```bash
git tag v0.1.1
git push origin v0.1.1
```

4. GitHub Actions runs `.github/workflows/release.yml` and publishes to PyPI.

Pre-tag checklist:

1. `python -m pytest -q` is green.
2. `python -m build` succeeds.
3. `python -m twine check dist/*` passes.
4. Version matches in `pyproject.toml` and `typst_pyexec/__init__.py`.
5. Git tag matches that version (`vX.Y.Z`).

Local preflight checks before tagging:

```bash
python -m build
python -m twine check dist/*
```

If you prefer token-based manual publishing:

```bash
python -m twine upload dist/*
```

## Use in Other Repositories

After publishing, install as a normal dependency.

With pip:

```bash
pip install typst_pyexec
```

With uv (project dependency):

```bash
uv add typst_pyexec
```

With uvx (run CLI without adding dependency):

```bash
uvx typst_pyexec build document.typ
```

## License

MIT
