nbdev 3.3.24__tar.gz → 3.3.26__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {nbdev-3.3.24/nbdev.egg-info → nbdev-3.3.26}/PKG-INFO +1 -1
  2. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/__init__.py +1 -1
  3. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/quarto.py +7 -6
  4. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/quarto_render.py +12 -2
  5. nbdev-3.3.26/nbdev/readme_filter.py +36 -0
  6. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/release.py +16 -13
  7. nbdev-3.3.26/nbdev/skill.py +63 -0
  8. {nbdev-3.3.24 → nbdev-3.3.26/nbdev.egg-info}/PKG-INFO +1 -1
  9. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev.egg-info/SOURCES.txt +1 -0
  10. nbdev-3.3.26/tests/test_quarto_render.py +94 -0
  11. nbdev-3.3.24/nbdev/skill.py +0 -82
  12. nbdev-3.3.24/tests/test_quarto_render.py +0 -54
  13. {nbdev-3.3.24 → nbdev-3.3.26}/CONTRIBUTING.md +0 -0
  14. {nbdev-3.3.24 → nbdev-3.3.26}/LICENSE +0 -0
  15. {nbdev-3.3.24 → nbdev-3.3.26}/README.md +0 -0
  16. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/_modidx.py +0 -0
  17. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/clean.py +0 -0
  18. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/cli.py +0 -0
  19. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/config.py +0 -0
  20. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/diff.py +0 -0
  21. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/doclinks.py +0 -0
  22. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/export.py +0 -0
  23. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/extract_attachments.py +0 -0
  24. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/frontmatter.py +0 -0
  25. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/imports.py +0 -0
  26. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/maker.py +0 -0
  27. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/merge.py +0 -0
  28. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/migrate.py +0 -0
  29. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/moddocs.py +0 -0
  30. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/process.py +0 -0
  31. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/processors.py +0 -0
  32. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/qmd.py +0 -0
  33. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/scrubmagics.py +0 -0
  34. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/serve.py +0 -0
  35. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/serve_drv.py +0 -0
  36. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/showdoc.py +0 -0
  37. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/sync.py +0 -0
  38. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev/test.py +0 -0
  39. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev.egg-info/dependency_links.txt +0 -0
  40. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev.egg-info/entry_points.txt +0 -0
  41. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev.egg-info/requires.txt +0 -0
  42. {nbdev-3.3.24 → nbdev-3.3.26}/nbdev.egg-info/top_level.txt +0 -0
  43. {nbdev-3.3.24 → nbdev-3.3.26}/pyproject.toml +0 -0
  44. {nbdev-3.3.24 → nbdev-3.3.26}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdev
3
- Version: 3.3.24
3
+ Version: 3.3.26
4
4
  Summary: Create delightful software with Jupyter Notebooks
5
5
  Author-email: "Jeremy Howard and the fast.ai community" <j@fast.ai>
6
6
  License-Expression: Apache-2.0
@@ -6,7 +6,7 @@ Modules:
6
6
  - `nbdev.moddocs`: # Module docs: creating them from existing notebooks
