nbdevAuto 0.3.76__tar.gz → 0.3.77__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdevAuto
3
- Version: 0.3.76
3
+ Version: 0.3.77
4
4
  Summary: automating nbdev
5
5
  Author-email: Benedict Thekkel <bthekkel1@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1 @@
1
+ __version__ = "0.3.77"
@@ -47,7 +47,9 @@ d = { 'settings': { 'branch': 'main',
47
47
  'nbdevAuto.fleet._hits': ('fleet.html#_hits', 'nbdevAuto/fleet.py'),
48
48
  'nbdevAuto.fleet._line_of': ('fleet.html#_line_of', 'nbdevAuto/fleet.py'),
49
49
  'nbdevAuto.fleet._porcelain': ('fleet.html#_porcelain', 'nbdevAuto/fleet.py'),
50
+ 'nbdevAuto.fleet._public_notebooks': ('fleet.html#_public_notebooks', 'nbdevAuto/fleet.py'),
50
51
  'nbdevAuto.fleet._render': ('fleet.html#_render', 'nbdevAuto/fleet.py'),
52
+ 'nbdevAuto.fleet._src': ('fleet.html#_src', 'nbdevAuto/fleet.py'),
51
53
  'nbdevAuto.fleet.ahead_of_remote': ('fleet.html#ahead_of_remote', 'nbdevAuto/fleet.py'),
52
54
  'nbdevAuto.fleet.copy_one': ('fleet.html#copy_one', 'nbdevAuto/fleet.py'),
53
55
  'nbdevAuto.fleet.dirty_submodules': ('fleet.html#dirty_submodules', 'nbdevAuto/fleet.py'),
@@ -55,6 +57,7 @@ d = { 'settings': { 'branch': 'main',
55
57
  'nbdevAuto.fleet.finish': ('fleet.html#finish', 'nbdevAuto/fleet.py'),
56
58
  'nbdevAuto.fleet.fleet': ('fleet.html#fleet', 'nbdevAuto/fleet.py'),
57
59
  'nbdevAuto.fleet.fleet_copy': ('fleet.html#fleet_copy', 'nbdevAuto/fleet.py'),
60
+ 'nbdevAuto.fleet.fleet_nblint': ('fleet.html#fleet_nblint', 'nbdevAuto/fleet.py'),
58
61
  'nbdevAuto.fleet.fleet_push': ('fleet.html#fleet_push', 'nbdevAuto/fleet.py'),
59
62
  'nbdevAuto.fleet.fleet_scan': ('fleet.html#fleet_scan', 'nbdevAuto/fleet.py'),
60
63
  'nbdevAuto.fleet.fleet_status': ('fleet.html#fleet_status', 'nbdevAuto/fleet.py'),
@@ -64,6 +67,9 @@ d = { 'settings': { 'branch': 'main',
64
67
  'nbdevAuto.fleet.format_summary': ('fleet.html#format_summary', 'nbdevAuto/fleet.py'),
65
68
  'nbdevAuto.fleet.init_new': ('fleet.html#init_new', 'nbdevAuto/fleet.py'),
66
69
  'nbdevAuto.fleet.is_published': ('fleet.html#is_published', 'nbdevAuto/fleet.py'),
70
+ 'nbdevAuto.fleet.lint_notebook': ('fleet.html#lint_notebook', 'nbdevAuto/fleet.py'),
71
+ 'nbdevAuto.fleet.lint_one': ('fleet.html#lint_one', 'nbdevAuto/fleet.py'),
72
+ 'nbdevAuto.fleet.lint_totals': ('fleet.html#lint_totals', 'nbdevAuto/fleet.py'),
67
73
  'nbdevAuto.fleet.looks_secret': ('fleet.html#looks_secret', 'nbdevAuto/fleet.py'),
68
74
  'nbdevAuto.fleet.notebook_text': ('fleet.html#notebook_text', 'nbdevAuto/fleet.py'),
69
75
  'nbdevAuto.fleet.parse_verb': ('fleet.html#parse_verb', 'nbdevAuto/fleet.py'),
@@ -74,6 +80,7 @@ d = { 'settings': { 'branch': 'main',
74
80
  'nbdevAuto.fleet.scan_one': ('fleet.html#scan_one', 'nbdevAuto/fleet.py'),
75
81
  'nbdevAuto.fleet.script_path': ('fleet.html#script_path', 'nbdevAuto/fleet.py'),
76
82
  'nbdevAuto.fleet.status_one': ('fleet.html#status_one', 'nbdevAuto/fleet.py'),
83
+ 'nbdevAuto.fleet.unlinked_from_index': ('fleet.html#unlinked_from_index', 'nbdevAuto/fleet.py'),
77
84
  'nbdevAuto.fleet.update_one': ('fleet.html#update_one', 'nbdevAuto/fleet.py'),
78
85
  'nbdevAuto.fleet.upload_one': ('fleet.html#upload_one', 'nbdevAuto/fleet.py'),
79
86
  'nbdevAuto.fleet.usage': ('fleet.html#usage', 'nbdevAuto/fleet.py'),
@@ -6,11 +6,13 @@ Docs: https://bthek1.github.io/nbdevAuto/fleet.html.md"""
6
6
 
7
7
  # %% auto #0
8
8
  __all__ = ['RAW_DOC_SUFFIXES', 'KNOWN_TOKEN', 'SECRET_PATTERNS', 'ALLOW', 'RENDERED_SUFFIXES', 'TEXT_SUFFIXES', 'EMAIL',
9
- 'RESERVED_DOMAINS', 'BULK_EMAIL_MIN', 'VERBS', 'Skip', 'Outcome', 'Report', 'format_summary', 'walk',
10
- 'finish', 'script_path', 'upload_one', 'fleet_upload', 'ahead_of_remote', 'push_one', 'copy_one',
11
- 'fleet_push', 'fleet_copy', 'status_one', 'fleet_status', 'fetch_all', 'update_one', 'dirty_submodules',
12
- 'init_new', 'pull_root', 'fleet_update', 'fleet_sync', 'looks_secret', 'notebook_text', 'is_published',
13
- 'scan_file', 'scan_one', 'fleet_scan', 'parse_verb', 'usage', 'run_verb', 'fleet']
9
+ 'RESERVED_DOMAINS', 'BULK_EMAIL_MIN', 'NB_RULES', 'DLTASK_SECTIONS', 'DLTASK_NOT_TASKS', 'LARGE_NB_BYTES',
10
+ 'NOT_PUBLISHED', 'VERBS', 'Skip', 'Outcome', 'Report', 'format_summary', 'walk', 'finish', 'script_path',
11
+ 'upload_one', 'fleet_upload', 'ahead_of_remote', 'push_one', 'copy_one', 'fleet_push', 'fleet_copy',
12
+ 'status_one', 'fleet_status', 'fetch_all', 'update_one', 'dirty_submodules', 'init_new', 'pull_root',
13
+ 'fleet_update', 'fleet_sync', 'looks_secret', 'notebook_text', 'is_published', 'scan_file', 'scan_one',
14
+ 'fleet_scan', 'lint_notebook', 'unlinked_from_index', 'lint_one', 'lint_totals', 'fleet_nblint',
15
+ 'parse_verb', 'usage', 'run_verb', 'fleet']
14
16
 
15
17
  # %% ../nbs/04_Fleet.ipynb #d361ee5f-9f09-464f-aec6-0e27cec90fe9
16
18
  import json
@@ -21,6 +23,7 @@ import sys
21
23
  from concurrent.futures import ThreadPoolExecutor
22
24
  from dataclasses import dataclass, field
23
25
  from pathlib import Path
26
+ from urllib.parse import unquote
24
27
 
25
28
  from fastcore.script import *
26
29
 
@@ -575,10 +578,187 @@ def fleet_scan(
575
578
  sys.exit(1 if published or (strict and rep.ran) else 0)
576
579
 
577
580
 
581
+ # %% ../nbs/04_Fleet.ipynb #7a605ea9
582
+ # (key, label, issue #24 phase). The phase is what a count is FOR: each phase of the
583
+ # cleanup drives its own rows to 0, so the table doubles as the plan's progress bar.
584
+ NB_RULES = [
585
+ ("mermaid_plain", "Plain ```mermaid fence (raw source on the site)", 1),
586
+ ("title_leading_rule","Title cell starts with --- (breaks nbdev-test)", 1),
587
+ ("title_no_h1", "Title cell has no # H1", 1),
588
+ ("index_unlinked", "Public notebook missing from index.ipynb", 1),
589
+ ("mermaid_braced", "Braced {mermaid} not yet an .svg file", 2),
590
+ ("svg_in_index", "Inline <svg> in index.ipynb (README strips it)", 2),
591
+ ("title_code", "Title cell is a code cell", 3),
592
+ ("title_no_desc", "Title cell has no > description", 3),
593
+ ("no_skip_showdoc", "Concept notebook without skip_showdoc", 3),
594
+ ("non_ascii", "Non-ASCII characters in code cells", 4),
595
+ ("naive_datetime", "Naive now() or utcnow(), no timezone", 4),
596
+ ("bad_filename", "Filename with a space or &", 4),
597
+ ("dl_matplotlib", "DL_methods chart in matplotlib", 5),
598
+ ("dltask_sections", "DL_tasks notebook missing a required section", 5),
599
+ ("dltask_no_require", "DL_tasks live-capture demo without require(...)", 5),
600
+ ("stale_outputs", "skip_exec notebook carrying outputs", 6),
601
+ ("large_file", "Notebook over 3 MB", 6),
602
+ ("multi_h2", "Markdown cell with several ## sections", 7),
603
+ ("no_trailing_rule", "## cell without a trailing ---", 7),
604
+ ]
605
+ # Matched case-insensitively against the `##` headings, as substrings, so "Datasets"
606
+ # also accepts "Datasets and splits". From dl-task-notebook-format.instructions.md.
607
+ DLTASK_SECTIONS = ["What is", "Real-world use cases", "Evaluation", "Datasets",
608
+ "Setup", "benchmark", "Common frameworks", "Going further"]
609
+ DLTASK_NOT_TASKS = ("Theory", "Frameworks")
610
+ LARGE_NB_BYTES = 3_000_000
611
+ # Never a site, so never held to the publishing standard (see special-submodules).
612
+ NOT_PUBLISHED = ("Personal/bthek1_career",)
613
+
614
+ def _src(c):
615
+ s = c.get("source", "")
616
+ return "".join(s) if isinstance(s, list) else s
617
+
618
+ def lint_notebook(path, sub=""):
619
+ """{rule key: count} for one notebook. A count is the number of offending items
620
+ (diagrams, characters, cells); 1 for a yes/no rule. Empty means it meets the standard."""
621
+ out = {}
622
+ name = path.name
623
+ if " " in name or "&" in name: out["bad_filename"] = 1
624
+ try: nb = json.loads(path.read_text(encoding="utf-8"))
625
+ except Exception: return out
626
+ cells = nb.get("cells", [])
627
+ if not cells: return out
628
+ code = [_src(c) for c in cells if c.get("cell_type") == "code"]
629
+ md = [_src(c) for c in cells if c.get("cell_type") == "markdown"]
630
+ allcode, allmd = "\n".join(code), "\n".join(md)
631
+ exports = re.search(r"^#\|\s*(export|exporti|exports|default_exp)\b", allcode, re.M)
632
+ first = _src(cells[0]).strip()
633
+
634
+ if cells[0].get("cell_type") != "markdown": out["title_code"] = 1
635
+ else:
636
+ if first.startswith("---"): out["title_leading_rule"] = 1
637
+ if not re.match(r"^(---[\s\S]*?---\s*)?#\s+\S", first): out["title_no_h1"] = 1
638
+ if not re.search(r"^>\s*\S", first, re.M): out["title_no_desc"] = 1
639
+ if not exports and "skip_showdoc: true" not in first: out["no_skip_showdoc"] = 1
640
+
641
+ # The title cell is checked above; a `##` packed into it is a title problem first.
642
+ multi = sum(len(re.findall(r"^##\s", m, re.M)) > 1 for m in md[1:])
643
+ trail = sum(bool(re.search(r"^##\s", m, re.M)) and not m.rstrip().endswith("---")
644
+ for m in md[1:])
645
+ if multi: out["multi_h2"] = multi
646
+ if trail: out["no_trailing_rule"] = trail
647
+
648
+ plain = len(re.findall(r"^```mermaid\s*$", allmd, re.M))
649
+ brace = len(re.findall(r"^```\{mermaid\}", allmd, re.M))
650
+ if plain: out["mermaid_plain"] = plain
651
+ if brace: out["mermaid_braced"] = brace
652
+ if name == "index.ipynb" and re.search(r"<svg[\s>]", allmd): out["svg_in_index"] = 1
653
+
654
+ nonascii = sum(ord(ch) > 127 for ch in allcode)
655
+ if nonascii: out["non_ascii"] = nonascii
656
+ if re.search(r"datetime\.utcnow\(|\b(?:dt|datetime)\.now\(\s*\)", allcode):
657
+ out["naive_datetime"] = 1
658
+
659
+ if sub == "DL/DL_methods" and re.search(
660
+ r"^\s*(import|from)\s+(matplotlib|seaborn|plotly)", allcode, re.M):
661
+ out["dl_matplotlib"] = 1
662
+ # Only a real task notebook: not the index, a library notebook, a stub, or the
663
+ # Theory/ and Frameworks/ reference pages, which have no task to benchmark.
664
+ if (sub == "DL/DL_tasks" and name != "index.ipynb" and not exports and len(cells) > 3
665
+ and path.parent.name not in DLTASK_NOT_TASKS):
666
+ heads = "\n".join(re.findall(r"^##\s+.*$", allmd, re.M)).lower()
667
+ missing = [s for s in DLTASK_SECTIONS if s.lower() not in heads]
668
+ if missing: out["dltask_sections"] = len(missing)
669
+ if re.search(r"cv2\.VideoCapture|sounddevice|pyaudio", allcode) and "require(" not in allcode:
670
+ out["dltask_no_require"] = 1
671
+
672
+ # DL notebooks keep outputs on purpose (benchmarks, demos), so only elsewhere is a
673
+ # skip_exec notebook with outputs a sign of stale results.
674
+ outs = sum(len(c.get("outputs", [])) for c in cells if c.get("cell_type") == "code")
675
+ if outs and "skip_exec: true" in first and not sub.startswith("DL/"):
676
+ out["stale_outputs"] = outs
677
+ if path.stat().st_size > LARGE_NB_BYTES: out["large_file"] = 1
678
+ return out
679
+
680
+ def _public_notebooks(nbs):
681
+ "Every published notebook under `nbs`, relative paths. `_`/`.` dirs are build output"
682
+ found = []
683
+ for p in sorted(nbs.rglob("*.ipynb")):
684
+ rel = p.relative_to(nbs)
685
+ if any(part.startswith((".", "_")) for part in rel.parts[:-1]): continue
686
+ if p.name.lower().startswith("p_"): continue
687
+ found.append(rel)
688
+ return found
689
+
690
+ def unlinked_from_index(nbs, public=None):
691
+ "Public notebooks (as posix paths) that `nbs/index.ipynb` does not link to"
692
+ idx = nbs/"index.ipynb"
693
+ if not idx.exists(): return []
694
+ public = public if public is not None else _public_notebooks(nbs)
695
+ body = "".join(_src(c) for c in json.loads(idx.read_text(encoding="utf-8")).get("cells", []))
696
+ links = {unquote(l).split("#")[0] for l in re.findall(r"\]\(([^)\s]+)\)", body)
697
+ if not l.startswith(("http", "#"))}
698
+ return [r.as_posix() for r in public
699
+ if r.name != "index.ipynb" and r.as_posix() not in links]
700
+
701
+ def lint_one(path, sub=None, nbs="nbs"):
702
+ "{notebook path relative to the superproject: {rule key: count}} for one submodule"
703
+ name = sub.path if sub else path.name
704
+ root = path/nbs
705
+ if name in NOT_PUBLISHED: raise Skip("not published")
706
+ if not root.is_dir(): raise Skip("without nbs")
707
+ public = _public_notebooks(root)
708
+ found = {}
709
+ for rel in public:
710
+ hits = lint_notebook(root/rel, name)
711
+ if hits: found[f"{name}/{nbs}/{rel.as_posix()}"] = hits
712
+ for rel in unlinked_from_index(root, public):
713
+ found.setdefault(f"{name}/{nbs}/{rel}", {})["index_unlinked"] = 1
714
+ return found
715
+
716
+ def lint_totals(found):
717
+ "{rule key: (notebooks, items)} over the output of `lint_one`, every rule present"
718
+ tot = {k: [0, 0] for k, _, _ in NB_RULES}
719
+ for hits in found.values():
720
+ for k, n in hits.items(): tot[k][0] += 1; tot[k][1] += n
721
+ return {k: tuple(v) for k, v in tot.items()}
722
+
723
+ def fleet_nblint(
724
+ detail:bool = False, # List the offending notebooks under each rule
725
+ phase:int = None, # Only the rules for this phase of issue #24
726
+ strict:bool = False, # Exit 1 when any rule is above 0
727
+ root:str = None, # Superproject root (default: nearest ancestor with .gitmodules)
728
+ ):
729
+ "Count public notebooks that break the notebook standard, one row per rule"
730
+ from rich.table import Table
731
+ found = {}
732
+ def _collect(p, s):
733
+ found.update(lint_one(p, s))
734
+ raise Skip("linted") # counted below in one table, not narrated per submodule
735
+ rep = walk(_collect, root=root, workers=8, style="line")
736
+ if rep.failed: sys.exit(finish(rep, "linted"))
737
+ rules = [r for r in NB_RULES if phase is None or r[2] == phase]
738
+ tot = lint_totals(found)
739
+ t = Table(title="Notebook standard (issue #24)", title_justify="left")
740
+ for col, kw in (("Phase", {"justify": "right"}), ("Rule", {}),
741
+ ("Notebooks", {"justify": "right"}), ("Items", {"justify": "right"})):
742
+ t.add_column(col, **kw)
743
+ for key, label, ph in rules:
744
+ nbks, items = tot[key]
745
+ style = "green" if nbks == 0 else None
746
+ t.add_row(str(ph), escape(label), str(nbks), str(items), style=style)
747
+ console.print(t)
748
+ left = sum(tot[k][0] for k, _, _ in rules)
749
+ console.print(f"[bold]{len(found)} notebooks flagged, {left} rule hits in view[/bold]")
750
+ if detail:
751
+ for key, label, _ in rules:
752
+ hits = sorted((p, h[key]) for p, h in found.items() if key in h)
753
+ if not hits: continue
754
+ console.print(f"\n[bold cyan]{escape(label)}[/bold cyan]")
755
+ for p, n in hits: console.print(f" {escape(p)} [dim]({n})[/dim]")
756
+ sys.exit(1 if strict and left else 0)
757
+
578
758
  # %% ../nbs/04_Fleet.ipynb #0281bd2b-91ba-4e49-90dc-d1a95d54d5a6
579
759
  VERBS = {"upload": fleet_upload, "push": fleet_push, "copy": fleet_copy,
580
760
  "status": fleet_status, "update": fleet_update, "sync": fleet_sync,
581
- "scan": fleet_scan}
761
+ "scan": fleet_scan, "nblint": fleet_nblint}
582
762
 
583
763
  def parse_verb(argv):
584
764
  "(verb, remaining args) for `fleet ...`, or None when `argv` does not name a verb"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbdevAuto
3
- Version: 0.3.76
3
+ Version: 0.3.77
4
4
  Summary: automating nbdev
5
5
  Author-email: Benedict Thekkel <bthekkel1@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -1 +0,0 @@
1
- __version__ = "0.3.76"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes