nbdev 2.3.25__py3-none-any.whl → 2.4.8__py3-none-any.whl

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.
nbdev/config.py CHANGED
@@ -1,17 +1,12 @@
1
- """Read and write nbdev's `settings.ini` file.
2
- `get_config` is the main function for reading settings."""
1
+ """Configuring nbdev and bootstrapping notebook export"""
3
2
 
4
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/01_config.ipynb.
5
4
 
6
5
  # %% auto 0
7
- __all__ = ['nbdev_create_config', 'get_config', 'config_key', 'create_output', 'show_src', 'update_version', 'add_init',
8
- 'write_cells']
6
+ __all__ = ['pyproj_tmpl', 'nbdev_create_config', 'get_config', 'config_key', 'is_nbdev', 'create_output', 'show_src',
7
+ 'update_version', 'update_proj', 'add_init', 'write_cells']
9
8
 
10
- # %% ../nbs/api/01_config.ipynb 2
11
- _doc_ = """Read and write nbdev's `settings.ini` file.
12
- `get_config` is the main function for reading settings."""
13
-
14
- # %% ../nbs/api/01_config.ipynb 3
9
+ # %% ../nbs/api/01_config.ipynb
15
10
  from datetime import datetime
16
11
  from fastcore.docments import *
17
12
  from fastcore.utils import *
@@ -25,16 +20,16 @@ from IPython.display import Markdown
25
20
  from execnb.nbio import read_nb,NbCell
26
21
  from urllib.error import HTTPError
27
22
 
28
- # %% ../nbs/api/01_config.ipynb 8
23
+ # %% ../nbs/api/01_config.ipynb
29
24
  _nbdev_home_dir = 'nbdev' # sub-directory of xdg base dir
30
25
  _nbdev_cfg_name = 'settings.ini'
31
26
 
32
- # %% ../nbs/api/01_config.ipynb 9
27
+ # %% ../nbs/api/01_config.ipynb
33
28
  def _git_repo():
34
29
  try: return repo_details(run('git config --get remote.origin.url'))[1]
35
30
  except OSError: return
36
31
 
37
- # %% ../nbs/api/01_config.ipynb 11
32
+ # %% ../nbs/api/01_config.ipynb
38
33
  # When adding a named default to the list below, be sure that that name
39
34
  # is also added to one of the sections in `_nbdev_cfg_sections` as well,
40
35
  # or it won't get written by `nbdev_create_config`:
