fix: Update and Switch branch install whatever FunPack needs and is missing (spaCy + its model now listed)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-07 13:51:50 +03:00
co-authored by Claude Opus 5.5
parent 429445126e
commit 33cd5fac71
3 changed files with 37 additions and 27 deletions
+23
View File
@@ -221,3 +221,26 @@ def test_a_stash_is_disclosed_even_when_the_failure_is_not_a_gitupdateerror(repo
update.rollback()
assert "FunPack: auto-stashed" in _git(repo, "stash", "list").stdout
def test_a_missing_requirement_is_installed_by_its_whole_line_so_a_pin_or_url_survives(repo, monkeypatch):
(repo / "requirements.txt").write_text(
"# comment\nnumpy\nzz_absent_pkg>=3.8,<3.9 # why\nzz_model @ https://example.com/zz_model.whl\n")
monkeypatch.setattr(update, "_installed_version", lambda n: "1.0" if n == "numpy" else None)
ran = []
monkeypatch.setattr(update.subprocess, "run", lambda cmd, **k: ran.append(cmd) or subprocess.CompletedProcess(cmd, 0, "", ""))
monkeypatch.setattr(update, "_pip_freeze", lambda: {})
assert update.install_requirements()["ok"]
assert ran[0][-2:] == ["zz_absent_pkg>=3.8,<3.9", "zz_model @ https://example.com/zz_model.whl"]
def test_a_branch_switch_installs_what_is_missing_even_when_the_requirements_file_did_not_change(repo, monkeypatch):
_git(repo, "checkout", "-q", "-b", "other")
(repo / "b.txt").write_text("x\n")
_git(repo, "add", "b.txt")
_git(repo, "commit", "-q", "-m", "other")
_git(repo, "checkout", "-q", "main")
called = []
monkeypatch.setattr(update, "install_requirements", lambda: called.append(1) or {"ok": True})
update.checkout("other", pull_after=False, install_deps=True)
assert called, "the switch itself must check for missing packages"
+9 -27
View File
@@ -297,23 +297,6 @@ def status(*, remote: bool = True) -> dict:
REQUIREMENTS = "requirements.txt"
def requirements_changed(before: str, after: str) -> bool:
"""Did this update touch requirements.txt?
The only honest trigger for installing: running pip on every pull would be slow and
surprising, and running it on none leaves an update that added a dependency looking
like a broken build instead of an unfinished install.
"""
if not before or not after or before == after:
return False
proc = _run_git("diff", "--name-only", f"{before}..{after}", "--", REQUIREMENTS)
if proc.returncode != 0:
# Cannot tell — install rather than skip. A redundant pip run costs seconds; a
# skipped one costs a broken node pack and a confusing error.
return True
return bool((proc.stdout or "").strip())
def parse_requirement(line: str):
"""(name, specifier) for one requirements line, or None for a blank/comment.
@@ -367,6 +350,8 @@ def requirement_status() -> dict:
have = _installed_version(name)
if have is None:
out["missing"].append(name)
# what pip is given: the line itself, so a version pin or a URL (spaCy's model is not on PyPI) survives
out.setdefault("install", []).append(line.split("#", 1)[0].split(";", 1)[0].strip())
continue
out["present"].append(name)
if spec and not _satisfies(have, spec):
@@ -425,14 +410,14 @@ def install_requirements(timeout: int = 900) -> dict:
# nothing under it is not installed, it is broken. The freeze diff below is what says
# whether anything already present moved as a result.
cmd = [sys.executable, "-m", "pip", "install", "--disable-pip-version-check",
*status["missing"]]
*status["install"]]
try:
proc = subprocess.run(cmd, cwd=REPO_ROOT, capture_output=True, text=True,
timeout=timeout)
except subprocess.TimeoutExpired:
return {"ran": True, "ok": False,
"detail": f"pip did not finish within {timeout}s — run it yourself: "
f"pip install {' '.join(status['missing'])}"}
f"pip install {' '.join(status['install'])}"}
except OSError as e:
return {"ran": True, "ok": False, "detail": f"could not run pip: {e}"}
if proc.returncode != 0:
@@ -440,7 +425,7 @@ def install_requirements(timeout: int = 900) -> dict:
return {"ran": True, "ok": False,
"detail": f"pip install failed — run it yourself:\n"
f" {sys.executable} -m pip install "
f"{' '.join(status['missing'])}\n\n{tail}"}
f"{' '.join(status['install'])}\n\n{tail}"}
changed = _pip_diff(before, _pip_freeze())
if changed:
print("[FunPack update] pip changed these packages: " + ", ".join(changed))
@@ -530,8 +515,8 @@ def pull(branch: str | None = None, *, install_deps: bool = False) -> dict:
# Dependencies are part of the update. Done BEFORE the response, so the restart the
# caller schedules cannot race an install that is still running.
deps = None
if install_deps and before != after and requirements_changed(before, after):
deps = install_requirements()
if install_deps:
deps = install_requirements() # a no-op unless something is missing; a branch switch's own requirements are not in this pull's diff
return {
"branch": branch,
"before": before,
@@ -587,11 +572,8 @@ def checkout(branch: str, *, pull_after: bool = True, install_deps: bool = False
else:
result["after"] = _current_commit()
result["updated"] = result["before"] != result["after"]
# A branch switch alone can cross a requirements change just as a pull can — the
# checkout above already moved the working tree onto the other branch's files.
if install_deps and result["updated"] and requirements_changed(before_commit,
result["after"]):
result["requirements"] = install_requirements()
if install_deps:
result["requirements"] = install_requirements() # whatever the branch needs and this environment lacks
return result
result = _disclosing(stashed, _do)
+5
View File
@@ -5,3 +5,8 @@
# not pull CUDA or any model. Without it a .gguf needs the ComfyUI-GGUF node
# pack instead.
gguf
# Shot camera picks the word a camera move aims at. Without it, simple word rules are used.
# Pinned to the model's own version: a newer spaCy will not load a 3.8 model.
spacy>=3.8,<3.9
en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl