From b66ee1002f8d220efd9a954a78933047889a4d6b Mon Sep 17 00:00:00 2001 From: Rens Date: Mon, 18 May 2026 09:39:42 +0200 Subject: [PATCH 1/3] add md export for module docstring --- nbdev/export.py | 2 +- nbdev/maker.py | 7 ++- nbs/api/02_maker.ipynb | 122 +++++++++++++++++++++++++++++++------- nbs/api/04_export.ipynb | 56 +++++++++++++---- tests/00_some.thing.ipynb | 27 +++++++-- 5 files changed, 171 insertions(+), 43 deletions(-) diff --git a/nbdev/export.py b/nbdev/export.py index f7fc6e7bd..94acee1cf 100644 --- a/nbdev/export.py +++ b/nbdev/export.py @@ -30,7 +30,7 @@ def _export_(self, cell, exp_to=None): def __call__(self, cell): src = cell.source if not src: return - if cell.cell_type=='markdown' and src.startswith('# '): self.modules['#'].append(cell) + if cell.cell_type=='markdown' and (src.startswith('# ') or 'export' in cell.directives_): self._exporti_(cell) _exports_=_export_ # %% ../nbs/api/04_export.ipynb #76717e36 diff --git a/nbdev/maker.py b/nbdev/maker.py index 78e886210..7e395ed87 100644 --- a/nbdev/maker.py +++ b/nbdev/maker.py @@ -175,14 +175,15 @@ def _import2relative(cells, lib_path=None): # %% ../nbs/api/02_maker.ipynb #5bff9d71 def _retr_mdoc(cells): - "Search for md meta quote lines, used to create module docstring" + "Search for markdown cells used to create module docstring" md1 = first(o for o in cells if o.cell_type=='markdown' and o.source.startswith('# ')) if not md1: return '' lines = dropwhile(lambda l: not l.startswith('> '), md1.source.splitlines()) lines = list(takewhile(lambda l: l.startswith('> '), lines)) - if not lines: return '' summ = '\n'.join(l.lstrip('> ').strip() for l in lines) - return f'"""{summ}"""\n\n' if summ else '' + docs = L(o.source.strip() for o in cells if o.cell_type=='markdown' and 'export' in getattr(o,'directives_',{})) + mdoc = '\n\n'.join(L(summ)+docs).strip() + return f'"""{mdoc}"""\n\n' if mdoc else '' # %% ../nbs/api/02_maker.ipynb #cdd205d6 @patch diff --git a/nbs/api/02_maker.ipynb b/nbs/api/02_maker.ipynb index a32f178a2..ff45bff13 100644 --- a/nbs/api/02_maker.ipynb +++ b/nbs/api/02_maker.ipynb @@ -491,14 +491,15 @@ "source": [ "#| export\n", "def _retr_mdoc(cells):\n", - " \"Search for md meta quote lines, used to create module docstring\"\n", + " \"Search for markdown cells used to create module docstring\"\n", " md1 = first(o for o in cells if o.cell_type=='markdown' and o.source.startswith('# '))\n", " if not md1: return ''\n", " lines = dropwhile(lambda l: not l.startswith('> '), md1.source.splitlines())\n", " lines = list(takewhile(lambda l: l.startswith('> '), lines))\n", - " if not lines: return ''\n", " summ = '\\n'.join(l.lstrip('> ').strip() for l in lines)\n", - " return f'\"\"\"{summ}\"\"\"\\n\\n' if summ else ''" + " docs = L(o.source.strip() for o in cells if o.cell_type=='markdown' and 'export' in getattr(o,'directives_',{}))\n", + " mdoc = '\\n\\n'.join(L(summ)+docs).strip()\n", + " return f'\"\"\"{mdoc}\"\"\"\\n\\n' if mdoc else ''" ] }, { @@ -510,7 +511,10 @@ "source": [ "#| hide\n", "nb = read_nb('02_maker.ipynb')\n", - "test_eq(_retr_mdoc(nb.cells), '\"\"\"Create one or more modules from selected notebook cells\"\"\"\\n\\n')" + "test_eq(_retr_mdoc(nb.cells), '\"\"\"Create one or more modules from selected notebook cells\"\"\"\\n\\n')\n", + "nb.cells.append(mk_cell('Extra module docs', 'markdown'))\n", + "nb.cells[-1].directives_ = {'export': ''}\n", + "test_eq(_retr_mdoc(nb.cells), '\"\"\"Create one or more modules from selected notebook cells\\n\\nExtra module docs\"\"\"\\n\\n')\n" ] }, { @@ -557,26 +561,46 @@ { "data": { "text/markdown": [ + "
\n", + "\n", "```python\n", "# AUTOGENERATED! DO NOT EDIT! File to edit: ../../04_export.ipynb.\n", "\n", - "# %% ../../04_export.ipynb #73b66309\n", + "# %% ../../04_export.ipynb #dec4cd88\n", "from __future__ import print_function\n", "\n", "# %% auto #0\n", "__all__ = ['b']\n", "\n", - "# %% ../../04_export.ipynb #c168485a\n", + "# %% ../../04_export.ipynb #b2a73a89\n", "#| export\n", "def a(): ...\n", "\n", - "# %% ../../04_export.ipynb #94c4db18\n", + "# %% ../../04_export.ipynb #470e0aea\n", "def b(): ...\n", "\n", - "```" + "```\n", + "\n", + "
" ], "text/plain": [ - "" + "Markdown(```python\n", + "# AUTOGENERATED! DO NOT EDIT! File to edit: ../../04_export.ipynb.\n", + "\n", + "# %% ../../04_export.ipynb #dec4cd88\n", + "from __future__ import print_function\n", + "\n", + "# %% auto #0\n", + "__all__ = ['b']\n", + "\n", + "# %% ../../04_export.ipynb #b2a73a89\n", + "#| export\n", + "def a(): ...\n", + "\n", + "# %% ../../04_export.ipynb #470e0aea\n", + "def b(): ...\n", + "\n", + "```)" ] }, "execution_count": null, @@ -637,24 +661,42 @@ { "data": { "text/markdown": [ + "
\n", + "\n", "```python\n", "# AUTOGENERATED! DO NOT EDIT! File to edit: ../../01_export.ipynb.\n", "\n", - "# %% ../../01_export.ipynb #cfd87ddf\n", + "# %% ../../01_export.ipynb #65e15f2e\n", "from __future__ import print_function\n", "\n", - "# %% ../../01_export.ipynb #23244dea\n", + "# %% ../../01_export.ipynb #1e9ea7e8\n", "#| export\n", "def a(): ...\n", "\n", - "# %% ../../01_export.ipynb #c710d552\n", + "# %% ../../01_export.ipynb #d73c328f\n", "#| export\n", "class A:\n", "\n", - "```" + "```\n", + "\n", + "
" ], "text/plain": [ - "" + "Markdown(```python\n", + "# AUTOGENERATED! DO NOT EDIT! File to edit: ../../01_export.ipynb.\n", + "\n", + "# %% ../../01_export.ipynb #65e15f2e\n", + "from __future__ import print_function\n", + "\n", + "# %% ../../01_export.ipynb #1e9ea7e8\n", + "#| export\n", + "def a(): ...\n", + "\n", + "# %% ../../01_export.ipynb #d73c328f\n", + "#| export\n", + "class A:\n", + "\n", + "```)" ] }, "execution_count": null, @@ -717,31 +759,56 @@ { "data": { "text/markdown": [ + "
\n", + "\n", "```python\n", "# AUTOGENERATED! DO NOT EDIT! File to edit: ../../04_export.ipynb.\n", "\n", - "# %% ../../04_export.ipynb #73b66309\n", + "# %% ../../04_export.ipynb #dec4cd88\n", "from __future__ import print_function\n", "\n", "# %% auto #0\n", "__all__ = ['b', 'c', 'd']\n", "\n", - "# %% ../../04_export.ipynb #c168485a\n", + "# %% ../../04_export.ipynb #b2a73a89\n", "#| export\n", "def a(): ...\n", "\n", - "# %% ../../04_export.ipynb #94c4db18\n", + "# %% ../../04_export.ipynb #470e0aea\n", "def b(): ...\n", "\n", - "# %% ../../04_export.ipynb #e53b62a7\n", + "# %% ../../04_export.ipynb #4e1e1d2e\n", "def c(): ...\n", "\n", - "# %% ../../04_export.ipynb #90b96fb2\n", + "# %% ../../04_export.ipynb #7199ab8d\n", "def d(): ...\n", - "```" + "```\n", + "\n", + "
" ], "text/plain": [ - "" + "Markdown(```python\n", + "# AUTOGENERATED! DO NOT EDIT! File to edit: ../../04_export.ipynb.\n", + "\n", + "# %% ../../04_export.ipynb #dec4cd88\n", + "from __future__ import print_function\n", + "\n", + "# %% auto #0\n", + "__all__ = ['b', 'c', 'd']\n", + "\n", + "# %% ../../04_export.ipynb #b2a73a89\n", + "#| export\n", + "def a(): ...\n", + "\n", + "# %% ../../04_export.ipynb #470e0aea\n", + "def b(): ...\n", + "\n", + "# %% ../../04_export.ipynb #4e1e1d2e\n", + "def c(): ...\n", + "\n", + "# %% ../../04_export.ipynb #7199ab8d\n", + "def d(): ...\n", + "```)" ] }, "execution_count": null, @@ -761,7 +828,9 @@ "outputs": [], "source": [ "try:\n", + " sys.path.append('')\n", " g = exec_import('tmp.test.testing', '*')\n", + " sys.path.pop()\n", " for s in \"b c d\".split(): assert s in g, s\n", " assert 'a' not in g\n", " assert g['b']() is None\n", @@ -816,7 +885,16 @@ ] } ], - "metadata": {}, + "metadata": { + "solveit": { + "default_code": true, + "mode": "learning", + "use_fence": false, + "use_thinking": true, + "use_tools": true, + "ver": 2 + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/nbs/api/04_export.ipynb b/nbs/api/04_export.ipynb index cd79c50ce..6c253e8c1 100644 --- a/nbs/api/04_export.ipynb +++ b/nbs/api/04_export.ipynb @@ -54,7 +54,7 @@ "from pdb import set_trace\n", "from importlib import reload\n", "from fastcore import shutil\n", - "from fastcore.nbio import read_nb" + "from fastcore.nbio import *" ] }, { @@ -76,7 +76,7 @@ " def __call__(self, cell):\n", " src = cell.source\n", " if not src: return\n", - " if cell.cell_type=='markdown' and src.startswith('# '): self.modules['#'].append(cell)\n", + " if cell.cell_type=='markdown' and (src.startswith('# ') or 'export' in cell.directives_): self._exporti_(cell)\n", " _exports_=_export_" ] }, @@ -107,6 +107,33 @@ "assert 'h_n' in exp.in_all['some.thing'][0].source" ] }, + { + "cell_type": "markdown", + "id": "9e635bb9", + "metadata": {}, + "source": [ + "Markdown title cells and `#| export` markdown cells are collected into the module so `ModuleMaker` can build the module docstring.\n", + "They must not be added to `in_all`, since `__all__` generation only applies to Python symbols from code cells." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddb7e224", + "metadata": {}, + "outputs": [], + "source": [ + "nb = dict2nb({'cells':[\n", + " mk_cell('#| default_exp tmp_doc'),\n", + " mk_cell('# Test module\\n> Short summary', 'markdown'),\n", + " mk_cell('#| export\\nExtra docs', 'markdown'),\n", + " mk_cell('#| export\\ndef f(): return 1')]})\n", + "exp = ExportModuleProc(); NBProcessor(nb=nb, procs=exp).process()\n", + "test_eq([(c.cell_type, c.source.splitlines()[0]) for c in exp.modules['#']], \n", + " [('markdown', '# Test module'), ('markdown', 'Extra docs'), ('code', 'def f(): return 1')])\n", + "test_eq([c.cell_type for c in exp.in_all['#']], ['code'])" + ] + }, { "cell_type": "markdown", "id": "94eb949b", @@ -168,9 +195,13 @@ "shutil.rmtree('tmp', ignore_errors=True)\n", "nb_export('../../tests/00_some.thing.ipynb', 'tmp')\n", "\n", + "sys.path.append('')\n", "g = exec_new('import tmp.some.thing')\n", + "sys.path.pop()\n", "test_eq(g['tmp'].some.thing.__all__, ['a'])\n", - "test_eq(g['tmp'].some.thing.a, 1)" + "test_eq(g['tmp'].some.thing.a, 1)\n", + "test_eq(g['tmp'].some.thing.__doc__, \n", + "\"Test module some.thing\\n\\nThis notebook is used to demonstrate exporting to an existing module. See the notebooks in `nbs` for how it's used.\")\n" ] }, { @@ -242,17 +273,18 @@ "g = exec_new('import nbdev.export')\n", "assert hasattr(g['nbdev'].export, 'nb_export')" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "184b448f", - "metadata": {}, - "outputs": [], - "source": [] } ], - "metadata": {}, + "metadata": { + "solveit": { + "default_code": false, + "mode": "learning", + "use_fence": false, + "use_thinking": false, + "use_tools": false, + "ver": 2 + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/tests/00_some.thing.ipynb b/tests/00_some.thing.ipynb index a7963e64d..1bb86040b 100644 --- a/tests/00_some.thing.ipynb +++ b/tests/00_some.thing.ipynb @@ -2,14 +2,27 @@ "cells": [ { "cell_type": "markdown", + "id": "f88a8d95", "metadata": {}, "source": [ + "# Test Module some.thing\n", + "\n", + "> Test module some.thing" + ] + }, + { + "cell_type": "markdown", + "id": "21cd9596", + "metadata": {}, + "source": [ + "#| export\n", "This notebook is used to demonstrate exporting to an existing module. See the notebooks in `nbs` for how it's used." ] }, { "cell_type": "code", "execution_count": null, + "id": "f23727ae", "metadata": {}, "outputs": [], "source": [ @@ -20,6 +33,7 @@ { "cell_type": "code", "execution_count": null, + "id": "581f3271", "metadata": {}, "outputs": [], "source": [ @@ -29,12 +43,15 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" + "solveit": { + "default_code": true, + "mode": "learning", + "use_fence": false, + "use_thinking": false, + "use_tools": true, + "ver": 2 } }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } From 11df0c4ee91dfad313434e4458f7e24be7ae4756 Mon Sep 17 00:00:00 2001 From: Rens Date: Mon, 18 May 2026 10:14:52 +0200 Subject: [PATCH 2/3] show exported markdown cells in docs --- nbdev/processors.py | 5 +++-- nbs/api/10_processors.ipynb | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/nbdev/processors.py b/nbdev/processors.py index cff43200b..539732edf 100644 --- a/nbdev/processors.py +++ b/nbdev/processors.py @@ -198,8 +198,9 @@ def rm_header_dash(cell): _hide_dirs = {'export','exporti', 'hide','default_exp'} def rm_export(cell): - "Remove cells that are exported or hidden" - if cell.directives_ and (cell.directives_.keys() & _hide_dirs): del(cell['source']) + "Remove code cells that are exported or hidden" + if cell.cell_type=='code' and cell.directives_ and (cell.directives_.keys() & _hide_dirs): del(cell['source']) + # %% ../nbs/api/10_processors.ipynb #2d9a0a30 _re_showdoc = re.compile(r'^show_doc', re.MULTILINE) diff --git a/nbs/api/10_processors.ipynb b/nbs/api/10_processors.ipynb index 9bdf6e85b..56326abc0 100644 --- a/nbs/api/10_processors.ipynb +++ b/nbs/api/10_processors.ipynb @@ -633,8 +633,8 @@ "_hide_dirs = {'export','exporti', 'hide','default_exp'}\n", "\n", "def rm_export(cell):\n", - " \"Remove cells that are exported or hidden\"\n", - " if cell.directives_ and (cell.directives_.keys() & _hide_dirs): del(cell['source'])" + " \"Remove code cells that are exported or hidden\"\n", + " if cell.cell_type=='code' and cell.directives_ and (cell.directives_.keys() & _hide_dirs): del(cell['source'])\n" ] }, { From 08ade47436fe9373390c44b66f17bd65fb24bcf3 Mon Sep 17 00:00:00 2001 From: Rens Date: Tue, 19 May 2026 06:25:10 +0200 Subject: [PATCH 3/3] fix stripping of module docstring components --- nbdev/maker.py | 4 ++-- nbs/api/02_maker.ipynb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nbdev/maker.py b/nbdev/maker.py index 7e395ed87..58cc7bf35 100644 --- a/nbdev/maker.py +++ b/nbdev/maker.py @@ -181,9 +181,9 @@ def _retr_mdoc(cells): lines = dropwhile(lambda l: not l.startswith('> '), md1.source.splitlines()) lines = list(takewhile(lambda l: l.startswith('> '), lines)) summ = '\n'.join(l.lstrip('> ').strip() for l in lines) - docs = L(o.source.strip() for o in cells if o.cell_type=='markdown' and 'export' in getattr(o,'directives_',{})) + docs = L(o.source.rstrip() for o in cells if o.cell_type=='markdown' and 'export' in getattr(o,'directives_',{})) mdoc = '\n\n'.join(L(summ)+docs).strip() - return f'"""{mdoc}"""\n\n' if mdoc else '' + return f'"""{mdoc}\n"""\n\n' if mdoc else '' # %% ../nbs/api/02_maker.ipynb #cdd205d6 @patch diff --git a/nbs/api/02_maker.ipynb b/nbs/api/02_maker.ipynb index ff45bff13..2049ab902 100644 --- a/nbs/api/02_maker.ipynb +++ b/nbs/api/02_maker.ipynb @@ -497,9 +497,9 @@ " lines = dropwhile(lambda l: not l.startswith('> '), md1.source.splitlines())\n", " lines = list(takewhile(lambda l: l.startswith('> '), lines))\n", " summ = '\\n'.join(l.lstrip('> ').strip() for l in lines)\n", - " docs = L(o.source.strip() for o in cells if o.cell_type=='markdown' and 'export' in getattr(o,'directives_',{}))\n", + " docs = L(o.source.rstrip() for o in cells if o.cell_type=='markdown' and 'export' in getattr(o,'directives_',{}))\n", " mdoc = '\\n\\n'.join(L(summ)+docs).strip()\n", - " return f'\"\"\"{mdoc}\"\"\"\\n\\n' if mdoc else ''" + " return f'\"\"\"{mdoc}\\n\"\"\"\\n\\n' if mdoc else ''" ] }, {