From 5305ac3c92e917e78a692f0a6c46498ee178f593 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 25 Aug 2026 13:50:54 +1000 Subject: [PATCH] Make execnb an optional `fill` extra --- README.md | 2 +- pyproject.toml | 5 +- python/mdhtml/fill.py | 14 +++-- tests/source/cmark-gfm/spec.txt | 54 +++++++++---------- .../php-markdown-extra.mdtest/Abbr.text | 6 +-- .../Definition Lists.text | 6 +-- .../php-markdown-extra.mdtest/Footnotes.text | 4 +- .../Headers with attributes.text | 6 +-- .../Headers with attributes.xhtml | 16 +++--- .../php-markdown-extra.mdtest/Tables.text | 8 +-- tests/test_fill.py | 6 +++ 11 files changed, 70 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 517a320..d1db7df 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ viewmd examples/sample.md --head examples/sample.css --head examples/sample.js viewmd examples/nbsample.ipynb ``` -`fillmd` instantiates a template from the command line: it executes the template's `{python}` blocks, fills tokens from frontmatter `formdata:` plus an optional YAML values file (scalars stay strings), and writes the filled Markdown to stdout or `--out`. `--lenient` defers unresolved tokens with warnings instead of raising, for staged fills. +`fillmd` instantiates a template from the command line: it executes the template's `{python}` blocks, fills tokens from frontmatter `formdata:` plus an optional YAML values file (scalars stay strings), and writes the filled Markdown to stdout or `--out`. `--lenient` defers unresolved tokens with warnings instead of raising, for staged fills. Executing code needs `execnb`, which the `fill` extra installs (`pip install 'mdhtml[fill]'`); everything else works without it. A dialog or notebook `.ipynb` works as the template too. Its code cells are the executable blocks (`eval: false` cells are skipped), each cell's rendered outputs weave in as prose in place of the source, messages participate per aidialog's `export_filter` rule (every exported message when any exist, otherwise every non-pinned one), and a leading frontmatter message is consumed for `formdata:` rather than emitted. diff --git a/pyproject.toml b/pyproject.toml index 282c7b0..ffd59b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "A bounded-time, Pandoc-leaning Markdown parser with GFM, Extra/kramdown, math, fenced divs, and MDHTML output." license = {text = "Apache-2.0"} requires-python = ">=3.11" -dependencies = ["fast5ever>=0.1.1", "fastcore>=2.2.7", "aidialog>=0.0.22", "pyyaml>=6.0.3", "execnb>=0.3.2"] +dependencies = ["fast5ever>=0.1.1", "fastcore>=2.2.7", "aidialog>=0.0.22", "pyyaml>=6.0.3"] readme = "README.md" authors = [{name = "Jeremy Howard", email = "j@fast.ai"}] classifiers = [ @@ -28,7 +28,8 @@ Repository = "https://github.com/AnswerDotAI/mdhtml" Issues = "https://github.com/AnswerDotAI/mdhtml/issues" [project.optional-dependencies] -dev = ["fastship>=0.0.14", "maturin~=1.0", "pytest", "fastpylight>=0.1.6", "math-core~=0.7.0"] +fill = ["execnb>=0.3.2"] +dev = ["fastship>=0.0.14", "maturin~=1.0", "pytest", "execnb>=0.3.2", "fastpylight>=0.1.6", "math-core~=0.7.0"] [project.entry-points.fastaudit_safe_native] mdhtml = "mdhtml" diff --git a/python/mdhtml/fill.py b/python/mdhtml/fill.py index b7c7f86..9452331 100644 --- a/python/mdhtml/fill.py +++ b/python/mdhtml/fill.py @@ -11,7 +11,7 @@ text; `instantiate` adds data gathering (frontmatter `formdata:` via `fastcore.xtras.frontmatter` with `strvals=True`: structure kept, every scalar a `str` except `true`/`True`/`false`/`False`, which are `bool`) and the one execution point for -`{python}` blocks (an `execnb` shell: IPython last-expression semantics, `_repr_markdown_` +`{python}` blocks (an `execnb` shell, from the `fill` extra: IPython last-expression semantics, `_repr_markdown_` preferred over `str()`, stdout discarded). Dialog templates (`instantiate_nb`) run code opt-in: only cells marked `#| eval: true` participate (all but `eval: false` cells when the dialog's own frontmatter says `eval: true`), and a cell that doesn't participate contributes nothing to the @@ -29,7 +29,6 @@ from fastcore.script import call_parse from fastcore.xtras import frontmatter, strloader from fastcore.nbio import nb_frontmatter, cell_frontmatter -from execnb.shell import CaptureShell from aidialog.dialog import dlg2md from aidialog.ipynb import read_ipynb from fast5ever import parse_fragment @@ -271,11 +270,18 @@ def frontmatter_data(src): +def _capture_shell(): + "A `CaptureShell`, imported lazily: a bare install carries no execnb or IPython (the `fill` extra provides them)." + try: from execnb.shell import CaptureShell + except ImportError as e: raise ImportError("executing code needs execnb: pip install 'mdhtml[fill]'") from e + return CaptureShell() + + def _weave(norm, data, tmpls): "Execute `{python}` blocks once each, in document order, in one shared `execnb` shell, and splice each block's rendered output: what a notebook's output area shows (`CaptureShell.run_text`). Blocks may read and mutate `__data__` (the live values dict); rebinding it is ignored." spans = [b for b in _blocks(norm, templates=tmpls) if b["type"] == "code_block" and b.get("info") == "{python}"] if not spans: return norm - shell = CaptureShell() + shell = _capture_shell() shell.user_ns["__data__"] = data starts = _line_starts(norm) out, cur = [], 0 @@ -323,7 +329,7 @@ def instantiate_nb( d = read_ipynb(fname) fd = nb_frontmatter(d, strvals=True).get("formdata") merged = {**(fd if isinstance(fd, dict) else {}), **(data or {})} - shell = CaptureShell() + shell = _capture_shell() shell.user_ns["__data__"] = merged ran = d.execute(default_eval=False, shell=shell) if shell.exc: diff --git a/tests/source/cmark-gfm/spec.txt b/tests/source/cmark-gfm/spec.txt index 4dda0b7..922010e 100644 --- a/tests/source/cmark-gfm/spec.txt +++ b/tests/source/cmark-gfm/spec.txt @@ -482,7 +482,7 @@ bar

Foo

```````````````````````````````` -```````````````````````````````` example +```````````````````````````````` example disabled *→*→*→ .
@@ -548,7 +548,7 @@ of three or more matching `-`, `_`, or `*` characters, each followed optionally by any number of spaces or tabs, forms a [thematic break](@). -```````````````````````````````` example +```````````````````````````````` example disabled *** --- ___ @@ -590,7 +590,7 @@ __

One to three spaces indent are allowed: -```````````````````````````````` example +```````````````````````````````` example disabled *** *** *** @@ -622,7 +622,7 @@ Foo More than three characters may be used: -```````````````````````````````` example +```````````````````````````````` example disabled _____________________________________ .
@@ -631,21 +631,21 @@ _____________________________________ Spaces are allowed between the characters: -```````````````````````````````` example +```````````````````````````````` example disabled - - - .
```````````````````````````````` -```````````````````````````````` example +```````````````````````````````` example disabled ** * ** * ** * ** .
```````````````````````````````` -```````````````````````````````` example +```````````````````````````````` example disabled - - - - .
@@ -654,7 +654,7 @@ Spaces are allowed between the characters: Spaces are allowed at the end: -```````````````````````````````` example +```````````````````````````````` example disabled - - - - .
@@ -688,7 +688,7 @@ So, this is not a thematic break: Thematic breaks do not need blank lines before or after: -```````````````````````````````` example +```````````````````````````````` example disabled - foo *** - bar @@ -705,7 +705,7 @@ Thematic breaks do not need blank lines before or after: Thematic breaks can interrupt a paragraph: -```````````````````````````````` example +```````````````````````````````` example disabled Foo *** bar @@ -738,7 +738,7 @@ bar When both a thematic break and a list item are possible interpretations of a line, the thematic break takes precedence: -```````````````````````````````` example +```````````````````````````````` example disabled * Foo * * * * Bar @@ -755,7 +755,7 @@ interpretations of a line, the thematic break takes precedence: If you want a thematic break in a list item, use a different bullet: -```````````````````````````````` example +```````````````````````````````` example disabled - Foo - * * * . @@ -955,7 +955,7 @@ of the closing sequence: ATX headings need not be separated from surrounding content by blank lines, and they can interrupt paragraphs: -```````````````````````````````` example +```````````````````````````````` example disabled **** ## foo **** @@ -1021,7 +1021,7 @@ Simple examples: Dialect deviation: no setext headings - a `---` underline is a thematic break and an `===` underline is paragraph text. -```````````````````````````````` example +```````````````````````````````` example disabled Foo *bar* ========= @@ -1065,7 +1065,7 @@ baz→ The underlining can be any length: -```````````````````````````````` example +```````````````````````````````` example disabled Foo ------------------------- @@ -1082,7 +1082,7 @@ Foo The heading content can be indented up to three spaces, and need not line up with the underlining: -```````````````````````````````` example +```````````````````````````````` example disabled Foo --- @@ -1122,7 +1122,7 @@ Foo The setext heading underline can be indented up to three spaces, and may have trailing spaces: -```````````````````````````````` example +```````````````````````````````` example disabled Foo ---- . @@ -1144,7 +1144,7 @@ Foo The setext heading underline cannot contain internal spaces: -```````````````````````````````` example +```````````````````````````````` example disabled Foo = = @@ -1160,7 +1160,7 @@ Foo Trailing spaces in the content line do not cause a line break: -```````````````````````````````` example +```````````````````````````````` example disabled Foo ----- . @@ -1171,7 +1171,7 @@ Foo Nor does a backslash at the end: -```````````````````````````````` example +```````````````````````````````` example disabled Foo\ ---- . @@ -1183,7 +1183,7 @@ Foo\ Since indicators of block structure take precedence over indicators of inline structure, the following are setext headings: -```````````````````````````````` example +```````````````````````````````` example disabled `Foo ---- ` @@ -1297,7 +1297,7 @@ in these examples gets interpreted as a thematic break: ```````````````````````````````` -```````````````````````````````` example +```````````````````````````````` example disabled - foo ----- . @@ -1318,7 +1318,7 @@ in these examples gets interpreted as a thematic break: ```````````````````````````````` -```````````````````````````````` example +```````````````````````````````` example disabled > foo ----- . @@ -1332,7 +1332,7 @@ in these examples gets interpreted as a thematic break: If you want a heading with `> foo` as its literal text, you can use backslash escapes: -```````````````````````````````` example +```````````````````````````````` example disabled \> foo ------ . @@ -1399,7 +1399,7 @@ bar

