Skip to content

Commit 0cff151

Browse files
committed
fixes #1619
1 parent 04d587d commit 0cff151

12 files changed

Lines changed: 474 additions & 20 deletions

File tree

nbdev/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
"""Create delightful software with Jupyter Notebooks
2+
3+
Modules:
4+
5+
- `nbdev.extract_attachments`: A preprocessor that extracts all of the attachments from the notebook file. The extracted attachments are returned in the 'resources' dictionary.
6+
- `nbdev.skill`: Author clear, executable nbdev notebooks where code, prose, examples, outputs, and tests form one coherent narrative."""
7+
18
__version__ = "3.3.2"
29

310
from .doclinks import nbdev_export

nbdev/_modidx.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@
5252
'nbdev.config._git_repo': ('api/config.html#_git_repo', 'nbdev/config.py'),
5353
'nbdev.config._has_nbdev': ('api/config.html#_has_nbdev', 'nbdev/config.py'),
5454
'nbdev.config._has_py': ('api/config.html#_has_py', 'nbdev/config.py'),
55+
'nbdev.config._index_mdoc': ('api/config.html#_index_mdoc', 'nbdev/config.py'),
5556
'nbdev.config._load_toml': ('api/config.html#_load_toml', 'nbdev/config.py'),
57+
'nbdev.config._mod_docstr': ('api/config.html#_mod_docstr', 'nbdev/config.py'),
58+
'nbdev.config._modline': ('api/config.html#_modline', 'nbdev/config.py'),
59+
'nbdev.config._pkg_docparts': ('api/config.html#_pkg_docparts', 'nbdev/config.py'),
60+
'nbdev.config._pkg_mods': ('api/config.html#_pkg_mods', 'nbdev/config.py'),
61+
'nbdev.config._substantive': ('api/config.html#_substantive', 'nbdev/config.py'),
5662
'nbdev.config._user_config': ('api/config.html#_user_config', 'nbdev/config.py'),
5763
'nbdev.config.add_init': ('api/config.html#add_init', 'nbdev/config.py'),
5864
'nbdev.config.bump_version': ('api/config.html#bump_version', 'nbdev/config.py'),
@@ -63,9 +69,13 @@
6369
'nbdev.config.nbdev_create_config': ('api/config.html#nbdev_create_config', 'nbdev/config.py'),
6470
'nbdev.config.nbpath2docurl': ('api/config.html#nbpath2docurl', 'nbdev/config.py'),
6571
'nbdev.config.nbpath2html': ('api/config.html#nbpath2html', 'nbdev/config.py'),
72+
'nbdev.config.pkg_llms': ('api/config.html#pkg_llms', 'nbdev/config.py'),
73+
'nbdev.config.pkg_mdoc': ('api/config.html#pkg_mdoc', 'nbdev/config.py'),
6674
'nbdev.config.read_version': ('api/config.html#read_version', 'nbdev/config.py'),
6775
'nbdev.config.set_version': ('api/config.html#set_version', 'nbdev/config.py'),
6876
'nbdev.config.show_src': ('api/config.html#show_src', 'nbdev/config.py'),
77+
'nbdev.config.update_init_mdoc': ('api/config.html#update_init_mdoc', 'nbdev/config.py'),
78+
'nbdev.config.update_llms_txt': ('api/config.html#update_llms_txt', 'nbdev/config.py'),
6979
'nbdev.config.update_proj': ('api/config.html#update_proj', 'nbdev/config.py'),
7080
'nbdev.config.update_version': ('api/config.html#update_version', 'nbdev/config.py'),
7181
'nbdev.config.write_cells': ('api/config.html#write_cells', 'nbdev/config.py')},
@@ -118,7 +128,8 @@
118128
'nbdev.export.ExportModuleProc._export_': ('api/export.html#exportmoduleproc._export_', 'nbdev/export.py'),
119129
'nbdev.export.ExportModuleProc._exporti_': ('api/export.html#exportmoduleproc._exporti_', 'nbdev/export.py'),
120130
'nbdev.export.ExportModuleProc.begin': ('api/export.html#exportmoduleproc.begin', 'nbdev/export.py'),
121-
'nbdev.export.nb_export': ('api/export.html#nb_export', 'nbdev/export.py')},
131+
'nbdev.export.nb_export': ('api/export.html#nb_export', 'nbdev/export.py'),
132+
'nbdev.export.nb_mdoc': ('api/export.html#nb_mdoc', 'nbdev/export.py')},
122133
'nbdev.extract_attachments': {},
123134
'nbdev.frontmatter': { 'nbdev.frontmatter.FrontmatterProc': ('api/frontmatter.html#frontmatterproc', 'nbdev/frontmatter.py'),
124135
'nbdev.frontmatter.FrontmatterProc._update': ( 'api/frontmatter.html#frontmatterproc._update',

nbdev/config.py

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
# %% auto #0
88
__all__ = ['pyproject_nm', 'pyproject_tmpl', 'nbdev_defaults', 'pyproj_tmpl', 'nbdev_create_config', 'ConfigToml', 'get_config',
99
'is_nbdev', 'create_output', 'show_src', 'nbpath2html', 'nbpath2docurl', 'read_version', 'set_version',
10-
'bump_version', 'update_version', 'update_proj', 'add_init', 'import_obj', 'write_cells']
10+
'bump_version', 'update_version', 'update_proj', 'add_init', 'pkg_mdoc', 'update_init_mdoc', 'pkg_llms',
11+
'update_llms_txt', 'import_obj', 'write_cells']
1112

1213
# %% ../nbs/api/01_config.ipynb #6fd14ecd
1314
from datetime import datetime
@@ -355,6 +356,104 @@ def add_init(path=None):
355356
if _has_py(fs) or any(filter(_has_py, subds)) and not (r/_init).exists(): (r/_init).touch()
356357
if get_config().get('put_version_in_init', True): update_version(path)
357358
if get_config().get('update_pyproject', True): update_proj(path.parent)
359+
update_init_mdoc(path)
360+
361+
# %% ../nbs/api/01_config.ipynb #a01b98bb
362+
def _mod_docstr(py_path):
363+
"Module docstring of `py_path` via ast, without importing ('' if none or unparseable)"
364+
try: return ast.get_docstring(ast.parse(Path(py_path).read_text(encoding='utf-8'))) or ''
365+
except SyntaxError: return ''
366+
367+
def _substantive(docstr):
368+
"Does `docstr` say more than the default summary line plus `Docs:` link?"
369+
return len(docstr.strip().splitlines())>3
370+
371+
def _pkg_mods(path):
372+
"`(name, summary, url)` per module of `path` with a substantive docstring: dotted name, one-line first para, `Docs:` url"
373+
res = []
374+
for p in sorted(Path(path).rglob('*.py')):
375+
if p.name.startswith('_'): continue
376+
d = _mod_docstr(p)
377+
if not _substantive(d): continue
378+
nm = '.'.join((Path(path).name, *p.relative_to(path).with_suffix('').parts))
379+
summ = ' '.join(d.split('\n\n')[0].splitlines())
380+
last = d.strip().splitlines()[-1]
381+
res.append((nm, summ, last[5:].strip() if last.startswith('Docs:') else ''))
382+
return res
383+
384+
def _index_mdoc(idx_path):
385+
"Package docstring intro: bodies of md cells with an export directive, and fenced `exportd` cells, from the index nb"
386+
if not idx_path or not Path(idx_path).exists(): return ''
387+
docs = []
388+
for c in read_nb(idx_path).cells:
389+
lines = c.source.splitlines()
390+
n = 0
391+
while n<len(lines) and lines[n].startswith('#|'): n += 1
392+
dirs = {l[2:].strip().split()[0] for l in lines[:n] if l[2:].strip()}
393+
body = '\n'.join(lines[n:]).strip()
394+
if not body or not {'export','exportd'}&dirs: continue
395+
docs.append(body if c.cell_type=='markdown' else fenced(body, 'python'))
396+
return '\n\n'.join(docs)
397+
398+
def _pkg_docparts(path=None, desc=None, index_nb=None):
399+
"`(path, desc, intro, mods)` shared by `pkg_mdoc` and `pkg_llms`"
400+
path = Path(path or get_config().lib_path)
401+
mods = _pkg_mods(path)
402+
if index_nb is None:
403+
cfg = get_config()
404+
if path==Path(cfg.lib_path): index_nb = cfg.nbs_path/cfg.readme_nb
405+
intro = _index_mdoc(index_nb)
406+
if (mods or intro) and desc is None: desc = get_config().description
407+
return path,desc,intro,mods
408+
409+
def pkg_mdoc(path=None, desc=None, index_nb=None):
410+
"Package docstring for `path`: `desc`, intro from `index_nb`, and a line per module with a substantive docstring"
411+
path,desc,intro,mods = _pkg_docparts(path, desc, index_nb)
412+
if not mods and not intro: return ''
413+
mods = ['Modules:', '\n'.join(f'- `{nm}`: {summ}' for nm,summ,url in mods)] if mods else []
414+
return '\n\n'.join(filter(None, [desc, intro, *mods]))
415+
416+
# %% ../nbs/api/01_config.ipynb #a03a0007
417+
def update_init_mdoc(path=None, desc=None):
418+
"Write `pkg_mdoc` as the docstring of `path/__init__.py`, replacing any existing docstring"
419+
path = Path(path or get_config().lib_path)
420+
mdoc = pkg_mdoc(path, desc=desc)
421+
if not mdoc: return
422+
fn = path/'__init__.py'
423+
txt = fn.read_text(encoding='utf-8') if fn.exists() else ''
424+
body = ast.parse(txt).body
425+
rest = txt
426+
if body and isinstance(body[0], ast.Expr) and isinstance(getattr(body[0].value, 'value', None), str):
427+
rest = ''.join(txt.splitlines(keepends=True)[body[0].end_lineno:])
428+
res = f'"""{mdoc}"""\n'
429+
if rest.strip(): res += '\n'+rest.lstrip('\n')
430+
fn.write_text(res, encoding='utf-8')
431+
432+
# %% ../nbs/api/01_config.ipynb #f9912ef7
433+
_llms_marker = '<!-- Generated by nbdev-export; edit the index notebook, not this file -->'
434+
435+
def _modline(nm, summ, url):
436+
"llms.txt list line: linked when `url` is a single URL, else the `Docs:` text carried verbatim"
437+
if url and ' ' not in url: return f'- [{nm}]({url}): {summ}'
438+
return f'- `{nm}`: {summ} (Docs: {url})' if url else f'- `{nm}`: {summ}'
439+
440+
def pkg_llms(path=None, desc=None, index_nb=None):
441+
"llms.txt for the package at `path`, from the same sources as `pkg_mdoc` ('' if no intro or modules)"
442+
path,desc,intro,mods = _pkg_docparts(path, desc, index_nb)
443+
if not mods and not intro: return ''
444+
if mods:
445+
links = '\n'.join(_modline(*o) for o in mods)
446+
mods = [f'## Modules\n\n{links}']
447+
parts = [f'# {path.name}', f'> {desc}' if desc else '', intro, *mods, _llms_marker]
448+
return '\n\n'.join(filter(None, parts)) + '\n'
449+
450+
def update_llms_txt(path=None, fn=None, desc=None, index_nb=None):
451+
"Write `pkg_llms` to `fn` when it's absent or generated (has the nbdev marker); never touch a hand-written file"
452+
if fn is None: fn = get_config().nbs_path/'llms.txt'
453+
txt = pkg_llms(path, desc=desc, index_nb=index_nb)
454+
if not txt: return
455+
if fn.exists() and _llms_marker not in fn.read_text(encoding='utf-8'): return
456+
fn.write_text(txt, encoding='utf-8')
358457

359458
# %% ../nbs/api/01_config.ipynb #95cebda6
360459
def import_obj(s):
@@ -367,7 +466,7 @@ def import_obj(s):
367466
def write_cells(cells, hdr, file, solo_nb=False):
368467
"Write `cells` to `file` along with header `hdr` (mainly for nbdev internal use)."
369468
for cell in cells:
370-
if cell.cell_type=='code' and cell.source.strip():
469+
if cell.cell_type=='code' and cell.source.strip() and 'exportd' not in getattr(cell,'directives_',{}):
371470
cell_id = f" #{cell.id}" if cell.get('id') else ""
372471
file.write(f'\n\n{hdr}{cell_id}\n{cell.source}') if not solo_nb else file.write(f'\n\n{cell.source}')
373472

nbdev/doclinks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def nbdev_export(
155155
files = nbglob(path=path, as_path=True, **kwargs).sorted('name')
156156
for f in files: nb_export(f, procs=procs)
157157
add_init(cfg.lib_path)
158+
update_llms_txt()
158159
_build_modidx()
159160

160161
# %% ../nbs/api/05_doclinks.ipynb #3134c22b

nbdev/export.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/04_export.ipynb.
66

77
# %% auto #0
8-
__all__ = ['ExportModuleProc', 'nb_export']
8+
__all__ = ['ExportModuleProc', 'nb_export', 'nb_mdoc']
99

1010
# %% ../nbs/api/04_export.ipynb #3b932371
1111
from .config import *
1212
from .maker import *
13+
from .maker import _retr_mdoc
1314
from .imports import *
1415
from .process import *
1516

@@ -32,8 +33,8 @@ def _export_(self, cell, exp_to=None):
3233
def __call__(self, cell):
3334
src = cell.source
3435
if not src: return
35-
if cell.cell_type=='markdown' and (src.startswith('# ') or 'export' in cell.directives_): self._exporti_(cell)
36-
_exports_=_export_
36+
if cell.cell_type=='markdown' and (src.startswith('# ') or {'export','exportd'}&set(cell.directives_)): self._exporti_(cell)
37+
_exports_,_exportd_=_export_,_exporti_
3738

3839
# %% ../nbs/api/04_export.ipynb #76717e36
3940
def nb_export(
@@ -51,7 +52,7 @@ def nb_export(
5152
nb = NBProcessor(nbname, [exp]+L(procs), debug=debug)
5253
nb.process()
5354
for mod,cells in exp.modules.items():
54-
if first(1 for o in cells if o.cell_type=='code'):
55+
if first(1 for o in cells if o.cell_type=='code' and 'exportd' not in o.directives_):
5556
all_cells = exp.in_all[mod]
5657
nm = ifnone(name, getattr(exp, 'default_exp', None) if mod=='#' else mod)
5758
if not nm:
@@ -61,3 +62,10 @@ def nb_export(
6162
return
6263
mm = mod_maker(dest=lib_path, name=nm, nb_path=nbname, is_new=bool(name) or mod=='#', solo_nb=solo_nb)
6364
mm.make(cells, all_cells, lib_path=lib_path)
65+
66+
# %% ../nbs/api/04_export.ipynb #f7ce4b21
67+
def nb_mdoc(nbname:str): # Filename of notebook
68+
"The module docstring `nb_export` would write for `nbname`, for previewing while authoring"
69+
exp = ExportModuleProc()
70+
NBProcessor(nbname, exp).process()
71+
return PrettyString(_retr_mdoc(exp.modules['#'], nbname))

nbdev/maker.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,17 @@ def _import2relative(cells, lib_path=None):
177177

178178
# %% ../nbs/api/02_maker.ipynb #5bff9d71
179179
def _retr_mdoc(cells, nb_path=None):
180-
"Search for markdown cells used to create module docstring"
180+
"Module docstring from md cells with an export directive, plus `exportd` cells (code fenced)"
181+
def _doc(o):
182+
d = getattr(o,'directives_',{})
183+
if o.cell_type=='markdown' and {'export','exportd'}&set(d): return o.source.rstrip()
184+
if o.cell_type=='code' and 'exportd' in d: return fenced(o.source.rstrip(), 'python')
181185
md1 = first(o for o in cells if o.cell_type=='markdown' and o.source.startswith('# '))
182186
if not md1: return ''
183187
lines = dropwhile(lambda l: not l.startswith('> '), md1.source.splitlines())
184188
lines = list(takewhile(lambda l: l.startswith('> '), lines))
185189
summ = '\n'.join(l.lstrip('> ').strip() for l in lines)
186-
docs = L(o.source.rstrip() for o in cells if o.cell_type=='markdown' and 'export' in getattr(o,'directives_',{}))
190+
docs = L(cells).map(_doc).filter()
187191
mdoc = '\n\n'.join(L(summ)+docs).strip()
188192
url = nbpath2docurl(nb_path) if nb_path else ''
189193
if url: mdoc = (mdoc + f'\n\nDocs: {url}').strip()

0 commit comments

Comments
 (0)