diff --git a/docs/changelog/188.bugfix.rst b/docs/changelog/188.bugfix.rst new file mode 100644 index 0000000..66de408 --- /dev/null +++ b/docs/changelog/188.bugfix.rst @@ -0,0 +1 @@ +Match literal interpreter names against the executable found on ``PATH``, including symlink names. diff --git a/src/python_discovery/_py_info.py b/src/python_discovery/_py_info.py index 09733de..fe8189f 100644 --- a/src/python_discovery/_py_info.py +++ b/src/python_discovery/_py_info.py @@ -283,7 +283,7 @@ def _satisfies_path(self, spec: PythonSpec) -> bool: return True if spec.is_abs: return True - basename = os.path.basename(self.original_executable) + basename = os.path.basename(self.executable) spec_path = spec.path if sys.platform == "win32": basename, suffix = os.path.splitext(basename) diff --git a/tests/py_info/test_py_info.py b/tests/py_info/test_py_info.py index b078617..9b0b802 100644 --- a/tests/py_info/test_py_info.py +++ b/tests/py_info/test_py_info.py @@ -304,9 +304,9 @@ def test_py_info_ignores_distutils_config(monkeypatch: pytest.MonkeyPatch, tmp_p def test_discover_exe_on_path_non_spec_name_match(mocker: MockerFixture) -> None: suffixed_name = f"python{CURRENT.version_info.major}.{CURRENT.version_info.minor}m" if sys.platform == "win32": # pragma: win32 cover - suffixed_name += Path(CURRENT.original_executable).suffix + suffixed_name += Path(CURRENT.executable).suffix spec = PythonSpec.from_string_spec(suffixed_name) - mocker.patch.object(CURRENT, "original_executable", str(Path(CURRENT.executable).parent / suffixed_name)) + mocker.patch.object(CURRENT, "executable", str(Path(CURRENT.executable).parent / suffixed_name)) assert CURRENT.satisfies(spec, impl_must_match=True) is True diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 2440a03..7d48ac3 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -74,6 +74,17 @@ def test_discovery_via_path_not_found(tmp_path: Path, monkeypatch: pytest.Monkey assert interpreter is None +@pytest.mark.skipif(sys.platform == "win32", reason="POSIX symlink names") +def test_discovery_via_path_literal_symlink(tmp_path: Path) -> None: + link = tmp_path / "python_custom" + link.symlink_to(sys.executable) + + interpreter = get_interpreter("python_custom", [], env={"PATH": str(tmp_path)}) + + assert interpreter is not None + assert interpreter.executable == str(link) + + def test_discovery_via_path_in_nonbrowseable_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: bad_perm = tmp_path / "bad_perm" bad_perm.mkdir(mode=0o000) diff --git a/tests/test_py_info_extra.py b/tests/test_py_info_extra.py index 016636a..6e357b4 100644 --- a/tests/test_py_info_extra.py +++ b/tests/test_py_info_extra.py @@ -277,7 +277,7 @@ def test_satisfies_path_not_abs_basename_no_match() -> None: def test_satisfies_path_win32(mocker: MockerFixture) -> None: info = copy.deepcopy(CURRENT) mocker.patch.object(sys, "platform", "win32") - info.original_executable = "/some/path/python.exe" + info.executable = "/some/path/python.exe" spec = PythonSpec.from_string_spec("python") spec.path = "python" assert info.satisfies(spec, impl_must_match=False) is True diff --git a/tests/windows/test_windows.py b/tests/windows/test_windows.py index a61aedd..65c0789 100644 --- a/tests/windows/test_windows.py +++ b/tests/windows/test_windows.py @@ -1,10 +1,22 @@ from __future__ import annotations import sys +from pathlib import Path import pytest -from python_discovery import PythonSpec +from python_discovery import PythonSpec, get_interpreter + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows executable alias") +def test_discovery_via_path_literal_symlink(tmp_path: Path) -> None: + link = tmp_path / "python_custom.exe" + link.symlink_to(Path(sys.base_prefix) / "python.exe") + + interpreter = get_interpreter("python_custom", [], env={"PATH": f"{tmp_path};{sys.base_prefix}"}) + + assert interpreter is not None + assert interpreter.executable == str(link) @pytest.mark.skipif(sys.platform != "win32", reason="propose_interpreters calls from_exe with Windows paths")