or use a thematic break that cannot count as a [setext heading underline], such as -```````````````````````````````` example +```````````````````````````````` example disabled Foo bar * * * @@ -1571,7 +1571,7 @@ blocks: Dialect deviation: no setext headings - a `---` underline is a thematic break and an `===` underline is paragraph text. -```````````````````````````````` example +```````````````````````````````` example disabled # Heading foo Heading @@ -3881,7 +3881,7 @@ foo In general, blank lines are not needed before or after block quotes: -```````````````````````````````` example +```````````````````````````````` example disabled > aaa *** > bbb diff --git a/tests/source/php-markdown-extra.mdtest/Abbr.text b/tests/source/php-markdown-extra.mdtest/Abbr.text index ae72f4e..3644199 100644 --- a/tests/source/php-markdown-extra.mdtest/Abbr.text +++ b/tests/source/php-markdown-extra.mdtest/Abbr.text @@ -18,14 +18,14 @@ Let's transfert documents through TCP/IP, using TCP packets. *[IP]: Internet Protocol *[TCP]: Transmission Control Protocol - --- +--- Bienvenue sur [CMS](http://www.bidulecms.com "Bidule CMS"). *[CMS]: Content Management System - --- +--- ATCCE -*[ATCCE]: Abbreviation "Testing" Correct 'Character' < Escapes > \ No newline at end of file +*[ATCCE]: Abbreviation "Testing" Correct 'Character' < Escapes > diff --git a/tests/source/php-markdown-extra.mdtest/Definition Lists.text b/tests/source/php-markdown-extra.mdtest/Definition Lists.text index 5b3bdb6..aa2cafb 100644 --- a/tests/source/php-markdown-extra.mdtest/Definition Lists.text +++ b/tests/source/php-markdown-extra.mdtest/Definition Lists.text @@ -68,7 +68,7 @@ Definition 1 paragraph 1 line 2 (lazy) Definition 1 paragraph 2 line 1 ... Definition 1 paragraph 2 line 2 (lazy) -* * * +--- A mix: @@ -105,11 +105,11 @@ Term 4 Definition 9 paragraph 2 line 1 : Definition 10 (no paragraph) -* * * +--- Special cases: Term : code block - as first element of a definition \ No newline at end of file + as first element of a definition diff --git a/tests/source/php-markdown-extra.mdtest/Footnotes.text b/tests/source/php-markdown-extra.mdtest/Footnotes.text index 95b5ea8..6b66a62 100644 --- a/tests/source/php-markdown-extra.mdtest/Footnotes.text +++ b/tests/source/php-markdown-extra.mdtest/Footnotes.text @@ -53,13 +53,13 @@ footnote test[^reference]. from another footnote. But [^reference] should be litteral since the footnote with that name has already been used. - - - - +--- Testing unusual footnote name[^1$^!"']. [^1$^!"']: Haha! - - - - +--- Footnotes mixed with images[^image-mixed] ![1800 Travel][img6] diff --git a/tests/source/php-markdown-extra.mdtest/Headers with attributes.text b/tests/source/php-markdown-extra.mdtest/Headers with attributes.text index 4242136..0a45d98 100644 --- a/tests/source/php-markdown-extra.mdtest/Headers with attributes.text +++ b/tests/source/php-markdown-extra.mdtest/Headers with attributes.text @@ -7,7 +7,7 @@ Header { #id2} ### Header {#id3 } #### Header ## { #id4 } - - - - +--- Header {.cl} ====== @@ -18,7 +18,7 @@ Header { .cl} ### Header {.cl } #### Header ## { .cl } - - - - +--- Header {.cl.class} ====== @@ -29,7 +29,7 @@ Header { .cl .class} ### Header {.cl .class } #### Header ## { .cl.class } - - - - +--- Header {#id5.cl.class} ====== diff --git a/tests/source/php-markdown-extra.mdtest/Headers with attributes.xhtml b/tests/source/php-markdown-extra.mdtest/Headers with attributes.xhtml index 9b51e65..b90d1d8 100644 --- a/tests/source/php-markdown-extra.mdtest/Headers with attributes.xhtml +++ b/tests/source/php-markdown-extra.mdtest/Headers with attributes.xhtml @@ -1,27 +1,27 @@

Header {#id1} ======

-

Header { #id2}

-
+

Header { #id2} +------

Header

Header


Header {.cl} ======

-

Header { .cl}

-
+

Header { .cl} +------

Header

Header


Header {.cl.class} ======

-

Header { .cl .class}

-
+

Header { .cl .class} +------

Header

Header


Header {#id5.cl.class} ======

-

Header { #id6 .cl .class}

-
+

Header { #id6 .cl .class} +------

Header

Header

diff --git a/tests/source/php-markdown-extra.mdtest/Tables.text b/tests/source/php-markdown-extra.mdtest/Tables.text index 720fb84..713cd89 100644 --- a/tests/source/php-markdown-extra.mdtest/Tables.text +++ b/tests/source/php-markdown-extra.mdtest/Tables.text @@ -26,7 +26,7 @@ With leading and tailing pipes: | Cell 1 | Cell 2 | | Cell 3 | Cell 4 | -* * * +--- # One-column one-row table @@ -48,7 +48,7 @@ With leading and tailing pipes: | ------- | | Cell | -* * * +--- Table alignement: @@ -64,7 +64,7 @@ Table alignement (alternate spacing): | Long Cell | Long Cell | Long Cell | Long Cell | | Cell | Cell | Cell | Cell | -* * * +--- # Empty cells @@ -78,7 +78,7 @@ Header 1 | Header 2 A | B | D -* * * +--- # Missing tailing pipe diff --git a/tests/test_fill.py b/tests/test_fill.py index 221744a..d4356b9 100644 --- a/tests/test_fill.py +++ b/tests/test_fill.py @@ -214,3 +214,9 @@ def test_instantiate_nb_error(tmp_path): p = mk_dlg(tmp_path, [bad]) with pytest.raises(ZeroDivisionError) as ei: instantiate_nb(p) assert any(bad.id in n for n in ei.value.__notes__) + + +def test_bare_import_skips_execnb(): + import subprocess, sys + code = "import sys, mdhtml; assert 'execnb' not in sys.modules; assert 'IPython' not in sys.modules" + subprocess.run([sys.executable, '-c', code], check=True)