From 7b5bb24900ee98dbaa79cd2e7e6fd91c0e8de003 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:23:27 +0000 Subject: [PATCH] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docker/fetch_llama_prebuilt.py | 4 +- docker/jupyter/install_sloth_stickers.py | 10 +- docker/unsloth_colab_compat.py | 10 +- docker/unsloth_ipython_startup.py | 1 - docker/unsloth_nb_compat.py | 7 +- docker/unsloth_nb_strip_colab.py | 31 ++--- docker/unsloth_nb_view.py | 40 +++--- tests/validate_studio_features.py | 151 ++++++++++++++++------- 8 files changed, 167 insertions(+), 87 deletions(-) diff --git a/docker/fetch_llama_prebuilt.py b/docker/fetch_llama_prebuilt.py index 3eb50d67a6..6e4c61c01c 100644 --- a/docker/fetch_llama_prebuilt.py +++ b/docker/fetch_llama_prebuilt.py @@ -51,7 +51,9 @@ def resolve_latest_tag(repo: str) -> str: final_url = response.geturl() marker = "/releases/tag/" if marker not in final_url: - raise SystemExit(f"FAIL: could not resolve latest release of {repo} (landed on {final_url})") + raise SystemExit( + f"FAIL: could not resolve latest release of {repo} (landed on {final_url})" + ) return final_url.rsplit(marker, 1)[1].strip("/") diff --git a/docker/jupyter/install_sloth_stickers.py b/docker/jupyter/install_sloth_stickers.py index a1e2bb36bf..12c1125bc0 100644 --- a/docker/jupyter/install_sloth_stickers.py +++ b/docker/jupyter/install_sloth_stickers.py @@ -49,14 +49,14 @@ CURATED = [ def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--src", required=True, help="Studio 'Sloth emojis' dir") - parser.add_argument("--dest", required=True, help="output dir (static/sloth)") + parser = argparse.ArgumentParser(description = __doc__) + parser.add_argument("--src", required = True, help = "Studio 'Sloth emojis' dir") + parser.add_argument("--dest", required = True, help = "output dir (static/sloth)") args = parser.parse_args() - os.makedirs(args.dest, exist_ok=True) + os.makedirs(args.dest, exist_ok = True) installed = 0 - for index, name in enumerate(CURATED, start=1): + for index, name in enumerate(CURATED, start = 1): source = os.path.join(args.src, name) target = os.path.join(args.dest, "%02d.png" % index) if not os.path.isfile(source): diff --git a/docker/unsloth_colab_compat.py b/docker/unsloth_colab_compat.py index 0cc12803db..be3741fc77 100644 --- a/docker/unsloth_colab_compat.py +++ b/docker/unsloth_colab_compat.py @@ -38,14 +38,14 @@ def colab_cell_magic_fix(lines): for i, line in enumerate(lines): stripped = line.strip() if stripped == "" or stripped.startswith("#"): - skipped.append(line) # blank or comment (incl. #@title) + skipped.append(line) # blank or comment (incl. #@title) continue # First real line. Only act if it is a cell magic that is not yet on # top (i.e. something was skipped before it). if stripped.startswith("%%") and i > 0: - return [line] + skipped + lines[i + 1:] - return lines # already on top, or not a magic - return lines # all blank/comment -> nothing to do + return [line] + skipped + lines[i + 1 :] + return lines # already on top, or not a magic + return lines # all blank/comment -> nothing to do except Exception: return lines @@ -62,4 +62,4 @@ def register_ipython(): ip.input_transformers_cleanup.append(colab_cell_magic_fix) ip._unsloth_colab_fix = True except Exception as e: # never break a kernel because of the helper - print(f"[unsloth-nb] colab-compat hook skipped: {e!r}", file=sys.stderr) + print(f"[unsloth-nb] colab-compat hook skipped: {e!r}", file = sys.stderr) diff --git a/docker/unsloth_ipython_startup.py b/docker/unsloth_ipython_startup.py index a4a990a864..b62bff0850 100644 --- a/docker/unsloth_ipython_startup.py +++ b/docker/unsloth_ipython_startup.py @@ -25,7 +25,6 @@ except Exception as _e: # never break a kernel because of the helper # failure here never disables the transformers-sidecar hook above and vice versa. try: import unsloth_colab_compat - unsloth_colab_compat.register_ipython() except Exception as _e: # never break a kernel because of the helper import sys diff --git a/docker/unsloth_nb_compat.py b/docker/unsloth_nb_compat.py index afb62c55d9..621a7ba4cd 100644 --- a/docker/unsloth_nb_compat.py +++ b/docker/unsloth_nb_compat.py @@ -34,9 +34,14 @@ def _logging_enabled() -> bool: `[unsloth-nb] activated transformers sidecar ...` line noisy. Set UNSLOTH_ENABLE_LOGGING=1 to surface it (and other [unsloth-nb] diagnostics).""" return os.environ.get("UNSLOTH_ENABLE_LOGGING", "").strip().lower() not in ( - "", "0", "false", "no", "off", + "", + "0", + "false", + "no", + "off", ) + # Model-name -> minimum transformers tier, ported from Studio's # transformers_version.py (substring match on the lowered model id). Used as a # fallback when a notebook does not pin transformers but names a new-family model. diff --git a/docker/unsloth_nb_strip_colab.py b/docker/unsloth_nb_strip_colab.py index 4e87bdae88..83807a2d1c 100644 --- a/docker/unsloth_nb_strip_colab.py +++ b/docker/unsloth_nb_strip_colab.py @@ -60,9 +60,9 @@ def _strip_lines(lines): or None if there was nothing to strip.""" for i, line in enumerate(lines): if line.lstrip().lower().startswith(_INTRO_PREFIX): - out = lines[:i] + lines[i + 1:] + out = lines[:i] + lines[i + 1 :] if i < len(out) and out[i].strip() == "": - out = out[:i] + out[i + 1:] + out = out[:i] + out[i + 1 :] return out return None @@ -77,7 +77,7 @@ def _strip_intro(nb): return False src = cell.get("source") if isinstance(src, str): - lines = src.splitlines(keepends=True) + lines = src.splitlines(keepends = True) as_str = True elif isinstance(src, list): lines = list(src) @@ -104,7 +104,8 @@ def _clean_widgets(nb): if not isinstance(outs, list): continue kept = [ - o for o in outs + o + for o in outs if not (isinstance(o, dict) and _WIDGET_VIEW_MIME in (o.get("data") or {})) ] if len(kept) != len(outs): @@ -120,7 +121,7 @@ def _clean_widgets(nb): def strip_notebook(path): """Return True if the notebook was modified and written back.""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, "r", encoding = "utf-8") as f: nb = json.load(f) except Exception: return False @@ -133,8 +134,8 @@ def strip_notebook(path): tmp = path + ".tmp" try: - with open(tmp, "w", encoding="utf-8") as f: - json.dump(nb, f, indent=1, ensure_ascii=False) + with open(tmp, "w", encoding = "utf-8") as f: + json.dump(nb, f, indent = 1, ensure_ascii = False) f.write("\n") os.replace(tmp, path) except Exception: @@ -157,7 +158,7 @@ def _sha256(path): def migrate(state_path, dest): """Strip owned+unedited notebooks listed in STATE and update their hashes.""" try: - with open(state_path, "r", encoding="utf-8") as f: + with open(state_path, "r", encoding = "utf-8") as f: lines = f.read().splitlines() except OSError: return 0 @@ -165,7 +166,7 @@ def migrate(state_path, dest): out = [] changed = 0 for line in lines: - parts = line.split(" ", 1) # " " + parts = line.split(" ", 1) # " " if len(parts) != 2: out.append(line) continue @@ -173,7 +174,7 @@ def migrate(state_path, dest): path = os.path.join(dest, rel) if rel.endswith(".ipynb") and os.path.isfile(path): try: - if _sha256(path) == rec: # we own it and it is unedited + if _sha256(path) == rec: # we own it and it is unedited if strip_notebook(path): rec = _sha256(path) changed += 1 @@ -184,7 +185,7 @@ def migrate(state_path, dest): if changed: tmp = state_path + ".tmp" try: - with open(tmp, "w", encoding="utf-8") as f: + with open(tmp, "w", encoding = "utf-8") as f: f.write("\n".join(out) + "\n") os.replace(tmp, state_path) except OSError: @@ -194,10 +195,10 @@ def migrate(state_path, dest): def main(argv): - ap = argparse.ArgumentParser(description="Strip the Colab-only intro sentence.") - ap.add_argument("--state", help="sync state file (enables migration mode)") - ap.add_argument("--dest", help="notebooks dir (with --state)") - ap.add_argument("paths", nargs="*", help="notebooks to strip in place") + ap = argparse.ArgumentParser(description = "Strip the Colab-only intro sentence.") + ap.add_argument("--state", help = "sync state file (enables migration mode)") + ap.add_argument("--dest", help = "notebooks dir (with --state)") + ap.add_argument("paths", nargs = "*", help = "notebooks to strip in place") args = ap.parse_args(argv) if args.state: diff --git a/docker/unsloth_nb_view.py b/docker/unsloth_nb_view.py index 92eb4be501..ecb26d70eb 100644 --- a/docker/unsloth_nb_view.py +++ b/docker/unsloth_nb_view.py @@ -68,11 +68,11 @@ def parse_readme(readme_path): filename is the urldecoded basename under nb/ (literal parens, matching disk). """ - with open(readme_path, "r", encoding="utf-8") as f: + with open(readme_path, "r", encoding = "utf-8") as f: text = f.read() rows = [] - seen_pairs = set() # (section, filename) already emitted + seen_pairs = set() # (section, filename) already emitted section = None for line in text.splitlines(): m = re.match(r"^###\s+(.*)$", line) @@ -102,7 +102,11 @@ def _ordered_sections(rows): return order -def build_view(dest, view, amd=False): +def build_view( + dest, + view, + amd = False, +): nb_dir = os.path.join(dest, "nb") readme = os.path.join(dest, "README.md") if not os.path.isdir(nb_dir): @@ -138,23 +142,23 @@ def build_view(dest, view, amd=False): # Rebuild VIEW from scratch. _rmtree(view) - os.makedirs(view, exist_ok=True) + os.makedirs(view, exist_ok = True) n_links = 0 - for i, section in enumerate(order, start=1): + for i, section in enumerate(order, start = 1): folder = os.path.join(view, f"{i:02d} {section}") - os.makedirs(folder, exist_ok=True) + os.makedirs(folder, exist_ok = True) for fname in by_section[section]: link = os.path.join(folder, fname) target = os.path.join(nb_dir, fname) - rel = os.path.relpath(target, folder) # ../../unsloth-notebooks/nb/ + rel = os.path.relpath(target, folder) # ../../unsloth-notebooks/nb/ try: if os.path.islink(link) or os.path.exists(link): os.remove(link) os.symlink(rel, link) n_links += 1 except OSError as e: - print(f"[unsloth-nb] view: skip {fname}: {e}", file=sys.stderr) + print(f"[unsloth-nb] view: skip {fname}: {e}", file = sys.stderr) return len(order), n_links @@ -165,7 +169,7 @@ def _rmtree(path): if os.path.islink(path): os.remove(path) return - for root, dirs, files in os.walk(path, topdown=False): + for root, dirs, files in os.walk(path, topdown = False): for name in files: try: os.remove(os.path.join(root, name)) @@ -184,12 +188,16 @@ def _rmtree(path): def main(argv): - ap = argparse.ArgumentParser(description="Build the categorized notebook view.") - ap.add_argument("dest", help="notebooks dir (contains README.md and nb/)") - ap.add_argument("view", nargs="?", help="output view dir (omit with --print)") - ap.add_argument("--amd", action="store_true", help="include AMD-* notebooks") - ap.add_argument("--print", dest="do_print", action="store_true", - help="print sectionfile rows instead of building") + ap = argparse.ArgumentParser(description = "Build the categorized notebook view.") + ap.add_argument("dest", help = "notebooks dir (contains README.md and nb/)") + ap.add_argument("view", nargs = "?", help = "output view dir (omit with --print)") + ap.add_argument("--amd", action = "store_true", help = "include AMD-* notebooks") + ap.add_argument( + "--print", + dest = "do_print", + action = "store_true", + help = "print sectionfile rows instead of building", + ) args = ap.parse_args(argv) if args.do_print: @@ -200,7 +208,7 @@ def main(argv): if not args.view: ap.error("view dir is required unless --print is given") - n_sections, n_links = build_view(args.dest, args.view, amd=args.amd) + n_sections, n_links = build_view(args.dest, args.view, amd = args.amd) print(f"[unsloth-nb] view: {n_links} notebooks in {n_sections} folders -> {args.view}") return 0 diff --git a/tests/validate_studio_features.py b/tests/validate_studio_features.py index f5b0b3bd10..f4117d3010 100644 --- a/tests/validate_studio_features.py +++ b/tests/validate_studio_features.py @@ -11,6 +11,7 @@ login branding fails CI on every device. Usage: python tests/validate_studio_features.py Exit 0 = all checks pass; non-zero = at least one failed. """ + from __future__ import annotations import importlib @@ -27,7 +28,11 @@ sys.path.insert(0, DOCKER) _failures: list[str] = [] -def check(name: str, cond: bool, detail: str = "") -> None: +def check( + name: str, + cond: bool, + detail: str = "", +) -> None: status = "PASS" if cond else "FAIL" print(f" [{status}] {name}" + (f" -- {detail}" if detail and not cond else "")) if not cond: @@ -40,13 +45,13 @@ def check(name: str, cond: bool, detail: str = "") -> None: def test_colab_compat() -> None: print("colab cell-magic compat (unsloth_colab_compat):") m = importlib.import_module("unsloth_colab_compat") - out = m.colab_cell_magic_fix(['#@title Setup\n', '%%capture\n', '!pip install x\n']) - check("magic hoisted above #@title", out[0] == '%%capture\n' and '#@title Setup\n' in out) + out = m.colab_cell_magic_fix(["#@title Setup\n", "%%capture\n", "!pip install x\n"]) + check("magic hoisted above #@title", out[0] == "%%capture\n" and "#@title Setup\n" in out) # idempotent / already on top - same = ['%%capture\n', 'print(1)\n'] + same = ["%%capture\n", "print(1)\n"] check("no-op when magic already first", m.colab_cell_magic_fix(same) == same) # non-magic cell untouched - plain = ['x = 1\n', 'y = 2\n'] + plain = ["x = 1\n", "y = 2\n"] check("plain cell untouched", m.colab_cell_magic_fix(plain) == plain) @@ -56,11 +61,16 @@ def test_colab_compat() -> None: def test_nb_view() -> None: print("notebook view (unsloth_nb_view):") v = importlib.import_module("unsloth_nb_view") - check("clean_section dash/slash -> space", - v.clean_section("### GRPO-Reinforcement/Learning Notebooks") - == "GRPO Reinforcement Learning Notebooks", - v.clean_section("### GRPO-Reinforcement/Learning Notebooks")) - check("clean_section strips hashes/space", v.clean_section("## Main Notebooks ") == "Main Notebooks") + check( + "clean_section dash/slash -> space", + v.clean_section("### GRPO-Reinforcement/Learning Notebooks") + == "GRPO Reinforcement Learning Notebooks", + v.clean_section("### GRPO-Reinforcement/Learning Notebooks"), + ) + check( + "clean_section strips hashes/space", + v.clean_section("## Main Notebooks ") == "Main Notebooks", + ) # -------------------------------------------------------------------------- @@ -72,27 +82,52 @@ def test_strip() -> None: nb = { "metadata": {"widgets": {"application/vnd.jupyter.widget-state+json": {"x": 1}}}, "cells": [ - {"cell_type": "markdown", - "source": ['To run this, press "Runtime" ... Tesla T4 Google Colab instance!\n', - '\n', 'You will learn how to ...\n']}, - {"cell_type": "code", "source": ["print(1)\n"], "outputs": [ - {"output_type": "stream", "name": "stdout", "text": "ok\n"}, - {"output_type": "display_data", - "data": {"application/vnd.jupyter.widget-view+json": {"model_id": "abc"}, - "text/plain": "0%| | 0/10"}}, - ]}, + { + "cell_type": "markdown", + "source": [ + 'To run this, press "Runtime" ... Tesla T4 Google Colab instance!\n', + "\n", + "You will learn how to ...\n", + ], + }, + { + "cell_type": "code", + "source": ["print(1)\n"], + "outputs": [ + {"output_type": "stream", "name": "stdout", "text": "ok\n"}, + { + "output_type": "display_data", + "data": { + "application/vnd.jupyter.widget-view+json": {"model_id": "abc"}, + "text/plain": "0%| | 0/10", + }, + }, + ], + }, ], } changed1 = s._strip_intro(nb) changed2 = s._clean_widgets(nb) - check("intro line stripped", changed1 and not any( - "to run this, press" in (l.lower()) for l in nb["cells"][0]["source"])) + check( + "intro line stripped", + changed1 and not any("to run this, press" in (l.lower()) for l in nb["cells"][0]["source"]), + ) check("intro body kept", any("You will learn" in l for l in nb["cells"][0]["source"])) - wv = sum(1 for c in nb["cells"] for o in (c.get("outputs", []) or []) - if "application/vnd.jupyter.widget-view+json" in (o.get("data", {}) or {})) + wv = sum( + 1 + for c in nb["cells"] + for o in (c.get("outputs", []) or []) + if "application/vnd.jupyter.widget-view+json" in (o.get("data", {}) or {}) + ) check("widget-view outputs removed", changed2 and wv == 0) - check("non-widget outputs kept", any( - o.get("output_type") == "stream" for c in nb["cells"] for o in (c.get("outputs", []) or []))) + check( + "non-widget outputs kept", + any( + o.get("output_type") == "stream" + for c in nb["cells"] + for o in (c.get("outputs", []) or []) + ), + ) check("metadata.widgets removed", "widgets" not in nb["metadata"]) # idempotent check("strip idempotent", not s._strip_intro(nb) and not s._clean_widgets(nb)) @@ -126,21 +161,34 @@ def test_overrides() -> None: check("overrides.json exists", os.path.isfile(path)) if not os.path.isfile(path): return - with open(path, encoding="utf-8") as f: + with open(path, encoding = "utf-8") as f: d = json.load(f) # raises -> CI fails if invalid JSON themes = d.get("@jupyterlab/apputils-extension:themes", {}) - check("default theme = Unsloth Dark", themes.get("theme") == "Unsloth Dark", str(themes.get("theme"))) + check( + "default theme = Unsloth Dark", + themes.get("theme") == "Unsloth Dark", + str(themes.get("theme")), + ) check("adaptive theme on", themes.get("adaptive-theme") is True) check("preferred dark = Unsloth Dark", themes.get("preferred-dark-theme") == "Unsloth Dark") tracker = d.get("@jupyterlab/notebook-extension:tracker", {}) - check("windowingMode none", tracker.get("windowingMode") == "none", str(tracker.get("windowingMode"))) + check( + "windowingMode none", + tracker.get("windowingMode") == "none", + str(tracker.get("windowingMode")), + ) notif = d.get("@jupyterlab/apputils-extension:notification", {}) - check("news prompt off", str(notif.get("fetchNews")) == "false" and notif.get("checkForUpdates") is False) + check( + "news prompt off", + str(notif.get("fetchNews")) == "false" and notif.get("checkForUpdates") is False, + ) panel = d.get("@jupyterlab/notebook-extension:panel", {}) labels = [t.get("label", "") for t in panel.get("toolbar", [])] - check("Restart & Run All label (single >>)", - any(l == "Restart & Run All" for l in labels) and not any(">>" in l for l in labels), - str(labels)) + check( + "Restart & Run All label (single >>)", + any(l == "Restart & Run All" for l in labels) and not any(">>" in l for l in labels), + str(labels), + ) # -------------------------------------------------------------------------- @@ -151,7 +199,7 @@ def test_labext_and_branding() -> None: pkg = os.path.join(LABEXT, "package.json") check("labext package.json exists", os.path.isfile(pkg)) if os.path.isfile(pkg): - with open(pkg, encoding="utf-8") as f: + with open(pkg, encoding = "utf-8") as f: p = json.load(f) check("labext name unsloth-jupyterlab", p.get("name") == "unsloth-jupyterlab") check("labext themePath set", bool(p.get("jupyterlab", {}).get("themePath"))) @@ -162,15 +210,20 @@ def test_labext_and_branding() -> None: if os.path.isdir(src_dir): for fn in sorted(os.listdir(src_dir)): if fn.endswith(".ts"): - with open(os.path.join(src_dir, fn), encoding="utf-8") as f: + with open(os.path.join(src_dir, fn), encoding = "utf-8") as f: all_src += f.read() + "\n" - for plug in ["unsloth-jupyterlab:theme", "unsloth-jupyterlab:cell-nav", - "unsloth-jupyterlab:logo", "unsloth-jupyterlab:colab-title", - "unsloth-jupyterlab:output-select-all", "unsloth-jupyterlab:ui-chrome"]: + for plug in [ + "unsloth-jupyterlab:theme", + "unsloth-jupyterlab:cell-nav", + "unsloth-jupyterlab:logo", + "unsloth-jupyterlab:colab-title", + "unsloth-jupyterlab:output-select-all", + "unsloth-jupyterlab:ui-chrome", + ]: check(f"plugin present: {plug}", plug in all_src) # The two newest plugins are also exported from index.ts (wired in). index = os.path.join(src_dir, "index.ts") - index_src = open(index, encoding="utf-8").read() if os.path.isfile(index) else "" + index_src = open(index, encoding = "utf-8").read() if os.path.isfile(index) else "" check("outputSelect wired in index.ts", "outputSelectPlugin" in index_src) check("uiChrome wired in index.ts", "uiChromePlugin" in index_src) # uiChrome hides the right activity bar; CTRL+A output-select selects nodes. @@ -178,18 +231,30 @@ def test_labext_and_branding() -> None: check("ctrl+A output select", "selectNodeContents" in all_src) # branding assets login = os.path.join(JUPYTER, "login.html") - login_src = open(login, encoding="utf-8").read() if os.path.isfile(login) else "" + login_src = open(login, encoding = "utf-8").read() if os.path.isfile(login) else "" check("login.html branded", "unsloth-login-card" in login_src) - check("login.html uses sloth stickers", 'static_url("sloth/' in login_src or "static_url('sloth/" in login_src) + check( + "login.html uses sloth stickers", + 'static_url("sloth/' in login_src or "static_url('sloth/" in login_src, + ) check("favicon.ico present", os.path.isfile(os.path.join(JUPYTER, "favicon.ico"))) check("logo.png present", os.path.isfile(os.path.join(JUPYTER, "logo.png"))) - check("sloth sticker installer present", os.path.isfile(os.path.join(JUPYTER, "install_sloth_stickers.py"))) + check( + "sloth sticker installer present", + os.path.isfile(os.path.join(JUPYTER, "install_sloth_stickers.py")), + ) def main() -> int: print("=== Unsloth Studio/notebook feature validation ===") - for t in (test_colab_compat, test_nb_view, test_strip, test_sidecar_log_gate, - test_overrides, test_labext_and_branding): + for t in ( + test_colab_compat, + test_nb_view, + test_strip, + test_sidecar_log_gate, + test_overrides, + test_labext_and_branding, + ): try: t() except Exception as e: # a thrown exception is a failure, not a crash