diff --git a/docs/changelog/+skip-path-dir-probe.bugfix.rst b/docs/changelog/+skip-path-dir-probe.bugfix.rst new file mode 100644 index 0000000..23d36ee --- /dev/null +++ b/docs/changelog/+skip-path-dir-probe.bugfix.rst @@ -0,0 +1,2 @@ +:func:`~python_discovery.iter_interpreters` with no key no longer probes ``PATH`` directories or subdirectories named +like Python executables as interpreters. diff --git a/src/python_discovery/_discovery.py b/src/python_discovery/_discovery.py index 07d8684..c04f070 100644 --- a/src/python_discovery/_discovery.py +++ b/src/python_discovery/_discovery.py @@ -496,12 +496,13 @@ def path_exe_finder( direct = f"{direct}.exe" def path_exes(path: Path) -> Generator[tuple[Path, bool], None, None]: + # an empty spec (enumerating everything) would make the direct path the PATH directory itself direct_path = path / direct - if direct_path.exists(): + if direct_path.is_file(): yield direct_path, impl_must_match for exe in path.iterdir(): - if match := pat.fullmatch(exe.name): + if (match := pat.fullmatch(exe.name)) and exe.is_file(): yield exe.absolute(), impl_must_match or match["impl"].lower() == "python" return path_exes diff --git a/tests/test_iter_interpreters.py b/tests/test_iter_interpreters.py index f81b2ec..7fca0e8 100644 --- a/tests/test_iter_interpreters.py +++ b/tests/test_iter_interpreters.py @@ -144,6 +144,30 @@ def test_iter_interpreters_no_key_probes_variant_builds_on_path( assert str(exe) in [call.args[0] for call in from_exe.call_args_list] +@pytest.mark.usefixtures("uv_dir") +@pytest.mark.parametrize( + "directory_name", + [ + pytest.param(".", id="path-directory"), + pytest.param(f"python3.99{'.exe' if IS_WIN else ''}", id="named-directory"), + ], +) +def test_iter_interpreters_skips_path_directories( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, directory_name: str +) -> None: + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + (bin_dir / directory_name).mkdir(exist_ok=True) + executable = bin_dir / f"python3.98{'.exe' if IS_WIN else ''}" + executable.touch() + monkeypatch.setenv("PATH", str(bin_dir)) + from_exe = mocker.patch("python_discovery._discovery.PathPythonInfo.from_exe", return_value=None) + + list(iter_interpreters()) + + assert [call.args[0] for call in from_exe.call_args_list] == [str(executable)] + + @pytest.mark.usefixtures("uv_dir") def test_iter_interpreters_dedups_symlinks( tmp_path: Path,