diff --git a/core/tests/test_update.py b/core/tests/test_update.py index b2933df3..8bdf1ba4 100644 --- a/core/tests/test_update.py +++ b/core/tests/test_update.py @@ -261,3 +261,14 @@ def test_a_branch_switch_installs_what_is_missing_even_when_the_requirements_fil 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" + + +def test_a_spacy_model_that_does_not_load_is_downloaded_on_the_switch(repo, monkeypatch): + (repo / "requirements.txt").write_text("spacy>=3.8,<3.9\nen_core_web_sm @ https://example.com/m.whl\n") + monkeypatch.setattr(update, "_installed_version", lambda n: "3.8.0") + ran = [] + monkeypatch.setattr(update, "_spacy_model_loads", lambda: bool(ran) and ran[-1] == ["spacy", "download", "en_core_web_sm"]) + monkeypatch.setattr(update.subprocess, "run", lambda cmd, **k: ran.append(cmd[-3:]) or subprocess.CompletedProcess(cmd, 0, "", "")) + monkeypatch.setattr(update, "_pip_freeze", lambda: {}) + assert update.install_requirements()["ok"] + assert ran[-1] == ["spacy", "download", "en_core_web_sm"] diff --git a/core/update.py b/core/update.py index 4de6d5e2..e1a502b6 100644 --- a/core/update.py +++ b/core/update.py @@ -370,6 +370,17 @@ def _satisfies(have: str, spec: str) -> bool: return True +def _spacy_model_loads() -> bool: + """Whether spaCy can load its English model, checked in a fresh interpreter: this process may + already hold an older spaCy. False when spaCy is not installed at all.""" + try: + proc = subprocess.run([sys.executable, "-c", "import spacy; spacy.load('en_core_web_sm')"], + capture_output=True, timeout=180) + except (subprocess.TimeoutExpired, OSError): + return False + return proc.returncode == 0 + + def install_requirements(timeout: int = 900) -> dict: """Install ONLY the requirements that are absent. Never upgrades anything. @@ -399,7 +410,11 @@ def install_requirements(timeout: int = 900) -> dict: for n, h, sp in status["below_floor"]) + f"\n\nTo do it yourself:\n {sys.executable} -m pip install -r {req}") print(f"[FunPack update] {floor_note}") - if not status["missing"]: + # The spaCy model is a package the metadata can see even when it does not load (a half-finished + # install, a model built for another spaCy). Presence is judged by loading it, when spaCy is here. + model_broken = ("en_core_web_sm" in status["present"] and _installed_version("spacy") is not None + and not _spacy_model_loads()) + if not status["missing"] and not model_broken: return {"ran": False, "ok": True, "changed": [], "detail": ("Nothing to install — every requirement is already present." + (f"\n\n{floor_note}" if floor_note else ""))} @@ -412,7 +427,7 @@ def install_requirements(timeout: int = 900) -> dict: # with nothing under it is not installed, it is broken. The freeze diff says whether anything # already present moved as a result. failed, done = [], [] - for line in status["install"]: + for line in status.get("install", []): cmd = [sys.executable, "-m", "pip", "install", "--disable-pip-version-check", line] try: proc = subprocess.run(cmd, cwd=REPO_ROOT, capture_output=True, text=True, timeout=timeout) @@ -426,6 +441,17 @@ def install_requirements(timeout: int = 900) -> dict: failed.append((line, ((proc.stderr or proc.stdout or "").strip() or "pip failed")[-400:])) else: done.append(line) + if model_broken: + cmd = [sys.executable, "-m", "spacy", "download", "en_core_web_sm"] + try: + proc = subprocess.run(cmd, cwd=REPO_ROOT, capture_output=True, text=True, timeout=timeout) + if proc.returncode != 0 or not _spacy_model_loads(): + failed.append(("en_core_web_sm (spacy download)", + ((proc.stderr or proc.stdout or "").strip() or "the model still does not load")[-400:])) + else: + done.append("en_core_web_sm (spacy download)") + except (subprocess.TimeoutExpired, OSError) as e: + failed.append(("en_core_web_sm (spacy download)", str(e))) changed = _pip_diff(before, _pip_freeze()) if changed: print("[FunPack update] pip changed these packages: " + ", ".join(changed))