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
1314from 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
360459def import_obj (s ):
@@ -367,7 +466,7 @@ def import_obj(s):
367466def 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
0 commit comments