Configuration¶
nbprint supports three complementary ways to configure a report:
Notebook-first: point nbprint at an existing
.ipynband drive sectioning, styling, and page layout from cell tags, cell metadata, and a small%%nbprintmagic.YAML-first: describe the full document in YAML (via
pydanticmodels, composed withlernaandomegaconf).Hybrid: start from a notebook and add YAML overlays that target cells by index, tag, section, or cell type without rewriting the notebook.
graph TB
subgraph Inputs
nb("notebook<br>(.ipynb)")
yml("YAML config<br>(.yaml)")
ovl("overlays<br>(YAML or<br>notebook metadata)")
end
nbct[/nbprint<br>template/]
pjs[/paged.js<br>layout engine/]
nbc{nbconvert}
o@{ shape: doc, label: "HTML / PDF" }
nb e1@-->nbc
yml e2@-->nbc
ovl -.->|merged at ingest| nbc
nbct --> nbc
pjs --- nbct
nbc e3@--> o
e1@{animate: true}
e2@{animate: true}
e3@{animate: true}
Notebook-First Workflow¶
Point nbprint at a .ipynb and it is treated as a full configuration input. All notebook metadata under notebook.metadata.nbprint becomes configuration; cell metadata and tags drive routing, styling, and visibility.
nbprint path/to/report.ipynb
Worked examples live in examples/:
notebook-sections.ipynb: section routing via tags and cell metadata, with page/output config in notebook metadata.notebook-runtime.ipynb: runtime API (NBPrintCell) and%%nbprintmagic.notebook-overlays.ipynb:overlaysandlayout_overlaysembedded in notebook metadata.
Section routing from cell metadata¶
Equivalent to a tag, but takes priority:
// cell.metadata.nbprint
{
"section": "frontmatter"
}
Notebook-level page and output config¶
Embed a full page/output spec directly in the notebook. No YAML is required:
// notebook.metadata.nbprint
{
"page": {
"_target_": "nbprint.PageGlobal",
"size": "Letter",
"orientation": "portrait"
},
"outputs": {
"_target_": "nbprint.NBConvertOutputs",
"target": "pdf"
}
}
YAML or CLI overrides always take precedence over notebook-embedded values, so you can use a notebook’s defaults and override on the command line.
NBPrintCell runtime API¶
Inside a running notebook you can express cell-level intent through a Python object. Metadata is persisted via a hidden display() output with a custom MIME type; nbprint picks it up at ingestion time.
from nbprint import NBPrintCell, Style, Font
NBPrintCell(section="covermatter", css=":scope { text-align: center; }")
with NBPrintCell(style=Style(font=Font(size=24))):
display(Markdown("# Cover Page"))
%%nbprint cell magic¶
A lighter alternative for quick one-liners:
%%nbprint section=frontmatter css=":scope { font-size: 18px; }"
display(Markdown("# Introduction"))
Explicit cell metadata takes precedence over runtime metadata, which takes precedence over the magic, so a hand-edited value in cell.metadata.nbprint always wins.
YAML-First Workflow¶
YAML is the most expressive way to describe a report: every field of every pydantic model is reachable, and the lerna config system makes it easy to share pieces across reports.
A minimal example:
---
debug: false
outputs:
_target_: nbprint.NBConvertOutputs
root: ./outputs
target: html
content:
- _target_: nbprint.ContentMarkdown
content: |
# A Generic Report
## A Subtitle
css: ":scope { text-align: center; }"
- _target_: nbprint.ContentPageBreak
- _target_: nbprint.ContentTableOfContents
- _target_: nbprint.ContentPageBreak
- _target_: nbprint.ContentFlexRowLayout
sizes: [1, 1]
content:
- _target_: nbprint.ContentFlexColumnLayout
content:
- _target_: nbprint.ContentMarkdown
content: "# Left column"
- _target_: nbprint.ContentFlexColumnLayout
content:
- _target_: nbprint.ContentMarkdown
content: "# Right column"
Run it:
nbprint examples/basic.yaml
Common content types include ContentMarkdown, ContentCode, ContentPageBreak, ContentTableOfContents, and the layout containers ContentFlexRowLayout / ContentFlexColumnLayout / ContentInlineLayout.
Output formats & nbconvert configuration¶
The outputs block (NBConvertOutputs) controls how the generated notebook is
executed and converted. Common fields:
Field |
Default |
Description |
|---|---|---|
|
|
Output format: |
|
|
Execute the notebook before converting |
|
|
Per-cell execution timeout (seconds) |
|
|
nbconvert template name |
Passing arbitrary nbconvert / traitlets options¶
Anything configurable on an nbconvert exporter, preprocessor, or the app itself
is reachable through nbconvert_config, so nbprint doesn’t need a field for each
option. Entries map 1:1 onto nbconvert’s --Class.trait=value CLI options
and are applied to the conversion (exporter) pass.
For example, WebPDFExporter.page_render_timeout
controls how long (in ms) the headless browser waits for JavaScript output to
settle before capturing the PDF. This helps with notebooks that have heavy
interactive charts:
outputs:
_target_: nbprint.NBConvertOutputs
target: webpdf
nbconvert_config:
# flat dotted keys ...
WebPDFExporter.page_render_timeout: 5000
# ... or nested namespaces — both are equivalent
TemplateExporter:
exclude_input: true
The nested form maps directly onto a hydra/lerna CLI override:
nbprint examples/basic.yaml \
++outputs.target=webpdf \
++outputs.nbconvert_config.WebPDFExporter.page_render_timeout=5000
Scalars pass through as-is, booleans render as True/False, and lists are
JSON-encoded so container traits round-trip through the CLI.
Traits that nbprint already manages (NbConvertApp.export_format (to),
TemplateExporter.template_name (template), NbConvertApp.output_base
(output), ExecutePreprocessor.enabled (execute), and
ExecutePreprocessor.timeout) are rejected in nbconvert_config with an error
pointing at the dedicated field (target, template, naming/root,
execute, timeout). Set those via their own fields instead.
Structured Sections¶
For book-style documents, nbprint supports 13 ordered sections grouped into 6 logical groups:
Group |
Sections |
Description |
|---|---|---|
Prematter |
|
Hidden content executed before report construction |
Covermatter |
|
Cover page content |
Frontmatter |
|
Title page, legal, dedication, TOC, preface |
Middlematter |
|
Main body and chapter dividers |
Endmatter |
|
Supplementary material, index, bibliography |
Rearmatter |
|
Back cover content |
Instead of a flat list, content: can be keyed by section name. Sections are rendered in document order; unused sections are simply empty.
content:
covermatter:
- _target_: nbprint.ContentMarkdown
content: "# My Report"
css: ":scope { text-align: center; }"
table_of_contents:
- _target_: nbprint.ContentTableOfContents
middlematter:
- _target_: nbprint.ContentMarkdown
content: "# Section One"
endmatter:
- _target_: nbprint.ContentMarkdown
content: "# Bibliography"
Flat-list content: is still supported and is treated as middlematter.
Chapters (list-of-lists middlematter)¶
For multi-chapter documents, supply middlematter as a list of lists. Each
sublist is a chapter: its first element is promoted to a chapter separator
and the remaining elements form the chapter body. nbprint interleaves them in
document order: [chapter1-title, *chapter1-body, chapter2-title, *chapter2-body, …].
content:
middlematter:
- # Chapter One — first item becomes the title page
- _target_: nbprint.ContentMarkdown
content: "# Chapter One"
- _target_: nbprint.ContentMarkdown
content: "Body of the first chapter…"
- # Chapter Two
- _target_: nbprint.ContentMarkdown
content: "# Chapter Two"
- _target_: nbprint.ContentMarkdown
content: "Body of the second chapter…"
By default each separator renders as a standalone chapter title page, with a hard page break is inserted before and after it. To keep separators inline with their chapter body instead, disable the behavior:
content:
separator_title_pages: false
middlematter:
- [...]
The promoted separators are also available directly under the
middlematter_separators section if you prefer to populate them explicitly
alongside a flat middlematter list.
See examples/chapters.yaml for a full working example.
Per-section page layout¶
Each section can carry its own page layout via PageGlobal.pages. This generates CSS @page <section> { ... } rules and maps cells to them via data-nbprint-section attributes:
page:
_target_: nbprint.PageGlobal
bottom_right:
_target_: nbprint.PageRegion
content:
_target_: nbprint.PageNumber
pages:
covermatter:
# no headers/footers on the cover
frontmatter:
counter_reset: true
counter_style: lower-roman
middlematter:
counter_reset: true
See examples/sections.yaml for a full working example.
Section-level style defaults¶
Each section can carry a default Style that every cell in that section inherits. A cell’s own Style fields take precedence; unset fields fall back to the section default.
content:
section_styles:
covermatter:
font: {size: 28}
horizontal_alignment: center
frontmatter:
font: {family: "Georgia, serif"}
covermatter:
- _target_: nbprint.ContentMarkdown
content: "# My Report"
Formatting Overlays¶
Overlays are rules that apply formatting to ingested notebook cells without modifying the notebook itself. There are three kinds.
Overlay — merge formatting onto matching cells¶
Matches cells by index, tag, cell type, or target section, and merges css / classname / attrs / style / ignore into the resulting Content:
overlays:
- match: {index: 0}
css: ":scope { text-align: center; }"
- match: {tag: "chart"}
style:
border: {bottom: {width: 2, style: solid, color: "#333"}}
- match: {cell_type: markdown}
classname: "prose-body"
- match: {section: covermatter}
style:
font: {size: 28}
Match fields combine with AND semantics, and an empty match matches every cell. Multiple overlays apply in list order; later overlays stack on top of earlier ones:
cssis appendedclassnameis appended (accumulated as a list)attrsis merged (overlay keys win on collision)styleis merged viaStyle.merge(overlay fields win)ignoreis set when provided
Overlays can also come from notebook metadata. YAML-declared overlays and notebook-declared overlays compose:
// notebook.metadata.nbprint
{
"overlays": [
{"match": {"tag": "emphasis"}, "css": ":scope { font-style: italic; }"}
]
}
LayoutOverlay — wrap contiguous cells in a flex container¶
A layout overlay matches the same way but wraps contiguous runs of matched cells (same section, consecutive notebook indices) in a ContentFlexRowLayout, ContentFlexColumnLayout, or ContentInlineLayout:
layout_overlays:
# Any two adjacent "sidebyside"-tagged cells become a row
- match: {tag: "sidebyside"}
layout: row
sizes: [1, 1]
# Cells at notebook indices 3..5 (inclusive) get stacked in a column
- index_range: [3, 5]
layout: column
# Wrapper itself can carry CSS / classname / style
- match: {tag: "gallery"}
layout: inline
css: ":scope { gap: 8px; }"
classname: gallery-wrap
Non-contiguous matches produce multiple wrappers. Both kinds of overlay can be combined: formatting overlays apply to the individual cells, and a layout overlay then wraps them.
PageBoxOverlay — wrap contiguous cells in a page box¶
Same matching rules, but the wrapper is a ContentPageBox: the matched run becomes one logical page, arranged by a named layout preset and followed by a page break. Select it with wrapper: page-box:
layout_overlays:
# Every run of "pair"-tagged cells becomes a two-column page
- match: {tag: "pair"}
wrapper: page-box
layout: columns-2
gap: 1rem
# A four-up dashboard page, in landscape
- match: {tag: "dashboard"}
wrapper: page-box
layout: grid-2x2
page_orientation: landscape
This is the multi-cell counterpart to NBPrintPage, which turns a single cell into a page box from inside the notebook. Reach for the overlay when a page is assembled from several cells.
Available fields mirror ContentPageBox: layout (any layout preset), fit, min_pages, gap, padding, align, justify, grid_template, page_size, page_orientation, page_margins, plus the wrapper formatting (css, classname, attrs, style) shared with LayoutOverlay. Overlay css is appended to the preset’s CSS rather than replacing it.
sizes is flex-only and is rejected on a page-box overlay; use a columns/grid preset, or grid_template for explicit tracks.
As with every overlay, these can live in notebook metadata instead of YAML:
// notebook.metadata.nbprint
{
"layout_overlays": [
{"match": {"tag": "pair"}, "wrapper": "page-box", "layout": "columns-2", "gap": "1rem"}
]
}
Footnotes¶
Wrap a note in <span class="nbprint-footnote"> and paged.js moves it to the foot of the page where it’s cited, leaving a superscript number in the text. Notes are numbered through the document.
In a markdown cell:
Revenue grew 12%<span class="nbprint-footnote">Unaudited, as of Q3.</span> year over year.
From a code cell, nbprint.footnote returns the same HTML, with the note text escaped:
from IPython.display import HTML
from nbprint import footnote
HTML(f"<p>Revenue grew 12%{footnote('Unaudited, as of Q3.')} year over year.</p>")
Without paged.js (pagedjs: false), the note stays in the text as a small parenthetical.
Lerna Config Composition¶
nbprint uses lerna (a Rust-powered, Hydra-compatible config framework) for YAML composition. Everything Hydra supports works identically, and lerna adds a few features that are particularly useful for reports.
Composing reports from parts¶
Pull in reusable pieces via a defaults: list:
# examples/hydra.yaml
defaults:
- config: inline # examples/hydra/config/inline.yaml
- page: report # examples/hydra/page/report.yaml
- parameters: string1 # examples/hydra/parameters/string1.yaml
- _self_
outputs:
naming: "{{name}}"
root: ./outputs
This lets a handful of reusable YAML files describe a large family of reports.
CLI list manipulation¶
lerna supports list operations directly from the CLI, which is useful for adding a single chart or section to an existing report without editing the YAML:
# Append a content item
nbprint examples/basic.yaml \
'content=append({_target_: nbprint.ContentMarkdown, content: "# Added"})'
# Prepend
nbprint report.yaml 'content=prepend({_target_: nbprint.ContentPageBreak})'
# Insert at a specific index
nbprint report.yaml 'content=insert(0, {_target_: nbprint.ContentMarkdown, content: "Intro"})'
# Remove by index / value / clear
nbprint report.yaml 'content=remove_at(-1)'
nbprint report.yaml 'tags=remove_value(draft)'
nbprint report.yaml 'overlays=list_clear()'
Operation |
Effect |
|---|---|
|
Add item(s) to end |
|
Add item(s) to beginning |
|
Insert at index |
|
Remove by index (negative allowed) |
|
Remove first matching item |
|
Clear all items |
Quote the whole override for shell safety. Works in bash, zsh, fish, PowerShell, and cmd.
Sweeps and multirun¶
Generate a family of reports from one command:
nbprint report.yaml -m \
parameters.model=choice(ridge,lasso,elasticnet) \
parameters.alpha=interval(0.01,0.5)
Combined with outputs.naming: "{{name}}-{{parameters.model}}-{{parameters.alpha}}" this produces one cleanly named HTML/PDF per combination.
_patch_ — modify composed configs before CLI overrides¶
Pull in a vendored config and modify it:
defaults:
- vendor/large_defaults
- _self_
- _patch_:
- ~unwanted_key # delete a key
- content=remove_value(stale) # drop a content item
- +overlays=append({match: {tag: draft}, ignore: true}) # add an overlay
Patches apply after composition but before CLI overrides, so CLI still wins. See the lerna README for the full _patch_ reference (scoped _patch_@package, _here_ / _global_ prefixes, nested accumulation, etc.).
Default overrides at the call site¶
nbprint’s CLI accepts default overrides without editing YAML:
# ++callable switches to the multirun model, nbprintx.parameters takes inline
# JSON or a file path, and execute=False skips execution
nbprint examples/basic.ipynb \
++callable=/nbprintx \
+nbprint.outputs.naming='{{name}}-{{date}}-{{a}}' \
+nbprintx.parameters=[{a: 1},{a: 2},{a: 3}] \
++nbprint.outputs.execute=False
Running a Report¶
nbprint examples/basic.yaml # YAML-first
nbprint path/to/report.ipynb # notebook-first
nbprint report.yaml notebook=foo.ipynb # hybrid: YAML + notebook
The output lands in the directory set by outputs.root (default ./outputs), named per outputs.naming (default {{name}}-{{date}}). Both the generated notebook and the chosen render target (html, pdf, …) are written; the notebook is useful for inspection and further experimentation.
You can also download a rendered PDF of the basic example. The gallery has rendered HTML for every example.
ccflow Integration¶
nbprint is compatible with ccflow callable models. nbprint outputs like NBConvertOutputs inherit from ccflow.ResultsBase, and parameters inherit from ccflow.ContextBase.
nbprint-cc +nbprint.name=test +context=[]
ccflow workflows are exposed as multi-run models:
nbprint examples/basic.ipynb \
++callable=/nbprintx \
+nbprint.outputs.naming='{{name}}-{{date}}-{{a}}' \
+nbprintx.parameters=[{a: 1},{a: 2},{a: 3}] \
++nbprint.outputs.execute=False
Precedence Summary¶
When the same piece of configuration can come from multiple places, resolution order is:
CLI overrides (always highest priority)
YAML explicit values (
config.yaml,_self_)YAML
_patch_directives (applied during composition)Notebook-level metadata (
notebook.metadata.nbprint.{page,outputs,overlays,layout_overlays})Defaults from composed configs and pydantic model defaults
For section assignment specifically:
cell.metadata.nbprint.sectionnbprint:section:<name>cell tagNBPrintCell(section=...)MIME metadata%%nbprint section=...magicDefault:
middlematter