Skip to content

Commit 15ac745

Browse files
marcelsafinCopilot
andauthored
fix(integrations): skip Windows Store python3 alias stub in resolve_python_interpreter (#3385)
* fix(integrations): skip Windows Store python3 alias stub in resolve_python_interpreter On stock Windows, python3 on PATH is the Microsoft Store App Execution Alias stub: it exists but only prints an installer hint and exits non-zero, so generated {SCRIPT} invocations for the py script type were broken. Verify the found interpreter actually runs before accepting it, on Windows only, mirroring the parse-success-not-availability approach of #3312/#3320 for the sh scripts. POSIX keeps the plain existence check. sys.executable remains the fallback and is always live. Fixes #3383 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(integrations): probe interpreter isolated and without site, discard I/O Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: pin POSIX platform in PATH-resolution tests The tests fake shutil.which with POSIX paths; on Windows CI the real sys.platform made the stub probe run against those fake paths and fall through to sys.executable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8e2e2d2 commit 15ac745

2 files changed

Lines changed: 107 additions & 3 deletions

File tree

src/specify_cli/integrations/base.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import re
1818
import shlex
1919
import shutil
20+
import subprocess
2021
import sys
2122
from abc import ABC
2223
from dataclasses import dataclass
@@ -604,10 +605,42 @@ def resolve_python_interpreter(project_root: Path | None = None) -> str:
604605
if candidate.exists():
605606
return relative
606607
for name in ("python3", "python"):
607-
if shutil.which(name):
608-
return name
608+
found = shutil.which(name)
609+
if not found:
610+
continue
611+
# On Windows, python3/python on PATH may be the Microsoft
612+
# Store App Execution Alias stub: it exists but only prints
613+
# an installer hint and exits non-zero, so existence is not
614+
# enough (see #3304 for the same defect in the sh scripts).
615+
if sys.platform == "win32" and not IntegrationBase._interpreter_runs(
616+
found
617+
):
618+
continue
619+
return name
609620
return sys.executable or "python3"
610621

622+
@staticmethod
623+
def _interpreter_runs(path: str) -> bool:
624+
"""Return True when *path* executes as a Python interpreter.
625+
626+
Runs isolated (``-I``) without ``site`` (``-S``) and discards
627+
I/O so the probe is a fast liveness check that cannot trigger
628+
``sitecustomize``/user startup hooks.
629+
"""
630+
try:
631+
return (
632+
subprocess.run(
633+
[path, "-I", "-S", "-c", ""],
634+
stdin=subprocess.DEVNULL,
635+
stdout=subprocess.DEVNULL,
636+
stderr=subprocess.DEVNULL,
637+
timeout=15,
638+
).returncode
639+
== 0
640+
)
641+
except (OSError, subprocess.SubprocessError):
642+
return False
643+
611644
@staticmethod
612645
def process_template(
613646
content: str,
@@ -1089,7 +1122,6 @@ def setup(
10891122
# YamlIntegration — YAML-format agents (Goose)
10901123
# ---------------------------------------------------------------------------
10911124

1092-
10931125
class YamlIntegration(IntegrationBase):
10941126
"""Concrete base for integrations that use YAML recipe format.
10951127

tests/integrations/test_base.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,12 @@ def test_placeholder_with_digits(self):
306306
class TestResolvePythonInterpreter:
307307
def test_returns_python_on_path(self, monkeypatch):
308308
# Positive: when python3 is on PATH it is preferred over python.
309+
# Pin a POSIX platform so the Windows stub probe (tested separately
310+
# below) does not reject the fake PATH entries on Windows CI.
309311
def fake_which(name):
310312
return f"/usr/bin/{name}" if name in ("python3", "python") else None
311313

314+
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
312315
monkeypatch.setattr(
313316
"specify_cli.integrations.base.shutil.which", fake_which
314317
)
@@ -318,6 +321,7 @@ def test_falls_back_to_python_when_no_python3(self, monkeypatch):
318321
def fake_which(name):
319322
return "/usr/bin/python" if name == "python" else None
320323

324+
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
321325
monkeypatch.setattr(
322326
"specify_cli.integrations.base.shutil.which", fake_which
323327
)
@@ -369,12 +373,79 @@ def test_prefers_project_venv_windows(self, monkeypatch, tmp_path):
369373

370374
def test_ignores_missing_venv(self, monkeypatch, tmp_path):
371375
# Negative: no venv directory -> PATH resolution is used instead.
376+
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
372377
monkeypatch.setattr(
373378
"specify_cli.integrations.base.shutil.which",
374379
lambda name: "/usr/bin/python3" if name == "python3" else None,
375380
)
376381
assert IntegrationBase.resolve_python_interpreter(tmp_path) == "python3"
377382

383+
def test_windows_skips_store_alias_stub(self, monkeypatch):
384+
# On Windows, python3 on PATH may be the Microsoft Store App
385+
# Execution Alias stub: it exists but only prints an installer
386+
# hint and exits non-zero. Existence is not enough; the
387+
# interpreter must actually run (mirrors #3304 for the CLI).
388+
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "win32")
389+
monkeypatch.setattr(
390+
"specify_cli.integrations.base.shutil.which",
391+
lambda name: f"C:\\WindowsApps\\{name}.exe"
392+
if name in ("python3", "python")
393+
else None,
394+
)
395+
monkeypatch.setattr(
396+
IntegrationBase, "_interpreter_runs", staticmethod(lambda path: False)
397+
)
398+
monkeypatch.setattr(
399+
"specify_cli.integrations.base.sys.executable", "C:\\Python\\python.exe"
400+
)
401+
result = IntegrationBase.resolve_python_interpreter()
402+
assert result == "C:\\Python\\python.exe"
403+
404+
def test_windows_keeps_working_interpreter(self, monkeypatch):
405+
# Positive: a real python3 on Windows PATH passes the run check.
406+
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "win32")
407+
monkeypatch.setattr(
408+
"specify_cli.integrations.base.shutil.which",
409+
lambda name: f"C:\\Python\\{name}.exe" if name == "python3" else None,
410+
)
411+
monkeypatch.setattr(
412+
IntegrationBase, "_interpreter_runs", staticmethod(lambda path: True)
413+
)
414+
assert IntegrationBase.resolve_python_interpreter() == "python3"
415+
416+
def test_windows_stub_python3_falls_through_to_working_python(self, monkeypatch):
417+
# python3 is the stub but python is a real install: pick python.
418+
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "win32")
419+
monkeypatch.setattr(
420+
"specify_cli.integrations.base.shutil.which",
421+
lambda name: f"C:\\somewhere\\{name}.exe"
422+
if name in ("python3", "python")
423+
else None,
424+
)
425+
monkeypatch.setattr(
426+
IntegrationBase,
427+
"_interpreter_runs",
428+
staticmethod(lambda path: path.endswith("python.exe")),
429+
)
430+
assert IntegrationBase.resolve_python_interpreter() == "python"
431+
432+
def test_posix_does_not_spawn_run_check(self, monkeypatch):
433+
# Non-Windows platforms have no App Execution Alias; existence
434+
# on PATH stays sufficient and no subprocess is spawned.
435+
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
436+
monkeypatch.setattr(
437+
"specify_cli.integrations.base.shutil.which",
438+
lambda name: "/usr/bin/python3" if name == "python3" else None,
439+
)
440+
441+
def boom(path):
442+
raise AssertionError("run check must not execute on POSIX")
443+
444+
monkeypatch.setattr(
445+
IntegrationBase, "_interpreter_runs", staticmethod(boom)
446+
)
447+
assert IntegrationBase.resolve_python_interpreter() == "python3"
448+
378449

379450
class TestProcessTemplatePyScriptType:
380451
CONTENT = (
@@ -390,6 +461,7 @@ class TestProcessTemplatePyScriptType:
390461
def test_py_prefixes_interpreter(self, monkeypatch):
391462
# Positive: py script type prefixes a resolved interpreter and the
392463
# script path is rewritten to the .specify location.
464+
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
393465
monkeypatch.setattr(
394466
"specify_cli.integrations.base.shutil.which",
395467
lambda name: "/usr/bin/python3" if name == "python3" else None,

0 commit comments

Comments
 (0)