Skip to content

Commit 973759d

Browse files
authored
Merge pull request #698 from seeM/jupyter-hooks
fixes #697
2 parents d88fe9a + 74521d0 commit 973759d

File tree

6 files changed

+183
-24
lines changed

6 files changed

+183
-24
lines changed

nbdev/_modidx.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
'nbdev_trust=nbdev.clean:nbdev_trust\n'
1313
'nbdev_clean=nbdev.clean:nbdev_clean\n'
1414
'nbdev_install_hooks=nbdev.clean:nbdev_install_hooks\n'
15+
'nbdev_install_jupyter_hooks=nbdev.clean:nbdev_install_jupyter_hooks\n'
1516
'nbdev_filter=nbdev.cli:nbdev_filter\n'
1617
'nbdev_quarto=nbdev.cli:nbdev_quarto\n'
1718
'nbdev_ghp_deploy=nbdev.cli:nbdev_ghp_deploy\n'
@@ -56,9 +57,11 @@
5657
'tst_flags': 'notest',
5758
'user': 'fastai',
5859
'version': '2.0.6'},
59-
'syms': { 'nbdev.clean': { 'nbdev.clean.clean_nb': 'https://nbdev.fast.ai/clean.html#clean_nb',
60+
'syms': { 'nbdev.clean': { 'nbdev.clean.clean_jupyter': 'https://nbdev.fast.ai/clean.html#clean_jupyter',
61+
'nbdev.clean.clean_nb': 'https://nbdev.fast.ai/clean.html#clean_nb',
6062
'nbdev.clean.nbdev_clean': 'https://nbdev.fast.ai/clean.html#nbdev_clean',
6163
'nbdev.clean.nbdev_install_hooks': 'https://nbdev.fast.ai/clean.html#nbdev_install_hooks',
64+
'nbdev.clean.nbdev_install_jupyter_hooks': 'https://nbdev.fast.ai/clean.html#nbdev_install_jupyter_hooks',
6265
'nbdev.clean.nbdev_trust': 'https://nbdev.fast.ai/clean.html#nbdev_trust',
6366
'nbdev.clean.process_write': 'https://nbdev.fast.ai/clean.html#process_write'},
6467
'nbdev.cli': { 'nbdev.cli.FilterDefaults': 'https://nbdev.fast.ai/cli.html#filterdefaults',

nbdev/clean.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/11_clean.ipynb.
22

33
# %% auto 0
4-
__all__ = ['nbdev_trust', 'clean_nb', 'process_write', 'nbdev_clean', 'nbdev_install_hooks']
4+
__all__ = ['nbdev_trust', 'clean_nb', 'process_write', 'nbdev_clean', 'nbdev_install_hooks', 'clean_jupyter',
5+
'nbdev_install_jupyter_hooks']
56

67
# %% ../nbs/11_clean.ipynb 2
78
import warnings,stat
@@ -96,6 +97,13 @@ def process_write(warn_msg, proc_nb, f_in, f_out=None, disp=False):
9697
warn(e)
9798

9899
# %% ../nbs/11_clean.ipynb 21
100+
def _nbdev_clean(nb, **kwargs):
101+
allowed_metadata_keys = config_key("allowed_metadata_keys", '', missing_ok=True, path=False).split()
102+
allowed_cell_metadata_keys = config_key("allowed_cell_metadata_keys", '', missing_ok=True, path=False).split()
103+
return clean_nb(nb, allowed_metadata_keys=allowed_metadata_keys,
104+
allowed_cell_metadata_keys=allowed_cell_metadata_keys, **kwargs)
105+
106+
# %% ../nbs/11_clean.ipynb 22
99107
@call_parse
100108
def nbdev_clean(
101109
fname:str=None, # A notebook name or glob to clean
@@ -105,18 +113,14 @@ def nbdev_clean(
105113
):
106114
"Clean all notebooks in `fname` to avoid merge conflicts"
107115
# Git hooks will pass the notebooks in stdin
108-
allowed_metadata_keys = config_key("allowed_metadata_keys", default='', missing_ok=True, path=False).split()
109-
allowed_cell_metadata_keys = config_key("allowed_cell_metadata_keys", default='', missing_ok=True, path=False).split()
110-
_clean = partial(clean_nb, clear_all=clear_all,
111-
allowed_metadata_keys=allowed_metadata_keys,
112-
allowed_cell_metadata_keys=allowed_cell_metadata_keys)
116+
_clean = partial(_nbdev_clean, clear_all=clear_all)
113117
_write = partial(process_write, warn_msg='Failed to clean notebook', proc_nb=_clean)
114118
if stdin: return _write(f_in=sys.stdin, f_out=sys.stdout)
115119

116120
if fname is None: fname = config_key("nbs_path", '.', missing_ok=True)
117121
for f in globtastic(fname, file_glob='*.ipynb', skip_folder_re='^[_.]'): _write(f_in=f, disp=disp)
118122

119-
# %% ../nbs/11_clean.ipynb 23
123+
# %% ../nbs/11_clean.ipynb 24
120124
@call_parse
121125
def nbdev_install_hooks():
122126
"Install git hooks to clean and trust notebooks automatically"
@@ -149,3 +153,38 @@ def nbdev_install_hooks():
149153
run(cmd)
150154
print("Hooks are installed and repo's .gitconfig is now trusted")
151155
(nb_path/'.gitattributes').write_text("**/*.ipynb filter=clean-nbs\n**/*.ipynb diff=ipynb\n")
156+
157+
# %% ../nbs/11_clean.ipynb 25
158+
def clean_jupyter(path, model, **kwargs):
159+
"Clean Jupyter `model` pre save to `path`"
160+
get_config.cache_clear() # Reset Jupyter's cache
161+
try: cfg = get_config(path=path)
162+
except FileNotFoundError: return
163+
in_nbdev_repo = 'nbs_path' in cfg
164+
jupyter_hooks = str2bool(cfg.get('jupyter_hooks', True))
165+
is_nb_v4 = (model['type'],model['content']['nbformat']) == ('notebook',4)
166+
if in_nbdev_repo and jupyter_hooks and is_nb_v4: _nbdev_clean(model['content'])
167+
168+
# %% ../nbs/11_clean.ipynb 27
169+
def _nested_setdefault(o, attr, default):
170+
"Same as `setdefault`, but if `attr` includes a `.`, then looks inside nested objects"
171+
attrs = attr.split('.')
172+
for a in attrs[:-1]: o = o.setdefault(a, type(o)())
173+
return o.setdefault(attrs[-1], default)
174+
175+
# %% ../nbs/11_clean.ipynb 31
176+
@call_parse
177+
def nbdev_install_jupyter_hooks():
178+
"Install Jupyter hooks to clean notebooks on save"
179+
cfg_path = Path.home()/'.jupyter'
180+
cfg_fns = [cfg_path/f'jupyter_{o}_config.json' for o in ('notebook','server')]
181+
attr,hook = 'ContentsManager.pre_save_hook','nbdev.clean.clean_jupyter'
182+
for fn in cfg_fns:
183+
cfg = dict2obj(fn.read_json() if fn.exists() else {})
184+
val = nested_attr(cfg, attr)
185+
if val is None:
186+
_nested_setdefault(cfg, attr, hook)
187+
fn.write_text(dumps(obj2dict(cfg), indent=2))
188+
elif val != hook:
189+
sys.stderr.write(f"Can't install hook to '{p}' since it already contains `{attr} = '{val}'`. "
190+
f"Manually update to `{attr} = '{hook}'` for this functionality.")

nbs/11_clean.ipynb

Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@
170170
"source": [
171171
"test_nb = read_nb('../tests/metadata.ipynb')\n",
172172
"\n",
173-
"assert set(['meta', 'jekyll', 'my_extra_key', 'my_removed_key']) <= set(test_nb.metadata.keys())\n",
174-
"assert set(['meta', 'hide_input', 'my_extra_cell_key', 'my_removed_cell_key']) == set(test_nb.cells[1].metadata.keys())"
173+
"assert {'meta', 'jekyll', 'my_extra_key', 'my_removed_key'} <= test_nb.metadata.keys()\n",
174+
"assert {'meta', 'hide_input', 'my_extra_cell_key', 'my_removed_cell_key'} == test_nb.cells[1].metadata.keys()"
175175
]
176176
},
177177
{
@@ -189,8 +189,8 @@
189189
"source": [
190190
"clean_nb(test_nb)\n",
191191
"\n",
192-
"assert set(['jekyll', 'kernelspec']) == set(test_nb.metadata.keys())\n",
193-
"assert set(['hide_input']) == set(test_nb.cells[1].metadata.keys())"
192+
"assert {'jekyll', 'kernelspec'} == test_nb.metadata.keys()\n",
193+
"assert {'hide_input'} == test_nb.cells[1].metadata.keys()"
194194
]
195195
},
196196
{
@@ -209,8 +209,8 @@
209209
"test_nb = read_nb('../tests/metadata.ipynb')\n",
210210
"clean_nb(test_nb, allowed_metadata_keys={'my_extra_key'}, allowed_cell_metadata_keys={'my_extra_cell_key'})\n",
211211
"\n",
212-
"assert set(['jekyll', 'kernelspec', 'my_extra_key']) == set(test_nb.metadata.keys())\n",
213-
"assert set(['hide_input', 'my_extra_cell_key']) == set(test_nb.cells[1].metadata.keys())"
212+
"assert {'jekyll', 'kernelspec', 'my_extra_key'} == test_nb.metadata.keys()\n",
213+
"assert {'hide_input', 'my_extra_cell_key'} == test_nb.cells[1].metadata.keys()"
214214
]
215215
},
216216
{
@@ -229,7 +229,7 @@
229229
"test_nb = read_nb('../tests/metadata.ipynb')\n",
230230
"clean_nb(test_nb, clear_all=True)\n",
231231
"\n",
232-
"assert set(['jekyll', 'kernelspec']) == set(test_nb.metadata.keys())\n",
232+
"assert {'jekyll', 'kernelspec'} == test_nb.metadata.keys()\n",
233233
"test_eq(test_nb.cells[1].metadata, {})"
234234
]
235235
},
@@ -272,6 +272,20 @@
272272
" warn(e)"
273273
]
274274
},
275+
{
276+
"cell_type": "code",
277+
"execution_count": null,
278+
"metadata": {},
279+
"outputs": [],
280+
"source": [
281+
"#|export\n",
282+
"def _nbdev_clean(nb, **kwargs):\n",
283+
" allowed_metadata_keys = config_key(\"allowed_metadata_keys\", '', missing_ok=True, path=False).split()\n",
284+
" allowed_cell_metadata_keys = config_key(\"allowed_cell_metadata_keys\", '', missing_ok=True, path=False).split()\n",
285+
" return clean_nb(nb, allowed_metadata_keys=allowed_metadata_keys,\n",
286+
" allowed_cell_metadata_keys=allowed_cell_metadata_keys, **kwargs)"
287+
]
288+
},
275289
{
276290
"cell_type": "code",
277291
"execution_count": null,
@@ -288,11 +302,7 @@
288302
"):\n",
289303
" \"Clean all notebooks in `fname` to avoid merge conflicts\"\n",
290304
" # Git hooks will pass the notebooks in stdin\n",
291-
" allowed_metadata_keys = config_key(\"allowed_metadata_keys\", default='', missing_ok=True, path=False).split()\n",
292-
" allowed_cell_metadata_keys = config_key(\"allowed_cell_metadata_keys\", default='', missing_ok=True, path=False).split()\n",
293-
" _clean = partial(clean_nb, clear_all=clear_all,\n",
294-
" allowed_metadata_keys=allowed_metadata_keys,\n",
295-
" allowed_cell_metadata_keys=allowed_cell_metadata_keys)\n",
305+
" _clean = partial(_nbdev_clean, clear_all=clear_all)\n",
296306
" _write = partial(process_write, warn_msg='Failed to clean notebook', proc_nb=_clean)\n",
297307
" if stdin: return _write(f_in=sys.stdin, f_out=sys.stdout)\n",
298308
" \n",
@@ -357,6 +367,112 @@
357367
" (nb_path/'.gitattributes').write_text(\"**/*.ipynb filter=clean-nbs\\n**/*.ipynb diff=ipynb\\n\")"
358368
]
359369
},
370+
{
371+
"cell_type": "code",
372+
"execution_count": null,
373+
"metadata": {},
374+
"outputs": [],
375+
"source": [
376+
"#|export\n",
377+
"def clean_jupyter(path, model, **kwargs):\n",
378+
" \"Clean Jupyter `model` pre save to `path`\"\n",
379+
" get_config.cache_clear() # Reset Jupyter's cache\n",
380+
" try: cfg = get_config(path=path)\n",
381+
" except FileNotFoundError: return\n",
382+
" in_nbdev_repo = 'nbs_path' in cfg\n",
383+
" jupyter_hooks = str2bool(cfg.get('jupyter_hooks', True))\n",
384+
" is_nb_v4 = (model['type'],model['content']['nbformat']) == ('notebook',4)\n",
385+
" if in_nbdev_repo and jupyter_hooks and is_nb_v4: _nbdev_clean(model['content'])"
386+
]
387+
},
388+
{
389+
"cell_type": "markdown",
390+
"metadata": {},
391+
"source": [
392+
"`clean_jupyter` implements Jupyter's [`ContentsManager.pre_save_hook`](https://jupyter-notebook.readthedocs.io/en/6.4.12/extending/savehooks.html). The easiest way to install it as a Jupyter Notebook or Lab pre-save hook is by running `nbdev_install_jupyter_hooks`."
393+
]
394+
},
395+
{
396+
"cell_type": "code",
397+
"execution_count": null,
398+
"metadata": {},
399+
"outputs": [],
400+
"source": [
401+
"#|export\n",
402+
"def _nested_setdefault(o, attr, default):\n",
403+
" \"Same as `setdefault`, but if `attr` includes a `.`, then looks inside nested objects\"\n",
404+
" attrs = attr.split('.')\n",
405+
" for a in attrs[:-1]: o = o.setdefault(a, type(o)())\n",
406+
" return o.setdefault(attrs[-1], default)"
407+
]
408+
},
409+
{
410+
"cell_type": "code",
411+
"execution_count": null,
412+
"metadata": {},
413+
"outputs": [],
414+
"source": [
415+
"#|hide\n",
416+
"o = {'e':'f'}\n",
417+
"test_eq(_nested_setdefault(o, 'a.b.c', 'd'), 'd')\n",
418+
"test_eq(o, {'a':{'b':{'c':'d'}},'e':'f'})"
419+
]
420+
},
421+
{
422+
"cell_type": "code",
423+
"execution_count": null,
424+
"metadata": {},
425+
"outputs": [],
426+
"source": [
427+
"#|hide\n",
428+
"o = {'a':'b'}\n",
429+
"test_eq(_nested_setdefault(o, 'a', 'c'), 'b')\n",
430+
"test_eq(o, {'a':'b'})"
431+
]
432+
},
433+
{
434+
"cell_type": "code",
435+
"execution_count": null,
436+
"metadata": {},
437+
"outputs": [],
438+
"source": [
439+
"#|hide\n",
440+
"o = {'a':{'b':'c'}}\n",
441+
"test_eq(_nested_setdefault(o, 'a.b', 'd'), 'c')\n",
442+
"test_eq(o,{'a':{'b':'c'}})"
443+
]
444+
},
445+
{
446+
"cell_type": "code",
447+
"execution_count": null,
448+
"metadata": {},
449+
"outputs": [],
450+
"source": [
451+
"#|export\n",
452+
"@call_parse\n",
453+
"def nbdev_install_jupyter_hooks():\n",
454+
" \"Install Jupyter hooks to clean notebooks on save\"\n",
455+
" cfg_path = Path.home()/'.jupyter'\n",
456+
" cfg_fns = [cfg_path/f'jupyter_{o}_config.json' for o in ('notebook','server')]\n",
457+
" attr,hook = 'ContentsManager.pre_save_hook','nbdev.clean.clean_jupyter'\n",
458+
" for fn in cfg_fns:\n",
459+
" cfg = dict2obj(fn.read_json() if fn.exists() else {})\n",
460+
" val = nested_attr(cfg, attr)\n",
461+
" if val is None:\n",
462+
" _nested_setdefault(cfg, attr, hook)\n",
463+
" fn.write_text(dumps(obj2dict(cfg), indent=2))\n",
464+
" elif val != hook:\n",
465+
" sys.stderr.write(f\"Can't install hook to '{p}' since it already contains `{attr} = '{val}'`. \"\n",
466+
" f\"Manually update to `{attr} = '{hook}'` for this functionality.\")"
467+
]
468+
},
469+
{
470+
"cell_type": "markdown",
471+
"metadata": {},
472+
"source": [
473+
"`nbdev`'s Jupyter hooks only run on notebooks in an `nbdev` repo. Hooks can also be disabled at any time by setting `jupyter_hooks = False` in `settings.ini`."
474+
]
475+
},
360476
{
361477
"cell_type": "markdown",
362478
"metadata": {},

nbs/getting_started.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
"cell_type": "markdown",
102102
"metadata": {},
103103
"source": [
104-
"(There's an [alternate version of the walkthru](https://youtu.be/67FdzLSt4aA) avaialable with the coding sections sped up using the `unsilence` python library -- it's 27 minutes faster, but it's be harder to follow along with.)\n",
104+
"(There's an [alternate version of the walkthru](https://youtu.be/67FdzLSt4aA) available with the coding sections sped up using the `unsilence` python library -- it's 27 minutes faster, but it's be harder to follow along with.)\n",
105105
"\n",
106106
"You can run `nbdev_help` from the terminal to see the full list of available commands:"
107107
]

nbs/tutorial.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,14 @@
157157
"cell_type": "markdown",
158158
"metadata": {},
159159
"source": [
160-
"## Install git hooks to avoid and handle conflicts"
160+
"## Install git and Jupyter hooks for git-friendly notebooks"
161161
]
162162
},
163163
{
164164
"cell_type": "markdown",
165165
"metadata": {},
166166
"source": [
167-
"Jupyter Notebooks can cause challenges with git conflicts, but life becomes much easier when you use `nbdev`. As a first step, run `nbdev_install_hooks` in the terminal from your project folder. This will set up hooks which will remove metadata from your notebooks when you commit, greatly reducing the chance you have a conflict.\n",
167+
"Jupyter Notebooks store additional metadata (like cell execution order) which cause challenges with git. `nbdev` makes working with notebooks becomes much easier. As a first step, run `nbdev_install_hooks` in the terminal from your project folder. This sets up git hooks which remove metadata from your notebooks automatically, avoiding unnecessary file changes and greatly reducing the chance of a conflict. You can also run `nbdev_install_jupyter_hooks` to install a similar hook that runs on save in Jupyter Notebook and Lab.\n",
168168
"\n",
169169
"But if you do get a conflict later, simply run `nbdev_fix filename.ipynb`. This will replace any conflicts in cell outputs with your version, and if there are conflicts in input cells, then both cells will be included in the merged file, along with standard conflict markers (e.g. `=====`). Then you can open the notebook in Jupyter and choose which version to keep."
170170
]
@@ -541,7 +541,7 @@
541541
"Do the saying"
542542
],
543543
"text/plain": [
544-
"<nbdev.showdoc.BasicMarkdownRenderer at 0x117e8bf70>"
544+
"<nbdev.showdoc.BasicMarkdownRenderer at 0x11595a6d0>"
545545
]
546546
},
547547
"execution_count": null,

settings.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ console_scripts = nbdev_create_config=nbdev.read:nbdev_create_config
3131
nbdev_trust=nbdev.clean:nbdev_trust
3232
nbdev_clean=nbdev.clean:nbdev_clean
3333
nbdev_install_hooks=nbdev.clean:nbdev_install_hooks
34+
nbdev_install_jupyter_hooks=nbdev.clean:nbdev_install_jupyter_hooks
3435
nbdev_filter=nbdev.cli:nbdev_filter
3536
nbdev_quarto=nbdev.cli:nbdev_quarto
3637
nbdev_ghp_deploy=nbdev.cli:nbdev_ghp_deploy

0 commit comments

Comments
 (0)