From 33cd5fac71d3841478056e9436ba86b824b3fa1c Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:51:50 +0300 Subject: [PATCH] fix: Update and Switch branch install whatever FunPack needs and is missing (spaCy + its model now listed) Co-Authored-By: Claude Opus 5.5 --- core/tests/test_update.py | 23 +++++++++++++++++++++++ core/update.py | 36 +++++++++--------------------------- requirements.txt | 5 +++++ 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/core/tests/test_update.py b/core/tests/test_update.py index e71ebddd..6a2d7b90 100644 --- a/core/tests/test_update.py +++ b/core/tests/test_update.py @@ -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" diff --git a/core/update.py b/core/update.py index 3ffa320b..d1b1fd0e 100644 --- a/core/update.py +++ b/core/update.py @@ -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) diff --git a/requirements.txt b/requirements.txt index c4cac9e6..84cbb7a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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