Skip to content

Commit 29c9652

Browse files
authored
Merge pull request #1292 from xl0/trailing-n
nbdev_clean: Add trailing newlines to mask diff between Jupyter and VSCode
2 parents 6df9d69 + 2600419 commit 29c9652

File tree

5 files changed

+102
-21
lines changed

5 files changed

+102
-21
lines changed

nbdev/_modidx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'git_url': 'https://github.com/fastai/nbdev',
77
'lib_path': 'nbdev'},
88
'syms': { 'nbdev.clean': { 'nbdev.clean._add_jupyter_hooks': ('api/clean.html#_add_jupyter_hooks', 'nbdev/clean.py'),
9+
'nbdev.clean._add_trailing_n': ('api/clean.html#_add_trailing_n', 'nbdev/clean.py'),
910
'nbdev.clean._clean_cell': ('api/clean.html#_clean_cell', 'nbdev/clean.py'),
1011
'nbdev.clean._clean_cell_output': ('api/clean.html#_clean_cell_output', 'nbdev/clean.py'),
1112
'nbdev.clean._clean_cell_output_id': ('api/clean.html#_clean_cell_output_id', 'nbdev/clean.py'),

nbdev/clean.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,25 @@ def _clean_cell_output_id(lines):
5555
return _skip_or_sub(lines) if isinstance(lines,str) else [_skip_or_sub(o) for o in lines]
5656

5757
# %% ../nbs/api/11_clean.ipynb 11
58+
def _add_trailing_n(img):
59+
if not isinstance(img,str): return [ _add_trailing_n(o) for o in img ]
60+
return img + "\n" if img[-1] != "\n" else img
61+
62+
# %% ../nbs/api/11_clean.ipynb 12
5863
def _clean_cell_output(cell, clean_ids):
5964
"Remove `cell` output execution count and optionally ids from text reprs"
6065
outputs = cell.get('outputs', [])
6166
for o in outputs:
6267
if 'execution_count' in o: o['execution_count'] = None
6368
data = o.get('data', {})
6469
data.pop("application/vnd.google.colaboratory.intrinsic+json", None)
65-
if clean_ids:
66-
for k in data:
67-
if k.startswith('text'): data[k] = _clean_cell_output_id(data[k])
68-
if 'text' in o: o['text'] = _clean_cell_output_id(o['text'])
70+
for k in data:
71+
if k.startswith('text') and clean_ids: data[k] = _clean_cell_output_id(data[k])
72+
if k.startswith('image'): data[k] = _add_trailing_n(data[k])
73+
if 'text' in o and clean_ids: o['text'] = _clean_cell_output_id(o['text'])
6974
o.get('metadata', {}).pop('tags', None)
7075

71-
# %% ../nbs/api/11_clean.ipynb 12
76+
# %% ../nbs/api/11_clean.ipynb 13
7277
def _clean_cell(cell, clear_all, allowed_metadata_keys, clean_ids):
7378
"Clean `cell` by removing superfluous metadata or everything except the input if `clear_all`"
7479
if 'execution_count' in cell: cell['execution_count'] = None
@@ -79,7 +84,7 @@ def _clean_cell(cell, clear_all, allowed_metadata_keys, clean_ids):
7984
cell['metadata'] = {} if clear_all else {
8085
k:v for k,v in cell['metadata'].items() if k in allowed_metadata_keys}
8186

82-
# %% ../nbs/api/11_clean.ipynb 13
87+
# %% ../nbs/api/11_clean.ipynb 14
8388
def clean_nb(
8489
nb, # The notebook to clean
8590
clear_all=False, # Remove all cell metadata and cell outputs?
@@ -97,12 +102,12 @@ def clean_nb(
97102
nb['metadata']['kernelspec']['display_name'] = nb.metadata.kernelspec.name
98103
nb['metadata'] = {k:v for k,v in nb['metadata'].items() if k in metadata_keys}
99104

100-
# %% ../nbs/api/11_clean.ipynb 24
105+
# %% ../nbs/api/11_clean.ipynb 27
101106
def _reconfigure(*strms):
102107
for s in strms:
103108
if hasattr(s,'reconfigure'): s.reconfigure(encoding='utf-8')
104109

105-
# %% ../nbs/api/11_clean.ipynb 25
110+
# %% ../nbs/api/11_clean.ipynb 28
106111
def process_write(warn_msg, proc_nb, f_in, f_out=None, disp=False):
107112
if not f_out: f_out = f_in
108113
if isinstance(f_in, (str,Path)): f_in = Path(f_in).open()
@@ -115,15 +120,15 @@ def process_write(warn_msg, proc_nb, f_in, f_out=None, disp=False):
115120
warn(f'{warn_msg}')
116121
warn(e)
117122

118-
# %% ../nbs/api/11_clean.ipynb 26
123+
# %% ../nbs/api/11_clean.ipynb 29
119124
def _nbdev_clean(nb, path=None, clear_all=None):
120125
cfg = get_config(path=path)
121126
clear_all = clear_all or cfg.clear_all
122127
allowed_metadata_keys = cfg.get("allowed_metadata_keys").split()
123128
allowed_cell_metadata_keys = cfg.get("allowed_cell_metadata_keys").split()
124129
return clean_nb(nb, clear_all, allowed_metadata_keys, allowed_cell_metadata_keys, cfg.clean_ids)
125130

126-
# %% ../nbs/api/11_clean.ipynb 27
131+
# %% ../nbs/api/11_clean.ipynb 31
127132
@call_parse
128133
def nbdev_clean(
129134
fname:str=None, # A notebook name or glob to clean
@@ -139,15 +144,15 @@ def nbdev_clean(
139144
if fname is None: fname = get_config().nbs_path
140145
for f in globtastic(fname, file_glob='*.ipynb', skip_folder_re='^[_.]'): _write(f_in=f, disp=disp)
141146

142-
# %% ../nbs/api/11_clean.ipynb 30
147+
# %% ../nbs/api/11_clean.ipynb 34
143148
def clean_jupyter(path, model, **kwargs):
144149
"Clean Jupyter `model` pre save to `path`"
145150
if not (model['type']=='notebook' and model['content']['nbformat']==4): return
146151
get_config.cache_clear() # Allow config changes without restarting Jupyter
147152
jupyter_hooks = get_config(path=path).jupyter_hooks
148153
if jupyter_hooks: _nbdev_clean(model['content'], path=path)
149154

150-
# %% ../nbs/api/11_clean.ipynb 33
155+
# %% ../nbs/api/11_clean.ipynb 37
151156
_pre_save_hook_src = '''
152157
def nbdev_clean_jupyter(**kwargs):
153158
try: from nbdev.clean import clean_jupyter
@@ -157,7 +162,7 @@ def nbdev_clean_jupyter(**kwargs):
157162
c.ContentsManager.pre_save_hook = nbdev_clean_jupyter'''.strip()
158163
_pre_save_hook_re = re.compile(r'c\.(File)?ContentsManager\.pre_save_hook')
159164

160-
# %% ../nbs/api/11_clean.ipynb 34
165+
# %% ../nbs/api/11_clean.ipynb 38
161166
def _add_jupyter_hooks(src, path):
162167
if _pre_save_hook_src in src: return
163168
mod = ast.parse(src)
@@ -175,12 +180,12 @@ def _add_jupyter_hooks(src, path):
175180
if src: src+='\n\n'
176181
return src+_pre_save_hook_src
177182

178-
# %% ../nbs/api/11_clean.ipynb 38
183+
# %% ../nbs/api/11_clean.ipynb 42
179184
def _git_root():
180185
try: return Path(run('git rev-parse --show-toplevel'))
181186
except OSError: return None
182187

183-
# %% ../nbs/api/11_clean.ipynb 41
188+
# %% ../nbs/api/11_clean.ipynb 45
184189
@call_parse
185190
def nbdev_install_hooks():
186191
"Install Jupyter and git hooks to automatically clean, trust, and fix merge conflicts in notebooks"

nbs/api/11_clean.ipynb

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,18 @@
144144
"test_eq(_clean_cell_output_id('foo\\n<function _add2 at 0x7f8252378820>\\nbar'), 'foo\\n<function _add2>\\nbar')"
145145
]
146146
},
147+
{
148+
"cell_type": "code",
149+
"execution_count": null,
150+
"metadata": {},
151+
"outputs": [],
152+
"source": [
153+
"#|exporti\n",
154+
"def _add_trailing_n(img):\n",
155+
" if not isinstance(img,str): return [ _add_trailing_n(o) for o in img ]\n",
156+
" return img + \"\\n\" if img[-1] != \"\\n\" else img"
157+
]
158+
},
147159
{
148160
"cell_type": "code",
149161
"execution_count": null,
@@ -158,10 +170,10 @@
158170
" if 'execution_count' in o: o['execution_count'] = None\n",
159171
" data = o.get('data', {})\n",
160172
" data.pop(\"application/vnd.google.colaboratory.intrinsic+json\", None)\n",
161-
" if clean_ids:\n",
162-
" for k in data:\n",
163-
" if k.startswith('text'): data[k] = _clean_cell_output_id(data[k])\n",
164-
" if 'text' in o: o['text'] = _clean_cell_output_id(o['text'])\n",
173+
" for k in data:\n",
174+
" if k.startswith('text') and clean_ids: data[k] = _clean_cell_output_id(data[k])\n",
175+
" if k.startswith('image'): data[k] = _add_trailing_n(data[k])\n",
176+
" if 'text' in o and clean_ids: o['text'] = _clean_cell_output_id(o['text'])\n",
165177
" o.get('metadata', {}).pop('tags', None)"
166178
]
167179
},
@@ -208,6 +220,27 @@
208220
" nb['metadata'] = {k:v for k,v in nb['metadata'].items() if k in metadata_keys}"
209221
]
210222
},
223+
{
224+
"attachments": {},
225+
"cell_type": "markdown",
226+
"metadata": {},
227+
"source": [
228+
"Jupyter adds a trailing <code>\\n</code> to images in cell outputs. Vscode-jupyter does not.\\\n",
229+
"Notebooks should be brought to a common style to avoid unnecessary diffs:"
230+
]
231+
},
232+
{
233+
"cell_type": "code",
234+
"execution_count": null,
235+
"metadata": {},
236+
"outputs": [],
237+
"source": [
238+
"test_nb = read_nb('../../tests/image.ipynb')\n",
239+
"assert test_nb.cells[0].outputs[0].data['image/png'][-1] != \"\\n\" # Make sure it was not converted by acccident\n",
240+
"clean_nb(test_nb)\n",
241+
"assert test_nb.cells[0].outputs[0].data['image/png'][-1] == \"\\n\""
242+
]
243+
},
211244
{
212245
"cell_type": "markdown",
213246
"metadata": {},
@@ -357,6 +390,13 @@
357390
" return clean_nb(nb, clear_all, allowed_metadata_keys, allowed_cell_metadata_keys, cfg.clean_ids)"
358391
]
359392
},
393+
{
394+
"cell_type": "code",
395+
"execution_count": null,
396+
"metadata": {},
397+
"outputs": [],
398+
"source": []
399+
},
360400
{
361401
"cell_type": "code",
362402
"execution_count": null,
@@ -819,7 +859,7 @@
819859
"split_at_heading": true
820860
},
821861
"kernelspec": {
822-
"display_name": "Python 3 (ipykernel)",
862+
"display_name": "torch",
823863
"language": "python",
824864
"name": "python3"
825865
}

