nbdev 3.2.2__tar.gz → 3.3.1__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 (41) hide show
  1. {nbdev-3.2.2/nbdev.egg-info → nbdev-3.3.1}/PKG-INFO +5 -5
  2. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/__init__.py +1 -1
  3. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/_modidx.py +20 -9
  4. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/clean.py +109 -19
  5. nbdev-3.3.1/nbdev/diff.py +198 -0
  6. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/migrate.py +1 -0
  7. nbdev-3.3.1/nbdev/process.py +88 -0
  8. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/processors.py +3 -3
  9. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/release.py +4 -2
  10. nbdev-3.3.1/nbdev/skill.py +56 -0
  11. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/sync.py +1 -2
  12. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/test.py +1 -1
  13. {nbdev-3.2.2 → nbdev-3.3.1/nbdev.egg-info}/PKG-INFO +5 -5
  14. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev.egg-info/SOURCES.txt +1 -0
  15. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev.egg-info/entry_points.txt +5 -0
  16. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev.egg-info/requires.txt +3 -3
  17. {nbdev-3.2.2 → nbdev-3.3.1}/pyproject.toml +7 -2
  18. nbdev-3.2.2/nbdev/diff.py +0 -94
  19. nbdev-3.2.2/nbdev/process.py +0 -138
  20. {nbdev-3.2.2 → nbdev-3.3.1}/CONTRIBUTING.md +0 -0
  21. {nbdev-3.2.2 → nbdev-3.3.1}/LICENSE +0 -0
  22. {nbdev-3.2.2 → nbdev-3.3.1}/MANIFEST.in +0 -0
  23. {nbdev-3.2.2 → nbdev-3.3.1}/README.md +0 -0
  24. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/cli.py +0 -0
  25. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/config.py +0 -0
  26. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/doclinks.py +0 -0
  27. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/export.py +0 -0
  28. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/extract_attachments.py +0 -0
  29. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/frontmatter.py +0 -0
  30. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/imports.py +0 -0
  31. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/maker.py +0 -0
  32. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/merge.py +0 -0
  33. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/qmd.py +0 -0
  34. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/quarto.py +0 -0
  35. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/scrubmagics.py +0 -0
  36. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/serve.py +0 -0
  37. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/serve_drv.py +0 -0
  38. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev/showdoc.py +0 -0
  39. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev.egg-info/dependency_links.txt +0 -0
  40. {nbdev-3.2.2 → nbdev-3.3.1}/nbdev.egg-info/top_level.txt +0 -0
  41. {nbdev-3.2.2 → nbdev-3.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdev
3
- Version: 3.2.2
3
+ Version: 3.3.1
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: Apache-2.0
@@ -13,13 +13,13 @@ Classifier: Natural Language :: English
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: License :: OSI Approved :: Apache Software License
16
- Requires-Python: >=3.9
16
+ Requires-Python: >=3.10
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
- Requires-Dist: fastcore>=1.14.6
20
- Requires-Dist: execnb>=0.2.4
19
+ Requires-Dist: fastcore>=2.1.3
20
+ Requires-Dist: execnb>=0.2.8
21
21
  Requires-Dist: astunparse
22
- Requires-Dist: ghapi>=2.0.0
22
+ Requires-Dist: ghapi>=2.0.2
23
23
  Requires-Dist: watchdog
24
24
  Requires-Dist: asttokens
25
25
  Requires-Dist: setuptools
@@ -1,4 +1,4 @@
1
- __version__ = "3.2.2"
1
+ __version__ = "3.3.1"
2
2
 
3
3
  from .doclinks import nbdev_export
4
4
  from .showdoc import show_doc
@@ -5,14 +5,23 @@ d = { 'settings': { 'branch': 'main',
5
5
  'doc_host': 'https://nbdev.fast.ai',
6
6
  'git_url': 'https://github.com/AnswerDotAI/nbdev',
7
7
  'lib_path': 'nbdev'},
8
- 'syms': { 'nbdev.clean': { 'nbdev.clean._add_jupyter_hooks': ('api/clean.html#_add_jupyter_hooks', 'nbdev/clean.py'),
8
+ 'syms': { 'nbdev.clean': { 'nbdev.clean._add_attrs': ('api/clean.html#_add_attrs', 'nbdev/clean.py'),
9
+ 'nbdev.clean._add_jupyter_hooks': ('api/clean.html#_add_jupyter_hooks', 'nbdev/clean.py'),
10
+ 'nbdev.clean._canon_dirs': ('api/clean.html#_canon_dirs', 'nbdev/clean.py'),
11
+ 'nbdev.clean._cfg_drivers': ('api/clean.html#_cfg_drivers', 'nbdev/clean.py'),
9
12
  'nbdev.clean._clean_cell': ('api/clean.html#_clean_cell', 'nbdev/clean.py'),
10
13
  'nbdev.clean._clean_cell_output': ('api/clean.html#_clean_cell_output', 'nbdev/clean.py'),
11
14
  'nbdev.clean._clean_cell_output_id': ('api/clean.html#_clean_cell_output_id', 'nbdev/clean.py'),
15
+ 'nbdev.clean._cmt_dirs': ('api/clean.html#_cmt_dirs', 'nbdev/clean.py'),
16
+ 'nbdev.clean._dir_moves': ('api/clean.html#_dir_moves', 'nbdev/clean.py'),
12
17
  'nbdev.clean._git_root': ('api/clean.html#_git_root', 'nbdev/clean.py'),
18
+ 'nbdev.clean._hoist_nb_meta': ('api/clean.html#_hoist_nb_meta', 'nbdev/clean.py'),
13
19
  'nbdev.clean._nbdev_clean': ('api/clean.html#_nbdev_clean', 'nbdev/clean.py'),
14
20
  'nbdev.clean._reconfigure': ('api/clean.html#_reconfigure', 'nbdev/clean.py'),
21
+ 'nbdev.clean._rm_dir_lines': ('api/clean.html#_rm_dir_lines', 'nbdev/clean.py'),
15
22
  'nbdev.clean._skip_or_sub': ('api/clean.html#_skip_or_sub', 'nbdev/clean.py'),
23
+ 'nbdev.clean._to_comments': ('api/clean.html#_to_comments', 'nbdev/clean.py'),
24
+ 'nbdev.clean._to_meta': ('api/clean.html#_to_meta', 'nbdev/clean.py'),
16
25
  'nbdev.clean.clean_jupyter': ('api/clean.html#clean_jupyter', 'nbdev/clean.py'),
17
26
  'nbdev.clean.clean_nb': ('api/clean.html#clean_nb', 'nbdev/clean.py'),
18
27
  'nbdev.clean.nbdev_clean': ('api/clean.html#nbdev_clean', 'nbdev/clean.py'),
@@ -61,11 +70,20 @@ d = { 'settings': { 'branch': 'main',
61
70
  'nbdev.config.update_version': ('api/config.html#update_version', 'nbdev/config.py'),
62
71
  'nbdev.config.write_cells': ('api/config.html#write_cells', 'nbdev/config.py')},
63
72
  'nbdev.diff': { 'nbdev.diff._cell_changes': ('api/diff.html#_cell_changes', 'nbdev/diff.py'),
73
+ 'nbdev.diff._diff_order': ('api/diff.html#_diff_order', 'nbdev/diff.py'),
74
+ 'nbdev.diff._dline': ('api/diff.html#_dline', 'nbdev/diff.py'),
75
+ 'nbdev.diff._file_srcs': ('api/diff.html#_file_srcs', 'nbdev/diff.py'),
64
76
  'nbdev.diff._nb_srcdict': ('api/diff.html#_nb_srcdict', 'nbdev/diff.py'),
77
+ 'nbdev.diff._src': ('api/diff.html#_src', 'nbdev/diff.py'),
78
+ 'nbdev.diff._srcdict': ('api/diff.html#_srcdict', 'nbdev/diff.py'),
65
79
  'nbdev.diff.cell_diffs': ('api/diff.html#cell_diffs', 'nbdev/diff.py'),
66
80
  'nbdev.diff.changed_cells': ('api/diff.html#changed_cells', 'nbdev/diff.py'),
81
+ 'nbdev.diff.nb_diff': ('api/diff.html#nb_diff', 'nbdev/diff.py'),
82
+ 'nbdev.diff.nbdev_diff': ('api/diff.html#nbdev_diff', 'nbdev/diff.py'),
83
+ 'nbdev.diff.nbdev_diff_driver': ('api/diff.html#nbdev_diff_driver', 'nbdev/diff.py'),
67
84
  'nbdev.diff.nbs_pair': ('api/diff.html#nbs_pair', 'nbdev/diff.py'),
68
85
  'nbdev.diff.read_nb_from_git': ('api/diff.html#read_nb_from_git', 'nbdev/diff.py'),
86
+ 'nbdev.diff.render_diff': ('api/diff.html#render_diff', 'nbdev/diff.py'),
69
87
  'nbdev.diff.source_diff': ('api/diff.html#source_diff', 'nbdev/diff.py')},
70
88
  'nbdev.doclinks': { 'nbdev.doclinks.NbdevLookup': ('api/doclinks.html#nbdevlookup', 'nbdev/doclinks.py'),
71
89
  'nbdev.doclinks.NbdevLookup.__getitem__': ( 'api/doclinks.html#nbdevlookup.__getitem__',
@@ -193,17 +211,9 @@ d = { 'settings': { 'branch': 'main',
193
211
  'nbdev.process.Processor.__call__': ('api/process.html#processor.__call__', 'nbdev/process.py'),
194
212
  'nbdev.process.Processor.__init__': ('api/process.html#processor.__init__', 'nbdev/process.py'),
195
213
  'nbdev.process.Processor.cell': ('api/process.html#processor.cell', 'nbdev/process.py'),
196
- 'nbdev.process._dir_pre': ('api/process.html#_dir_pre', 'nbdev/process.py'),
197
- 'nbdev.process._directive': ('api/process.html#_directive', 'nbdev/process.py'),
198
214
  'nbdev.process._is_direc': ('api/process.html#_is_direc', 'nbdev/process.py'),
199
215
  'nbdev.process._mk_procs': ('api/process.html#_mk_procs', 'nbdev/process.py'),
200
- 'nbdev.process._norm_quarto': ('api/process.html#_norm_quarto', 'nbdev/process.py'),
201
- 'nbdev.process._partition_cell': ('api/process.html#_partition_cell', 'nbdev/process.py'),
202
- 'nbdev.process._quarto_re': ('api/process.html#_quarto_re', 'nbdev/process.py'),
203
- 'nbdev.process.extract_directives': ('api/process.html#extract_directives', 'nbdev/process.py'),
204
- 'nbdev.process.first_code_ln': ('api/process.html#first_code_ln', 'nbdev/process.py'),
205
216
  'nbdev.process.instantiate': ('api/process.html#instantiate', 'nbdev/process.py'),
206
- 'nbdev.process.nb_lang': ('api/process.html#nb_lang', 'nbdev/process.py'),
207
217
  'nbdev.process.opt_set': ('api/process.html#opt_set', 'nbdev/process.py')},
208
218
  'nbdev.processors': { 'nbdev.processors.FilterDefaults': ('api/processors.html#filterdefaults', 'nbdev/processors.py'),
209
219
  'nbdev.processors.FilterDefaults.__call__': ( 'api/processors.html#filterdefaults.__call__',
@@ -357,6 +367,7 @@ d = { 'settings': { 'branch': 'main',
357
367
  'nbdev.showdoc.doc': ('api/showdoc.html#doc', 'nbdev/showdoc.py'),
358
368
  'nbdev.showdoc.show_doc': ('api/showdoc.html#show_doc', 'nbdev/showdoc.py'),
359
369
  'nbdev.showdoc.showdoc_nm': ('api/showdoc.html#showdoc_nm', 'nbdev/showdoc.py')},
370
+ 'nbdev.skill': {},
360
371
  'nbdev.sync': { 'nbdev.sync._mod_files': ('api/sync.html#_mod_files', 'nbdev/sync.py'),
361
372
  'nbdev.sync._to_absolute': ('api/sync.html#_to_absolute', 'nbdev/sync.py'),
362
373
  'nbdev.sync._update_mod': ('api/sync.html#_update_mod', 'nbdev/sync.py'),
@@ -13,6 +13,7 @@ from astunparse import unparse
13
13
  from textwrap import indent
14
14
 
15
15
  from fastcore.nbio import *
16
+ from fastcore.nbio import _directive, _dir_line, _meta_directives, _unparse_dir
16
17
  from fastcore.script import *
17
18
  from fastcore.utils import *
18
19
  from fastcore.xtras import *
@@ -93,11 +94,13 @@ def clean_nb(
93
94
  allowed_cell_metadata_keys:list=None, # Preserve the list of keys in cell level metadata
94
95
  clean_ids=True, # Remove ids from plaintext reprs?
95
96
  allowed_out_metadata_keys:list=None, # Preserve the list of keys in output metadata
97
+ repair:bool=True, # Fix structural problems first (see `repair_nb`)?
96
98
  ):
97
99
  "Clean `nb` from superfluous metadata"
98
- metadata_keys = {"kernelspec", "jekyll", "jupytext", "doc", "widgets"}
100
+ if repair: repair_nb(nb)
101
+ metadata_keys = {"kernelspec", "jekyll", "jupytext", "doc", "widgets", "nbdev"}
99
102
  if allowed_metadata_keys: metadata_keys.update(allowed_metadata_keys)
100
- cell_metadata_keys = {"hide_input"}
103
+ cell_metadata_keys = {"hide_input", "nbdev"}
101
104
  if allowed_cell_metadata_keys: cell_metadata_keys.update(allowed_cell_metadata_keys)
102
105
  out_meta_keys = set()
103
106
  if allowed_out_metadata_keys: out_meta_keys.update(allowed_out_metadata_keys)
@@ -126,14 +129,73 @@ def process_write(warn_msg, proc_nb, f_in, f_out=None, disp=False):
126
129
  warn(f'{warn_msg}')
127
130
  warn(e)
128
131
 
132
+ # %% ../nbs/api/11_clean.ipynb #f6e854ac
133
+ def _cmt_dirs(cell):
134
+ "Comment directives in `cell` as `{name: value}`, plus the partitioned `(dirs,code)` lines"
135
+ dirs,code = cell._partition()
136
+ return dict(t for t in (_directive(s, cell.lang_) for s in dirs) if t),dirs,code
137
+
138
+ def _rm_dir_lines(cell, dirs, code, names):
139
+ "Rewrite `cell` source without the directive lines in `names`"
140
+ cell.set_source(''.join([o for o in dirs if (t:=_directive(o, cell.lang_)) is None or t[0] not in names] + code))
141
+
142
+ def _to_meta(cell, names):
143
+ "Move comment directives in `names` to the cell's `nbdev` metadata key"
144
+ cmts,dirs,code = _cmt_dirs(cell)
145
+ move = {k:v for k,v in cmts.items() if k in names}
146
+ if not move: return
147
+ cell.setdefault('metadata',{}).setdefault('nbdev',{}).update({k:_unparse_dir(v) for k,v in move.items()})
148
+ _rm_dir_lines(cell, dirs, code, move)
149
+
150
+ def _to_comments(cell, names):
151
+ "Move directives in `names` from the cell's `nbdev` metadata key to comments"
152
+ move = {k:v for k,v in _meta_directives(cell.get('metadata')).items() if k in names}
153
+ if not move: return
154
+ nbd = cell.metadata['nbdev']
155
+ for k in move: nbd.pop(k, None)
156
+ if not nbd: del cell.metadata['nbdev']
157
+ dirs,code = cell._partition()
158
+ cell.set_source(''.join(dirs + [_dir_line(k, v, cell.lang_) for k,v in move.items()] + code))
159
+
160
+ # %% ../nbs/api/11_clean.ipynb #6e8d013d
161
+ def _canon_dirs(cell):
162
+ "Rewrite `cell`'s comment directives in canonical form (colon-separated, bare for true)"
163
+ dirs,code = cell._partition()
164
+ new = [o if (t:=_directive(o, cell.lang_)) is None else _dir_line(*t, lang=cell.lang_) for o in dirs]
165
+ if new != dirs: cell.set_source(''.join(new + code))
166
+
167
+ def _hoist_nb_meta(nb, names=('default_exp',)):
168
+ "Move notebook-scope directives in `names` to notebook metadata, dropping any cell left empty"
169
+ for c in list(nb.cells):
170
+ cmts,dirs,code = _cmt_dirs(c)
171
+ move = {k:v for k,v in cmts.items() if k in names}
172
+ if not move: continue
173
+ nb.setdefault('metadata',{}).setdefault('nbdev',{}).update({k:_unparse_dir(v) for k,v in move.items()})
174
+ _rm_dir_lines(c, dirs, code, move)
175
+ left = set(c.directives) - {'hide'}
176
+ if not left and not ''.join(c._partition()[1]).strip(): nb.cells.remove(c)
177
+
178
+ def _dir_moves(nb, dirs=False, to_meta=None, to_comments=None, nb_meta=False):
179
+ "Apply directive migrations to loaded notebook dict `nb` in place"
180
+ nbo = dict2nb(nb)
181
+ if to_meta:
182
+ for c in nbo.cells: _to_meta(c, to_meta.split())
183
+ if to_comments:
184
+ for c in nbo.cells: _to_comments(c, to_comments.split())
185
+ if nb_meta: _hoist_nb_meta(nbo)
186
+ if dirs:
187
+ for c in nbo.cells: _canon_dirs(c)
188
+ nb['cells'],nb['metadata'] = nbo.cells,nbo.metadata
189
+
129
190
  # %% ../nbs/api/11_clean.ipynb #714357ce
130
- def _nbdev_clean(nb, path=None, clear_all=None):
191
+ def _nbdev_clean(nb, path=None, clear_all=None, repair=True, dirs=False, to_meta=None, to_comments=None, nb_meta=False):
131
192
  cfg = get_config(path=path)
132
193
  clear_all = clear_all or cfg.clear_all
133
194
  allowed_metadata_keys = cfg.get("allowed_metadata_keys") or []
134
195
  allowed_cell_metadata_keys = cfg.get("allowed_cell_metadata_keys") or []
135
196
  allowed_out_metadata_keys = cfg.get("allowed_out_metadata_keys") or []
136
- clean_nb(nb, clear_all, allowed_metadata_keys, allowed_cell_metadata_keys, cfg.clean_ids, allowed_out_metadata_keys)
197
+ if dirs or to_meta or to_comments or nb_meta: _dir_moves(nb, dirs, to_meta, to_comments, nb_meta)
198
+ clean_nb(nb, clear_all, allowed_metadata_keys, allowed_cell_metadata_keys, cfg.clean_ids, allowed_out_metadata_keys, repair=repair)
137
199
  if path: nbdev_trust.__wrapped__(path)
138
200
 
139
201
  # %% ../nbs/api/11_clean.ipynb #6af3b9d4
@@ -142,11 +204,16 @@ def nbdev_clean(
142
204
  fname:str=None, # A notebook name or glob to clean
143
205
  clear_all:bool=False, # Remove all cell metadata and cell outputs?
144
206
  disp:bool=False, # Print the cleaned outputs
145
- stdin:bool=False # Read notebook from input stream
207
+ stdin:bool=False, # Read notebook from input stream
208
+ repair:bool_arg=True, # Fix structural problems, e.g. stray outputs on non-code cells (see `repair_nb`)?
209
+ dirs:bool=False, # Rewrite comment directives in canonical form?
210
+ to_meta:str=None, # Space-separated directive names to move from comments to cell metadata
211
+ to_comments:str=None, # Space-separated directive names to move from cell metadata to comments
212
+ nb_meta:bool=False # Move `default_exp` into notebook metadata?
146
213
  ):
147
214
  "Clean all notebooks in `fname` to avoid merge conflicts"
148
215
  # Git hooks will pass the notebooks in stdin
149
- _clean = partial(_nbdev_clean, clear_all=clear_all)
216
+ _clean = partial(_nbdev_clean, clear_all=clear_all, repair=repair, dirs=dirs, to_meta=to_meta, to_comments=to_comments, nb_meta=nb_meta)
150
217
  _write = partial(process_write, warn_msg='Failed to clean notebook', proc_nb=_clean)
151
218
  if stdin: return _write(f_in=sys.stdin, f_out=sys.stdout)
152
219
  if fname is None: fname = get_config().nbs_path
@@ -193,8 +260,29 @@ def _git_root():
193
260
  except OSError: return None
194
261
 
195
262
  # %% ../nbs/api/11_clean.ipynb #e6083614
263
+ def _add_attrs(path, attrs):
264
+ "Append missing attribute lines to git attributes file at `path`"
265
+ txt = path.read_text() if path.exists() else ''
266
+ have = [l.split() for l in txt.splitlines()] # whitespace-insensitive: nbdime writes its lines with tabs
267
+ for attr in attrs:
268
+ if attr.split() not in have:
269
+ if txt and not txt.endswith('\n'): txt+='\n'
270
+ txt += attr+'\n'
271
+ path.write_text(txt)
272
+
273
+ def _cfg_drivers(loc, merge, diff):
274
+ "Define the `jupyternotebook` merge/diff drivers via `git config <loc>`"
275
+ if merge:
276
+ run(f'git config {loc} merge.jupyternotebook.name "resolve conflicts with nbdev_fix"')
277
+ run(f'git config {loc} merge.jupyternotebook.driver "nbdev-merge %O %A %B %P"')
278
+ if diff: run(f'git config {loc} diff.jupyternotebook.command nbdev-diff-driver')
279
+
196
280
  @call_parse
197
- def nbdev_install_hooks():
281
+ def nbdev_install_hooks(
282
+ merge:bool_arg=True, # Install the notebook merge driver?
283
+ diff:bool_arg=True, # Install the notebook diff driver?
284
+ globally:bool_arg=False # Define the drivers in `~/.gitconfig` and the global attributes file, instead of repo files?
285
+ ):
198
286
  "Install Jupyter and git hooks to automatically clean, trust, and fix merge conflicts in notebooks"
199
287
  cfg_path = Path.home()/'.jupyter'
200
288
  cfg_path.mkdir(exist_ok=True)
@@ -204,6 +292,16 @@ def nbdev_install_hooks():
204
292
  upd = _add_jupyter_hooks(src, fn)
205
293
  if upd is not None: fn.write_text(upd)
206
294
 
295
+ nbdev_attrs = (['*.ipynb merge=jupyternotebook'] if merge else []) + (['*.ipynb diff=jupyternotebook'] if diff else [])
296
+ if globally:
297
+ _cfg_drivers('--global', merge, diff)
298
+ rc,p = run('git config --global --get core.attributesFile', ignore_ex=True)
299
+ if rc: p = os.environ.get('XDG_CONFIG_HOME', '~/.config') + '/git/attributes'
300
+ attrs_path = Path(p).expanduser()
301
+ attrs_path.parent.mkdir(parents=True, exist_ok=True)
302
+ _add_attrs(attrs_path, nbdev_attrs)
303
+ return print("Hooks are installed globally.")
304
+
207
305
  repo_path = _git_root()
208
306
  if repo_path is None:
209
307
  sys.stderr.write('Not in a git repository, git hooks cannot be installed.\n')
@@ -215,7 +313,8 @@ def nbdev_install_hooks():
215
313
  os.chmod(fn, os.stat(fn).st_mode | stat.S_IEXEC)
216
314
 
217
315
  cmd = 'git config --local include.path ../.gitconfig'
218
- (repo_path/'.gitconfig').write_text(f'''# Generated by nbdev-install-hooks
316
+ cfg_fn = repo_path/'.gitconfig'
317
+ cfg_fn.write_text(f'''# Generated by nbdev-install-hooks
219
318
  #
220
319
  # If you need to disable this instrumentation do:
221
320
  # git config --local --unset include.path
@@ -223,19 +322,10 @@ def nbdev_install_hooks():
223
322
  # To restore:
224
323
  # {cmd}
225
324
  #
226
- [merge "nbdev-merge"]
227
- name = resolve conflicts with nbdev_fix
228
- driver = nbdev-merge %O %A %B %P
229
325
  ''')
326
+ _cfg_drivers(f'--file "{cfg_fn}"', merge, diff)
230
327
  run(cmd)
231
328
 
232
- attrs_path = repo_path/'.gitattributes'
233
- nbdev_attr = '*.ipynb merge=nbdev-merge\n'
234
- try:
235
- attrs = attrs_path.read_text()
236
- if nbdev_attr not in attrs:
237
- if not attrs.endswith('\n'): attrs+='\n'
238
- attrs_path.write_text(attrs+nbdev_attr)
239
- except FileNotFoundError: attrs_path.write_text(nbdev_attr)
329
+ _add_attrs(repo_path/'.gitattributes', nbdev_attrs)
240
330
 
241
331
  print("Hooks are installed.")
@@ -0,0 +1,198 @@
1
+ """Cell-level git diffs for notebooks
2
+
3
+ Docs: https://nbdev.fast.ai/api/diff.html.md"""
4
+
5
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/19_diff.ipynb.
6
+
7
+ # %% auto #0
8
+ __all__ = ['read_nb_from_git', 'nbs_pair', 'changed_cells', 'source_diff', 'cell_diffs', 'render_diff', 'nb_diff',
9
+ 'nbdev_diff_driver', 'nbdev_diff']
10
+
11
+ # %% ../nbs/api/19_diff.ipynb #10c8ad0b
12
+ import json
13
+ from fastcore.utils import *
14
+ from fastcore.meta import delegates
15
+ from fastcore.script import *
16
+ from difflib import unified_diff
17
+ from subprocess import CalledProcessError
18
+ from typing import Annotated
19
+ from fastgit import Git
20
+ from fastcore.nbio import *
21
+ from .doclinks import nbglob
22
+
23
+ # %% ../nbs/api/19_diff.ipynb #a8981115
24
+ def read_nb_from_git(
25
+ g:Git, # The git object
26
+ path, # The path to the notebook (absolute or relative to git root)
27
+ ref=None # The git ref to read from (e.g. HEAD); None for working dir
28
+ )->AttrDict: # The notebook; empty if `path` doesn't exist at `ref`
29
+ "Read notebook from git ref (e.g. HEAD) at path, or working dir if ref is None"
30
+ path = Path(path)
31
+ if path.is_absolute(): path = path.relative_to(g.top())
32
+ if ref is None: return read_nb(g.top()/path)
33
+ try: raw = g.show(f'{ref}:{path}', raise_exc=True)
34
+ except CalledProcessError as e:
35
+ if " in '" not in str(e.stderr): raise
36
+ return new_nb([])
37
+ return dict2nb(json.loads(raw))
38
+
39
+ # %% ../nbs/api/19_diff.ipynb #3ac25702
40
+ def _src(c): return c.get('source','')
41
+
42
+ def _srcdict(nb, f=noop):
43
+ "Dict of cell id->`f(cell)`, with positional fallback ids for pre-4.5 notebooks"
44
+ return {c.get('id', f'c{c.idx_}'): f(c) for c in nb.cells}
45
+
46
+ def _nb_srcdict(g:Git, nb_path, ref=None, f=noop): return _srcdict(read_nb_from_git(g, nb_path, ref), f)
47
+
48
+ # %% ../nbs/api/19_diff.ipynb #27ff9a7b
49
+ def nbs_pair(
50
+ nb_path, # Path to the notebook
51
+ ref_a='HEAD', # First git ref (None for working dir)
52
+ ref_b=None, # Second git ref (None for working dir)
53
+ f=noop # Function to call on contents
54
+ ): # Tuple of two notebooks
55
+ "NBs at two refs; None means working dir. By default provides HEAD and working dir"
56
+ nb_path = Path(nb_path).resolve()
57
+ g = Git(nb_path.parent)
58
+ return _nb_srcdict(g, nb_path, ref_a, f), _nb_srcdict(g, nb_path, ref_b, f)
59
+
60
+ # %% ../nbs/api/19_diff.ipynb #958aeaf8
61
+ def _cell_changes(
62
+ nb_path, # Path to the notebook
63
+ fn, # function to call to get dict values
64
+ ref_a='HEAD', # First git ref (None for working dir)
65
+ ref_b=None, # Second git ref (None for working dir)
66
+ adds=True, # Include cells in b but not in a
67
+ changes=True, # Include cells with different content
68
+ dels=False, # Include cells in a but not in b
69
+ metadata=False, # Consider cell metadata when comparing
70
+ outputs=False # Consider cell outputs when comparing
71
+ ): # Dict of results
72
+ "Apply fn(cell_id, old_content, new_content) to changed cells between two refs"
73
+ def cell_content(c):
74
+ res = c.get('source', '')
75
+ if metadata: res += '\n# metadata: ' + json.dumps(c.get('metadata', {}), sort_keys=True)
76
+ if outputs: res += '\n# outputs: ' + json.dumps(c.get('outputs', []), sort_keys=True)
77
+ return res
78
+ old,new = nbs_pair(nb_path, ref_a, ref_b, f=cell_content)
79
+ res = {}
80
+ if adds: res |= {cid: fn(cid, '', new[cid]) for cid in new if cid not in old}
81
+ if changes: res |= {cid: fn(cid, old[cid], new[cid]) for cid in new if cid in old and new[cid] != old[cid]}
82
+ if dels: res |= {cid: fn(cid, old[cid], '') for cid in old if cid not in new}
83
+ return res
84
+
85
+ # %% ../nbs/api/19_diff.ipynb #6a723bc8
86
+ @delegates(_cell_changes)
87
+ def changed_cells(nb_path, **kwargs):
88
+ "Return set of cell IDs for changed/added/deleted cells between two refs"
89
+ def f(cid,o,n): return cid
90
+ return set(_cell_changes(nb_path, f, **kwargs).keys())
91
+
92
+ # %% ../nbs/api/19_diff.ipynb #079eedb0
93
+ def source_diff(
94
+ old_source, # Original source string
95
+ new_source # New source string
96
+ ): # Unified diff string
97
+ "Return unified diff string for source change"
98
+ return '\n'.join(unified_diff(old_source.splitlines(), new_source.splitlines(), lineterm=''))
99
+
100
+ # %% ../nbs/api/19_diff.ipynb #38216afa
101
+ @delegates(_cell_changes)
102
+ def cell_diffs(nb_path, **kwargs):
103
+ "{cell_id:diff} for changed/added/deleted cells between two refs"
104
+ def f(cid,o,n): return source_diff(o,n)
105
+ return _cell_changes(nb_path, f, **kwargs)
106
+
107
+ # %% ../nbs/api/19_diff.ipynb #d670e58e
108
+ _dcolors = {'-':'\x1b[31m', '+':'\x1b[32m', '@':'\x1b[36m', '#':'\x1b[1m', '# ':'\x1b[1;7m'}
109
+
110
+ def _dline(l, maxlen=120, color=False):
111
+ "Truncate `l` to `maxlen` with an ellipsis, optionally colored by its leading diff marker"
112
+ if maxlen and len(l)>maxlen: l = l[:maxlen]+'…'
113
+ c = (_dcolors.get(l[:2]) or _dcolors.get(l[:1])) if color else None
114
+ return f'{c}{l}\x1b[0m' if c else l
115
+
116
+ # %% ../nbs/api/19_diff.ipynb #342fc231
117
+ def _diff_order(old, new):
118
+ "All cell ids in notebook order: `new`'s order, with deleted ids after their predecessor in `old`"
119
+ res,olds = list(new),list(old)
120
+ for i,cid in enumerate(olds):
121
+ if cid in new: continue
122
+ pred = first(o for o in reversed(olds[:i]) if o in res)
123
+ res.insert(res.index(pred)+1 if pred else 0, cid)
124
+ return res
125
+
126
+ # %% ../nbs/api/19_diff.ipynb #eb3fc128
127
+ def render_diff(
128
+ old, # `{id: source}` for the old version of the notebook
129
+ new, # `{id: source}` for the new version
130
+ maxlen:int=120, # Truncate diff lines to this width (falsy: no limit)
131
+ color:bool=False # Add ANSI colors?
132
+ ): # One section per changed cell, in notebook order
133
+ "Render cell-level changes between two notebooks as truncated unified diffs"
134
+ res = []
135
+ for cid in _diff_order(old, new):
136
+ o,n = old.get(cid,''), new.get(cid,'')
137
+ if cid in old and cid in new and o==n: continue
138
+ kind = 'modified' if cid in old and cid in new else 'added' if cid in new else 'deleted'
139
+ lines = [f'## {kind} {cid}:'] + source_diff(o,n).splitlines()[2:]
140
+ res.append('\n'.join(_dline(l, maxlen, color) for l in lines))
141
+ return '\n\n'.join(res)
142
+
143
+ # %% ../nbs/api/19_diff.ipynb #7f90d675
144
+ def nb_diff(
145
+ nb_path, # Path to the notebook
146
+ ref_a='HEAD', # First git ref
147
+ ref_b=None, # Second git ref; None for working dir
148
+ maxlen:int=120, # Truncate diff lines to this width (falsy: no limit)
149
+ color:bool=False # Add ANSI colors?
150
+ ): # Rendered diff of changed cells
151
+ "Rendered cell diff for `nb_path` between two refs"
152
+ a,b = nbs_pair(nb_path, ref_a, ref_b, f=_src)
153
+ return render_diff(a, b, maxlen=maxlen, color=color)
154
+
155
+ # %% ../nbs/api/19_diff.ipynb #ab9f0b26
156
+ def _file_srcs(path):
157
+ "`{id: source}` from an ipynb file; `{}` when missing or empty (e.g. `/dev/null`)"
158
+ p = Path(path)
159
+ if not p.exists() or not p.stat().st_size: return {}
160
+ return _srcdict(read_nb(p), _src)
161
+
162
+ _pos = Annotated[str, {'opt':False, 'nargs':'?'}] # optional positional CLI arg, as git passes them
163
+
164
+ @call_parse
165
+ def nbdev_diff_driver(
166
+ path:str, # Repo path of the notebook being diffed
167
+ old_file:_pos=None, # Pre-change file (git temp file, or /dev/null)
168
+ old_hex:_pos=None, # Pre-change blob hash
169
+ old_mode:_pos=None, # Pre-change file mode
170
+ new_file:_pos=None, # Post-change file
171
+ new_hex:_pos=None, # Post-change blob hash
172
+ new_mode:_pos=None, # Post-change file mode
173
+ rename_to:_pos=None, # New repo path, when git detected a rename
174
+ similarity:_pos=None, # Similarity score, when git detected a rename
175
+ maxlen:int=120 # Truncate diff lines to this width (0 for no limit)
176
+ ):
177
+ "Git external diff driver for notebooks; installed by `nbdev-install-hooks`"
178
+ if not new_file: return print(f'unmerged: {path}')
179
+ color = sys.stdout.isatty() or os.environ.get('GIT_PAGER_IN_USE')=='true'
180
+ res = render_diff(_file_srcs(old_file), _file_srcs(new_file), maxlen=maxlen, color=color)
181
+ if not res: return
182
+ print(_dline(f'# {path}', 0, color))
183
+ print(res, end='\n\n')
184
+
185
+ # %% ../nbs/api/19_diff.ipynb #2604c719
186
+ @call_parse
187
+ def nbdev_diff(
188
+ path:str=None, # Notebook or directory (default: project's notebooks folder)
189
+ ref_a:str='HEAD', # First git ref
190
+ ref_b:str=None, # Second git ref (default: working directory)
191
+ maxlen:int=120, # Truncate diff lines to this width (0 for no limit)
192
+ color:bool_arg=None # Add ANSI colors? (default: only if stdout is a tty)
193
+ ):
194
+ "Cell-level diffs for changed notebooks between two git refs"
195
+ if color is None: color = sys.stdout.isatty()
196
+ for p in sorted(nbglob(path)):
197
+ res = nb_diff(p, ref_a, ref_b, maxlen=maxlen, color=color)
198
+ if res: print(_dline(f'# {p}', 0, color), res, sep='\n', end='\n\n')
@@ -9,6 +9,7 @@ __all__ = ['MigrateProc', 'fp_md_fm', 'migrate_nb', 'migrate_md', 'nbdev_migrate
9
9
 
10
10
  # %% ../nbs/api/16_migrate.ipynb #5b687fa0-dc50-48df-8bfc-e98df34e7572
11
11
  from .process import *
12
+ from fastcore.nbio import first_code_ln, langs
12
13
  from .frontmatter import *
13
14
  from .frontmatter import _fm2dict, _re_fm_md, _dict2fm, _insertfm
14
15
  from .processors import *
@@ -0,0 +1,88 @@
1
+ """A notebook processor
2
+
3
+ Docs: https://nbdev.fast.ai/api/process.html.md"""
4
+
5
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/03_process.ipynb.
6
+
7
+ # %% auto #0
8
+ __all__ = ['opt_set', 'instantiate', 'NBProcessor', 'Processor']
9
+
10
+ # %% ../nbs/api/03_process.ipynb #59172c3d
11
+ from .config import *
12
+ from .maker import *
13
+ from .imports import *
14
+
15
+ from fastcore.nbio import *
16
+ from fastcore.nbio import _meta_directives
17
+ from fastcore.script import *
18
+ from fastcore.imports import *
19
+
20
+ from collections import defaultdict
21
+
22
+ # %% ../nbs/api/03_process.ipynb #e6701805
23
+ def opt_set(var, newval):
24
+ "newval if newval else var"
25
+ return newval if newval else var
26
+
27
+ # %% ../nbs/api/03_process.ipynb #98c9d556
28
+ def instantiate(x, **kwargs):
29
+ "Instantiate `x` if it's a type"
30
+ return x(**kwargs) if isinstance(x,type) else x
31
+
32
+ def _mk_procs(procs, nb): return L(procs).map(instantiate, nb=nb)
33
+
34
+ # %% ../nbs/api/03_process.ipynb #ab147efe
35
+ def _is_direc(f): return getattr(f, '__name__', '-')[-1]=='_'
36
+
37
+ # %% ../nbs/api/03_process.ipynb #7c81f109
38
+ class NBProcessor:
39
+ "Process cells and nbdev comments in a notebook"
40
+ def __init__(self, path=None, procs=None, nb=None, debug=False, rm_directives=True, process=False):
41
+ self.nb = read_nb(path) if nb is None else nb
42
+ self.lang = nb_lang(self.nb)
43
+ for cell in self.nb.cells: cell.directives_ = cell.directives
44
+ nbdirs = {k:v for k,v in _meta_directives(self.nb.get('metadata')).items() if not any(k in c.directives_ for c in self.nb.cells)}
45
+ if nbdirs:
46
+ fc = first(c for c in self.nb.cells if c.cell_type=='code')
47
+ if fc is not None: fc.directives_ = nbdirs | fc.directives_
48
+ if rm_directives:
49
+ for cell in self.nb.cells: cell.remove_directives(quarto=rm_directives=='quarto')
50
+ self.procs = _mk_procs(procs, nb=self.nb)
51
+ self.debug,self.rm_directives = debug,rm_directives
52
+ if process: self.process()
53
+
54
+ def _process_cell(self, proc, cell):
55
+ if not hasattr(cell,'source'): return
56
+ if cell.cell_type=='code' and cell.directives_:
57
+ # Option 1: `proc` is directive name with `_` suffix
58
+ f = getattr(proc, '__name__', '-').rstrip('_')
59
+ if f in cell.directives_: self._process_comment(proc, cell, f)
60
+
61
+ # Option 2: `proc` contains a method named `_{directive}_`
62
+ for cmd in cell.directives_:
63
+ f = getattr(proc, f'_{cmd}_', None)
64
+ if f: self._process_comment(f, cell, cmd)
65
+ if callable(proc) and not _is_direc(proc): cell = opt_set(cell, proc(cell))
66
+
67
+ def _process_comment(self, proc, cell, cmd):
68
+ args = cell.directives_[cmd].split()
69
+ if self.debug: print(cmd, args, proc)
70
+ return proc(cell, *args)
71
+
72
+ def _proc(self, proc):
73
+ if hasattr(proc,'begin'): proc.begin()
74
+ for cell in self.nb.cells: self._process_cell(proc, cell)
75
+ if hasattr(proc,'end'): proc.end()
76
+ self.nb.cells = [c for c in self.nb.cells if c and getattr(c,'source',None) is not None]
77
+ for i,cell in enumerate(self.nb.cells): cell.idx_ = i
78
+
79
+ def process(self):
80
+ "Process all cells with all processors"
81
+ for proc in self.procs: self._proc(proc)
82
+
83
+ # %% ../nbs/api/03_process.ipynb #fa1f8668
84
+ class Processor:
85
+ "Base class for processors"
86
+ def __init__(self, nb): self.nb = nb
87
+ def cell(self, cell): pass
88
+ def __call__(self, cell): return self.cell(cell)
@@ -68,7 +68,7 @@ def cell_lang(cell): return nested_attr(cell, 'metadata.language', 'python')
68
68
  def _want_doc(c):
69
69
  d = c.directives_
70
70
  show_d = set(['export', 'exports', 'exec_doc']).intersection(d)
71
- return c.source and c.cell_type=='code' and show_d and 'hide' not in d and d.get('include:') != ['false']
71
+ return c.source and c.cell_type=='code' and show_d and 'hide' not in d and d.get('include') != 'false'
72
72
 
73
73
  class add_show_docs(Processor):
74
74
  "Add show_doc cells after exported cells, unless they are already documented"
@@ -234,7 +234,7 @@ def _do_eval(cell):
234
234
  if not cell.source or 'nbdev_export'+'()' in cell.source: return
235
235
  trees = cell.parsed_()
236
236
  if cell.cell_type != 'code' or not trees: return
237
- if cell.directives_.get('eval:', [''])[0].lower() == 'false': return
237
+ if cell.directives_.get('eval', '').lower() == 'false': return
238
238
 
239
239
  _show_dirs = {'export','exports','exporti','exec_doc'}
240
240
  if cell.directives_.keys() & _show_dirs: return True
@@ -292,6 +292,6 @@ class FilterDefaults:
292
292
 
293
293
  def nb_proc(self, nb):
294
294
  "Get an `NBProcessor` with these processors"
295
- return NBProcessor(nb=nb, procs=self.procs())
295
+ return NBProcessor(nb=nb, procs=self.procs(), rm_directives='quarto')
296
296
 
297
297
  def __call__(self, nb): return self.nb_proc(nb).process()
@@ -63,7 +63,7 @@ def _release_head():
63
63
  def update_changelog(txt, ver, notes, marker='<!-- do not remove -->\n'):
64
64
  "Insert `notes` into changelog `txt` after `marker`, replacing any existing section for `ver`"
65
65
  txt = re.sub(rf'\n## {re.escape(ver)}\n.*?(?=\n## |\Z)', '', txt, flags=re.S)
66
- return txt.replace(marker, marker+notes+'\n')
66
+ return txt.replace(marker, marker+notes.rstrip('\n')+'\n\n').rstrip('\n')+'\n'
67
67
 
68
68
  # %% ../nbs/api/18_release.ipynb #0b36471a
69
69
  class Release:
@@ -121,7 +121,9 @@ async def release(self:Release):
121
121
  "Tag and create a release in GitHub for the current version"
122
122
  ver = self.cfg.version
123
123
  notes = self.latest_notes()
124
- await self.gh.create_release(ver, branch=_release_head(), body=notes)
124
+ default = (await self.gh.repos.get()).default_branch
125
+ latest = 'true' if _release_branch()==default else 'false'
126
+ await self.gh.create_release(ver, branch=_release_head(), body=notes, make_latest=latest)
125
127
  return ver
126
128
 
127
129
  # %% ../nbs/api/18_release.ipynb #22101171
@@ -0,0 +1,56 @@
1
+ """Author clear, executable nbdev notebooks where code, prose, examples, outputs, and tests form one coherent narrative.
2
+
3
+ # The notebook is the product
4
+
5
+ An nbdev notebook is source, docs, examples, and tests at once: the rendered page explains the public API, and executing the same cells builds and verifies it. Write it to be read top to bottom, interleaving implementation with explanation, examples, outputs, and failure demonstrations. These conventions matter most for published libraries with docs sites; in internal projects, match the surrounding notebooks instead.
6
+
7
+ # Notebooks generate modules
8
+
9
+ `nbdev-export` writes exported cells to the module named by `#| default_exp`; never hand-edit generated `.py` files, `__all__`, `_modidx.py`, or generated module docstrings: the next export overwrites them. Each exported section's marker (`# %% ../nbs/04_usage.ipynb #a45f753a`) leads back to the cell to change. The module docstring is the title cell's `>` description joined with every exported markdown cell after the H1; for a long docstring, prefer one exported markdown cell per section.
10
+
11
+ Projects mix notebook-sourced and plain modules deliberately: notebooks where narrative earns its keep (public API with a docs page, code best explained through examples, tests that double as documentation), plain `.py` for dense mechanism iterated quickly under pytest. Check the file, never the repo: view its first few lines - a generated module opens with the `# AUTOGENERATED! DO NOT EDIT!` warning naming its source notebook, and `# %%` cell markers precede each definition (so a few lines of context around any def also answer it). No marker means hand-written and edited directly. Tests follow the source form: cells for notebook modules, `tests/*.py` for plain ones.
12
+
13
+ For project-level questions, read `nbs/index.ipynb`, not `README.md`: the README is generated from it, renders worse, and can be stale if regeneration lagged. When they disagree, index.ipynb is the truth; regenerate with `nbdev-readme` after editing it.
14
+
15
+ # Structure
16
+
17
+ Pick one documentation form deliberately rather than mixing accidentally: tutorial (guided learning), how-to (a practical task), explanation (a focused topic), or reference (a component and its API, easy to scan).
18
+
19
+ Open with an H1 title cell plus `>` subtitle line, introduce the page in a sentence or two, and get to the subject quickly. H2 headings divide major concepts; don't fragment a short narrative into tiny sections. Every heading goes in its own markdown cell, separate from body prose: Jupyter collapses sections by cell.
20
+
21
+ # Develop one idea at a time
22
+
23
+ The rhythm: add the smallest useful implementation, explain what it does and why it has that form, demonstrate it executably, assert where that improves the example, display the result a reader should notice, move on. A cell may bundle several closely related checks, but needs a markdown introduction saying what it establishes; if the introduction gets complicated, split the cell. Prefer extending an existing example over adding a near-duplicate cell. Build classes incrementally with `@patch` so each method sits beside its explanation, unless splitting makes the API harder to understand.
24
+
25
+ # Helpers
26
+
27
+ Internal use is often the first evidence an abstraction is worth exposing, not a reason to hide it. Reserve underscores for machinery with no coherent independent contract; a helper only useful to the library itself hints at an awkward internal design.
28
+
29
+ # Prose and docstrings
30
+
31
+ Markdown explains what the code cannot: why an abstraction exists, what distinction the next example demonstrates, which details are guarantees and which merely describe an example. Never narrate code line by line or write empty transitions ("Now we test the function"); state the lesson ("A missing leaf returns `None`, so success status alone does not establish existence"). Place prose where it reads correctly on the generated page, normally right after the definition it explains.
32
+
33
+ Keep docstrings to a short statement of what the symbol does; extended explanation, examples, and warnings go in markdown cells, where they render properly and can include executable results. Document parameters and returns with docments, keeping their docs beside the signature instead of repeating it in the docstring. Backtick symbol names in prose; nbdev links them, so prefer names over hand-maintained URLs.
34
+
35
+ # Examples are documentation and tests
36
+
37
+ Write each example as page content first, then make it verify behavior: realistic values, the shortest path to the idea, an informative displayed result, direct assertions that reinforce the lesson, reuse of objects introduced earlier, and important errors demonstrated executably with `expect_fail` (one focused example per contract). Keep test plumbing out of reader-facing cells: mocks, dense comprehensions, and long setup make poor documentation, so extract a tiny helper or hide the check. Assertions verify; the final expression teaches: end cells with the value worth displaying, and design a compact `_repr_markdown_` or structured summary when it turns later examples into documentation for free. Plots, tables, images, and rich HTML all count as evidence. Stored outputs are part of the explanation: keep them focused, and never dump a large structure without saying what matters in it. Test helpers come from `fastcore.test` (`test_eq`, `expect_fail`, ...), in plain code cells. Show rather than claim; executable evidence keeps documentation synchronized with behavior.
38
+
39
+ # State flows downward
40
+
41
+ Keep imports in dedicated import cells, define values near first use, reuse established objects, don't reassign names later cells depend on, introduce shared setup explicitly, and end exploratory cells with the expression whose output records what was learned, so a reader never searches far upward for where a value came from. The import rule is strict and covers test cells: the docs build runs each import-containing cell in a fresh namespace where no other cell has run, so a cell mixing imports with other code breaks the build or silently runs at documentation time.
42
+
43
+ # Directives
44
+
45
+ `#| default_exp` names the module. `#| export` marks exported cells; underscore-prefixed helpers may export without joining the public API. `#| hide` keeps necessary but distracting material off the page. `#| eval: false` is for cells that genuinely must not run, not for suppressing broken ones, and it cascades: an unevaluated cell cannot create state for later evaluated cells.
46
+
47
+ # nbdev v3
48
+
49
+ Everything here describes nbdev v3 (released Jan 2026, possibly after a model's training cutoff). Key user-visible changes from v2: config moved from `settings.ini` to `pyproject.toml` - standard metadata in `[project]`, nbdev-specific keys in `[tool.nbdev]` (defaults `nbs_path='nbs'`, `doc_path='_docs'`), version in `__init__.py` via `dynamic = ["version"]`, and `_modidx` registered under `[project.entry-points.nbdev]`. CLI commands use hyphens (`nbdev-export`, `nbdev-test`, ...) though Python functions keep underscores. GitHub workflows use the v3 actions (`fastai/workflows/nbdev3-ci`, `quarto-ghp3`).
50
+
51
+ # Before you finish
52
+
53
+ Each new test cell ran red then green: it failed before the change and passed after. Re-read the touched section as a reader would, against the conventions above - style damage fails silently: tests pass, export succeeds, and the harm only shows on the docs page.
54
+
55
+ The governing test: pleasant and informative to read, convincing when executed, useful as the source of the library it documents.
56
+ """
@@ -12,7 +12,6 @@ from .imports import *
12
12
  from .config import *
13
13
  from .maker import *
14
14
  from .process import *
15
- from .process import _partition_cell
16
15
  from .export import *
17
16
  from .doclinks import _iter_py_cells
18
17
 
@@ -61,7 +60,7 @@ def _update_nb(nb_path, cells, lib_dir):
61
60
  nbcell = nb_cells_by_id.get(cell.cell_id)
62
61
  if nbcell is None:
63
62
  raise ValueError(f"Cell ID '{cell.cell_id}' not found in notebook '{nb_path}'")
64
- dirs,_ = _partition_cell(nbcell, 'python')
63
+ dirs,_ = nbcell._partition()
65
64
  nbcell.source = ''.join(dirs) + _to_absolute(cell.code, cell.py_path, lib_dir)
66
65
  write_nb(nbp.nb, nb_path)
67
66
 
@@ -52,7 +52,7 @@ def test_nb(
52
52
  if cell.cell_type != 'code': return True
53
53
  if 'nbdev_export'+'(' in cell.source: return True
54
54
  direc = getattr(cell, 'directives_', {}) or {}
55
- if direc.get('eval:', [''])[0].lower() == 'false': return True
55
+ if direc.get('eval', '').lower() == 'false': return True
56
56
  return flags & direc.keys()
57
57
 
58
58
  start = time.time()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdev
3
- Version: 3.2.2
3
+ Version: 3.3.1
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: Apache-2.0
@@ -13,13 +13,13 @@ Classifier: Natural Language :: English
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: License :: OSI Approved :: Apache Software License
16
- Requires-Python: >=3.9
16
+ Requires-Python: >=3.10
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
- Requires-Dist: fastcore>=1.14.6
20
- Requires-Dist: execnb>=0.2.4
19
+ Requires-Dist: fastcore>=2.1.3
20
+ Requires-Dist: execnb>=0.2.8
21
21
  Requires-Dist: astunparse
22
- Requires-Dist: ghapi>=2.0.0
22
+ Requires-Dist: ghapi>=2.0.2
23
23
  Requires-Dist: watchdog
24
24
  Requires-Dist: asttokens
25
25
  Requires-Dist: setuptools
@@ -26,6 +26,7 @@ nbdev/scrubmagics.py
26
26
  nbdev/serve.py
27
27
  nbdev/serve_drv.py
28
28
  nbdev/showdoc.py
29
+ nbdev/skill.py
29
30
  nbdev/sync.py
30
31
  nbdev/test.py
31
32
  nbdev.egg-info/PKG-INFO
@@ -6,6 +6,8 @@ nbdev-clean = nbdev.clean:nbdev_clean
6
6
  nbdev-conda = nbdev.release:release_conda
7
7
  nbdev-contributing = nbdev.quarto:nbdev_contributing
8
8
  nbdev-create-config = nbdev.config:nbdev_create_config
9
+ nbdev-diff = nbdev.diff:nbdev_diff
10
+ nbdev-diff-driver = nbdev.diff:nbdev_diff_driver
9
11
  nbdev-docs = nbdev.quarto:nbdev_docs
10
12
  nbdev-export = nbdev.doclinks:nbdev_export
11
13
  nbdev-filter = nbdev.cli:nbdev_filter
@@ -36,3 +38,6 @@ watch-export = nbdev.cli:watch_export
36
38
 
37
39
  [nbdev]
38
40
  nbdev = nbdev._modidx:d
41
+
42
+ [pyskills]
43
+ nbdev.skill = nbdev.skill
@@ -1,7 +1,7 @@
1
- fastcore>=1.14.6
2
- execnb>=0.2.4
1
+ fastcore>=2.1.3
2
+ execnb>=0.2.8
3
3
  astunparse
4
- ghapi>=2.0.0
4
+ ghapi>=2.0.2
5
5
  watchdog
6
6
  asttokens
7
7
  setuptools
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
  name = "nbdev"
7
7
  description = "Create delightful software with Jupyter Notebooks"
8
8
  readme = "README.md"
9
- requires-python = ">=3.9"
9
+ requires-python = ">=3.10"
10
10
  license = {text = "Apache-2.0"}
11
11
  authors = [{name = "Jeremy Howard and the fast.ai community", email = "j@fast.ai"}]
12
12
  keywords = ["nbdev", "fastai", "jupyter", "notebook", "export"]
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "License :: OSI Approved :: Apache Software License",
20
20
  ]
21
21
  dynamic = ["version"]
22
- dependencies = [ "fastcore>=1.14.6", "execnb>=0.2.4", "astunparse", "ghapi>=2.0.0", "watchdog", "asttokens",
22
+ dependencies = [ "fastcore>=2.1.3", "execnb>=0.2.8", "astunparse", "ghapi>=2.0.2", "watchdog", "asttokens",
23
23
  "setuptools", "build", "fastgit>=0.0.7", "pyyaml", "tomli; python_version < '3.11'", ]
24
24
 
25
25
  [project.optional-dependencies]
@@ -37,6 +37,8 @@ nbdev-update-license = "nbdev.cli:nbdev_update_license"
37
37
  nbdev-export = "nbdev.doclinks:nbdev_export"
38
38
  nbdev-fix = "nbdev.merge:nbdev_fix"
39
39
  nbdev-merge = "nbdev.merge:nbdev_merge"
40
+ nbdev-diff = "nbdev.diff:nbdev_diff"
41
+ nbdev-diff-driver = "nbdev.diff:nbdev_diff_driver"
40
42
  nbdev-trust = "nbdev.clean:nbdev_trust"
41
43
  nbdev-clean = "nbdev.clean:nbdev_clean"
42
44
  nbdev-install-hooks = "nbdev.clean:nbdev_install_hooks"
@@ -69,6 +71,9 @@ watch-export = "nbdev.cli:watch_export"
69
71
  [project.entry-points.nbdev]
70
72
  nbdev = "nbdev._modidx:d"
71
73
 
74
+ [project.entry-points.pyskills]
75
+ "nbdev.skill" = "nbdev.skill"
76
+
72
77
  [tool.setuptools.dynamic]
73
78
  version = {attr = "nbdev.__version__"}
74
79
 
nbdev-3.2.2/nbdev/diff.py DELETED
@@ -1,94 +0,0 @@
1
- """Get ipynb diffs by cell
2
-
3
- Docs: https://nbdev.fast.ai/api/diff.html.md"""
4
-
5
- # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/19_diff.ipynb.
6
-
7
- # %% auto #0
8
- __all__ = ['read_nb_from_git', 'nbs_pair', 'changed_cells', 'source_diff', 'cell_diffs']
9
-
10
- # %% ../nbs/api/19_diff.ipynb #10c8ad0b
11
- import json
12
- from fastcore.utils import *
13
- from fastcore.meta import delegates
14
- from difflib import unified_diff
15
- from fastgit import Git
16
- from fastcore.nbio import *
17
-
18
- # %% ../nbs/api/19_diff.ipynb #a8981115
19
- def read_nb_from_git(
20
- g:Git, # The git object
21
- path, # The path to the notebook (absolute or relative to git root)
22
- ref=None # The git ref to read from (e.g. HEAD); None for working dir
23
- )->AttrDict: # The notebook
24
- "Read notebook from git ref (e.g. HEAD) at path, or working dir if ref is None"
25
- path = Path(path)
26
- if path.is_absolute(): path = path.relative_to(g.top())
27
- if ref is None: return read_nb(g.top()/path)
28
- raw = g.show(f'{ref}:{path}')
29
- return dict2nb(json.loads(raw))
30
-
31
- # %% ../nbs/api/19_diff.ipynb #3ac25702
32
- def _nb_srcdict(g:Git, nb_path, ref=None, f=noop):
33
- "Dict of id->source"
34
- nb = read_nb_from_git(g, nb_path, ref)
35
- return {c['id']: f(c) for c in nb.cells}
36
-
37
- # %% ../nbs/api/19_diff.ipynb #27ff9a7b
38
- def nbs_pair(
39
- nb_path, # Path to the notebook
40
- ref_a='HEAD', # First git ref (None for working dir)
41
- ref_b=None, # Second git ref (None for working dir)
42
- f=noop # Function to call on contents
43
- ): # Tuple of two notebooks
44
- "NBs at two refs; None means working dir. By default provides HEAD and working dir"
45
- nb_path = Path(nb_path).resolve()
46
- g = Git(nb_path.parent)
47
- return _nb_srcdict(g, nb_path, ref_a, f), _nb_srcdict(g, nb_path, ref_b, f)
48
-
49
- # %% ../nbs/api/19_diff.ipynb #958aeaf8
50
- def _cell_changes(
51
- nb_path, # Path to the notebook
52
- fn, # function to call to get dict values
53
- ref_a='HEAD', # First git ref (None for working dir)
54
- ref_b=None, # Second git ref (None for working dir)
55
- adds=True, # Include cells in b but not in a
56
- changes=True, # Include cells with different content
57
- dels=False, # Include cells in a but not in b
58
- metadata=False, # Consider cell metadata when comparing
59
- outputs=False # Consider cell outputs when comparing
60
- ): # Dict of results
61
- "Apply fn(cell_id, old_content, new_content) to changed cells between two refs"
62
- def cell_content(c):
63
- res = c.get('source', '')
64
- if metadata: res += '\n# metadata: ' + json.dumps(c.get('metadata', {}), sort_keys=True)
65
- if outputs: res += '\n# outputs: ' + json.dumps(c.get('outputs', []), sort_keys=True)
66
- return res
67
- old,new = nbs_pair(nb_path, ref_a, ref_b, f=cell_content)
68
- res = {}
69
- if adds: res |= {cid: fn(cid, '', new[cid]) for cid in new if cid not in old}
70
- if changes: res |= {cid: fn(cid, old[cid], new[cid]) for cid in new if cid in old and new[cid] != old[cid]}
71
- if dels: res |= {cid: fn(cid, old[cid], '') for cid in old if cid not in new}
72
- return res
73
-
74
- # %% ../nbs/api/19_diff.ipynb #6a723bc8
75
- @delegates(_cell_changes)
76
- def changed_cells(nb_path, **kwargs):
77
- "Return set of cell IDs for changed/added/deleted cells between two refs"
78
- def f(cid,o,n): return cid
79
- return set(_cell_changes(nb_path, f, **kwargs).keys())
80
-
81
- # %% ../nbs/api/19_diff.ipynb #079eedb0
82
- def source_diff(
83
- old_source, # Original source string
84
- new_source # New source string
85
- ): # Unified diff string
86
- "Return unified diff string for source change"
87
- return '\n'.join(unified_diff(old_source.splitlines(), new_source.splitlines(), lineterm=''))
88
-
89
- # %% ../nbs/api/19_diff.ipynb #38216afa
90
- @delegates(_cell_changes)
91
- def cell_diffs(nb_path, **kwargs):
92
- "{cell_id:diff} for changed/added/deleted cells between two refs"
93
- def f(cid,o,n): return source_diff(o,n)
94
- return _cell_changes(nb_path, f, **kwargs)
@@ -1,138 +0,0 @@
1
- """A notebook processor
2
-
3
- Docs: https://nbdev.fast.ai/api/process.html.md"""
4
-
5
- # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/03_process.ipynb.
6
-
7
- # %% auto #0
8
- __all__ = ['langs', 'nb_lang', 'first_code_ln', 'extract_directives', 'opt_set', 'instantiate', 'NBProcessor', 'Processor']
9
-
10
- # %% ../nbs/api/03_process.ipynb #59172c3d
11
- from .config import *
12
- from .maker import *
13
- from .imports import *
14
-
15
- from fastcore.nbio import *
16
- from fastcore.script import *
17
- from fastcore.imports import *
18
-
19
- from collections import defaultdict
20
-
21
- # %% ../nbs/api/03_process.ipynb #48e04902-f4a1-4247-ae0c-1e30bc054166
22
- # from https://github.com/quarto-dev/quarto-cli/blob/main/src/resources/jupyter/notebook.py
23
- langs = defaultdict(
24
- lambda: '#', r = "#", python = "#", julia = "#", scala = "//", matlab = "%", csharp = "//", fsharp = "//",
25
- c = ["/*","*/"], css = ["/*","*/"], sas = ["*",";"], powershell = "#", bash = "#", sql = "--", mysql = "--", psql = "--",
26
- lua = "--", cpp = "//", cc = "//", stan = "#", octave = "#", fortran = "!", fortran95 = "!", awk = "#", gawk = "#", stata = "*",
27
- java = "//", groovy = "//", sed = "#", perl = "#", ruby = "#", tikz = "%", javascript = "//", js = "//", d3 = "//", node = "//",
28
- sass = "//", coffee = "#", go = "//", asy = "//", haskell = "--", dot = "//", apl = "⍝")
29
-
30
- # %% ../nbs/api/03_process.ipynb #c54390ab-1cb9-4146-9e88-609f1fc5544a
31
- def nb_lang(nb): return nested_attr(nb, 'metadata.kernelspec.language', 'python')
32
-
33
- # %% ../nbs/api/03_process.ipynb #2974d8cc-870c-4362-b2b1-e35f94c28dd9
34
- def _dir_pre(lang=None): return fr"\s*{langs[lang]}\s*\|"
35
- def _quarto_re(lang=None): return re.compile(_dir_pre(lang) + r'\s*[\w|-]+\s*:')
36
-
37
- # %% ../nbs/api/03_process.ipynb #484a1df8
38
- def _directive(s, lang='python'):
39
- s = re.sub('^'+_dir_pre(lang), f"{langs[lang]}|", s)
40
- if s.strip().endswith(':'): s = s.replace(':', '') # You can append colon at the end to be Quarto compliant. Ex: #| hide:
41
- if ':' in s: s = s.replace(':', ': ')
42
- s = (s.strip()[2:]).strip().split()
43
- if not s: return None
44
- direc,*args = s
45
- return direc,args
46
-
47
- # %% ../nbs/api/03_process.ipynb #100a91fb-8b46-4feb-afbf-d1e78c14cfd6
48
- def _norm_quarto(s, lang='python'):
49
- "normalize quarto directives so they have a space after the colon"
50
- m = _quarto_re(lang).match(s)
51
- return m.group(0) + ' ' + _quarto_re(lang).sub('', s).lstrip() if m else s
52
-
53
- # %% ../nbs/api/03_process.ipynb #69befb76-aa77-4923-bfcc-2606613d706f
54
- _cell_mgc = re.compile(r"^\s*%%\w+")
55
-
56
- def first_code_ln(code_list, re_pattern=None, lang='python'):
57
- "get first line number where code occurs, where `code_list` is a list of code"
58
- if re_pattern is None: re_pattern = _dir_pre(lang)
59
- return first(i for i,o in enumerate(code_list) if o.strip() != '' and not re.match(re_pattern, o) and not _cell_mgc.match(o))
60
-
61
- # %% ../nbs/api/03_process.ipynb #b3eee2d6-59b9-45f3-affd-4de2bd1284d1
62
- def _partition_cell(cell, lang):
63
- if not cell.source: return [],[]
64
- lines = cell.source.splitlines(True)
65
- first_code = first_code_ln(lines, lang=lang)
66
- return lines[:first_code],lines[first_code:]
67
-
68
- # %% ../nbs/api/03_process.ipynb #3cca78a4
69
- def extract_directives(cell, remove=True, lang='python'):
70
- "Take leading comment directives from lines of code in `ss`, remove `#|`, and split"
71
- dirs,code = _partition_cell(cell, lang)
72
- if not dirs: return {}
73
- if remove:
74
- # Leave Quarto directives and cell magic in place for later processing
75
- cell['source'] = ''.join([_norm_quarto(o, lang) for o in dirs if _quarto_re(lang).match(o) or _cell_mgc.match(o)] + code)
76
- return dict(L(_directive(s, lang) for s in dirs).filter())
77
-
78
- # %% ../nbs/api/03_process.ipynb #e6701805
79
- def opt_set(var, newval):
80
- "newval if newval else var"
81
- return newval if newval else var
82
-
83
- # %% ../nbs/api/03_process.ipynb #98c9d556
84
- def instantiate(x, **kwargs):
85
- "Instantiate `x` if it's a type"
86
- return x(**kwargs) if isinstance(x,type) else x
87
-
88
- def _mk_procs(procs, nb): return L(procs).map(instantiate, nb=nb)
89
-
90
- # %% ../nbs/api/03_process.ipynb #ab147efe
91
- def _is_direc(f): return getattr(f, '__name__', '-')[-1]=='_'
92
-
93
- # %% ../nbs/api/03_process.ipynb #7c81f109
94
- class NBProcessor:
95
- "Process cells and nbdev comments in a notebook"
96
- def __init__(self, path=None, procs=None, nb=None, debug=False, rm_directives=True, process=False):
97
- self.nb = read_nb(path) if nb is None else nb
98
- self.lang = nb_lang(self.nb)
99
- for cell in self.nb.cells: cell.directives_ = extract_directives(cell, remove=rm_directives, lang=self.lang)
100
- self.procs = _mk_procs(procs, nb=self.nb)
101
- self.debug,self.rm_directives = debug,rm_directives
102
- if process: self.process()
103
-
104
- def _process_cell(self, proc, cell):
105
- if not hasattr(cell,'source'): return
106
- if cell.cell_type=='code' and cell.directives_:
107
- # Option 1: `proc` is directive name with `_` suffix
108
- f = getattr(proc, '__name__', '-').rstrip('_')
109
- if f in cell.directives_: self._process_comment(proc, cell, f)
110
-
111
- # Option 2: `proc` contains a method named `_{directive}_`
112
- for cmd in cell.directives_:
113
- f = getattr(proc, f'_{cmd}_', None)
114
- if f: self._process_comment(f, cell, cmd)
115
- if callable(proc) and not _is_direc(proc): cell = opt_set(cell, proc(cell))
116
-
117
- def _process_comment(self, proc, cell, cmd):
118
- args = cell.directives_[cmd]
119
- if self.debug: print(cmd, args, proc)
120
- return proc(cell, *args)
121
-
122
- def _proc(self, proc):
123
- if hasattr(proc,'begin'): proc.begin()
124
- for cell in self.nb.cells: self._process_cell(proc, cell)
125
- if hasattr(proc,'end'): proc.end()
126
- self.nb.cells = [c for c in self.nb.cells if c and getattr(c,'source',None) is not None]
127
- for i,cell in enumerate(self.nb.cells): cell.idx_ = i
128
-
129
- def process(self):
130
- "Process all cells with all processors"
131
- for proc in self.procs: self._proc(proc)
132
-
133
- # %% ../nbs/api/03_process.ipynb #fa1f8668
134
- class Processor:
135
- "Base class for processors"
136
- def __init__(self, nb): self.nb = nb
137
- def cell(self, cell): pass
138
- def __call__(self, cell): return self.cell(cell)
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