7
7
  - `nbdev.skill`: Write, read and review nbdev notebooks as source, documentation, examples and tests. You MUST read this before any work on a project with notebooks."""
8
8
 
9
- __version__ = "3.3.24"
9
+ __version__ = "3.3.26"
10
10
 
11
11
  from .doclinks import nbdev_export
12
12
  from .showdoc import show_doc
@@ -23,7 +23,7 @@ from fastcore.shutil import rmtree,move,copytree
23
23
  from fastcore.meta import delegates
24
24
  from .serve import proc_nbs,_proc_file
25
25
  from . import serve_drv
26
- from .quarto_render import render_quarto
26
+ from .quarto_render import render_quarto, quarto_stale
27
27
  import yaml
28
28
 
29
29
  # %% ../nbs/api/14_quarto.ipynb #aae2d2be-ad03-4536-bf70-c4575f39cea3
@@ -75,7 +75,7 @@ def _sort(a):
75
75
 
76
76
  _def_file_re = r'\.(?:ipynb|qmd|html)$'
77
77
 
78
- @delegates
78
+ @delegates(nbglob_cli)
79
79
  def _nbglob_docs(
80
80
  path:str=None, # Path to notebooks
81
81
  file_glob:str=None, # Only include files matching glob
@@ -214,9 +214,8 @@ def _pre_docs(path=None, n_workers:int=defaults.cpus, **kwargs):
214
214
  path = Path(path).absolute() if path else cfg.nbs_path
215
215
  if not (path/'nbdev.yml').exists(): path = cfg.nbs_path # given a project root, fall back to its configured nbs
216
216
  os.chdir(cfg.config_path) # bare `get_config()` runs downstream (including in render workers), so anchor the cwd
217
- _chk_nbdev_yml(path)
218
- _ensure_quarto()
219
217
  refresh_quarto_yml()
218
+ _ensure_quarto()
220
219
  import nbdev.doclinks
221
220
  nbdev.doclinks._build_modidx()
222
221
  nbdev_sidebar.__wrapped__(path=path, **kwargs)
@@ -290,7 +289,9 @@ def nbdev_readme(
290
289
  _strip_sidebar(cache) # to avoid rendering whole website
291
290
  for f in _readme_cands(cache, cfg):
292
291
  if f.exists(): f.unlink() # remove stale renders from either quarto layout
293
- _sprun(f'cd "{cache}" && quarto render "{cache/cfg.readme_nb}" -o README.md -t gfm --no-execute -M wrap:preserve')
292
+ from nbdev import readme_filter
293
+ filt = fr""" -M 'filters:["{readme_filter.__file__}"]'""" if cfg.doc_host else ''
294
+ _sprun(f'cd "{cache}" && quarto render "{cache/cfg.readme_nb}" -o README.md -t gfm --no-execute -M wrap:preserve{filt}')
294
295
 
295
296
  _save_cached_readme(cache, cfg)
296
297
 
@@ -390,7 +391,7 @@ def nbdev_preview(
390
391
  cache,cfg,path = _pre_docs(path, n_workers=n_workers, **kwargs)
391
392
  site = cache/cfg.doc_path.name
392
393
  if not (render or site.exists()) and cfg.doc_path.exists(): copytree(cfg.doc_path, site)
393
- if render or not site.exists(): render_quarto(cache, site, n_workers=n_workers)
394
+ if render or not site.exists() or quarto_stale(cache, site): render_quarto(cache, site, n_workers=n_workers)
394
395
  xtra = []
395
396
  if port: xtra += ['--port', str(port)]
396
397
  if host: xtra += ['--host', host]
@@ -1,10 +1,9 @@
1
1
  "Parallel Quarto website rendering in isolated project copies."
2
2
 
3
3
  import json, shutil, subprocess, tempfile
4
- from pathlib import Path
5
4
  from xml.etree import ElementTree as ET
6
5
  from fastcore.parallel import parallel
7
- from fastcore.utils import defaults
6
+ from fastcore.utils import *
8
7
 
9
8
 
10
9
  def _quarto(path, *args):
@@ -13,6 +12,17 @@ def _quarto(path, *args):
13
12
  return res.stdout
14
13
 
15
14
 
15
+ def quarto_stale(path, output_dir):
16
+ "Whether a page's HTML output is missing or older than its processed source."
17
+ path, output_dir = Path(path).resolve(), Path(output_dir).resolve()
18
+ info = loads(_quarto(path, 'inspect'))
19
+ for name,entry in info['fileInformation'].items():
20
+ p = Path(name)
21
+ output = output_dir/p.parent/(entry['metadata'].get('output-file') or p.with_suffix('.html').name)
22
+ if not output.exists() or (path/p).stat().st_mtime>output.stat().st_mtime: return True
23
+ return False
24
+
25
+
16
26
  def _render_batch(job):
17
27
  path, inputs, output = job
18
28
  _quarto(path, 'render', *inputs, '--no-cache', '--no-clean', '--output-dir', output)
@@ -0,0 +1,36 @@
1
+ r"Pandoc filter for README documentation links."
2
+
3
+ from fastcore.utils import *
4
+ from fastcore.script import *
5
+ from nbdev.config import *
6
+ from urllib.parse import urlsplit, urlunsplit, urljoin
7
+
8
+
9
+ def doc_link(target, base, nbdir='.'):
10
+ "Convert a relative documentation link to its hosted URL."
11
+ u = urlsplit(target)
12
+ p = Path(u.path)
13
+ if u.scheme or u.netloc or p.suffix not in ('.ipynb', '.qmd', '.html', '.txt'): return target
14
+ if p.suffix=='.ipynb': p = nbpath2html(p)
15
+ elif p.suffix=='.qmd': p = p.with_suffix('.html')
16
+ path = urljoin(nbdir.rstrip('/')+'/', p.as_posix()).lstrip('/')
17
+ return urljoin(base.rstrip('/')+'/', urlunsplit(('', '', path, u.query, u.fragment)))
18
+
19
+
20
+ def rewrite_links(node, base, nbdir='.'):
21
+ "Rewrite documentation links in a Pandoc syntax tree."
22
+ if isinstance(node, list):
23
+ for item in node: rewrite_links(item, base, nbdir)
24
+ elif isinstance(node, dict):
25
+ if node.get('t')=='Link': node['c'][2][0] = doc_link(node['c'][2][0], base, nbdir)
26
+ for item in node.values(): rewrite_links(item, base, nbdir)
27
+
28
+
29
+ @call_parse
30
+ def main(
31
+ fmt:str, # Output format supplied by Pandoc
32
+ ):
33
+ "Rewrite documentation links in Pandoc JSON from stdin."
34
+ cfg,doc = get_config(),loads(sys.stdin.read())
35
+ rewrite_links(doc['blocks'], cfg.doc_url, Path(cfg.readme_nb).parent.as_posix())
36
+ print(dumps(doc))
@@ -5,9 +5,10 @@ Docs: https://nbdev.fast.ai/api/release.html.md"""
5
5
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/18_release.ipynb.
6
6
 