@@ -54,7 +49,7 @@ def _apply_defaults(
54
49
  license='apache2', # License for the package
55
50
  copyright:str=None, # Copyright for the package, defaults to '`current_year` onwards, `author`'
56
51
  status='3', # Development status PyPI classifier
57
- min_python='3.7', # Minimum Python version PyPI classifier
52
+ min_python='3.9', # Minimum Python version PyPI classifier
58
53
  audience='Developers', # Intended audience PyPI classifier
59
54
  language='English', # Language PyPI classifier
60
55
  recursive:bool_arg=True, # Include subfolders in notebook globs?
@@ -66,7 +61,10 @@ def _apply_defaults(
66
61
  jupyter_hooks:bool_arg=False, # Run Jupyter hooks?
67
62
  clean_ids:bool_arg=True, # Remove ids from plaintext reprs?
68
63
  clear_all:bool_arg=False, # Remove all cell metadata and cell outputs?
64
+ cell_number:bool_arg=True, # Add cell number to the exported file
69
65
  put_version_in_init:bool_arg=True, # Add the version to the main __init__.py in nbdev_export
66
+ update_pyproject:bool_arg=True, # Create/update pyproject.toml with correct project name
67
+ skip_procs:str='', # A comma-separated list of processors that you want to skip
70
68
  ):
71
69
  "Apply default settings where missing in `cfg`."
72
70
  if getattr(cfg,'repo',None) is None:
@@ -81,7 +79,7 @@ def _apply_defaults(
81
79
  cfg[k] = v
82
80
  return cfg
83
81
 
84
- # %% ../nbs/api/01_config.ipynb 12
82
+ # %% ../nbs/api/01_config.ipynb
85
83
  def _get_info(owner, repo, default_branch='main', default_kw='nbdev'):
86
84
  from ghapi.all import GhApi
87
85
  api = GhApi(owner=owner, repo=repo, token=os.getenv('GITHUB_TOKEN'))
@@ -97,7 +95,7 @@ https://nbdev.fast.ai/api/release.html#setup"""]
97
95
 
98
96
  return r.default_branch, default_kw if not getattr(r, 'topics', []) else ' '.join(r.topics), r.description
99
97
 
100
- # %% ../nbs/api/01_config.ipynb 14
98
+ # %% ../nbs/api/01_config.ipynb
101
99
  def _fetch_from_git(raise_err=False):
102
100
  "Get information for settings.ini from the user."
103
101
  res={}
@@ -113,7 +111,7 @@ def _fetch_from_git(raise_err=False):
113
111
  else: res['lib_name'] = res['repo'].replace('-','_')
114
112
  return res
115
113
 
116
- # %% ../nbs/api/01_config.ipynb 16
114
+ # %% ../nbs/api/01_config.ipynb
117
115
  def _prompt_user(cfg, inferred):
118
116
  "Let user input values not in `cfg` or `inferred`."
119
117
  res = cfg.copy()
@@ -127,7 +125,7 @@ def _prompt_user(cfg, inferred):
127
125
  print(msg+res[k]+' # Automatically inferred from git')
128
126
  return res
129
127
 
130
- # %% ../nbs/api/01_config.ipynb 18
128
+ # %% ../nbs/api/01_config.ipynb
131
129
  def _cfg2txt(cfg, head, sections, tail=''):
132
130
  "Render `cfg` with commented sections."
133
131
  nm = cfg.d.name
@@ -139,13 +137,13 @@ def _cfg2txt(cfg, head, sections, tail=''):
139
137
  res += tail
140
138
  return res.strip()
141
139
 
142
- # %% ../nbs/api/01_config.ipynb 20
140
+ # %% ../nbs/api/01_config.ipynb
143
141
  _nbdev_cfg_head = '''# All sections below are required unless otherwise specified.
144
- # See https://github.com/fastai/nbdev/blob/master/settings.ini for examples.
142
+ # See https://github.com/AnswerDotAI/nbdev/blob/main/settings.ini for examples.
145
143
 
146
144
  '''
147
145
  _nbdev_cfg_sections = {'Python library': 'repo lib_name version min_python license black_formatting',
148
- 'nbdev': 'doc_path lib_path nbs_path recursive tst_flags put_version_in_init',
146
+ 'nbdev': 'doc_path lib_path nbs_path recursive tst_flags put_version_in_init update_pyproject',
149
147
  'Docs': 'branch custom_sidebar doc_host doc_baseurl git_url title',
150
148
  'PyPI': 'audience author author_email copyright description keywords language status user'}
151
149
  _nbdev_cfg_tail = '''### Optional ###
@@ -156,7 +154,7 @@ _nbdev_cfg_tail = '''### Optional ###
156
154
  # package_data =
157
155
  '''
158
156
 
159
- # %% ../nbs/api/01_config.ipynb 21
157
+ # %% ../nbs/api/01_config.ipynb
160
158
  @call_parse
161
159
  @delegates(_apply_defaults, but='cfg')
162
160
  def nbdev_create_config(
@@ -182,19 +180,17 @@ def nbdev_create_config(
182
180
  cfg_fn = Path(path)/cfg_name
183
181
  print(f'{cfg_fn} created.')
184
182
 
185
- # %% ../nbs/api/01_config.ipynb 24
183
+ # %% ../nbs/api/01_config.ipynb
186
184
  def _nbdev_config_file(cfg_name=_nbdev_cfg_name, path=None):
187
- cfg_path = path = Path.cwd() if path is None else Path(path)
188
- while cfg_path != cfg_path.parent and not (cfg_path/cfg_name).exists(): cfg_path = cfg_path.parent
189
- if not (cfg_path/cfg_name).exists(): cfg_path = path
190
- return cfg_path/cfg_name
185
+ cfg_path = Path.cwd() if path is None else Path(path)
186
+ return getattr(Config.find(cfg_name), 'config_file', cfg_path/cfg_name)
191
187
 
192
- # %% ../nbs/api/01_config.ipynb 26
188
+ # %% ../nbs/api/01_config.ipynb
193
189
  def _xdg_config_paths(cfg_name=_nbdev_cfg_name):
194
190
  xdg_config_paths = reversed([xdg_config_home()]+xdg_config_dirs())
195
191
  return [o/_nbdev_home_dir/cfg_name for o in xdg_config_paths]
196
192
 
197
- # %% ../nbs/api/01_config.ipynb 27
193
+ # %% ../nbs/api/01_config.ipynb
198
194
  def _type(t): return bool if t==bool_arg else t
199
195
  _types = {k:_type(v['anno']) for k,v in docments(_apply_defaults,full=True,returns=False).items() if k != 'cfg'}
200
196
 
@@ -206,24 +202,44 @@ def get_config(cfg_name=_nbdev_cfg_name, path=None):
206
202
  cfg = Config(cfg_file.parent, cfg_file.name, extra_files=extra_files, types=_types)
207
203
  return _apply_defaults(cfg)
208
204
 
209
- # %% ../nbs/api/01_config.ipynb 42
205
+ # %% ../nbs/api/01_config.ipynb
210
206
  def config_key(c, default=None, path=True, missing_ok=None):
211
207
  "Deprecated: use `get_config().get` or `get_config().path` instead."
212
208
  warn("`config_key` is deprecated. Use `get_config().get` or `get_config().path` instead.", DeprecationWarning)
213
209
  return get_config().path(c, default) if path else get_config().get(c, default)
214
210
 
215
- # %% ../nbs/api/01_config.ipynb 44
211
+ # %% ../nbs/api/01_config.ipynb
212
+ def is_nbdev(): return _nbdev_config_file().exists()
213
+
214
+ # %% ../nbs/api/01_config.ipynb
216
215
  def create_output(txt, mime):
217
216
  "Add a cell output containing `txt` of the `mime` text MIME sub-type"
218
217
  return [{"data": { f"text/{mime}": str(txt).splitlines(True) },
219
218
  "execution_count": 1, "metadata": {}, "output_type": "execute_result"}]
220
219
 
221
- # %% ../nbs/api/01_config.ipynb 45
220
+ # %% ../nbs/api/01_config.ipynb
222
221
  def show_src(src, lang='python'): return Markdown(f'```{lang}\n{src}\n```')
223
222
 
224
- # %% ../nbs/api/01_config.ipynb 48
225
- _re_version = re.compile('^__version__\s*=.*$', re.MULTILINE)
223
+ # %% ../nbs/api/01_config.ipynb
224
+ pyproj_tmpl = """[build-system]
225
+ requires = ["setuptools>=64.0"]
226
+ build-backend = "setuptools.build_meta"
227
+
228
+ [project]
229
+ name = "FILL_IN"
230
+ requires-python="FILL_IN"
231
+ dynamic = [ "keywords", "description", "version", "dependencies", "optional-dependencies", "readme", "license", "authors", "classifiers", "entry-points", "scripts", "urls"]
232
+
233
+ [tool.uv]
234
+ cache-keys = [{ file = "pyproject.toml" }, { file = "settings.ini" }, { file = "setup.py" }]
235
+ """
236
+
237
+ # %% ../nbs/api/01_config.ipynb
238
+ _re_version = re.compile(r'^__version__\s*=.*$', re.MULTILINE)
239
+ _re_proj = re.compile(r'^name\s*=\s*".*$', re.MULTILINE)
240
+ _re_reqpy = re.compile(r'^requires-python\s*=\s*".*$', re.MULTILINE)
226
241
  _init = '__init__.py'
242
+ _pyproj = 'pyproject.toml'
227
243
 
228
244
  def update_version(path=None):
229
245
  "Add or update `__version__` in the main `__init__.py` of the library."
@@ -238,6 +254,15 @@ def update_version(path=None):
238
254
 
239
255
  def _has_py(fs): return any(1 for f in fs if f.endswith('.py'))
240
256
 
257
+ def update_proj(path):
258
+ "Create or update `pyproject.toml` in the project root."
259
+ fname = path/_pyproj
260
+ if not fname.exists(): fname.write_text(pyproj_tmpl)
261
+ txt = fname.read_text()
262
+ txt = _re_proj.sub(f'name="{get_config().lib_name}"', txt)
263
+ txt = _re_reqpy.sub(f'requires-python=">={get_config().min_python}"', txt)
264
+ fname.write_text(txt)
265
+
241
266
  def add_init(path=None):
242
267
  "Add `__init__.py` in all subdirs of `path` containing python files if it's not there already."
243
268
  # we add the lowest-level `__init__.py` files first, which ensures _has_py succeeds for parent modules
@@ -249,14 +274,17 @@ def add_init(path=None):
249
274
  subds = (os.listdir(r/d) for d in ds)
250
275
  if _has_py(fs) or any(filter(_has_py, subds)) and not (r/_init).exists(): (r/_init).touch()
251
276
  if get_config().get('put_version_in_init', True): update_version(path)
277
+ if get_config().get('update_pyproject', True): update_proj(path.parent)
252
278
 
253
- # %% ../nbs/api/01_config.ipynb 51
254
- def write_cells(cells, hdr, file, offset=0):
279
+ # %% ../nbs/api/01_config.ipynb
280
+ def write_cells(cells, hdr, file, offset=0, cell_number=True, solo_nb=False):
255
281
  "Write `cells` to `file` along with header `hdr` starting at index `offset` (mainly for nbdev internal use)."
256
282
  for cell in cells:
257
- if cell.source.strip(): file.write(f'\n\n{hdr} {cell.idx_+offset}\n{cell.source}')
283
+ if cell.cell_type=='code' and cell.source.strip():
284
+ idx = f" {cell.idx_+offset}" if cell_number else ""
285
+ file.write(f'\n\n{hdr}{idx}\n{cell.source}') if not solo_nb else file.write(f'\n\n{cell.source}')
258
286
 
259
- # %% ../nbs/api/01_config.ipynb 52
287
+ # %% ../nbs/api/01_config.ipynb
260
288
  def _basic_export_nb(fname, name, dest=None):
261
289
  "Basic exporter to bootstrap nbdev."
262
290
  if dest is None: dest = get_config().lib_path
nbdev/diff.py ADDED
@@ -0,0 +1,92 @@
1
+ """Get ipynb diffs by cell"""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/19_diff.ipynb.
4
+
5
+ # %% auto 0
6
+ __all__ = ['read_nb_from_git', 'nbs_pair', 'changed_cells', 'source_diff', 'cell_diffs']
7
+
8
+ # %% ../nbs/api/19_diff.ipynb
9
+ import json
10
+ from fastcore.utils import *
11
+ from fastcore.meta import delegates
12
+ from difflib import unified_diff
13
+ from fastgit import Git
14
+ from execnb.nbio import *
15
+
16
+ # %% ../nbs/api/19_diff.ipynb
17
+ def read_nb_from_git(
18
+ g:Git, # The git object
19
+ path, # The path to the notebook (absolute or relative to git root)
20
+ ref=None # The git ref to read from (e.g. HEAD); None for working dir
21
+ )->AttrDict: # The notebook
22
+ "Read notebook from git ref (e.g. HEAD) at path, or working dir if ref is None"
23
+ path = Path(path)
24
+ if path.is_absolute(): path = path.relative_to(g.top())
25
+ if ref is None: return read_nb(g.top()/path)
26
+ raw = g.show(f'{ref}:{path}', split=False)
27
+ return dict2nb(json.loads(raw))
28
+
29
+ # %% ../nbs/api/19_diff.ipynb
30
+ def _nb_srcdict(g:Git, nb_path, ref=None, f=noop):
31
+ "Dict of id->source"
32
+ nb = read_nb_from_git(g, nb_path, ref)
33
+ return {c['id']: f(c) for c in nb.cells}
34
+
35
+ # %% ../nbs/api/19_diff.ipynb
36
+ def nbs_pair(
37
+ nb_path, # Path to the notebook
38
+ ref_a='HEAD', # First git ref (None for working dir)
39
+ ref_b=None, # Second git ref (None for working dir)
40
+ f=noop # Function to call on contents
41
+ ): # Tuple of two notebooks
42
+ "NBs at two refs; None means working dir. By default provides HEAD and working dir"
43
+ nb_path = Path(nb_path).resolve()
44
+ g = Git(nb_path.parent)
45
+ return _nb_srcdict(g, nb_path, ref_a, f), _nb_srcdict(g, nb_path, ref_b, f)
46
+
47
+ # %% ../nbs/api/19_diff.ipynb
48
+ def _cell_changes(
49
+ nb_path, # Path to the notebook
50
+ fn, # function to call to get dict values
51
+ ref_a='HEAD', # First git ref (None for working dir)
52
+ ref_b=None, # Second git ref (None for working dir)
53
+ adds=True, # Include cells in b but not in a
54
+ changes=True, # Include cells with different content
55
+ dels=False, # Include cells in a but not in b
56
+ metadata=False, # Consider cell metadata when comparing
57
+ outputs=False # Consider cell outputs when comparing
58
+ ): # Dict of results
59
+ "Apply fn(cell_id, old_content, new_content) to changed cells between two refs"
60
+ def cell_content(c):
61
+ res = c.get('source', '')
62
+ if metadata: res += '\n# metadata: ' + json.dumps(c.get('metadata', {}), sort_keys=True)
63
+ if outputs: res += '\n# outputs: ' + json.dumps(c.get('outputs', []), sort_keys=True)
64
+ return res
65
+ old,new = nbs_pair(nb_path, ref_a, ref_b, f=cell_content)
66
+ res = {}
67
+ if adds: res |= {cid: fn(cid, '', new[cid]) for cid in new if cid not in old}
68
+ if changes: res |= {cid: fn(cid, old[cid], new[cid]) for cid in new if cid in old and new[cid] != old[cid]}
69
+ if dels: res |= {cid: fn(cid, old[cid], '') for cid in old if cid not in new}
70
+ return res
71
+
72
+ # %% ../nbs/api/19_diff.ipynb
73
+ @delegates(_cell_changes)
74
+ def changed_cells(nb_path, **kwargs):
75
+ "Return set of cell IDs for changed/added/deleted cells between two refs"
76
+ def f(cid,o,n): return cid
77
+ return set(_cell_changes(nb_path, f, **kwargs).keys())
78
+
79
+ # %% ../nbs/api/19_diff.ipynb
80
+ def source_diff(
81
+ old_source, # Original source string
82
+ new_source # New source string
83
+ ): # Unified diff string
84
+ "Return unified diff string for source change"
85
+ return '\n'.join(unified_diff(old_source.splitlines(), new_source.splitlines(), lineterm=''))
86
+
87
+ # %% ../nbs/api/19_diff.ipynb
88
+ @delegates(_cell_changes)
89
+ def cell_diffs(nb_path, **kwargs):
90
+ "{cell_id:diff} for changed/added/deleted cells between two refs"
91
+ def f(cid,o,n): return source_diff(o,n)
92
+ return _cell_changes(nb_path, f, **kwargs)
nbdev/doclinks.py CHANGED
@@ -1,9 +1,11 @@
1
+ """Generating a documentation index from a module"""
2
+
1
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/05_doclinks.ipynb.
2
4
 
3
5
  # %% auto 0
4
- __all__ = ['patch_name', 'nbglob', 'nbglob_cli', 'nbdev_export', 'NbdevLookup']
6
+ __all__ = ['typs', 'bset', 'patch_name', 'nbglob', 'nbglob_cli', 'nbdev_export', 'create_index', 'NbdevLookup']
5
7
 
6
- # %% ../nbs/api/05_doclinks.ipynb 2
8
+ # %% ../nbs/api/05_doclinks.ipynb
7
9
  from .config import *
8
10
  from .maker import *
9
11
  from .export import *
@@ -12,16 +14,22 @@ from .imports import *
12
14
  from fastcore.script import *
13
15
  from fastcore.utils import *
14
16
  from fastcore.meta import delegates
17
+ from fastcore.net import urlread
15
18
 
16
- import ast,contextlib
17
- import pkg_resources,importlib
18
- from astunparse import unparse
19
+ import ast,builtins,contextlib
20
+ import importlib
19
21
 
22
+ from astunparse import unparse
23
+ from io import BytesIO
24
+ from collections import defaultdict
20
25
  from pprint import pformat
21
26
  from urllib.parse import urljoin
22
27
  from functools import lru_cache
28
+ from types import ModuleType
23
29
 
24
- # %% ../nbs/api/05_doclinks.ipynb 5
30
+ from importlib.metadata import entry_points
31
+
32
+ # %% ../nbs/api/05_doclinks.ipynb
25
33
  def _sym_nm(klas, sym): return f'{unparse(klas).strip()}.{sym.name}'
26
34
 
27
35
  def _binop_leafs(bo, o):
@@ -42,26 +50,32 @@ def patch_name(o):
42
50
  else: return o.name
43
51
  return _sym_nm(a,o)
44
52
 
45
- # %% ../nbs/api/05_doclinks.ipynb 9
53
+ # %% ../nbs/api/05_doclinks.ipynb
46
54
  def _iter_py_cells(p):
47
55
  "Yield cells from an exported Python file."
48
56
  p = Path(p)
49
57
  cells = p.read_text(encoding='utf-8').split("\n# %% ")
58
+ has_cell_number = get_config().cell_number
50
59
  for cell in cells[1:]:
51
60
  top,code = cell.split('\n', 1)
52
61
  try:
53
- *nb,idx = top.split()
54
- nb = ' '.join(nb)
62
+ if has_cell_number:
63
+ *nb,idx = top.split()
64
+ nb = ' '.join(nb)
65
+ idx = int(idx)
66
+ else:
67
+ nb = top
68
+ idx = None
55
69
  except ValueError: raise ValueError(f"Unexpected format in '{p}' at cell:\n```\n# %% {cell.strip()}.\n```\n"
56
70
  "The expected format is: '# %% {nb_path} {cell_idx}'.")
57
71
  nb_path = None if nb=='auto' else (p.parent/nb).resolve() # NB paths are stored relative to .py file
58
72
  if code.endswith('\n'): code=code[:-1]
59
- yield AttrDict(nb=nb, idx=int(idx), code=code, nb_path=nb_path, py_path=p.resolve())
73
+ yield AttrDict(nb=nb, idx=idx, code=code, nb_path=nb_path, py_path=p.resolve())
60
74
 
61
- # %% ../nbs/api/05_doclinks.ipynb 11
75
+ # %% ../nbs/api/05_doclinks.ipynb
62
76
  def _nbpath2html(p): return p.with_name(re.sub(r'^\d+[a-zA-Z0-9]*_', '', p.name.lower())).with_suffix('.html')
63
77
 
64
- # %% ../nbs/api/05_doclinks.ipynb 13
78
+ # %% ../nbs/api/05_doclinks.ipynb
65
79
  def _get_modidx(py_path, code_root, nbs_path):
66
80
  "Get module symbol index for a Python source file"
67
81
  cfg = get_config()
@@ -71,7 +85,7 @@ def _get_modidx(py_path, code_root, nbs_path):
71
85
  _def_types = ast.FunctionDef,ast.AsyncFunctionDef,ast.ClassDef
72
86
  d = {}
73
87
  for cell in _iter_py_cells(py_path):
74
- if cell.nb == 'auto': continue
88
+ if 'auto' in cell.nb: continue
75
89
  loc = _nbpath2html(cell.nb_path.relative_to(nbs_path))
76
90
 
77
91
  def _stor(nm):
@@ -83,7 +97,7 @@ def _get_modidx(py_path, code_root, nbs_path):
83
97
  if isinstance(t2, _def_types): _stor(f'{tree.name}.{t2.name}')
84
98
  return {mod_name: d}
85
99
 
86
- # %% ../nbs/api/05_doclinks.ipynb 15
100
+ # %% ../nbs/api/05_doclinks.ipynb
87
101
  def _build_modidx(dest=None, nbs_path=None, skip_exists=False):
88
102
  "Create _modidx.py"
89
103
  if dest is None: dest = get_config().lib_path
@@ -98,11 +112,12 @@ def _build_modidx(dest=None, nbs_path=None, skip_exists=False):
98
112
  res['settings'] = {k:v for k,v in get_config().d.items()
99
113
  if k in ('doc_host','doc_baseurl','lib_path','git_url','branch')}
100
114
  code_root = dest.parent.resolve()
101
- for file in globtastic(dest, file_glob="*.py", skip_file_re='^_', skip_folder_re="\.ipynb_checkpoints"):
102
- res['syms'].update(_get_modidx((dest.parent/file).resolve(), code_root, nbs_path=nbs_path))
115
+ for file in globtastic(dest, file_glob="*.py", skip_file_re='^_', skip_folder_re=r"\.ipynb_checkpoints"):
116
+ try: res['syms'].update(_get_modidx((dest.parent/file).resolve(), code_root, nbs_path=nbs_path))
117
+ except ValueError: pass
103
118
  idxfile.write_text("# Autogenerated by nbdev\n\nd = "+pformat(res, width=140, indent=2, compact=True)+'\n')
104
119
 
105
- # %% ../nbs/api/05_doclinks.ipynb 20
120
+ # %% ../nbs/api/05_doclinks.ipynb
106
121
  @delegates(globtastic)
107
122
  def nbglob(path=None, skip_folder_re = '^[_.]', file_glob='*.ipynb', skip_file_re='^[_.]', key='nbs_path', as_path=False, **kwargs):
108
123
  "Find all files in a directory matching an extension given a config key."
@@ -112,7 +127,7 @@ def nbglob(path=None, skip_folder_re = '^[_.]', file_glob='*.ipynb', skip_file_r
112
127
  skip_file_re=skip_file_re, recursive=recursive, **kwargs)
113
128
  return res.map(Path) if as_path else res
114
129
 
115
- # %% ../nbs/api/05_doclinks.ipynb 21
130
+ # %% ../nbs/api/05_doclinks.ipynb
116
131
  def nbglob_cli(
117
132
  path:str=None, # Path to notebooks
118
133
  symlinks:bool=False, # Follow symlinks?
@@ -126,7 +141,7 @@ def nbglob_cli(
126
141
  return nbglob(path, symlinks=symlinks, file_glob=file_glob, file_re=file_re, folder_re=folder_re,
127
142
  skip_file_glob=skip_file_glob, skip_file_re=skip_file_re, skip_folder_re=skip_folder_re)
128
143
 
129
- # %% ../nbs/api/05_doclinks.ipynb 22
144
+ # %% ../nbs/api/05_doclinks.ipynb
130
145
  @call_parse
131
146
  @delegates(nbglob_cli)
132
147
  def nbdev_export(
@@ -135,19 +150,42 @@ def nbdev_export(
135
150
  **kwargs):
136
151
  "Export notebooks in `path` to Python modules"
137
152
  if os.environ.get('IN_TEST',0): return
153
+ if not is_nbdev(): raise Exception('`nbdev_export` must be called from a directory within a nbdev project.')
138
154
  if procs:
139
- import nbdev.export
140
- procs = [getattr(nbdev.export, p) for p in L(procs)]
155
+ import nbdev.export
156
+ procs = [getattr(nbdev.export, p) for p in L(procs)]
141
157
  files = nbglob(path=path, as_path=True, **kwargs).sorted('name')
142
158
  for f in files: nb_export(f, procs=procs)
143
159
  add_init(get_config().lib_path)
144
160
  _build_modidx()
145
161
 
146
- # %% ../nbs/api/05_doclinks.ipynb 25
162
+ # %% ../nbs/api/05_doclinks.ipynb
163
+ typs = 'module','class','method','function'
164
+ bset = set(dir(builtins))
165
+
166
+ # %% ../nbs/api/05_doclinks.ipynb
167
+ def create_index(url, pre=None):
168
+ "Create a documentation index from a sphinx inventory file at `url`, with optional prefix `pre`"
169
+ try: from sphinx.util.inventory import InventoryFile
170
+ except ImportError: raise ImportError('`sphinx` is a dependency for building indexes. Run `pip install sphinx` to use `create_index`.')
171
+ pre = ifnone(pre, f"{url}/")
172
+ invs = urlread(f'{url}/objects.inv', decode=False)
173
+ idx = InventoryFile.load(stream=BytesIO(invs), uri=pre, joinfunc=urljoin)
174
+ _get = lambda o: {k:v[2] for k,v in idx[f'py:{o}'].items() if k[0]!='_'}
175
+ d = {o:_get(o) for o in typs}
176
+ syms = defaultdict(dict)
177
+ for o in typs:
178
+ for k,v in d[o].items():
179
+ if k.split('.')[0] in bset: k = 'builtins.' + k
180
+ modparts = k.split(".")[:-2 if o=='method' else -1]
181
+ if modparts: syms['.'.join(modparts)][k] = v
182
+ return syms
183
+
184
+ # %% ../nbs/api/05_doclinks.ipynb
147
185
  import importlib,ast
148
186
  from functools import lru_cache
149
187
 
150
- # %% ../nbs/api/05_doclinks.ipynb 26
188
+ # %% ../nbs/api/05_doclinks.ipynb
151
189
  def _find_mod(mod):
152
190
  mp,_,mr = mod.partition('/')
153
191
  spec = importlib.util.find_spec(mp)
@@ -157,6 +195,7 @@ def _find_mod(mod):
157
195
 
158
196
  @lru_cache(None)
159
197
  def _get_exps(mod):
198
+ "Get the line numbers for function and class definitions in module"
160
199
  mf = _find_mod(mod)
161
200
  if not mf: return {}
162
201
  txt = mf.read_text(encoding='utf-8')
@@ -170,8 +209,9 @@ def _get_exps(mod):
170
209
 
171
210
  def _lineno(sym, fname): return _get_exps(fname).get(sym, None) if fname else None
172
211
 
173
- # %% ../nbs/api/05_doclinks.ipynb 28
212
+ # %% ../nbs/api/05_doclinks.ipynb
174
213
  def _qual_sym(s, settings):
214
+ "Get qualified nb, py, and github paths for a symbol s"
175
215
  if not isinstance(s,tuple): return s
176
216
  nb,py = s
177
217
  nbbase = urljoin(settings["doc_host"]+'/',settings["doc_baseurl"])
@@ -185,35 +225,53 @@ def _qual_syms(entries):
185
225
  if 'doc_host' not in settings: return entries
186
226
  return {'syms': {mod:_qual_mod(d, settings) for mod,d in entries['syms'].items()}, 'settings':settings}
187
227
 
188
- # %% ../nbs/api/05_doclinks.ipynb 29
189
- _re_backticks = re.compile(r'`([^`\s]+)`')
228
+ # %% ../nbs/api/05_doclinks.ipynb
229
+ _re_backticks = re.compile(r'`([^`\s]+?)(?:\(\))?`')
190
230
 
191
- # %% ../nbs/api/05_doclinks.ipynb 30
231
+ # %% ../nbs/api/05_doclinks.ipynb
192
232
  @lru_cache(None)
233
+ def _build_lookup_table(strip_libs=None, incl_libs=None, skip_mods=None):
234
+ cfg = get_config()
235
+ if strip_libs is None:
236
+ try: strip_libs = cfg.get('strip_libs', cfg.get('lib_path', 'nbdev').name).split()
237
+ except FileNotFoundError: strip_libs = 'nbdev'
238
+ skip_mods = setify(skip_mods)
239
+ strip_libs = L(strip_libs)
240
+ if incl_libs is not None: incl_libs = (L(incl_libs)+strip_libs).unique()
241
+ entries = {}
242
+ try: eps = entry_points(group='nbdev')
243
+ # Python 3.9 fallback - entry_points() doesn't accept group parameter
244
+ except TypeError: eps = entry_points().get('nbdev', [])
245
+
246
+ for o in eps:
247
+ if incl_libs is not None and o.dist.name not in incl_libs: continue
248
+ try: entries[o.name] = _qual_syms(o.load())
249
+ except Exception: pass
250
+ py_syms = merge(*L(o['syms'].values() for o in entries.values()).concat())
251
+ for m in strip_libs:
252
+ if m in entries:
253
+ _d = entries[m]
254
+ stripped = {}
255
+ for mod, dets in _d['syms'].items():
256
+ if mod not in skip_mods:
257
+ for k,v in dets.items():
258
+ k = remove_prefix(k,f"{mod}.")
259
+ if k not in stripped: stripped[k] = v
260
+ py_syms = merge(stripped, py_syms)
261
+ return entries,py_syms
262
+
263
+ # %% ../nbs/api/05_doclinks.ipynb
193
264
  class NbdevLookup:
194
265
  "Mapping from symbol names to docs and source URLs"
195
- def __init__(self, strip_libs=None, incl_libs=None, skip_mods=None):
196
- cfg = get_config()
197
- if strip_libs is None:
198
- try: strip_libs = cfg.get('strip_libs', cfg.get('lib_path', 'nbdev').name).split()
199
- except FileNotFoundError: strip_libs = 'nbdev'
200
- skip_mods = setify(skip_mods)
201
- strip_libs = L(strip_libs)
202
- if incl_libs is not None: incl_libs = (L(incl_libs)+strip_libs).unique()
203
- # Dict from lib name to _nbdev module for incl_libs (defaults to all)
204
- self.entries = {o.name: _qual_syms(o.resolve()) for o in list(pkg_resources.iter_entry_points(group='nbdev'))
205
- if incl_libs is None or o.dist.key in incl_libs}
206
- py_syms = merge(*L(o['syms'].values() for o in self.entries.values()).concat())
207
- for m in strip_libs:
208
- if m in self.entries:
209
- _d = self.entries[m]
210
- stripped = {remove_prefix(k,f"{mod}."):v
211
- for mod,dets in _d['syms'].items() if mod not in skip_mods
212
- for k,v in dets.items()}
213
- py_syms = merge(stripped, py_syms)
214
- self.syms = py_syms
215
-
216
- def __getitem__(self, s): return self.syms.get(s, None)
266
+ def __init__(self, strip_libs=None, incl_libs=None, skip_mods=None, ns=None):
267
+ self.entries,self.syms = _build_lookup_table(strip_libs, incl_libs, skip_mods)
268
+ self.aliases = {n:o.__name__ for n,o in (ns or {}).items() if isinstance(o, ModuleType)}
269
+
270
+ def __getitem__(self, s):
271
+ if '.' in s:
272
+ pre,post = s.split('.', 1)
273
+ if pre in self.aliases: s = f"{self.aliases[pre]}.{post}"
274
+ return self.syms.get(s, None)
217
275
 
218
276
  def doc(self, sym):
219
277
  "Link to docs for `sym`"
@@ -227,12 +285,13 @@ class NbdevLookup:
227
285
  _,py,gh = res
228
286
  line = _lineno(sym, py)
229
287
  return f'{gh}#L{line}'
230
-
288
+
231
289
  def _link_sym(self, m):
232
290
  l = m.group(1)
233
291
  s = self.doc(l)
234
292
  if s is None: return m.group(0)
235
293
  l = l.replace('\\', r'\\')
294
+ if m.group(0).endswith('()`'): l += '()'
236
295
  return rf"[`{l}`]({s})"
237
296
 
238
297
  def link_line(self, l): return _re_backticks.sub(self._link_sym, l)