nbdev 3.0.18__tar.gz → 3.2.0__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 (38) hide show
  1. {nbdev-3.0.18/nbdev.egg-info → nbdev-3.2.0}/PKG-INFO +8 -5
  2. {nbdev-3.0.18 → nbdev-3.2.0}/README.md +4 -1
  3. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/__init__.py +1 -1
  4. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/_modidx.py +4 -0
  5. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/cli.py +22 -18
  6. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/config.py +8 -9
  7. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/doclinks.py +10 -8
  8. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/export.py +9 -8
  9. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/merge.py +9 -6
  10. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/migrate.py +9 -7
  11. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/processors.py +15 -8
  12. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/qmd.py +34 -24
  13. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/quarto.py +22 -8
  14. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/release.py +36 -24
  15. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/showdoc.py +6 -4
  16. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/test.py +54 -40
  17. {nbdev-3.0.18 → nbdev-3.2.0/nbdev.egg-info}/PKG-INFO +8 -5
  18. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev.egg-info/requires.txt +3 -3
  19. {nbdev-3.0.18 → nbdev-3.2.0}/pyproject.toml +4 -1
  20. {nbdev-3.0.18 → nbdev-3.2.0}/CONTRIBUTING.md +0 -0
  21. {nbdev-3.0.18 → nbdev-3.2.0}/LICENSE +0 -0
  22. {nbdev-3.0.18 → nbdev-3.2.0}/MANIFEST.in +0 -0
  23. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/clean.py +0 -0
  24. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/diff.py +0 -0
  25. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/extract_attachments.py +0 -0
  26. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/frontmatter.py +0 -0
  27. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/imports.py +0 -0
  28. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/maker.py +0 -0
  29. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/process.py +0 -0
  30. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/scrubmagics.py +0 -0
  31. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/serve.py +0 -0
  32. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/serve_drv.py +0 -0
  33. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev/sync.py +0 -0
  34. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev.egg-info/SOURCES.txt +0 -0
  35. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev.egg-info/dependency_links.txt +0 -0
  36. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev.egg-info/entry_points.txt +0 -0
  37. {nbdev-3.0.18 → nbdev-3.2.0}/nbdev.egg-info/top_level.txt +0 -0
  38. {nbdev-3.0.18 → nbdev-3.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdev
3
- Version: 3.0.18
3
+ Version: 3.2.0
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
@@ -16,10 +16,10 @@ Classifier: License :: OSI Approved :: Apache Software License
16
16
  Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
- Requires-Dist: fastcore>=1.12.35
20
- Requires-Dist: execnb>=0.1.12
19
+ Requires-Dist: fastcore>=1.14.6
20
+ Requires-Dist: execnb>=0.2.4
21
21
  Requires-Dist: astunparse
22
- Requires-Dist: ghapi>=1.0.3
22
+ Requires-Dist: ghapi>=2.0.0
23
23
  Requires-Dist: watchdog
24
24
  Requires-Dist: asttokens
25
25
  Requires-Dist: setuptools
@@ -158,7 +158,7 @@ available commands:
158
158
  nbdev-pypi Create and upload Python package to PyPI
159
159
  nbdev-readme Create README.md from readme_nb (index.ipynb by default)
160
160
  nbdev-release-both Release both conda and PyPI packages
161
- nbdev-release-gh Calls `nbdev_changelog`, lets you edit the result, then pushes to git and calls `nbdev_release_git`
161
+ nbdev-release-gh Calls `nbdev-changelog`, lets you edit the result, then pushes to git and calls `nbdev-release-git`
162
162
  nbdev-release-git Tag and create a release in GitHub for the current version
163
163
  nbdev-requirements Writes a `requirements.txt` file to `directory` based on pyproject.toml.
164
164
  nbdev-sidebar Create sidebar.yml
@@ -184,6 +184,9 @@ some_module.something()
184
184
  Instead, split this into two cells, one which does `import some_module`,
185
185
  and the other which does `some_module.something()`.
186
186
 
187
+ Only top-level statements count: `try: import` blocks and imports inside
188
+ function definitions are fine.
189
+
187
190
  The reason for this is that when we create your documentation website,
188
191
  we ensure that all of the signatures for functions you document are up
189
192
  to date, by running the imports, exported cells, and
@@ -114,7 +114,7 @@ available commands:
114
114
  nbdev-pypi Create and upload Python package to PyPI
115
115
  nbdev-readme Create README.md from readme_nb (index.ipynb by default)
116
116
  nbdev-release-both Release both conda and PyPI packages
117
- nbdev-release-gh Calls `nbdev_changelog`, lets you edit the result, then pushes to git and calls `nbdev_release_git`
117
+ nbdev-release-gh Calls `nbdev-changelog`, lets you edit the result, then pushes to git and calls `nbdev-release-git`
118
118
  nbdev-release-git Tag and create a release in GitHub for the current version
119
119
  nbdev-requirements Writes a `requirements.txt` file to `directory` based on pyproject.toml.
120
120
  nbdev-sidebar Create sidebar.yml
@@ -140,6 +140,9 @@ some_module.something()
140
140
  Instead, split this into two cells, one which does `import some_module`,
141
141
  and the other which does `some_module.something()`.
142
142
 
143
+ Only top-level statements count: `try: import` blocks and imports inside
144
+ function definitions are fine.
145
+
143
146
  The reason for this is that when we create your documentation website,
144
147
  we ensure that all of the signatures for functions you document are up
145
148
  to date, by running the imports, exported cells, and
@@ -1,4 +1,4 @@
1
- __version__ = "3.0.18"
1
+ __version__ = "3.2.0"
2
2
 
3
3
  from .doclinks import nbdev_export
4
4
  from .showdoc import show_doc
@@ -225,6 +225,7 @@ d = { 'settings': { 'branch': 'main',
225
225
  'nbdev.processors._is_showdoc': ('api/processors.html#_is_showdoc', 'nbdev/processors.py'),
226
226
  'nbdev.processors._re_hideline': ('api/processors.html#_re_hideline', 'nbdev/processors.py'),
227
227
  'nbdev.processors._show_docs': ('api/processors.html#_show_docs', 'nbdev/processors.py'),
228
+ 'nbdev.processors._tl_contains': ('api/processors.html#_tl_contains', 'nbdev/processors.py'),
228
229
  'nbdev.processors._want_doc': ('api/processors.html#_want_doc', 'nbdev/processors.py'),
229
230
  'nbdev.processors.add_fold': ('api/processors.html#add_fold', 'nbdev/processors.py'),
230
231
  'nbdev.processors.add_links': ('api/processors.html#add_links', 'nbdev/processors.py'),
@@ -278,6 +279,7 @@ d = { 'settings': { 'branch': 'main',
278
279
  'nbdev/quarto.py'),
279
280
  'nbdev.quarto._SidebarYmlRemoved.__init__': ( 'api/quarto.html#_sidebarymlremoved.__init__',
280
281
  'nbdev/quarto.py'),
282
+ 'nbdev.quarto._chk_nbdev_yml': ('api/quarto.html#_chk_nbdev_yml', 'nbdev/quarto.py'),
281
283
  'nbdev.quarto._copytree': ('api/quarto.html#_copytree', 'nbdev/quarto.py'),
282
284
  'nbdev.quarto._doc_mtime_not_older': ('api/quarto.html#_doc_mtime_not_older', 'nbdev/quarto.py'),
283
285
  'nbdev.quarto._ensure_quarto': ('api/quarto.html#_ensure_quarto', 'nbdev/quarto.py'),
@@ -286,6 +288,7 @@ d = { 'settings': { 'branch': 'main',
286
288
  'nbdev.quarto._nbglob_docs': ('api/quarto.html#_nbglob_docs', 'nbdev/quarto.py'),
287
289
  'nbdev.quarto._pre': ('api/quarto.html#_pre', 'nbdev/quarto.py'),
288
290
  'nbdev.quarto._pre_docs': ('api/quarto.html#_pre_docs', 'nbdev/quarto.py'),
291
+ 'nbdev.quarto._readme_cands': ('api/quarto.html#_readme_cands', 'nbdev/quarto.py'),
289
292
  'nbdev.quarto._recursive_parser': ('api/quarto.html#_recursive_parser', 'nbdev/quarto.py'),
290
293
  'nbdev.quarto._save_cached_contributing': ('api/quarto.html#_save_cached_contributing', 'nbdev/quarto.py'),
291
294
  'nbdev.quarto._save_cached_readme': ('api/quarto.html#_save_cached_readme', 'nbdev/quarto.py'),
@@ -330,6 +333,7 @@ d = { 'settings': { 'branch': 'main',
330
333
  'nbdev.release.release_gh': ('api/release.html#release_gh', 'nbdev/release.py'),
331
334
  'nbdev.release.release_git': ('api/release.html#release_git', 'nbdev/release.py'),
332
335
  'nbdev.release.release_pypi': ('api/release.html#release_pypi', 'nbdev/release.py'),
336
+ 'nbdev.release.update_changelog': ('api/release.html#update_changelog', 'nbdev/release.py'),
333
337
  'nbdev.release.write_conda_meta': ('api/release.html#write_conda_meta', 'nbdev/release.py'),
334
338
  'nbdev.release.write_requirements': ('api/release.html#write_requirements', 'nbdev/release.py')},
335
339
  'nbdev.scrubmagics': {},
@@ -71,25 +71,25 @@ def _render_nb(fn, cfg):
71
71
  fn.write_text(txt)
72
72
 
73
73
  # %% ../nbs/api/13_cli.ipynb #dd385911-aa8f-44e7-8d46-7b8a20f3b010
74
- def _update_repo_meta(cfg):
74
+ async def _update_repo_meta(cfg):
75
75
  "Enable gh pages and update the homepage and description in your GitHub repo."
76
76
  token=os.getenv('GITHUB_TOKEN')
77
77
  if token:
78
- from ghapi.core import GhApi
78
+ from ghapi.core import GhApi, APIError
79
79
  api = GhApi(owner=cfg.user, repo=cfg.repo, token=token)
80
- try: api.repos.update(homepage=f'{cfg.doc_host}{cfg.doc_baseurl}', description=cfg.description)
81
- except HTTPError:print(f"Could not update the description & URL on the repo: {cfg.user}/{cfg.repo} using $GITHUB_TOKEN.\n"
80
+ try: await api.repos.update(homepage=f'{cfg.doc_host}{cfg.doc_baseurl}', description=cfg.description)
81
+ except APIError:print(f"Could not update the description & URL on the repo: {cfg.user}/{cfg.repo} using $GITHUB_TOKEN.\n"
82
82
  "Use a token with the correction permissions or perform these steps manually.")
83
83
 
84
84
  # %% ../nbs/api/13_cli.ipynb #c4a663d9
85
85
  @call_parse
86
86
  @delegates(nbdev_create_config)
87
- def nbdev_new(**kwargs):
87
+ async def nbdev_new(**kwargs):
88
88
  "Create an nbdev project."
89
89
  from ghapi.core import GhApi
90
90
  nbdev_create_config.__wrapped__(**kwargs)
91
91
  cfg = get_config()
92
- if (Path('.git')).exists(): _update_repo_meta(cfg)
92
+ if (Path('.git')).exists(): await _update_repo_meta(cfg)
93
93
  else: print(f"No git repo found. Run: gh repo create {cfg.user}/{cfg.repo} --public --source=.")
94
94
  path = Path()
95
95
 
@@ -103,8 +103,8 @@ def nbdev_new(**kwargs):
103
103
  if tag is None:
104
104
  with warnings.catch_warnings():
105
105
  warnings.simplefilter('ignore', UserWarning)
106
- tag = GhApi(gh_host='https://api.github.com', authenticate=False
107
- ).repos.get_latest_release(org_or_usr, repo).tag_name
106
+ tag = (await GhApi(gh_host='https://api.github.com', authenticate=False
107
+ ).repos.get_latest_release(org_or_usr, repo)).tag_name
108
108
 
109
109
  url = f"https://github.com/{org_or_usr}/{repo}/archive/{tag}.tar.gz"
110
110
  extract_tgz(url)
@@ -137,20 +137,21 @@ mapping = {
137
137
 
138
138
  # %% ../nbs/api/13_cli.ipynb #a1116ee1-f5cb-4b63-8630-b64cd2ae3c12
139
139
  @call_parse
140
- def nbdev_update_license(
140
+ async def nbdev_update_license(
141
141
  to: str=None, # update license to
142
142
  ):
143
143
  "Allows you to update the license of your project."
144
144
  from ghapi.core import GhApi
145
145
  warnings.filterwarnings("ignore")
146
- avail_lic = GhApi().licenses.get_all_commonly_used().map(lambda x: x['key'])
146
+ api = GhApi()
147
+ avail_lic = (await api.licenses.get_all_commonly_used()).map(lambda x: x['key'])
147
148
 
148
149
  cfg = get_config()
149
150
  curr_lic = cfg.license
150
151
 
151
152
  mapped = mapping.get(to, None)
152
153
  if mapped not in avail_lic: raise ValueError(f"{to} is not an available license")
153
- body = GhApi().licenses.get(mapped)['body']
154
+ body = (await api.licenses.get(mapped))['body']
154
155
 
155
156
  copyright = f"{datetime.now().year}, {cfg.author}"
156
157
  body = body.replace('[year], [fullname]', copyright)
@@ -168,18 +169,21 @@ def nbdev_update_license(
168
169
  # %% ../nbs/api/13_cli.ipynb #412b4cd2
169
170
  @call_parse
170
171
  @delegates(nb_export, but=['procs', 'mod_maker'])
171
- def nb_export_cli(nbname,
172
- debug:store_true=False, # Debug flag
173
- **kwargs):
172
+ def nb_export_cli(
173
+ nbname,
174
+ debug:store_true=False, # Debug flag
175
+ **kwargs
176
+ ):
174
177
  "Export a single nbdev notebook to a python script."
175
178
  return nb_export(nbname=nbname, debug=debug, **kwargs)
176
179
 
177
180
  # %% ../nbs/api/13_cli.ipynb #aaa472e7
178
181
  @call_parse
179
- def watch_export(nbs:str=None, # Nb directory to watch for changes
180
- lib:str=None, # Export directory to write py files to
181
- force:bool=False # Ignore nbdev config if in nbdev project
182
- ):
182
+ def watch_export(
183
+ nbs:str=None, # Nb directory to watch for changes
184
+ lib:str=None, # Export directory to write py files to
185
+ force:bool=False # Ignore nbdev config if in nbdev project
186
+ ):
183
187
  '''Use `nb_export` on ipynb files in `nbs` directory on changes using nbdev config if available'''
184
188
  cfg = get_config() if is_nbdev() else None
185
189
  nbs = nbs or (cfg.nbs_path if cfg else '.')
@@ -38,18 +38,16 @@ def _git_repo():
38
38
 
39
39
  # %% ../nbs/api/01_config.ipynb #6eeafafd
40
40
  def _get_info(owner, repo, default_branch='main', default_kw='nbdev'):
41
- from ghapi.all import GhApi
42
- api = GhApi(owner=owner, repo=repo, token=os.getenv('GITHUB_TOKEN'))
43
-
44
- try: r = api.repos.get()
45
- except HTTPError:
41
+ from ghapi.all import call_gh, APIError
42
+ try: r = call_gh('repos.get', owner=owner, repo=repo)
43
+ except APIError:
46
44
  msg= [f"""Could not access repo: {owner}/{repo} to find your default branch - `{default_branch}` assumed.
47
45
  Edit `pyproject.toml` if this is incorrect.
48
46
  In the future, you can allow nbdev to see private repos by setting the environment variable GITHUB_TOKEN as described here:
49
47
  https://nbdev.fast.ai/api/release.html#setup"""]
50
48
  print(''.join(msg))
51
49
  return default_branch,default_kw,''
52
-
50
+
53
51
  return r.default_branch, default_kw if not getattr(r, 'topics', []) else ' '.join(r.topics), r.description
54
52
 
55
53
  # %% ../nbs/api/01_config.ipynb #35d5c037
@@ -170,7 +168,7 @@ def _find_nbdev_pyproject(path=None):
170
168
  # %% ../nbs/api/01_config.ipynb #3dac70e0
171
169
  nbdev_defaults = dict(nbs_path='nbs', doc_path='_docs', tst_flags='notest', recursive=True, readme_nb='index.ipynb',
172
170
  clean_ids=True, clear_all=False, put_version_in_init=True, jupyter_hooks=False, custom_sidebar=False, branch='main',
173
- doc_procs=[], export_procs=[])
171
+ doc_procs=[], export_procs=[], exec_profile=True)
174
172
 
175
173
  _path_keys = 'lib_path', 'nbs_path', 'doc_path'
176
174
 
@@ -238,7 +236,7 @@ def get_config(path=None, also_settings=False):
238
236
  nbdev = {**user, **d.get('tool', {}).get('nbdev', {})}
239
237
  return ConfigToml(nbdev, d.get('project', {}), cfg_file)
240
238
  if also_settings:
241
- from fastcore.foundation import Config
239
+ from fastcore.xtras import Config
242
240
  cfg = Config.find('settings.ini', path)
243
241
  if cfg: return cfg
244
242
  cfg_path = Path(path or Path.cwd()).expanduser().absolute()
@@ -267,7 +265,8 @@ def nbpath2docurl(nb_path):
267
265
  "Hosted docs URL (with `.md` suffix) for notebook `nb_path`, or '' if no `doc_host`"
268
266
  cfg = get_config()
269
267
  if not cfg.doc_host: return ''
270
- p = Path(nb_path).resolve().relative_to(cfg.path('nbs_path'))
268
+ try: p = Path(nb_path).resolve().relative_to(cfg.path('nbs_path'))
269
+ except ValueError: return ""
271
270
  return f"{cfg.doc_host.rstrip('/')}{cfg.doc_baseurl.rstrip('/')}/{nbpath2html(p).as_posix()}.md"
272
271
 
273
272
  # %% ../nbs/api/01_config.ipynb #163177f2
@@ -146,14 +146,16 @@ def nbdev_export(
146
146
  **kwargs):
147
147
  "Export notebooks in `path` to Python modules"
148
148
  if os.environ.get('IN_TEST',0): return
149
- if not is_nbdev(): raise Exception('`nbdev-export` must be called from a directory within a nbdev project.')
150
- cfg = get_config()
151
- procs = procs.split() if procs else cfg.get('export_procs', [])
152
- procs = [import_obj(p) for p in procs] if procs else None
153
- files = nbglob(path=path, as_path=True, **kwargs).sorted('name')
154
- for f in files: nb_export(f, procs=procs)
155
- add_init(cfg.lib_path)
156
- _build_modidx()
149
+ if path: path = Path(path).resolve()
150
+ if not is_nbdev(path): raise Exception('`nbdev-export` must be called from a directory within a nbdev project.')
151
+ with working_directory(get_config(path).config_path if path else '.'):
152
+ cfg = get_config()
153
+ procs = procs.split() if procs else cfg.get('export_procs', [])
154
+ procs = [import_obj(p) for p in procs] if procs else None
155
+ files = nbglob(path=path, as_path=True, **kwargs).sorted('name')
156
+ for f in files: nb_export(f, procs=procs)
157
+ add_init(cfg.lib_path)
158
+ _build_modidx()
157
159
 
158
160
  # %% ../nbs/api/05_doclinks.ipynb #3134c22b
159
161
  typs = 'module','class','method','function'
@@ -36,14 +36,15 @@ class ExportModuleProc:
36
36
  _exports_=_export_
37
37
 
38
38
  # %% ../nbs/api/04_export.ipynb #76717e36
39
- def nb_export(nbname:str, # Filename of notebook
40
- lib_path:str=None, # Path to destination library. If not in a nbdev project, defaults to current directory.
41
- procs=None, # Processors to use
42
- name:str=None, # Name of python script {name}.py to create.
43
- mod_maker=ModuleMaker,
44
- debug:bool=False, # Debug mode
45
- solo_nb:bool=False # Export single notebook outside of an nbdev project.
46
- ):
39
+ def nb_export(
40
+ nbname:str, # Filename of notebook
41
+ lib_path:str=None, # Path to destination library. If not in a nbdev project, defaults to current directory.
42
+ procs=None, # Processors to use
43
+ name:str=None, # Name of python script {name}.py to create.
44
+ mod_maker=ModuleMaker,
45
+ debug:bool=False, # Debug mode
46
+ solo_nb:bool=False # Export single notebook outside of an nbdev project.
47
+ ):
47
48
  "Create module(s) from notebook"
48
49
  if lib_path is None: lib_path = get_config().lib_path if is_nbdev() else '.'
49
50
  exp = ExportModuleProc()
@@ -46,7 +46,8 @@ def _make_conflict(a,b, branch1, branch2):
46
46
  return _make_md(f'{_BEG} {branch1}') + a+_make_md(_MID)+b + _make_md(f'{_END} {branch2}')
47
47
 
48
48
  def _merge_cells(a, b, brancha, branchb, theirs):
49
- matches = SequenceMatcher(None, a, b).get_matching_blocks()
49
+ def _k(cs): return [(c.cell_type,c.source) for c in cs]
50
+ matches = SequenceMatcher(None, _k(a), _k(b)).get_matching_blocks()
50
51
  res,prev_sa,prev_sb,conflict = [],0,0,False
51
52
  for sa,sb,sz in matches:
52
53
  ca,cb = a[prev_sa:sa],b[prev_sb:sb]
@@ -59,11 +60,13 @@ def _merge_cells(a, b, brancha, branchb, theirs):
59
60
 
60
61
  # %% ../nbs/api/07_merge.ipynb #cc492d30
61
62
  @call_parse
62
- def nbdev_fix(nbname:str, # Notebook filename to fix
63
- outname:str=None, # Filename of output notebook (defaults to `nbname`)
64
- nobackup:bool_arg=True, # Do not backup `nbname` to `nbname`.bak if `outname` not provided
65
- theirs:bool=False, # Use their outputs and metadata instead of ours
66
- noprint:bool=False): # Do not print info about whether conflicts are found
63
+ def nbdev_fix(
64
+ nbname:str, # Notebook filename to fix
65
+ outname:str=None, # Filename of output notebook (defaults to `nbname`)
66
+ nobackup:bool_arg=True, # Do not backup `nbname` to `nbname`.bak if `outname` not provided
67
+ theirs:bool=False, # Use their outputs and metadata instead of ours
68
+ noprint:bool=False # Do not print info about whether conflicts are found
69
+ ):
67
70
  "Create working notebook from conflicted notebook `nbname`"
68
71
  nbname = Path(nbname)
69
72
  if not nobackup and not outname: shutil.copy(nbname, nbname.with_suffix('.ipynb.bak'))
@@ -32,11 +32,12 @@ def _file_slug(fname):
32
32
  return dt + p.stem[11:]
33
33
 
34
34
  # %% ../nbs/api/16_migrate.ipynb #689bf354
35
- def _replace_fm(d:dict, # dictionary you wish to conditionally change
36
- k:str, # key to check
37
- val:str,# value to check if d[k] == v
38
- repl_dict:dict #dictionary that will be used as a replacement
39
- ):
35
+ def _replace_fm(
36
+ d:dict, # dictionary you wish to conditionally change
37
+ k:str, # key to check
38
+ val:str,# value to check if d[k] == v
39
+ repl_dict:dict #dictionary that will be used as a replacement
40
+ ):
40
41
  "replace key `k` in dict `d` if d[k] == val with `repl_dict`"
41
42
  if str(d.get(k, '')).lower().strip() == str(val.lower()).strip():
42
43
  d.pop(k)
@@ -191,8 +192,9 @@ def _migrate_workflows(path):
191
192
  wf_path = Path(path) / '.github/workflows'
192
193
  if not wf_path.exists(): return
193
194
  replacements = [
194
- ('fastai/workflows/quarto-ghp@', 'fastai/workflows/quarto-ghp3@'),
195
- ('fastai/workflows/nbdev-ci@', 'fastai/workflows/nbdev3-ci@'),
195
+ ('fastai/workflows/quarto-ghp@', 'AnswerDotAI/workflows/quarto-ghp3@'),
196
+ ('fastai/workflows/nbdev-ci@', 'AnswerDotAI/workflows/nbdev3-ci@'),
197
+ ('fastai/workflows/', 'AnswerDotAI/workflows/'),
196
198
  ]
197
199
  for f in (*wf_path.glob('*.yml'), *wf_path.glob('*.yaml')):
198
200
  txt = f.read_text()
@@ -128,7 +128,8 @@ def add_links(cell):
128
128
  if cell.cell_type == 'markdown': cell.source = nl.linkify(cell.source)
129
129
  for o in cell.get('outputs', []):
130
130
  if hasattr(o, 'data') and hasattr(o['data'], 'text/markdown'):
131
- o.data['text/markdown'] = [nl.link_line(s) for s in o.data['text/markdown']]
131
+ md = o.data['text/markdown']
132
+ o.data['text/markdown'] = nl.linkify(''.join(md) if isinstance(md, list) else md)
132
133
 
133
134
  # %% ../nbs/api/10_processors.ipynb #809e9225
134
135
  def add_fold(cell):
@@ -142,7 +143,7 @@ _re_ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
142
143
  def strip_ansi(cell):
143
144
  "Strip Ansi Characters."
144
145
  for outp in cell.get('outputs', []):
145
- if outp.get('name')=='stdout': outp['text'] = [_re_ansi_escape.sub('', o) for o in outp.text]
146
+ if outp.get('name')=='stdout': outp['text'] = _re_ansi_escape.sub('', outp.text)
146
147
 
147
148
  # %% ../nbs/api/10_processors.ipynb #640b7eca
148
149
  def strip_hidden_metadata(cell):
@@ -169,7 +170,7 @@ def filter_stream_(cell, *words):
169
170
  if not words: return
170
171
  for outp in cell.get('outputs', []):
171
172
  if outp.output_type == 'stream':
172
- outp['text'] = [l for l in outp.text if not re.search('|'.join(words), l)]
173
+ outp['text'] = ''.join(l for l in outp.text.splitlines(True) if not re.search('|'.join(words), l))
173
174
 
174
175
  # %% ../nbs/api/10_processors.ipynb #848fd452-3d63-4c41-aaa6-e14cbeb9fdcd
175
176
  _aimagics_pattern = re.compile(r'^\s*(%%ai.? |%%ai.?$)', re.MULTILINE)
@@ -181,11 +182,15 @@ def ai_magics(cell):
181
182
  cell.source = '\n'.join(cell.source.splitlines()[1:])
182
183
 
183
184
  # %% ../nbs/api/10_processors.ipynb #5a9c8fd4
184
- _magics_pattern = re.compile(r'^\s*(%%|%).*', re.MULTILINE)
185
+ _def_strip_magics = '%load_ext %autoreload %reload_ext %matplotlib %config'
185
186
 
186
187
  def clean_magics(cell):
187
- "A preprocessor to remove cell magic commands"
188
- if cell.cell_type == 'code': cell.source = _magics_pattern.sub('', cell.source).strip()
188
+ "Remove housekeeping magics: those named in the `strip_magics` config (space-separated, default `_def_strip_magics`)"
189
+ mm = str(get_config().get('strip_magics', _def_strip_magics)).split()
190
+ if not mm or cell.cell_type != 'code': return
191
+ pat = re.compile(rf"^\s*({'|'.join(re.escape(m) for m in mm)})\b.*", re.MULTILINE)
192
+ cell.source = pat.sub('', cell.source).strip()
193
+
189
194
 
190
195
  # %% ../nbs/api/10_processors.ipynb #93e27a52
191
196
  _re_hdr_dash = re.compile(r'^#+\s+.*\s+-\s*$', re.MULTILINE)
@@ -222,6 +227,8 @@ def _ast_contains(trees, types):
222
227
  for node in ast.walk(tree):
223
228
  if isinstance(node, types): return True
224
229
 
230
+ def _tl_contains(trees, types): return any(isinstance(t, types) for t in trees)
231
+
225
232
  def _do_eval(cell):
226
233
  if cell_lang(cell) != 'python': return
227
234
  if not cell.source or 'nbdev_export'+'()' in cell.source: return
@@ -232,7 +239,7 @@ def _do_eval(cell):
232
239
  _show_dirs = {'export','exports','exporti','exec_doc'}
233
240
  if cell.directives_.keys() & _show_dirs: return True
234
241
  if _ast_contains(trees, (ast.Import, ast.ImportFrom)):
235
- if _ast_contains(trees, (ast.Expr, ast.Assign)):
242
+ if _tl_contains(trees, (ast.Import, ast.ImportFrom)) and _tl_contains(trees, (ast.Expr, ast.Assign)):
236
243
  warn(f'Found cells containing imports and other code. See FAQ.\n---\n{cell.source}\n---\n')
237
244
  return True
238
245
  if _show_docs(trees): return True
@@ -242,7 +249,7 @@ class exec_show_docs(Processor):
242
249
  "Execute cells needed for `show_docs` output, including exported cells and imports"
243
250
  def begin(self):
244
251
  if nb_lang(self.nb) != 'python': return
245
- self.k = CaptureShell()
252
+ self.k = CaptureShell(profile=bool(get_config().exec_profile))
246
253
  self.k.run_cell('from nbdev.showdoc import show_doc')
247
254
 
248
255
  def __call__(self, cell):
@@ -14,10 +14,12 @@ from fastcore.utils import *
14
14
  from fastcore.meta import delegates
15
15
 
16
16
  # %% ../nbs/api/15_qmd.ipynb #5a64f1f4
17
- def meta(md, # Markdown to add meta to
18
- classes=None, # List of CSS classes to add
19
- style=None, # Dict of CSS styles to add
20
- **kwargs): # Additional attributes to add to meta
17
+ def meta(
18
+ md, # Markdown to add meta to
19
+ classes=None, # List of CSS classes to add
20
+ style=None, # Dict of CSS styles to add
21
+ **kwargs # Additional attributes to add to meta
22
+ ):
21
23
  "A metadata section for qmd div in `{}`"
22
24
  if style: kwargs['style'] = "; ".join(f'{k}: {v}' for k,v in style.items())
23
25
  props = ' '.join(f'{k}="{v}"' for k,v in kwargs.items())
@@ -29,21 +31,25 @@ def meta(md, # Markdown to add meta to
29
31
  return md + ("{" + meta + "}" if meta else "")
30
32
 
31
33
  # %% ../nbs/api/15_qmd.ipynb #52637a70
32
- def div(txt, # Markdown to add meta to
33
- classes=None, # List of CSS classes to add
34
- style=None, # Dict of CSS styles to add
35
- **kwargs):
34
+ def div(
35
+ txt, # Markdown to add meta to
36
+ classes=None, # List of CSS classes to add
37
+ style=None, # Dict of CSS styles to add
38
+ **kwargs
39
+ ):
36
40
  "A qmd div with optional metadata section"
37
41
  return meta("::: ", classes=classes, style=style, **kwargs) + f"\n\n{txt}\n\n:::\n\n"
38
42
 
39
43
  # %% ../nbs/api/15_qmd.ipynb #f9f499f4
40
- def img(fname, # Image to link to
41
- classes=None, # List of CSS classes to add
42
- style=None, # Dict of CSS styles to add
43
- height=None, # Height attribute
44
- relative=None, # Tuple of (position,px)
45
- link=False, # Hyperlink to this image
46
- **kwargs):
44
+ def img(
45
+ fname, # Image to link to
46
+ classes=None, # List of CSS classes to add
47
+ style=None, # Dict of CSS styles to add
48
+ height=None, # Height attribute
49
+ relative=None, # Tuple of (position,px)
50
+ link=False, # Hyperlink to this image
51
+ **kwargs
52
+ ):
47
53
  "A qmd image"
48
54
  kwargs,style = kwargs or {}, style or {}
49
55
  if height: kwargs["height"]= f"{height}px"
@@ -55,23 +61,27 @@ def img(fname, # Image to link to
55
61
  return f'[{res}]({fname})' if link else res
56
62
 
57
63
  # %% ../nbs/api/15_qmd.ipynb #16d7aa5f
58
- def btn(txt, # Button text
59
- link, # Button link URL
60
- classes=None, # List of CSS classes to add
61
- style=None, # Dict of CSS styles to add
62
- **kwargs):
64
+ def btn(
65
+ txt, # Button text
66
+ link, # Button link URL
67
+ classes=None, # List of CSS classes to add
68
+ style=None, # Dict of CSS styles to add
69
+ **kwargs
70
+ ):
63
71
  "A qmd button"
64
72
  return meta(f'[{txt}]({link})', classes=classes, style=style, role="button")
65
73
 
66
74
  # %% ../nbs/api/15_qmd.ipynb #e414ed33
67
- def tbl_row(cols:list, # Auto-stringified columns to show in the row
68
- ):
75
+ def tbl_row(
76
+ cols:list, # Auto-stringified columns to show in the row
77
+ ):
69
78
  "Create a markdown table row from `cols`"
70
79
  return '|' + '|'.join(str(c or '') for c in cols) + '|'
71
80
 
72
81
  # %% ../nbs/api/15_qmd.ipynb #38f0641b
73
- def tbl_sep(sizes:int|list=3 # List of column sizes, or single `int` if all sizes the same
74
- ):
82
+ def tbl_sep(
83
+ sizes:int|list=3 # List of column sizes, or single `int` if all sizes the same
84
+ ):
75
85
  "Create a markdown table separator with relative column size `sizes`"
76
86
  if isinstance(sizes,int): sizes = [3]*sizes
77
87
  return tbl_row('-'*s for s in sizes)
@@ -196,10 +196,16 @@ def _ensure_quarto():
196
196
  print("Quarto is not installed. We will download and install it for you.")
197
197
  install.__wrapped__()
198
198
 
199
+ # %% ../nbs/api/14_quarto.ipynb #66163c30
200
+ def _chk_nbdev_yml(path):
201
+ "Fail early if `nbdev.yml` is missing from `path`, since without it quarto silently renders to the wrong location"
202
+ if not (path/'nbdev.yml').exists(): raise FileNotFoundError(f"{path/'nbdev.yml'} not found, so docs/README can not be built correctly.")
203
+
199
204
  # %% ../nbs/api/14_quarto.ipynb #6b880922
200
205
  def _pre_docs(path=None, n_workers:int=defaults.cpus, **kwargs):
201
206
  cfg = get_config()
202
207
  path = Path(path) if path else cfg.nbs_path
208
+ _chk_nbdev_yml(path)
203
209
  _ensure_quarto()
204
210
  refresh_quarto_yml()
205
211
  import nbdev.doclinks
@@ -243,15 +249,20 @@ def _copytree(a,b):
243
249
  copy_tree(a, b)
244
250
 
245
251
  # %% ../nbs/api/14_quarto.ipynb #caeaa153
252
+ def _readme_cands(cache, cfg):
253
+ "Locations where `quarto render -o README.md` may put its output, which differs by quarto version"
254
+ return cache/cfg.doc_path.name/'README.md', cache/'README.md'
255
+
246
256
  def _save_cached_readme(cache, cfg):
247
- tmp_doc_path = cache/cfg.doc_path.name
248
- readme = tmp_doc_path/'README.md'
249
- if readme.exists():
250
- readme_path = cfg.config_path/'README.md'
251
- if readme_path.exists(): readme_path.unlink() # py37 doesn't have `missing_ok`
252
- move(readme, cfg.config_path)
253
- _rdmi = tmp_doc_path/((cache/cfg.readme_nb).stem + '_files') # Supporting files for README
254
- if _rdmi.exists(): _copytree(_rdmi, cfg.config_path/_rdmi.name)
257
+ cands = _readme_cands(cache, cfg)
258
+ readme = next((f for f in cands if f.exists()), None)
259
+ if not readme: raise FileNotFoundError(f"quarto render did not produce a README.md in any of: {cands}")
260
+ readme_path = cfg.config_path/'README.md'
261
+ if readme_path.exists(): readme_path.unlink() # py37 doesn't have `missing_ok`
262
+ move(readme, cfg.config_path)
263
+ _rdmi = readme.parent/((cache/cfg.readme_nb).stem + '_files') # Supporting files for README
264
+ if _rdmi.exists(): _copytree(_rdmi, cfg.config_path/_rdmi.name)
265
+
255
266
 
256
267
  # %% ../nbs/api/14_quarto.ipynb #45d6bb5d
257
268
  @call_parse
@@ -261,10 +272,13 @@ def nbdev_readme(
261
272
  "Create README.md from readme_nb (index.ipynb by default)"
262
273
  cfg = get_config()
263
274
  path = Path(path) if path else cfg.nbs_path
275
+ _chk_nbdev_yml(path)
264
276
  if chk_time and _doc_mtime_not_older(cfg.config_path/'README.md', path/cfg.readme_nb): return
265
277
 
266
278
  with _SidebarYmlRemoved(path): # to avoid rendering whole website
267
279
  cache = proc_nbs(path)
280
+ for f in _readme_cands(cache, cfg):
281
+ if f.exists(): f.unlink() # remove stale renders from either quarto layout
268
282
  _sprun(f'cd "{cache}" && quarto render "{cache/cfg.readme_nb}" -o README.md -t gfm --no-execute')
269
283
 
270
284
  _save_cached_readme(cache, cfg)
@@ -5,8 +5,8 @@ Docs: https://nbdev.fast.ai/api/release.html.md"""
5
5
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/18_release.ipynb.
6
6
 
7
7
  # %% auto #0
8
- __all__ = ['GH_HOST', 'CONDA_WARNING', 'Release', 'changelog', 'push_release', 'release_git', 'release_gh', 'pypi_json',
9
- 'latest_pypi', 'pypi_details', 'conda_output_path', 'write_conda_meta', 'write_requirements',
8
+ __all__ = ['GH_HOST', 'CONDA_WARNING', 'update_changelog', 'Release', 'changelog', 'push_release', 'release_git', 'release_gh',
9
+ 'pypi_json', 'latest_pypi', 'pypi_details', 'conda_output_path', 'write_conda_meta', 'write_requirements',
10
10
  'anaconda_upload', 'release_conda', 'chk_conda_rel', 'release_pypi', 'release_both', 'nbdev_bump_version']
11
11
 
12
12
  # %% ../nbs/api/18_release.ipynb #c35cc2b8
@@ -15,7 +15,7 @@ from ghapi.core import *
15
15
 
16
16
  from datetime import datetime
17
17
  from packaging.version import Version
18
- import shutil,subprocess
18
+ import shutil,subprocess,asyncio
19
19
 
20
20
  from .doclinks import *
21
21
 
@@ -42,6 +42,12 @@ def _load_json(cfg, k):
42
42
  try: return json.loads(cfg[k])
43
43
  except json.JSONDecodeError as e: raise Exception(f"Key: `{k}` in .ini file is not a valid JSON string: {e}")
44
44
 
45
+ # %% ../nbs/api/18_release.ipynb #dcaba486
46
+ def update_changelog(txt, ver, notes, marker='<!-- do not remove -->\n'):
47
+ "Insert `notes` into changelog `txt` after `marker`, replacing any existing section for `ver`"
48
+ txt = re.sub(rf'\n## {re.escape(ver)}\n.*?(?=\n## |\Z)', '', txt, flags=re.S)
49
+ return txt.replace(marker, marker+notes+'\n')
50
+
45
51
  # %% ../nbs/api/18_release.ipynb #0b36471a
46
52
  class Release:
47
53
  def __init__(self, owner=None, repo=None, token=None, **groups):
@@ -52,6 +58,7 @@ class Release:
52
58
  default_groups=dict(breaking="Breaking Changes", enhancement="New Features", bug="Bugs Squashed")
53
59
  groups=_load_json(self.cfg, 'label_groups') if 'label_groups' in self.cfg else default_groups
54
60
  os.chdir(self.cfg.config_path)
61
+ if repo and '/' in repo: owner,repo = repo.split('/', 1)
55
62
  owner,repo = owner or self.cfg.user, repo or self.cfg.repo
56
63
  token = ifnone(token, os.getenv('NBDEV_TOKEN',None))
57
64
  if not token and Path('token').exists(): token = Path('token').read_text().strip()
@@ -60,38 +67,40 @@ class Release:
60
67
  self.gh = GhApi(owner, repo, token)
61
68
  self.groups = groups
62
69
 
63
- def _issues(self, label):
64
- return self.gh.issues.list_for_repo(state='closed', sort='created', filter='all', since=self.commit_date, labels=label)
65
- def _issue_groups(self): return parallel(self._issues, self.groups.keys(), progress=False, threadpool=True)
70
+ async def _issues(self, label):
71
+ return await self.gh.issues.list_for_repo(state='closed', sort='created', filter='all', since=self.commit_date, labels=label)
72
+ async def _issue_groups(self): return await asyncio.gather(*map(self._issues, self.groups.keys()))
66
73
 
67
74
  # %% ../nbs/api/18_release.ipynb #aa22dfe3
68
75
  @patch
69
- def changelog(self:Release,
76
+ async def changelog(self:Release,
70
77
  debug=False): # Just print the latest changes, instead of updating file
71
78
  "Create the CHANGELOG.md file, or return the proposed text if `debug` is `True`"
72
79
  if not self.changefile.exists(): self.changefile.write_text("# Release notes\n\n<!-- do not remove -->\n")
73
80
  marker = '<!-- do not remove -->\n'
74
- try: self.commit_date = (lr:=self.gh.repos.get_latest_release()).published_at
75
- except HTTP404NotFoundError: lr,self.commit_date = None,'2000-01-01T00:00:004Z'
81
+ try: self.commit_date = (lr:=await self.gh.repos.get_latest_release()).published_at
82
+ except APIError as e:
83
+ if e.status_code != 404: raise
84
+ lr,self.commit_date = None,'2000-01-01T00:00:004Z'
76
85
  if lr and (Version(self.cfg.version) <= Version(lr.tag_name)):
77
86
  print(f'Error: Version bump required: expected: >{lr.tag_name}, got: {self.cfg.version}.')
78
87
  raise SystemExit(1)
79
88
  res = f"\n## {self.cfg.version}\n"
80
- issues = self._issue_groups()
89
+ issues = await self._issue_groups()
81
90
  res += '\n'.join(_issues_txt(*o) for o in zip(issues, self.groups.values()))
82
91
  if debug: return res
83
- res = self.changefile.read_text().replace(marker, marker+res+"\n")
92
+ res = update_changelog(self.changefile.read_text(), self.cfg.version, res, marker)
84
93
  shutil.copy(self.changefile, self.changefile.with_suffix(".bak"))
85
94
  self.changefile.write_text(res)
86
95
  run(f'git add {self.changefile}')
87
96
 
88
97
  # %% ../nbs/api/18_release.ipynb #068421c6
89
98
  @patch
90
- def release(self:Release):
99
+ async def release(self:Release):
91
100
  "Tag and create a release in GitHub for the current version"
92
101
  ver = self.cfg.version
93
102
  notes = self.latest_notes()
94
- self.gh.create_release(ver, branch=self.cfg.branch, body=notes)
103
+ await self.gh.create_release(ver, branch=self.cfg.branch, body=notes)
95
104
  return ver
96
105
 
97
106
  # %% ../nbs/api/18_release.ipynb #22101171
@@ -105,38 +114,41 @@ def latest_notes(self:Release):
105
114
 
106
115
  # %% ../nbs/api/18_release.ipynb #01cb1ca4
107
116
  @call_parse
108
- def changelog(
117
+ async def changelog(
109
118
  debug:store_true=False, # Print info to be added to CHANGELOG, instead of updating file
110
- repo:str=None, # repo to use instead of `lib_name` from pyproject.toml
119
+ repo:str=None, # "repo" or "owner/repo" to use instead of pyproject.toml values
120
+ token:str=None, # Optional GitHub token (otherwise `token` file is used)
111
121
  ):
112
122
  "Create a CHANGELOG.md file from closed and labeled GitHub issues"
113
- res = Release(repo=repo).changelog(debug=debug)
123
+ res = await Release(repo=repo, token=token).changelog(debug=debug)
114
124
  if debug: print(res)
115
125
 
116
126
  # %% ../nbs/api/18_release.ipynb #15b66643
117
- def push_release(token:str=None):
127
+ async def push_release(token:str=None, repo:str=None):
118
128
  "Create a GitHub release (changelog should already be committed/pushed). Returns the release."
119
- return Release(token=token).release()
129
+ return await Release(repo=repo, token=token).release()
120
130
 
121
131
  # %% ../nbs/api/18_release.ipynb #6d3c5cd8
122
132
  @call_parse
123
- def release_git(token:str=None):
133
+ async def release_git(token:str=None):
124
134
  "Tag and create a release in GitHub for the current version"
125
- print(f"Released {push_release(token)}")
135
+ print(f"Released {await push_release(token)}")
126
136
 
127
137
  # %% ../nbs/api/18_release.ipynb #94ee72b1
128
138
  @call_parse
129
- def release_gh(
130
- token:str=None # Optional GitHub token (otherwise `token` file is used)
139
+ async def release_gh(
140
+ token:str=None, # Optional GitHub token (otherwise `token` file is used)
141
+ repo:str=None, # "repo" or "owner/repo" to use instead of pyproject.toml values
142
+ no_changelog:store_true=False # Skip changelog creation (assumes CHANGELOG.md is up to date)
131
143
  ):
132
144
  "Calls `nbdev-changelog`, lets you edit the result, then pushes to git and calls `nbdev-release-git`"
133
145
  cfg = _find_config()
134
- Release().changelog()
146
+ if not no_changelog: await Release(repo=repo).changelog()
135
147
  subprocess.run([os.environ.get('EDITOR','nano'), cfg.config_path/'CHANGELOG.md'])
136
148
  if not input("Make release now? (y/n) ").lower().startswith('y'): sys.exit(1)
137
149
  run('git commit -am release')
138
150
  run('git push')
139
- print(f"Released {push_release(token)}")
151
+ print(f"Released {await push_release(token, repo=repo)}")
140
152
 
141
153
  # %% ../nbs/api/18_release.ipynb #5b4d4aa2
142
154
  from fastcore.all import *
@@ -34,10 +34,12 @@ class BasicMarkdownRenderer(MarkdownRenderer):
34
34
  return doc+super()._repr_markdown_()
35
35
 
36
36
  # %% ../nbs/api/08_showdoc.ipynb #1256ef79
37
- def show_doc(sym, # Symbol to document
38
- renderer=None, # Optional renderer (defaults to markdown)
39
- name:str|None=None, # Optionally override displayed name of `sym`
40
- title_level:int=3): # Heading level to use for symbol name
37
+ def show_doc(
38
+ sym, # Symbol to document
39
+ renderer=None, # Optional renderer (defaults to markdown)
40
+ name:str|None=None, # Optionally override displayed name of `sym`
41
+ title_level:int=3 # Heading level to use for symbol name
42
+ ):
41
43
  "Show signature and docstring for `sym`"
42
44
  if renderer is None: renderer = get_config().get('renderer', None)
43
45
  if renderer is None: renderer=BasicMarkdownRenderer
@@ -25,48 +25,60 @@ from fastcore.nbio import *
25
25
  from execnb.shell import *
26
26
 
27
27
  # %% ../nbs/api/12_test.ipynb #3f4fa1ad
28
- def test_nb(fn, # file name of notebook to test
29
- skip_flags=None, # list of flags marking cells to skip
30
- force_flags=None, # list of flags marking cells to always run
31
- do_print=False, # print completion?
32
- showerr=True, # print errors to stderr?
33
- basepath=None, # path to add to sys.path
34
- verbose=False, # stream stdout/stderr from cells to console?
35
- save=False): # write outputs back to notebook on success?
28
+ def test_nb(
29
+ fn, # file name of notebook to test
30
+ skip_flags=None, # list of flags marking cells to skip
31
+ force_flags=None, # list of flags marking cells to always run
32
+ do_print=False, # print completion?
33
+ showerr=True, # print errors to stderr?
34
+ basepath=None, # path to add to sys.path
35
+ verbose=False, # stream stdout/stderr from cells to console?
36
+ save=False, # write outputs back to notebook on success?
37
+ profile:bool=None # load the IPython profile, as `ipykernel` does? (default: `exec_profile` config key)
38
+ ):
36
39
  "Execute tests in notebook in `fn` except those with `skip_flags`"
37
40
  faulthandler.register(signal.SIGINT, file=sys.__stderr__, all_threads=True, chain=True)
41
+ fn = Path(fn)
38
42
  if basepath: sys.path.insert(0, str(basepath))
39
- if not IN_NOTEBOOK: os.environ["IN_TEST"] = '1'
40
- flags=set(L(skip_flags)) - set(L(force_flags))
41
- nb = NBProcessor(fn, rm_directives=False, process=True).nb
42
- fm = nb_frontmatter(nb)
43
- if str2bool(fm.get('skip_exec', False)) or nb_lang(nb) != 'python': return True, 0
44
-
45
- def _no_eval(cell):
46
- if cell.cell_type != 'code': return True
47
- if 'nbdev_export'+'(' in cell.source: return True
48
- direc = getattr(cell, 'directives_', {}) or {}
49
- if direc.get('eval:', [''])[0].lower() == 'false': return True
50
- return flags & direc.keys()
51
-
52
- start = time.time()
53
- k = CaptureShell(fn)
54
- if do_print: print(f'Starting {fn}')
43
+ prev_test = os.environ.get('IN_TEST')
44
+ if not IN_NOTEBOOK: os.environ['IN_TEST'] = '1'
55
45
  try:
56
- with working_directory(fn.parent):
57
- k.run_all(nb, exc_stop=True, preproc=_no_eval, verbose=verbose)
58
- if save: write_nb(nb, fn)
59
- res = True
60
- except:
61
- if showerr: sys.stderr.write(k.prettytb(fname=fn)+'\n')
62
- res=False
63
- if do_print: print(f'- Completed {fn}')
64
- return res,time.time()-start
46
+ flags=set(L(skip_flags)) - set(L(force_flags))
47
+ nb = NBProcessor(fn, rm_directives=False, process=True).nb
48
+ fm = nb_frontmatter(nb)
49
+ if str2bool(fm.get('skip_exec', False)) or nb_lang(nb) != 'python': return True, 0
50
+
51
+ def _no_eval(cell):
52
+ if cell.cell_type != 'code': return True
53
+ if 'nbdev_export'+'(' in cell.source: return True
54
+ direc = getattr(cell, 'directives_', {}) or {}
55
+ if direc.get('eval:', [''])[0].lower() == 'false': return True
56
+ return flags & direc.keys()
57
+
58
+ start = time.time()
59
+ if profile is None: profile = bool(get_config(fn.parent).exec_profile)
60
+ k = CaptureShell(fn, profile=profile)
61
+ if do_print: print(f'Starting {fn}')
62
+ try:
63
+ with working_directory(fn.parent):
64
+ k.run_all(nb, exc_stop=True, preproc=_no_eval, verbose=verbose)
65
+ if save: write_nb(nb, fn)
66
+ res = True
67
+ except:
68
+ if showerr: sys.stderr.write(k.prettytb(fname=fn)+'\n')
69
+ res=False
70
+ if do_print: print(f'- Completed {fn}')
71
+ return res,time.time()-start
72
+ finally:
73
+ if prev_test is None: os.environ.pop('IN_TEST', None)
74
+ else: os.environ['IN_TEST'] = prev_test
75
+
65
76
 
66
77
  # %% ../nbs/api/12_test.ipynb #d8bf1f1b-935d-4b69-ba96-827c5d7213f0
67
- def _keep_file(p:Path, # filename for which to check for `indicator_fname`
68
- ignore_fname:str # filename that will result in siblings being ignored
69
- ) -> bool:
78
+ def _keep_file(
79
+ p:Path, # filename for which to check for `indicator_fname`
80
+ ignore_fname:str # filename that will result in siblings being ignored
81
+ ) -> bool:
70
82
  "Returns False if `indicator_fname` is a sibling to `fname` else True"
71
83
  if p.exists(): return not bool(p.parent.ls().attrgot('name').filter(lambda x: x == ignore_fname))
72
84
  else: True
@@ -84,9 +96,11 @@ def nbdev_test(
84
96
  ignore_fname:str='.notest', # Filename that will result in siblings being ignored
85
97
  verbose:bool=False, # Print stdout/stderr from notebook cells?
86
98
  save:bool=False, # Write outputs back to notebooks on success?
87
- **kwargs):
99
+ **kwargs
100
+ ):
88
101
  "Test in parallel notebooks matching `path`, passing along `flags`"
89
- skip_flags = get_config().tst_flags
102
+ cfg = get_config(Path(path).resolve() if path else None)
103
+ skip_flags = cfg.tst_flags
90
104
  if isinstance(skip_flags, str): skip_flags = skip_flags.split()
91
105
  force_flags = flags.split()
92
106
  files = nbglob(path, as_path=True, **kwargs)
@@ -96,10 +110,10 @@ def nbdev_test(
96
110
  if n_workers is None: n_workers = 0 if len(files)==1 else min(num_cpus(), 8)
97
111
  if IN_NOTEBOOK: kw = {'method':'spawn'} if os.name=='nt' else {'method':'forkserver'}
98
112
  else: kw = {'method':'forkserver'} if sys.platform=='darwin' else {}
99
- wd_pth = get_config().nbs_path
113
+ wd_pth = cfg.nbs_path
100
114
  with working_directory(wd_pth if (wd_pth and wd_pth.exists()) else os.getcwd()):
101
115
  results = parallel(test_nb, files, skip_flags=skip_flags, force_flags=force_flags, n_workers=n_workers,
102
- basepath=get_config().config_path, pause=pause, do_print=do_print, verbose=verbose, save=save, **kw)
116
+ basepath=cfg.config_path, pause=pause, do_print=do_print, verbose=verbose, save=save, **kw)
103
117
  passed,times = zip(*results)
104
118
  if all(passed): print("Success.")
105
119
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdev
3
- Version: 3.0.18
3
+ Version: 3.2.0
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
@@ -16,10 +16,10 @@ Classifier: License :: OSI Approved :: Apache Software License
16
16
  Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
- Requires-Dist: fastcore>=1.12.35
20
- Requires-Dist: execnb>=0.1.12
19
+ Requires-Dist: fastcore>=1.14.6
20
+ Requires-Dist: execnb>=0.2.4
21
21
  Requires-Dist: astunparse
22
- Requires-Dist: ghapi>=1.0.3
22
+ Requires-Dist: ghapi>=2.0.0
23
23
  Requires-Dist: watchdog
24
24
  Requires-Dist: asttokens
25
25
  Requires-Dist: setuptools
@@ -158,7 +158,7 @@ available commands:
158
158
  nbdev-pypi Create and upload Python package to PyPI
159
159
  nbdev-readme Create README.md from readme_nb (index.ipynb by default)
160
160
  nbdev-release-both Release both conda and PyPI packages
161
- nbdev-release-gh Calls `nbdev_changelog`, lets you edit the result, then pushes to git and calls `nbdev_release_git`
161
+ nbdev-release-gh Calls `nbdev-changelog`, lets you edit the result, then pushes to git and calls `nbdev-release-git`
162
162
  nbdev-release-git Tag and create a release in GitHub for the current version
163
163
  nbdev-requirements Writes a `requirements.txt` file to `directory` based on pyproject.toml.
164
164
  nbdev-sidebar Create sidebar.yml
@@ -184,6 +184,9 @@ some_module.something()
184
184
  Instead, split this into two cells, one which does `import some_module`,
185
185
  and the other which does `some_module.something()`.
186
186
 
187
+ Only top-level statements count: `try: import` blocks and imports inside
188
+ function definitions are fine.
189
+
187
190
  The reason for this is that when we create your documentation website,
188
191
  we ensure that all of the signatures for functions you document are up
189
192
  to date, by running the imports, exported cells, and
@@ -1,7 +1,7 @@
1
- fastcore>=1.12.35
2
- execnb>=0.1.12
1
+ fastcore>=1.14.6
2
+ execnb>=0.2.4
3
3
  astunparse
4
- ghapi>=1.0.3
4
+ ghapi>=2.0.0
5
5
  watchdog
6
6
  asttokens
7
7
  setuptools
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "License :: OSI Approved :: Apache Software License",
20
20
  ]
21
21
  dynamic = ["version"]
22
- dependencies = [ "fastcore>=1.12.35", "execnb>=0.1.12", "astunparse", "ghapi>=1.0.3", "watchdog", "asttokens",
22
+ dependencies = [ "fastcore>=1.14.6", "execnb>=0.2.4", "astunparse", "ghapi>=2.0.0", "watchdog", "asttokens",
23
23
  "setuptools", "build", "fastgit", "pyyaml", "tomli; python_version < '3.11'", ]
24
24
 
25
25
  [project.optional-dependencies]
@@ -82,3 +82,6 @@ readme_nb = "getting_started.ipynb"
82
82
  jupyter_hooks = true
83
83
  allowed_cell_metadata_keys = ['solveit_ai']
84
84
 
85
+
86
+ [tool.chkstyle]
87
+ skip_paths = ["_modidx.py"]
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