Skip to content

Commit b4cd2ac

Browse files
authored
Merge pull request #1058 from seeM/fix-sync
fix `nbdev_update`
2 parents efd44e4 + a002d16 commit b4cd2ac

File tree

9 files changed

+225
-154
lines changed

9 files changed

+225
-154
lines changed

nbdev/_modidx.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
'nbdev.doclinks._find_mod': ('api/doclinks.html#_find_mod', 'nbdev/doclinks.py'),
5858
'nbdev.doclinks._get_exps': ('api/doclinks.html#_get_exps', 'nbdev/doclinks.py'),
5959
'nbdev.doclinks._get_modidx': ('api/doclinks.html#_get_modidx', 'nbdev/doclinks.py'),
60+
'nbdev.doclinks._iter_py_cells': ('api/doclinks.html#_iter_py_cells', 'nbdev/doclinks.py'),
6061
'nbdev.doclinks._lineno': ('api/doclinks.html#_lineno', 'nbdev/doclinks.py'),
6162
'nbdev.doclinks._nbpath2html': ('api/doclinks.html#_nbpath2html', 'nbdev/doclinks.py'),
6263
'nbdev.doclinks._qual_mod': ('api/doclinks.html#_qual_mod', 'nbdev/doclinks.py'),
@@ -164,6 +165,7 @@
164165
'nbdev.process._is_direc': ('api/process.html#_is_direc', 'nbdev/process.py'),
165166
'nbdev.process._mk_procs': ('api/process.html#_mk_procs', 'nbdev/process.py'),
166167
'nbdev.process._norm_quarto': ('api/process.html#_norm_quarto', 'nbdev/process.py'),
168+
'nbdev.process._partition_cell': ('api/process.html#_partition_cell', 'nbdev/process.py'),
167169
'nbdev.process._quarto_re': ('api/process.html#_quarto_re', 'nbdev/process.py'),
168170
'nbdev.process.extract_directives': ('api/process.html#extract_directives', 'nbdev/process.py'),
169171
'nbdev.process.first_code_ln': ('api/process.html#first_code_ln', 'nbdev/process.py'),
@@ -317,11 +319,10 @@
317319
'nbdev.showdoc.doc': ('api/showdoc.html#doc', 'nbdev/showdoc.py'),
318320
'nbdev.showdoc.show_doc': ('api/showdoc.html#show_doc', 'nbdev/showdoc.py'),
319321
'nbdev.showdoc.showdoc_nm': ('api/showdoc.html#showdoc_nm', 'nbdev/showdoc.py')},
320-
'nbdev.sync': { 'nbdev.sync._get_call': ('api/sync.html#_get_call', 'nbdev/sync.py'),
321-
'nbdev.sync._mod_files': ('api/sync.html#_mod_files', 'nbdev/sync.py'),
322-
'nbdev.sync._script2notebook': ('api/sync.html#_script2notebook', 'nbdev/sync.py'),
322+
'nbdev.sync': { 'nbdev.sync._mod_files': ('api/sync.html#_mod_files', 'nbdev/sync.py'),
323323
'nbdev.sync._to_absolute': ('api/sync.html#_to_absolute', 'nbdev/sync.py'),
324-
'nbdev.sync._update_lib': ('api/sync.html#_update_lib', 'nbdev/sync.py'),
324+
'nbdev.sync._update_mod': ('api/sync.html#_update_mod', 'nbdev/sync.py'),
325+
'nbdev.sync._update_nb': ('api/sync.html#_update_nb', 'nbdev/sync.py'),
325326
'nbdev.sync.absolute_import': ('api/sync.html#absolute_import', 'nbdev/sync.py'),
326327
'nbdev.sync.nbdev_update': ('api/sync.html#nbdev_update', 'nbdev/sync.py')},
327328
'nbdev.test': { 'nbdev.test._keep_file': ('api/test.html#_keep_file', 'nbdev/test.py'),

nbdev/doclinks.py

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,35 +43,43 @@ def patch_name(o):
4343
return _sym_nm(a,o)
4444

4545
# %% ../nbs/api/doclinks.ipynb 9
46-
def _nbpath2html(p): return p.with_name(re.sub(r'\d+[a-zA-Z0-9]*_', '', p.name.lower())).with_suffix('.html')
46+
def _iter_py_cells(p):
47+
"Yield cells from an exported Python file."
48+
p = Path(p)
49+
cells = p.read_text().split("\n# %% ")
50+
for cell in cells[1:]:
51+
top,code = cell.split('\n', 1)
52+
nb,idx = top.split()
53+
nb_path = None if nb=='auto' else (p.parent/nb).resolve() # NB paths are stored relative to .py file
54+
if code.endswith('\n'): code=code[:-1]
55+
yield AttrDict(nb=nb, idx=int(idx), code=code, nb_path=nb_path, py_path=p.resolve())
4756

4857
# %% ../nbs/api/doclinks.ipynb 11
49-
def _get_modidx(pyfile, code_root, nbs_path):
58+
def _nbpath2html(p): return p.with_name(re.sub(r'\d+[a-zA-Z0-9]*_', '', p.name.lower())).with_suffix('.html')
59+
60+
# %% ../nbs/api/doclinks.ipynb 13
61+
def _get_modidx(py_path, code_root, nbs_path):
5062
"Get module symbol index for a Python source file"
5163
cfg = get_config()
52-
rel_name = pyfile.resolve().relative_to(code_root).as_posix()
53-
mod_name = '.'.join(rel_name.rpartition('.')[0].split('/')) # module name created by pyfile
54-
cells = Path(pyfile).read_text().split("\n# %% ")
64+
rel_name = py_path.resolve().relative_to(code_root).as_posix()
65+
mod_name = '.'.join(rel_name.rpartition('.')[0].split('/')) # module name created by py_path
5566

5667
_def_types = ast.FunctionDef,ast.AsyncFunctionDef,ast.ClassDef
5768
d = {}
58-
for cell in cells[1:]: # First cell is autogenerated header
59-
top,*rest = cell.splitlines() # First line is cell header
60-
nb = top.split()[0]
61-
if nb != 'auto':
62-
nbpath = (pyfile.parent/nb).resolve() # NB paths are stored relative to .py file
63-
loc = _nbpath2html(nbpath.relative_to(nbs_path))
64-
65-
def _stor(nm):
66-
for n in L(nm): d[f'{mod_name}.{n}'] = f'{loc.as_posix()}#{n.lower()}',rel_name
67-
for tree in ast.parse('\n'.join(rest)).body:
68-
if isinstance(tree, _def_types): _stor(patch_name(tree))
69-
if isinstance(tree, ast.ClassDef):
70-
for t2 in tree.body:
71-
if isinstance(t2, _def_types): _stor(f'{tree.name}.{t2.name}')
69+
for cell in _iter_py_cells(py_path):
70+
if cell.nb == 'auto': continue
71+
loc = _nbpath2html(cell.nb_path.relative_to(nbs_path))
72+
73+
def _stor(nm):
74+
for n in L(nm): d[f'{mod_name}.{n}'] = f'{loc.as_posix()}#{n.lower()}',rel_name
75+
for tree in ast.parse(cell.code).body:
76+
if isinstance(tree, _def_types): _stor(patch_name(tree))
77+
if isinstance(tree, ast.ClassDef):
78+
for t2 in tree.body:
79+
if isinstance(t2, _def_types): _stor(f'{tree.name}.{t2.name}')
7280
return {mod_name: d}
7381

74-
# %% ../nbs/api/doclinks.ipynb 12
82+
# %% ../nbs/api/doclinks.ipynb 15
7583
def _build_modidx(dest=None, nbs_path=None, skip_exists=False):
7684
"Create _modidx.py"
7785
if dest is None: dest = get_config().lib_path
@@ -89,7 +97,7 @@ def _build_modidx(dest=None, nbs_path=None, skip_exists=False):
8997
res['syms'].update(_get_modidx((dest.parent/file).resolve(), code_root, nbs_path=nbs_path))
9098
idxfile.write_text("# Autogenerated by nbdev\n\nd = "+pformat(res, width=140, indent=2, compact=True))
9199

92-
# %% ../nbs/api/doclinks.ipynb 17
100+
# %% ../nbs/api/doclinks.ipynb 20
93101
@delegates(globtastic)
94102
def nbglob(path=None, skip_folder_re = '^[_.]', file_glob='*.ipynb', skip_file_re='^[_.]', key='nbs_path', as_path=False, **kwargs):
95103
"Find all files in a directory matching an extension given a config key."
@@ -99,7 +107,7 @@ def nbglob(path=None, skip_folder_re = '^[_.]', file_glob='*.ipynb', skip_file_r
99107
skip_file_re=skip_file_re, recursive=recursive, **kwargs)
100108
return res.map(Path) if as_path else res
101109

102-
# %% ../nbs/api/doclinks.ipynb 18
110+
# %% ../nbs/api/doclinks.ipynb 21
103111
def nbglob_cli(
104112
path:str=None, # Path to notebooks
105113
symlinks:bool=False, # Follow symlinks?
@@ -113,7 +121,7 @@ def nbglob_cli(
113121
return nbglob(path, symlinks=symlinks, file_glob=file_glob, file_re=file_re, folder_re=folder_re,
114122
skip_file_glob=skip_file_glob, skip_file_re=skip_file_re, skip_folder_re=skip_folder_re)
115123

116-
# %% ../nbs/api/doclinks.ipynb 19
124+
# %% ../nbs/api/doclinks.ipynb 22
117125
@call_parse
118126
@delegates(nbglob_cli)
119127
def nbdev_export(
@@ -126,11 +134,11 @@ def nbdev_export(
126134
add_init(get_config().lib_path)
127135
_build_modidx()
128136

129-
# %% ../nbs/api/doclinks.ipynb 21
137+
# %% ../nbs/api/doclinks.ipynb 24
130138
import importlib,ast
131139
from functools import lru_cache
132140

133-
# %% ../nbs/api/doclinks.ipynb 22
141+
# %% ../nbs/api/doclinks.ipynb 25
134142
def _find_mod(mod):
135143
mp,_,mr = mod.partition('/')
136144
spec = importlib.util.find_spec(mp)
@@ -153,7 +161,7 @@ def _get_exps(mod):
153161

154162
def _lineno(sym, fname): return _get_exps(fname).get(sym, None) if fname else None
155163

156-
# %% ../nbs/api/doclinks.ipynb 24
164+
# %% ../nbs/api/doclinks.ipynb 27
157165
def _qual_sym(s, settings):
158166
if not isinstance(s,tuple): return s
159167
nb,py = s
@@ -168,10 +176,10 @@ def _qual_syms(entries):
168176
if 'doc_host' not in settings: return entries
169177
return {'syms': {mod:_qual_mod(d, settings) for mod,d in entries['syms'].items()}, 'settings':settings}
170178

171-
# %% ../nbs/api/doclinks.ipynb 25
179+
# %% ../nbs/api/doclinks.ipynb 28
172180
_re_backticks = re.compile(r'`([^`\s]+)`')
173181

174-
# %% ../nbs/api/doclinks.ipynb 26
182+
# %% ../nbs/api/doclinks.ipynb 29
175183
@lru_cache(None)
176184
class NbdevLookup:
177185
"Mapping from symbol names to docs and source URLs"

nbdev/maker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def update_import(source, tree, libname, f=relative_import):
149149
nmod = f(imp.module, libname, imp.level)
150150
lin = imp.lineno-1
151151
sec = src[lin][imp.col_offset:imp.end_col_offset]
152-
newsec = re.sub(f"(from +){'.'*imp.level}{imp.module}", fr"\1{nmod}", sec)
152+
newsec = re.sub(f"(from +){'.'*imp.level}{imp.module or ''}", fr"\1{nmod}", sec)
153153
src[lin] = src[lin].replace(sec,newsec)
154154
return src
155155

nbdev/process.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,34 +54,38 @@ def first_code_ln(code_list, re_pattern=None, lang='python'):
5454
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))
5555

5656
# %% ../nbs/api/process.ipynb 17
57+
def _partition_cell(cell, lang):
58+
if not cell.source: return [],[]
59+
lines = cell.source.splitlines(True)
60+
first_code = first_code_ln(lines, lang=lang)
61+
return lines[:first_code],lines[first_code:]
62+
63+
# %% ../nbs/api/process.ipynb 18
5764
def extract_directives(cell, remove=True, lang='python'):
5865
"Take leading comment directives from lines of code in `ss`, remove `#|`, and split"
59-
if cell.source:
60-
ss = cell.source.splitlines(True)
61-
first_code = first_code_ln(ss, lang=lang)
62-
if not ss or first_code==0: return {}
63-
pre = ss[:first_code]
64-
if remove:
65-
# Leave Quarto directives and cell magic in place for later processing
66-
cell['source'] = ''.join([_norm_quarto(o, lang) for o in pre if _quarto_re(lang).match(o) or _cell_mgc.match(o)] + ss[first_code:])
67-
return dict(L(_directive(s, lang) for s in pre).filter())
68-
69-
# %% ../nbs/api/process.ipynb 21
66+
dirs,code = _partition_cell(cell, lang)
67+
if not dirs: return {}
68+
if remove:
69+
# Leave Quarto directives and cell magic in place for later processing
70+
cell['source'] = ''.join([_norm_quarto(o, lang) for o in dirs if _quarto_re(lang).match(o) or _cell_mgc.match(o)] + code)
71+
return dict(L(_directive(s, lang) for s in dirs).filter())
72+
73+
# %% ../nbs/api/process.ipynb 22
7074
def opt_set(var, newval):
7175
"newval if newval else var"
7276
return newval if newval else var
7377

74-
# %% ../nbs/api/process.ipynb 22
78+
# %% ../nbs/api/process.ipynb 23
7579
def instantiate(x, **kwargs):
7680
"Instantiate `x` if it's a type"
7781
return x(**kwargs) if isinstance(x,type) else x
7882

7983
def _mk_procs(procs, nb): return L(procs).map(instantiate, nb=nb)
8084

81-
# %% ../nbs/api/process.ipynb 23
85+
# %% ../nbs/api/process.ipynb 24
8286
def _is_direc(f): return getattr(f, '__name__', '-')[-1]=='_'
8387

84-
# %% ../nbs/api/process.ipynb 24
88+
# %% ../nbs/api/process.ipynb 25
8589
class NBProcessor:
8690
"Process cells and nbdev comments in a notebook"
8791
def __init__(self, path=None, procs=None, nb=None, debug=False, rm_directives=True, process=False):
@@ -121,7 +125,7 @@ def process(self):
121125
"Process all cells with all processors"
122126
for proc in self.procs: self._proc(proc)
123127

124-
# %% ../nbs/api/process.ipynb 34
128+
# %% ../nbs/api/process.ipynb 35
125129
class Processor:
126130
"Base class for processors"
127131
def __init__(self, nb): self.nb = nb

nbdev/sync.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from .config import *
99
from .maker import *
1010
from .process import *
11+
from .process import _partition_cell
1112
from .export import *
13+
from .doclinks import _iter_py_cells
1214

1315
from execnb.nbio import *
1416
from fastcore.script import *
@@ -26,51 +28,46 @@ def absolute_import(name, fname, level):
2628
return '.'.join(mods[:len(mods)-level+1]) + f".{name}"
2729

2830
# %% ../nbs/api/sync.ipynb 7
31+
@functools.lru_cache(maxsize=None)
32+
def _mod_files():
33+
midx = import_module(f'{get_config().lib_name}._modidx')
34+
return L(files for mod in midx.d['syms'].values() for _,files in mod.values()).unique()
35+
36+
# %% ../nbs/api/sync.ipynb 8
2937
_re_import = re.compile("from\s+\S+\s+import\s+\S")
3038

31-
# %% ../nbs/api/sync.ipynb 9
32-
def _to_absolute(code, lib_name):
39+
# %% ../nbs/api/sync.ipynb 10
40+
def _to_absolute(code, py_path, lib_dir):
3341
if not _re_import.search(code): return code
34-
res = update_import(code, ast.parse(code).body, lib_name, absolute_import)
42+
res = update_import(code, ast.parse(code).body, str(py_path.relative_to(lib_dir).parent), absolute_import)
3543
return ''.join(res) if res else code
3644

37-
def _update_lib(nbname, nb_locs, lib_name=None):
38-
if lib_name is None: lib_name = get_config().lib_name
39-
absnm = get_config().path('lib_path')/nbname
40-
nbp = NBProcessor(absnm, ExportModuleProc(), rm_directives=False)
45+
# %% ../nbs/api/sync.ipynb 11
46+
def _update_nb(nb_path, cells, lib_dir):
47+
"Update notebook `nb_path` with contents from `cells`"
48+
nbp = NBProcessor(nb_path, ExportModuleProc(), rm_directives=False)
4149
nbp.process()
42-
nb = nbp.nb
43-
44-
for name,idx,code in nb_locs:
45-
assert name==nbname
46-
cell = nb.cells[int(idx)]
47-
directives = ''.join(cell.source.splitlines(True)[:len(cell.directives_)])
48-
cell.source = directives + _to_absolute(code, lib_name)
49-
write_nb(nb, absnm)
50-
51-
# %% ../nbs/api/sync.ipynb 10
52-
@functools.lru_cache(maxsize=None)
53-
def _mod_files():
54-
midx = import_module(f'{get_config().lib_name}._modidx')
55-
return L(files for mod in midx.d['syms'].values() for _,files in mod.values()).unique()
50+
for cell in cells:
51+
assert cell.nb_path == nb_path
52+
nbcell = nbp.nb.cells[cell.idx]
53+
dirs,_ = _partition_cell(nbcell, 'python')
54+
nbcell.source = ''.join(dirs) + _to_absolute(cell.code, cell.py_path, lib_dir)
55+
write_nb(nbp.nb, nb_path)
5656

5757
# %% ../nbs/api/sync.ipynb 12
58-
def _get_call(s):
59-
top,*rest = s.splitlines()
60-
return (*top.split(),'\n'.join(rest))
61-
62-
def _script2notebook(fname:str):
63-
code_cells = Path(fname).read_text().split("\n# %% ")[1:]
64-
locs = L(_get_call(s) for s in code_cells if not s.startswith('auto '))
65-
for nbname,nb_locs in groupby(locs, 0).items(): _update_lib(nbname, nb_locs)
58+
def _update_mod(py_path, lib_dir):
59+
"Propagate changes from cells in module `py_path` to corresponding notebooks"
60+
py_cells = L(_iter_py_cells(py_path)).filter(lambda o: o.nb != 'auto')
61+
for nb_path,cells in groupby(py_cells, 'nb_path').items(): _update_nb(nb_path, cells, lib_dir)
6662

63+
# %% ../nbs/api/sync.ipynb 14
6764
@call_parse
6865
def nbdev_update(fname:str=None): # A Python file name to update
6966
"Propagate change in modules matching `fname` to notebooks that created them"
7067
if fname and fname.endswith('.ipynb'): raise ValueError("`nbdev_update` operates on .py files. If you wish to convert notebooks instead, see `nbdev_export`.")
7168
if os.environ.get('IN_TEST',0): return
72-
fname = Path(fname or get_config().path('lib_path'))
73-
lib_dir = get_config().path("lib_path").parent
69+
cfg = get_config()
70+
fname = Path(fname or cfg.lib_path)
71+
lib_dir = cfg.lib_path.parent
7472
files = globtastic(fname, file_glob='*.py').filter(lambda x: str(Path(x).absolute().relative_to(lib_dir) in _mod_files()))
75-
files.map(_script2notebook)
76-
73+
files.map(_update_mod, lib_dir=lib_dir)

0 commit comments

Comments
 (0)