7
7
  # %% auto #0
8
- __all__ = ['GH_HOST', 'CONDA_WARNING', 'update_changelog', 'Release', 'changelog', 'push_release', 'release_git', 'release_gh',
9
- 'pypi_json', 'latest_pypi', 'pypi_details', 'conda_output_path', 'write_conda_meta', 'write_requirements',
10
- 'anaconda_upload', 'release_conda', 'chk_conda_rel', 'release_pypi', 'release_both', 'nbdev_bump_version']
8
+ __all__ = ['GH_HOST', 'CHANGELOG_MARKER', 'CHANGELOG_TMPL', 'CONDA_WARNING', 'update_changelog', 'Release', 'changelog',
9
+ 'push_release', 'release_git', 'release_gh', 'pypi_json', 'latest_pypi', 'pypi_details', 'conda_output_path',
10
+ 'write_conda_meta', 'write_requirements', 'anaconda_upload', 'release_conda', 'chk_conda_rel',
11
+ 'release_pypi', 'release_both', 'nbdev_bump_version']
11
12
 
12
13
  # %% ../nbs/api/18_release.ipynb #c35cc2b8
13
14
  from fastcore.all import *
@@ -15,7 +16,7 @@ from ghapi.core import *
15
16
 
16
17
  from datetime import datetime
17
18
  from packaging.version import Version
18
- import shutil,subprocess,asyncio
19
+ import subprocess,asyncio
19
20
 
20
21
  from .doclinks import *
21
22
 
@@ -60,8 +61,12 @@ def _release_head():
60
61
  return head
61
62
 
62
63
  # %% ../nbs/api/18_release.ipynb #dcaba486
63
- def update_changelog(txt, ver, notes, marker='<!-- do not remove -->\n'):
64
+ CHANGELOG_MARKER = '<!-- do not remove -->\n'
65
+ CHANGELOG_TMPL = f'# Release notes\n\n{CHANGELOG_MARKER}'
66
+
67
+ def update_changelog(txt, ver, notes, marker=CHANGELOG_MARKER):
64
68
  "Insert `notes` into changelog `txt` after `marker`, replacing any existing section for `ver`"
69
+ if marker not in txt: raise ValueError(f"The changelog has no {marker.strip()!r} marker: add it near the top")
65
70
  txt = re.sub(rf'\n## {re.escape(ver)}\n.*?(?=\n## |\Z)', '', txt, flags=re.S)
66
71
  return txt.replace(marker, marker+notes.rstrip('\n')+'\n\n').rstrip('\n')+'\n'
67
72
 
@@ -86,7 +91,7 @@ class Release:
86
91
  self.groups = groups
87
92
 
88
93
  async def _issues(self, label):
89
- return await self.gh.issues.list_for_repo(state='closed', sort='created', filter='all', since=self.commit_date, labels=label)
94
+ return await self.gh.issues.list_for_repo(state='closed', sort='created', since=self.commit_date, labels=label)
90
95
  async def _issue_groups(self): return await asyncio.gather(*map(self._issues, self.groups.keys()))
91
96
 
92
97
  # %% ../nbs/api/18_release.ipynb #aa22dfe3
@@ -94,8 +99,7 @@ class Release:
94
99
  async def changelog(self:Release,
95
100
  debug=False): # Just print the latest changes, instead of updating file
96
101
  "Create the CHANGELOG.md file, or return the proposed text if `debug` is `True`"
97
- if not self.changefile.exists(): self.changefile.write_text("# Release notes\n\n<!-- do not remove -->\n")
98
- marker = '<!-- do not remove -->\n'
102
+ if not self.changefile.exists(): self.changefile.write_text(CHANGELOG_TMPL, encoding='utf-8')
99
103
  try: self.commit_date = (lr:=await self.gh.repos.get_latest_release()).published_at
100
104
  except APIError as e:
101
105
  if e.status_code != 404: raise