nbs/tutorials/tutorial.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -974,7 +974,7 @@
974974
{
975975
"data": {
976976
"image/svg+xml": [
977-
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"100\"><circle cx=\"50\" cy=\"50\" r=\"40\"/></svg>"
977+
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"100\"><circle cx=\"50\" cy=\"50\" r=\"40\"/></svg>\n"
978978
],
979979
"text/plain": [
980980
"<IPython.core.display.SVG object>"

tests/image.ipynb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [
8+
{
9+
"data": {
10+
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAE0lEQVR4nGNkaGDACpiwCw9WCQBqCACQJ5at+QAAAABJRU5ErkJggg==",
11+
"text/plain": [
12+
"<PIL.Image.Image image mode=RGB size=8x8>"
13+
]
14+
},
15+
"execution_count": null,
16+
"metadata": {},
17+
"output_type": "execute_result"
18+
}
19+
],
20+
"source": [
21+
"from PIL import Image\n",
22+
"Image.new(mode='RGB', size=(8, 8), color=\"green\")"
23+
]
24+
}
25+
],
26+
"metadata": {
27+
"kernelspec": {
28+
"display_name": "torch",
29+
"language": "python",
30+
"name": "python3"
31+
}
32+
},
33+
"nbformat": 4,
34+
"nbformat_minor": 2
35+
}

0 commit comments

Comments
 (0)