Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/158.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Accept relative interpreter paths that step up a directory, such as ``../venv/bin/python``, which were found and
queried but never matched the spec.
3 changes: 2 additions & 1 deletion src/python_discovery/_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ def _check_exe(path: str, tested_exes: set[str]) -> str | None:
if exe_id in tested_exes:
return None
tested_exes.add(exe_id)
return str(Path(path).absolute())
# unlike Path.absolute(), abspath collapses "..", which PythonInfo._satisfies_path relies on
return os.path.abspath(path)


def _is_new_exe(exe_raw: str, tested_exes: set[str]) -> bool:
Expand Down
19 changes: 19 additions & 0 deletions tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ def test_relative_path(session_cache: DiskCache, monkeypatch: pytest.MonkeyPatch
assert result is not None


@pytest.mark.parametrize(
"sep",
[
pytest.param("/", id="slash"),
pytest.param("\\", id="backslash", marks=pytest.mark.skipif(not IS_WIN, reason="Windows separator")),
],
)
def test_relative_path_with_parent_reference(
session_cache: DiskCache, monkeypatch: pytest.MonkeyPatch, sep: str
) -> None:
sys_executable = Path(PythonInfo.current_system(session_cache).system_executable)
monkeypatch.chdir(str(sys_executable.parent))
relative = sep.join((os.pardir, sys_executable.parent.name, sys_executable.name))
result = get_interpreter(relative, [], session_cache)
assert result is not None
assert os.pardir not in Path(result.executable).parts
assert Path(result.executable).samefile(sys_executable)


def test_discovery_fallback_fail(session_cache: DiskCache, caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.DEBUG)
result = get_interpreter(["magic-one", "magic-two"], cache=session_cache)
Expand Down
14 changes: 14 additions & 0 deletions tests/test_iter_interpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ def test_iter_interpreters_with_abs_path_yields_one(session_cache: DiskCache) ->
assert results[0].executable == sys.executable


@pytest.mark.usefixtures("uv_dir")
def test_iter_interpreters_relative_path_with_parent_reference(
session_cache: DiskCache, monkeypatch: pytest.MonkeyPatch
) -> None:
sys_executable = Path(PythonInfo.current_system(session_cache).system_executable)
monkeypatch.chdir(str(sys_executable.parent))
monkeypatch.setenv("PATH", "")
relative = str(Path(os.pardir) / sys_executable.parent.name / sys_executable.name)
results = list(iter_interpreters(relative, cache=session_cache))
assert len(results) == 1
assert os.pardir not in Path(results[0].executable).parts
assert Path(results[0].executable).samefile(sys_executable)


def test_iter_interpreters_sequence_dedups_across_keys(session_cache: DiskCache) -> None:
results = list(iter_interpreters([sys.executable, sys.executable], cache=session_cache))
assert len(results) == 1
Expand Down