@@ -111,9 +115,8 @@ async def changelog(self:Release,
111
115
  sections = (_issues_txt(*o) for o in zip(issues, self.groups.values()))
112
116
  res += '\n\n'.join(filter(None, sections))
113
117
  if debug: return res
114
- res = update_changelog(self.changefile.read_text(), self.cfg.version, res, marker)
115
- shutil.copy(self.changefile, self.changefile.with_suffix(".bak"))
116
- self.changefile.write_text(res)
118
+ res = update_changelog(self.changefile.read_text(encoding='utf-8'), self.cfg.version, res)
119
+ self.changefile.write_text(res, encoding='utf-8')
117
120
  run(f'git add {self.changefile}')
118
121
 
119
122
  # %% ../nbs/api/18_release.ipynb #068421c6
@@ -132,8 +135,8 @@ async def release(self:Release):
132
135
  def latest_notes(self:Release):
133
136
  "Latest CHANGELOG entry"
134
137
  if not self.changefile.exists(): return ''
135
- its = re.split(r'^## ', self.changefile.read_text(), flags=re.MULTILINE)
136
- if not len(its)>0: return ''
138
+ its = re.split(r'^## ', self.changefile.read_text(encoding='utf-8'), flags=re.MULTILINE)
139
+ if len(its) < 2: return ''
137
140
  return '\n'.join(its[1].splitlines()[1:]).strip()
138
141
 
139
142
  # %% ../nbs/api/18_release.ipynb #01cb1ca4
@@ -0,0 +1,63 @@
1
+ """Write, read and review nbdev notebooks as source, documentation, examples and tests. You MUST read this before any work on a project with notebooks.
2
+
3
+ ## Source and reading
4
+
5
+ Published-library notebooks: interleave implementation, explanation, examples, outputs and failure demonstrations for top-to-bottom reading. Internal projects: match surrounding notebooks.
6
+
7
+ Check each file, not the repo: `# AUTOGENERATED! DO NOT EDIT!` names the source notebook; `# %% ../nbs/04_usage.ipynb #a45f753a` identifies its source cell. Edit that notebook, never generated modules, `__all__`, `_modidx.py`, or module docstrings; `nbdev-export` regenerates them. Unmarked modules are hand-written. Understand and review work through notebooks and their `git diff`, never generated modules: those lack prose, examples and tests.
8
+
9
+ Use notebooks to teach APIs through narrative/examples, plain `.py` for dense mechanism iterated under pytest (rarely; never add pytests to a project without them unless asked). Lessons test notebook code; `tests/*.py` test plain modules. For project-level questions, read `nbs/index.ipynb` (README.md's source); regenerate with `nbdev-readme` after changes.
10
+
11
+ ## Authoring
12
+
13
+ Before notebook edits, read the whole notebook or a summary of every cell. For a new notebook, read a role model such as claudette's `00_core.ipynb` to learn the literate form. Choose tutorial, how-to, explanation, or reference. Open with an H1 title cell, `>` subtitle and brief introduction. Use H2s for major concepts without fragmenting short narratives; each heading gets its own markdown cell for Jupyter collapse.
14
+
15
+ Develop one idea at a time: small implementation, explanation, lesson for every substantive API. Basic functionality outside the API, with little logic, generally needs no lesson. Introduce lessons in markdown; split when the introduction becomes complicated. Extend examples rather than adding near-duplicates. Build classes with `@patch` beside each method's explanation unless that obscures the API.
16
+
17
+ When new behavior needs teaching (often unnecessary for trivial logic), choose its narrative position, write the lesson first (better, slightly modify an existing lesson), run preceding cells as needed, see it fail. Add its markdown introduction and implementation above it; run implementation and lesson to see it pass. For bugs, revise an existing example to exercise the failing path; if no revision fits, add an assertion to it. No documented behavior change: change no lesson cells. Never add a cell merely to witness a fix.
18
+
19
+ Read existing examples before experimenting. Explore in the notebook, not a separate kernel check; keep useful explorations as lessons, delete the rest. Always try to leave the notebook better. Reserve underscore names for machinery without a coherent independent contract, not everything used internally. A helper useful only internally often signals awkward design.
20
+
21
+ ## Lessons
22
+
23
+ Readability comes first: guide readers through what code does using code and displayed output. If a displayed value is worth testing, assign, test, then show it. Test the point of the code, not incidentals such as formatting. Show errors only when readers are likely to hit them.
24
+
25
+ Use realistic values, minimal setup, reused objects and direct supporting assertions. Show evidence with compact `_repr_markdown_`, structured summaries, plots, tables, images or HTML; explain what matters in large outputs and keep stored results focused. Use `fastcore.test` assertions and `expect_fail` in ordinary code cells.
26
+
27
+ Test-serving cells don't belong on the page: signs include safety-only setup and glue checks. Usually remove them; move substantive logic checks to `#| hide` or pytest. Discuss such changes carefully. Keep plumbing, mocks, dense comprehensions and long setup out of reader-facing examples: extract a small helper or hide the check.
28
+
29
+ Consider these rewrites:
30
+ - Commented examples -> prose/code; numbered steps -> markdown/code pairs. Keep comments stating constraints code cannot show.
31
+ - Sentence-wrapped `print` -> prose/displayed value; final `print(x)` -> bare `x`.
32
+ - Reassignment stages and blank-line groups -> separate cells, each introduced.
33
+ - Near-duplicates: explain the differing argument; don't repeat setup.
34
+ - Errors: `expect_fail`, not `try`/`except`. Long docstrings -> markdown.
35
+ - Resolve TODO/FIXME example comments or move them to issues.
36
+
37
+ ## Prose and exports
38
+
39
+ In markdown, explain rationale, distinctions and guarantees, not code line-by-line or filler transitions. Place it for correct generated-page reading, normally after definitions. Use short symbol docstrings, parameter/return docments beside signatures and backticked symbols for nbdev links. Extended explanation, examples and warnings belong in markdown.
40
+
41
+ `nbdev-export` assembles module docstrings from the title's `>` description and exported markdown after H1; use one exported markdown cell per major section for long docstrings. `doc(module)` lists functions; in module prose, explain only shared constraints/workflows.
42
+
43
+ `#| exportd` includes a code cell as fenced docstring source, not module code. Use compact runnable demos after required definitions; end the preceding exported summary with a colon. Intervening unexported definitions don't appear in the docstring. Check claims by running code before documenting; preview assembly with `nbdev.export.nb_mdoc`. A docstring beyond the default summary makes `nbdev-export` list the module in package docs and `llms.txt`. Only extend docstrings of modules read independently. Export `index.ipynb`'s opening paragraphs for the package intro.
44
+
45
+ ## State and execution
46
+
47
+ Define values near first use, introduce shared setup, reuse established objects; don't reassign names later cells depend on. End explorations with the expression recording what was learned when useful. Keep imports in dedicated cells, lessons included: docs builds execute import-containing cells in fresh namespaces; mixed state-dependent code can fail or run at documentation time.
48
+
49
+ Outputs are generated and may be stale during edits. Don't stop work, clear them manually, or edit notebook JSON because of source/output mismatch. At PR time, regenerate with `nbdev-test --save` if the project keeps saved outputs. `nbdev-test --file-re` runs several named notebooks in parallel.
50
+
51
+ ## Directives and nbdev v3
52
+
53
+ - `#| default_exp` names the generated module; `#| export` includes a cell there. Exported underscore helpers need not be public.
54
+ - `#| hide` hides distracting necessities from the page.
55
+ - `#| eval: false`: code that must not run, not broken code. Later cells can't use its state.
56
+ - Metadata directives work too: `{"nbdev": {"export": "true"}}`; `fastcore.nbio.mk_cell(...).directives` merges both forms, content winning conflicts.
57
+
58
+ nbdev v3 (Jan 2026): `pyproject.toml`, not `settings.ini`; metadata in `[project]`, settings in `[tool.nbdev]` (defaults `nbs_path='nbs'`, `doc_path='_docs'`), version in `__init__.py` via `dynamic = ["version"]`, `_modidx` in `[project.entry-points.nbdev]`. CLIs use hyphens (`nbdev-export`, `nbdev-test`); Python names retain underscores. GitHub actions: `fastai/workflows/nbdev3-ci`, `quarto-ghp3`.
59
+
60
+ ## Finish
61
+
62
+ Revise affected prose, displays and assertions together; changed assertions must fail on old code and pass on new. Re-read touched narrative for readability. Run `nbdev-test <nb>`.
63
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdev
3
- Version: 3.3.24
3
+ Version: 3.3.26
4
4
  Summary: Create delightful software with Jupyter Notebooks
5
5
  Author-email: "Jeremy Howard and the fast.ai community" <j@fast.ai>
6
6
  License-Expression: Apache-2.0
@@ -22,6 +22,7 @@ nbdev/processors.py
22
22
  nbdev/qmd.py
23
23
  nbdev/quarto.py
24
24
  nbdev/quarto_render.py
25
+ nbdev/readme_filter.py
25
26
  nbdev/release.py
26
27
  nbdev/scrubmagics.py
27
28
  nbdev/serve.py
@@ -0,0 +1,94 @@
1
+ import json, shutil, subprocess, pytest
2
+ from xml.etree import ElementTree as ET
3
+ from fastcore.test import test_eq as teq
4
+ from nbdev.quarto_render import render_quarto, quarto_stale
5
+ from nbdev.quarto import nbdev_readme, refresh_quarto_yml
6
+ from nbdev.readme_filter import doc_link
7
+ from fastcore.nbio import new_nb, mk_cell, write_nb
8
+
9
+
10
+ @pytest.mark.skipif(not shutil.which('quarto'), reason='requires Quarto')
11
+ def test_readme_links(tmp_path, monkeypatch):
12
+ monkeypatch.chdir(tmp_path)
13
+ (tmp_path/'pyproject.toml').write_text(r'''[project]
14
+ name = "example"
15
+ [project.urls]
16
+ Documentation = "https://example.com/project/"
17
+ [tool.nbdev]
18
+ ''')
19
+ nbs = tmp_path/'nbs'
20
+ nbs.mkdir()
21
+ base = 'https://example.com/project/'
22
+ links = [('guide/01_Intro.ipynb?q=1#intro', base+'guide/intro.html?q=1#intro'),
23
+ ('guide/02_intro.qmd', base+'guide/02_intro.html'), ('guide/intro.html', base+'guide/intro.html'),
24
+ ('/llms.txt', base+'llms.txt'), ('llms-ctx.txt', base+'llms-ctx.txt'),
25
+ ('https://other.com/guide.html', 'https://other.com/guide.html'),
26
+ ('#section', '#section'), ('LICENSE', 'LICENSE')]
27
+ text = '\n\n'.join([*(f'[Link]({src})' for src,_ in links),
28
+ r'[Reference][guide] ![Image](image.png)', r'[guide]: guide/01_Intro.ipynb#intro',
29
+ '```markdown\n[Example](guide/01_Intro.ipynb)\n```'])
30
+ write_nb(new_nb([mk_cell('# Example', 'markdown'), mk_cell(text, 'markdown')]), nbs/'index.ipynb')
31
+ refresh_quarto_yml()
32
+ nbdev_readme.__wrapped__()
33
+ result = (tmp_path/'README.md').read_text()
34
+ for _,dest in links: assert f']({dest})' in result
35
+ assert f'[Reference]({base}guide/intro.html#intro)' in result
36
+ assert '![Image](image.png)' in result and '[Example](guide/01_Intro.ipynb)' in result
37
+ teq(doc_link('../guide/01_intro.ipynb', base, 'start'), base+'guide/intro.html')
38
+ teq(doc_link('/llms.txt', base, 'start'), base+'llms.txt')
39
+ teq(doc_link('//other.com/llms.txt', base), '//other.com/llms.txt')
40
+
41
+
42
+ @pytest.mark.skipif(not shutil.which('quarto'), reason='requires Quarto')
43
+ def test_website(tmp_path):
44
+ root = tmp_path/'source'
45
+ root.mkdir()
46
+ (root/'_quarto.yml').write_text(r'''project:
47
+ type: website
48
+ website:
49
+ title: Parallel render
50
+ site-url: https://example.com/
51
+ sidebar:
52
+ contents: [index.qmd, alpha.qmd, beta.qmd]
53
+ format:
54
+ html: default
55
+ commonmark: default
56
+ execute:
57
+ enabled: false
58
+ ''')
59
+ for name in ('index', 'alpha', 'beta'):
60
+ (root/f'{name}.qmd').write_text(fr'''---
61
+ title: {name}
62
+ ---
63
+ ## Detail
64
+
65
+ Page {name}. [Alpha](alpha.qmd#detail). [Download](data.txt).
66
+ ''')
67
+ (root/'data.txt').write_text('shared resource')
68
+ (root/'other.qmd').write_text('---\noutput-file: renamed.html\n---\n# Other')
69
+ serial, parallel = tmp_path/'serial', root/'_site'
70
+ assert quarto_stale(root, parallel)
71
+ render_quarto(root, serial, n_workers=1)
72
+ render_quarto(root, parallel, n_workers=2)
73
+ assert not quarto_stale(root, parallel)
74
+ search = lambda d: {e['href']: e['text'] for e in json.loads((d/'search.json').read_text())}
75
+ urls = lambda d: {e.text for e in ET.parse(d/'sitemap.xml').findall('.//{*}loc')}
76
+ teq(search(serial), search(parallel))
77
+ teq(urls(serial), urls(parallel))
78
+ teq(len(urls(parallel)), 4)
79
+ for name in ('index', 'alpha', 'beta'):
80
+ html = (parallel/f'{name}.html').read_text()
81
+ assert 'alpha.html#detail' in html and 'data.txt' in html
82
+ assert (parallel/f'{name}.md').exists()
83
+ teq((parallel/'data.txt').read_text(), 'shared resource')
84
+ assert (parallel/'site_libs').is_dir()
85
+ assert 'http-equiv="refresh"' not in (parallel/'index.html').read_text()
86
+ (root/'beta.qmd').write_text('---\ntitle: beta\n---\n## Detail\n\nUpdated beta.')
87
+ assert quarto_stale(root, parallel)
88
+ subprocess.run(['quarto', 'render', 'beta.qmd', '--quiet', '--output-dir', str(parallel)], cwd=root, check=True)
89
+ assert not quarto_stale(root, parallel)
90
+ teq(set(search(serial)), set(search(parallel)))
91
+ assert 'Updated beta.' in search(parallel)['beta.html#detail']
92
+ (root/'beta.qmd').write_text('---\nfilters: [missing.lua]\n---\nBroken.')
93
+ with pytest.raises(RuntimeError, match='missing.lua'): render_quarto(root, parallel, n_workers=2)
94
+ assert 'Updated beta.' in search(parallel)['beta.html#detail']
@@ -1,82 +0,0 @@
1
- """Write, read and review nbdev notebooks as source, documentation, examples and tests. You MUST read this before any work on a project with notebooks.
2
-
3
- # Source
4
-
5
- Write notebooks to be read top-to-bottom, interleaving implementation, explanation, examples, outputs, and failure demonstrations. Follow these conventions in published libraries. In internal projects, match the surrounding notebooks.
6
-
7
- Check the file, not the repo: `# AUTOGENERATED! DO NOT EDIT!` names the source notebook; `# %% ../nbs/04_usage.ipynb #a45f753a` identifies the source cell. Edit that notebook, never generated modules, `__all__`, `_modidx.py`, or module docstrings; `nbdev-export` regenerates them. Unmarked modules are hand-written. Use notebooks to teach the API through narrative and examples, and plain `.py` for dense mechanism iterated under pytest (rarely; never add pytests to a project without them unless asked). Lessons test notebook code. Test plain modules in `tests/*.py`.
8
-
9
- Understand and review work through the notebooks and their `git diff`, never generated modules. Those lack the prose, examples and tests.
10
-
11
- For project-level questions, read `nbs/index.ipynb`, the source of README.md; regenerate with `nbdev-readme` after changes.
12
-
13
- # Structure and authoring
14
-
15
- Choose tutorial, how-to, explanation, or reference. Open with an H1 title cell and `>` subtitle, then a brief introduction. Use H2s for major concepts without fragmenting short narratives. Each heading belongs in its own markdown cell so Jupyter can collapse sections.
16
-
17
- Before changing a notebook, read the whole notebook or a summary of every cell. For a new notebook, read a role model such as claudette's `00_core.ipynb` to learn the literate form.
18
-
19
- Develop one idea at a time: a small implementation, its explanation, and a lesson if it's a substantive API. Introduce each lesson in markdown; split it when the introduction becomes complicated. Extend existing examples rather than adding near-duplicates. Build classes with `@patch` beside each method's explanation unless that obscures the API.
20
-
21
- When new behavior needs a lesson, choose its narrative position, write the lesson first, run preceding cells as needed, and see it fail. Add its markdown introduction and implementation above it, then run both to see it pass. For bugs, revise an existing example to exercise the failing path; if no revision fits, add an assertion to it. If no documented behavior changes, change no lesson cells. Never add a cell merely to witness a fix.
22
-
23
- Read existing examples before experimenting. Explore in the notebook, not in a separate kernel check; keep useful explorations as lessons and delete the rest. Always try to leave a notebook better than you found it.
24
-
25
- Reserve underscore names for machinery without a coherent independent contract, not for everything used only internally. If a helper is only useful internally, the design is often awkward.
26
-
27
- # Prose and module documentation
28
-
29
- In markdown, explain rationale, distinctions and guarantees. Don't narrate code line by line or write filler transitions. Place markdown where it reads correctly on the generated page, normally after the definition. Use short symbol docstrings, parameter/return docments beside signatures, and backticked symbol names for nbdev links. Extended explanation, examples, and warnings belong in markdown.
30
-
31
- `nbdev-export` builds the module docstring from the title's `>` description and exported markdown after the H1. Use one exported markdown cell per major section for long docstrings. `doc(module)` lists the functions. In module prose, explain only shared constraints and workflows.
32
-
33
- `#| exportd` includes a code cell as fenced source in the module docstring, not module code. Use compact runnable demos after their required definitions; end the preceding exported summary with a colon. Unexported definitions between the summary and demo don't appear in the docstring.
34
-
35
- Check claims by running code before documenting them; preview assembly with `nbdev.export.nb_mdoc`. `nbdev-export` lists a module in package documentation and `llms.txt` if its docstring has more than the default summary. Only extend docstrings of modules read on their own. Export the opening paragraphs of `index.ipynb` for the package intro.
36
-
37
- # Lesson cells
38
-
39
- A notebook guides a reader through what the code does. Readability comes first.
40
-
41
- Every substantive API needs a lesson. Basic functionality outside the API, with little logic, generally doesn't.
42
-
43
- Explain with code and its displayed output. When a displayed value is also worth testing, assign it, test it, then show it. Test results that are the point of the code, not incidentals such as formatting.
44
-
45
- Show an error only when readers are likely to hit it.
46
-
47
- A cell that serves a test instead of a reader doesn't belong on the page. Signs include setup that exists only so a check can run safely, and checks on glue code. Usually, remove such a test. If it checks substantive logic, move it to a `#| hide` cell or to pytest. Discuss such changes carefully.
48
-
49
- Write page content: realistic values, minimal setup, reused objects, and direct assertions that support the lesson. Show evidence with compact `_repr_markdown_` or structured summaries, plots, tables, images or HTML. Explain what matters in large outputs and keep stored results focused.
50
-
51
- Use `fastcore.test` assertions and `expect_fail` in ordinary code cells. Keep check plumbing, mocks, dense comprehensions, and long setup out of reader-facing examples: extract a small helper or hide the check. Use `nbdev-test --file-re` to run several named notebooks in parallel.
52
-
53
- Consider a rewrite when you see these:
54
-
55
- - Split commented examples into prose and code. Split numbered steps into markdown/code pairs. Keep comments that state constraints code cannot show.
56
- - Replace a sentence-wrapped `print` with prose and a displayed value, and a final `print(x)` with bare `x`.
57
- - Split reassignment stages and blank-line groups into cells, each with its own introduction.
58
- - In near-duplicate examples, explain the argument that differs. Don't repeat the setup.
59
- - Show errors with `expect_fail`, not `try`/`except`. Move long docstrings to markdown.
60
- - Resolve TODO/FIXME example comments or move them to issues.
61
-
62
- # State and execution
63
-
64
- Define values near first use, introduce shared setup, reuse established objects, and don't reassign names later cells depend on. End exploratory cells with the expression recording what was learned when useful.
65
-
66
- Keep imports in dedicated cells, including in lessons: docs builds execute import-containing cells in fresh namespaces, so mixing imports with state-dependent code can fail or run code at documentation time.
67
-
68
- Stored outputs are generated and may be stale during editing. Don't stop work, clear them by hand, or edit notebook JSON because they no longer match the source. At PR time, regenerate them with `nbdev-test --save` if the project keeps saved outputs.
69
-
70
- # Directives and nbdev v3
71
-
72
- - `#| default_exp` names the generated module; `#| export` includes a cell there. Exported underscore helpers need not be public.
73
- - `#| hide` hides distracting necessities from the page.
74
- - `#| eval: false` is for code that must not run, not broken code. Later cells can't use its state.
75
- - Directives may instead be metadata: `{"nbdev": {"export": "true"}}`. nbdev reads both; `fastcore.nbio.mk_cell(...).directives` merges them with content winning conflicts.
76
-
77
- nbdev v3 (Jan 2026) uses `pyproject.toml`, not `settings.ini`: metadata in `[project]`, nbdev settings in `[tool.nbdev]` (default `nbs_path='nbs'`, `doc_path='_docs'`), version in `__init__.py` via `dynamic = ["version"]`, and `_modidx` in `[project.entry-points.nbdev]`. CLIs use hyphens (`nbdev-export`, `nbdev-test`); Python names retain underscores. GitHub actions are `fastai/workflows/nbdev3-ci` and `quarto-ghp3`.
78
-
79
- # Finish
80
-
81
- Revise affected prose, displays, and assertions together; changed assertions must fail on old code and pass on new. Re-read the touched narrative for readability. Run `nbdev-test <nb>`.
82
- """
@@ -1,54 +0,0 @@
1
- import json, shutil, subprocess, pytest
2
- from xml.etree import ElementTree as ET
3
- from fastcore.test import test_eq as teq
4
- from nbdev.quarto_render import render_quarto
5
-
6
-
7
- @pytest.mark.skipif(not shutil.which('quarto'), reason='requires Quarto')
8
- def test_website(tmp_path):
9
- root = tmp_path/'source'
10
- root.mkdir()
11
- (root/'_quarto.yml').write_text(r'''project:
12
- type: website
13
- website:
14
- title: Parallel render
15
- site-url: https://example.com/
16
- sidebar:
17
- contents: [index.qmd, alpha.qmd, beta.qmd]
18
- format:
19
- html: default
20
- commonmark: default
21
- execute:
22
- enabled: false
23
- ''')
24
- for name in ('index', 'alpha', 'beta'):
25
- (root/f'{name}.qmd').write_text(fr'''---
26
- title: {name}
27
- ---
28
- ## Detail
29
-
30
- Page {name}. [Alpha](alpha.qmd#detail). [Download](data.txt).
31
- ''')
32
- (root/'data.txt').write_text('shared resource')
33
- serial, parallel = tmp_path/'serial', tmp_path/'parallel'
34
- render_quarto(root, serial, n_workers=1)
35
- render_quarto(root, parallel, n_workers=2)
36
- search = lambda d: {e['href']: e['text'] for e in json.loads((d/'search.json').read_text())}
37
- urls = lambda d: {e.text for e in ET.parse(d/'sitemap.xml').findall('.//{*}loc')}
38
- teq(search(serial), search(parallel))
39
- teq(urls(serial), urls(parallel))
40
- teq(len(urls(parallel)), 3)
41
- for name in ('index', 'alpha', 'beta'):
42
- html = (parallel/f'{name}.html').read_text()
43
- assert 'alpha.html#detail' in html and 'data.txt' in html
44
- assert (parallel/f'{name}.md').exists()
45
- teq((parallel/'data.txt').read_text(), 'shared resource')
46
- assert (parallel/'site_libs').is_dir()
47
- assert 'http-equiv="refresh"' not in (parallel/'index.html').read_text()
48
- (root/'beta.qmd').write_text('---\ntitle: beta\n---\n## Detail\n\nUpdated beta.')
49
- subprocess.run(['quarto', 'render', 'beta.qmd', '--quiet', '--output-dir', str(parallel)], cwd=root, check=True)
50
- teq(set(search(serial)), set(search(parallel)))
51
- assert 'Updated beta.' in search(parallel)['beta.html#detail']
52
- (root/'beta.qmd').write_text('---\nfilters: [missing.lua]\n---\nBroken.')
53
- with pytest.raises(RuntimeError, match='missing.lua'): render_quarto(root, parallel, n_workers=2)
54
- assert 'Updated beta.' in search(parallel)['beta.html#detail']
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes