diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 834db8fe8fa..95a669d0bea 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -3,6 +3,7 @@ import functools import os +from pathlib import Path from cuda.pathfinder._binaries import supported_nvidia_binaries from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES @@ -28,22 +29,23 @@ def _normalize_utility_name(utility_name: str) -> str: return utility_name -def _is_executable_candidate(path: str) -> bool: - if not os.path.isfile(path): +def _is_executable_candidate(path: Path) -> bool: + if not path.is_file(): return False if IS_WINDOWS: return True + # pathlib has no access() equivalent. return os.access(path, os.X_OK) -def _ctk_bin_subdirs(root: str) -> list[str]: +def _ctk_bin_subdirs(root: Path) -> list[Path]: if IS_WINDOWS: return [ - os.path.join(root, "bin", "x64"), - os.path.join(root, "bin", "x86_64"), - os.path.join(root, "bin"), + root / "bin" / "x64", + root / "bin" / "x86_64", + root / "bin", ] - return [os.path.join(root, "bin")] + return [root / "bin"] def _resolve_ctk_root_via_canary() -> str | None: @@ -53,19 +55,22 @@ def _resolve_ctk_root_via_canary() -> str | None: return ctk_root -def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[str]) -> str | None: +def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[Path]) -> Path | None: """Resolve ``normalized_name`` against ``dirs`` in order.""" - seen: set[str] = set() + seen: set[Path] = set() for directory in dirs: if directory in seen: continue - assert directory + # Path("") is Path("."), which would silently search the CWD (#2119). + assert directory != Path() seen.add(directory) - candidate = os.path.join(directory, normalized_name) + candidate = directory / normalized_name if _is_executable_candidate(candidate): # Return an absolute path, as the docstring promises (a relative - # search dir would otherwise leak a relative result). - return os.path.abspath(candidate) + # search dir would otherwise leak a relative result). os.path.abspath + # has no pathlib equivalent: Path.absolute() does not normalize and + # Path.resolve() would also follow symlinks. + return Path(os.path.abspath(candidate)) return None @@ -134,29 +139,28 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: # 1. Search in site-packages (NVIDIA wheels) candidate_dirs = supported_nvidia_binaries.SITE_PACKAGES_BINDIRS.get(utility_name, ()) - dirs = [] + dirs: list[Path] = [] for sub_dir in candidate_dirs: - dirs.extend(find_sub_dirs_all_sitepackages(sub_dir.split(os.sep))) + dirs.extend(Path(abs_dir) for abs_dir in find_sub_dirs_all_sitepackages(sub_dir)) # 2. Search in Conda environment if (conda_prefix := os.environ.get("CONDA_PREFIX")) is not None: - if IS_WINDOWS: - dirs.append(os.path.join(conda_prefix, "Library", "bin")) - else: - dirs.append(os.path.join(conda_prefix, "bin")) + conda_root = Path(conda_prefix) + dirs.append(conda_root / "Library" / "bin" if IS_WINDOWS else conda_root / "bin") # 3. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH) if (cuda_home := get_cuda_path_or_home()) is not None: - dirs.extend(_ctk_bin_subdirs(cuda_home)) + dirs.extend(_ctk_bin_subdirs(Path(cuda_home))) normalized_name = _normalize_utility_name(utility_name) found = _resolve_in_trusted_dirs(normalized_name, dirs) if found is not None: - return found + return str(found) # 4. CTK-root canary fallback. ctk_root = _resolve_ctk_root_via_canary() if ctk_root is not None: - return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(ctk_root)) + found = _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(Path(ctk_root))) + return None if found is None else str(found) return None diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py b/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py index ac70378f112..19b79fd45f3 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py @@ -1,13 +1,13 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -import os # Site-packages bin directories where binaries might be found -# Based on NVIDIA wheel layouts (same for Linux and Windows) -_CUDA_NVCC_BIN = os.path.join("nvidia", "cuda_nvcc", "bin") -_CUDA13_BIN = os.path.join("nvidia", "cu13", "bin") -_NSIGHT_SYSTEMS_BIN = os.path.join("nvidia", "nsight_systems", "bin") -_NSIGHT_COMPUTE_BIN = os.path.join("nvidia", "nsight_compute", "bin") +# Based on NVIDIA wheel layouts (same for Linux and Windows). +# Path components, because that is what find_sub_dirs_all_sitepackages takes. +_CUDA_NVCC_BIN = ("nvidia", "cuda_nvcc", "bin") +_CUDA13_BIN = ("nvidia", "cu13", "bin") +_NSIGHT_SYSTEMS_BIN = ("nvidia", "nsight_systems", "bin") +_NSIGHT_COMPUTE_BIN = ("nvidia", "nsight_compute", "bin") # Common CUDA binary utilities available on both Linux and Windows SITE_PACKAGES_BINDIRS = { diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index 12198ac9f7f..e23fd35c610 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -22,6 +22,7 @@ import functools import os import warnings +from pathlib import Path _CUDA_PATH_ENV_VARS_ORDERED = ("CUDA_PATH", "CUDA_HOME") @@ -36,15 +37,19 @@ def _paths_differ(a: str, b: str) -> bool: 2) If still different AND both exist, use os.path.samefile to resolve symlinks/junctions. 3) Otherwise (nonexistent paths or samefile unavailable), treat as different. """ + # normcase/normpath have no pathlib equivalent: PurePath does not collapse + # "..", Path.resolve() would also follow symlinks, and comparing PurePath + # objects would only case-fold on Windows. norm_a = os.path.normcase(os.path.normpath(a)) norm_b = os.path.normcase(os.path.normpath(b)) if norm_a == norm_b: return False + path_a, path_b = Path(a), Path(b) try: - if os.path.exists(a) and os.path.exists(b): + if path_a.exists() and path_b.exists(): # samefile raises on non-existent paths; only call when both exist. - return not os.path.samefile(a, b) + return not path_a.samefile(path_b) except OSError: # Fall through to "different" if samefile isn't applicable/available. pass diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py index ebb7b13f488..f4154f089e8 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py @@ -2,36 +2,51 @@ # SPDX-License-Identifier: Apache-2.0 import functools -import os import site import sys from collections.abc import Sequence +from pathlib import Path + + +def _is_dir(path: Path) -> bool: + """``path.is_dir()``, but False instead of raising on an inaccessible path. + + This walks directories nobody here controls, so it has to tolerate whatever + it runs into. Path.is_dir() only swallows the errnos in pathlib's ignore + list, and raises for the rest (EACCES, ENAMETOOLONG); os.path.isdir, which + this replaces, returned False for all of them. + """ + try: + return path.is_dir() + except OSError: + return False def find_sub_dirs_no_cache(parent_dirs: Sequence[str], sub_dirs: Sequence[str]) -> list[str]: + # Results stay str: they are consumed by _binaries, _dynamic_libs, _headers + # and _static_libs, so the type flip belongs in its own change. results = [] for base in parent_dirs: - stack = [(base, 0)] # (current_path, index into sub_dirs) + stack = [(Path(base), 0)] # (current_path, index into sub_dirs) while stack: current_path, idx = stack.pop() if idx == len(sub_dirs): - if os.path.isdir(current_path): - results.append(current_path) + if _is_dir(current_path): + results.append(str(current_path)) continue sub = sub_dirs[idx] if sub == "*": try: - entries = sorted(os.listdir(current_path)) + entries = sorted(current_path.iterdir(), key=lambda entry: entry.name) except OSError: continue - for entry in entries: - entry_path = os.path.join(current_path, entry) - if os.path.isdir(entry_path): + for entry_path in entries: + if _is_dir(entry_path): stack.append((entry_path, idx + 1)) else: - next_path = os.path.join(current_path, sub) - if os.path.isdir(next_path): + next_path = current_path / sub + if _is_dir(next_path): stack.append((next_path, idx + 1)) return results diff --git a/cuda_pathfinder/tests/test_find_nvidia_binaries.py b/cuda_pathfinder/tests/test_find_nvidia_binaries.py index 2784633ff38..89c43462dd9 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_binaries.py +++ b/cuda_pathfinder/tests/test_find_nvidia_binaries.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import os +from pathlib import Path import pytest @@ -25,7 +26,7 @@ def test_find_binary_utilities(info_summary_append, utility_name): bin_path = find_nvidia_binary_utility(utility_name) info_summary_append(f"{bin_path=!r}") - assert bin_path is None or os.path.isfile(bin_path) + assert bin_path is None or Path(bin_path).is_file() def test_supported_binaries_consistency(): @@ -48,7 +49,7 @@ def _patch_exec_probe(mocker, existing=()): candidates so tests can assert the deterministic search order. """ existing = set(existing) - checked: list[str] = [] + checked: list[Path] = [] def fake_is_executable_candidate(path): checked.append(path) @@ -60,10 +61,10 @@ def fake_is_executable_candidate(path): @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_search_path_includes_site_packages_conda_cuda(monkeypatch, mocker): - conda_prefix = os.path.join(os.sep, "conda") - cuda_home = os.path.join(os.sep, "cuda") - site_key = os.path.join("nvidia", "cuda_nvcc", "bin") - site_dir = os.path.join("site-packages", "cuda_nvcc", "bin") + conda_prefix = Path(os.sep, "conda") + cuda_home = Path(os.sep, "cuda") + site_key = ("nvidia", "cuda_nvcc", "bin") + site_dir = Path("site-packages", "cuda_nvcc", "bin") mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object( @@ -72,15 +73,15 @@ def test_find_binary_search_path_includes_site_packages_conda_cuda(monkeypatch, {"nvcc": (site_key,)}, ) find_sub_dirs_mock = mocker.patch.object( - binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir] + binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[str(site_dir)] ) - monkeypatch.setenv("CONDA_PREFIX", conda_prefix) - mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) + monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=str(cuda_home)) mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) expected_dirs = [ site_dir, - os.path.join(conda_prefix, "bin"), - os.path.join(cuda_home, "bin"), + conda_prefix / "bin", + cuda_home / "bin", ] checked = _patch_exec_probe(mocker) @@ -88,16 +89,16 @@ def test_find_binary_search_path_includes_site_packages_conda_cuda(monkeypatch, # No directory contains the binary, so every trusted dir is probed in order. assert result is None - find_sub_dirs_mock.assert_called_once_with(site_key.split(os.sep)) - assert checked == [os.path.join(d, "nvcc") for d in expected_dirs] + find_sub_dirs_mock.assert_called_once_with(site_key) + assert checked == [d / "nvcc" for d in expected_dirs] @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_windows_extension_and_search_dirs(monkeypatch, mocker): - conda_prefix = os.path.join(os.sep, "conda") - cuda_home = os.path.join(os.sep, "cuda") - site_key = os.path.join("nvidia", "cuda_nvcc", "bin") - site_dir = os.path.join("site-packages", "cuda_nvcc", "bin") + conda_prefix = Path(os.sep, "conda") + cuda_home = Path(os.sep, "cuda") + site_key = ("nvidia", "cuda_nvcc", "bin") + site_dir = Path("site-packages", "cuda_nvcc", "bin") mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) mocker.patch.object( @@ -106,17 +107,17 @@ def test_find_binary_windows_extension_and_search_dirs(monkeypatch, mocker): {"nvcc": (site_key,)}, ) find_sub_dirs_mock = mocker.patch.object( - binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir] + binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[str(site_dir)] ) - monkeypatch.setenv("CONDA_PREFIX", conda_prefix) - mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) + monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=str(cuda_home)) mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) expected_dirs = [ site_dir, - os.path.join(conda_prefix, "Library", "bin"), - os.path.join(cuda_home, "bin", "x64"), - os.path.join(cuda_home, "bin", "x86_64"), - os.path.join(cuda_home, "bin"), + conda_prefix / "Library" / "bin", + cuda_home / "bin" / "x64", + cuda_home / "bin" / "x86_64", + cuda_home / "bin", ] checked = _patch_exec_probe(mocker) @@ -124,16 +125,16 @@ def test_find_binary_windows_extension_and_search_dirs(monkeypatch, mocker): # The .exe extension is appended and the Windows-specific dirs are probed in order. assert result is None - find_sub_dirs_mock.assert_called_once_with(site_key.split(os.sep)) - assert checked == [os.path.join(d, "nvcc.exe") for d in expected_dirs] + find_sub_dirs_mock.assert_called_once_with(site_key) + assert checked == [d / "nvcc.exe" for d in expected_dirs] @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_first_matching_dir_wins(monkeypatch, mocker): - conda_prefix = os.path.join(os.sep, "conda") - cuda_home = os.path.join(os.sep, "cuda") - site_key = os.path.join("nvidia", "cuda_nvcc", "bin") - site_dir = os.path.join("site-packages", "cuda_nvcc", "bin") + conda_prefix = Path(os.sep, "conda") + cuda_home = Path(os.sep, "cuda") + site_key = ("nvidia", "cuda_nvcc", "bin") + site_dir = Path("site-packages", "cuda_nvcc", "bin") mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object( @@ -141,34 +142,34 @@ def test_find_binary_first_matching_dir_wins(monkeypatch, mocker): "SITE_PACKAGES_BINDIRS", {"nvcc": (site_key,)}, ) - mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) - monkeypatch.setenv("CONDA_PREFIX", conda_prefix) - mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[str(site_dir)]) + monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=str(cuda_home)) mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) - conda_nvcc = os.path.join(conda_prefix, "bin", "nvcc") - cuda_nvcc = os.path.join(cuda_home, "bin", "nvcc") + conda_nvcc = conda_prefix / "bin" / "nvcc" + cuda_nvcc = cuda_home / "bin" / "nvcc" checked = _patch_exec_probe(mocker, existing=[conda_nvcc, cuda_nvcc]) result = find_nvidia_binary_utility("nvcc") # Conda comes before CUDA_HOME, so the Conda hit wins and CUDA_HOME is never probed. assert result == os.path.abspath(conda_nvcc) - assert checked == [os.path.join(site_dir, "nvcc"), conda_nvcc] + assert checked == [site_dir / "nvcc", conda_nvcc] @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_ctk_root_canary_fallback(monkeypatch, mocker): # When the explicit trusted dirs (wheels, conda, CUDA_HOME/PATH) all miss, # the cudart-canary-derived CTK root is searched last. - ctk_root = os.path.join(os.sep, "opt", "cuda") + ctk_root = Path(os.sep, "opt", "cuda") mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) monkeypatch.delenv("CONDA_PREFIX", raising=False) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) - canary_mock = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=ctk_root) - ctk_nvcc = os.path.join(ctk_root, "bin", "nvcc") + canary_mock = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=str(ctk_root)) + ctk_nvcc = ctk_root / "bin" / "nvcc" checked = _patch_exec_probe(mocker, existing=[ctk_nvcc]) result = find_nvidia_binary_utility("nvcc") @@ -181,39 +182,39 @@ def test_find_binary_ctk_root_canary_fallback(monkeypatch, mocker): @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_canary_windows_bin_layout(monkeypatch, mocker): - ctk_root = os.path.join("C:", os.sep, "cuda") + ctk_root = Path("C:", os.sep, "cuda") mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) monkeypatch.delenv("CONDA_PREFIX", raising=False) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) - mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=ctk_root) + mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=str(ctk_root)) expected_dirs = [ - os.path.join(ctk_root, "bin", "x64"), - os.path.join(ctk_root, "bin", "x86_64"), - os.path.join(ctk_root, "bin"), + ctk_root / "bin" / "x64", + ctk_root / "bin" / "x86_64", + ctk_root / "bin", ] checked = _patch_exec_probe(mocker) result = find_nvidia_binary_utility("nvcc") assert result is None - assert checked == [os.path.join(d, "nvcc.exe") for d in expected_dirs] + assert checked == [d / "nvcc.exe" for d in expected_dirs] @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_canary_not_consulted_when_found_earlier(monkeypatch, mocker): # An earlier trusted dir hit must short-circuit before the canary subprocess. - conda_prefix = os.path.join(os.sep, "conda") + conda_prefix = Path(os.sep, "conda") mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) - monkeypatch.setenv("CONDA_PREFIX", conda_prefix) + monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) canary_mock = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) - conda_nvcc = os.path.join(conda_prefix, "bin", "nvcc") + conda_nvcc = conda_prefix / "bin" / "nvcc" _patch_exec_probe(mocker, existing=[conda_nvcc]) result = find_nvidia_binary_utility("nvcc") @@ -224,7 +225,7 @@ def test_find_binary_canary_not_consulted_when_found_earlier(monkeypatch, mocker @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_returns_none_with_no_candidates(monkeypatch, mocker): - site_key = os.path.join("nvidia", "cuda_nvcc", "bin") + site_key = ("nvidia", "cuda_nvcc", "bin") mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object( @@ -241,25 +242,25 @@ def test_find_binary_returns_none_with_no_candidates(monkeypatch, mocker): result = find_nvidia_binary_utility("nvcc") assert result is None - find_sub_dirs_mock.assert_called_once_with(site_key.split(os.sep)) + find_sub_dirs_mock.assert_called_once_with(site_key) # No trusted dirs were assembled, so nothing is probed at all. assert checked == [] @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_without_site_packages_entry(monkeypatch, mocker): - conda_prefix = os.path.join(os.sep, "conda") - cuda_home = os.path.join(os.sep, "cuda") + conda_prefix = Path(os.sep, "conda") + cuda_home = Path(os.sep, "cuda") mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) find_sub_dirs_mock = mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) - monkeypatch.setenv("CONDA_PREFIX", conda_prefix) - mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) + monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=str(cuda_home)) mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) expected_dirs = [ - os.path.join(conda_prefix, "bin"), - os.path.join(cuda_home, "bin"), + conda_prefix / "bin", + cuda_home / "bin", ] checked = _patch_exec_probe(mocker) @@ -267,7 +268,7 @@ def test_find_binary_without_site_packages_entry(monkeypatch, mocker): assert result is None find_sub_dirs_mock.assert_not_called() - assert checked == [os.path.join(d, "nvcc") for d in expected_dirs] + assert checked == [d / "nvcc" for d in expected_dirs] @pytest.mark.usefixtures("clear_find_binary_cache") @@ -296,10 +297,9 @@ class TestResolveInTrustedDirs: @staticmethod def _make_executable(directory, name): - path = os.path.join(str(directory), name) - with open(path, "w", encoding="utf-8") as handle: - handle.write("") - os.chmod(path, 0o700) + path = directory / name + path.write_text("", encoding="utf-8") + path.chmod(0o700) return path def test_cwd_is_not_searched(self, tmp_path, monkeypatch): @@ -316,9 +316,9 @@ def test_cwd_is_not_searched(self, tmp_path, monkeypatch): monkeypatch.chdir(evil_cwd) # A trusted dir with no binary returns None, never the CWD copy. - assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(empty)]) is None + assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [empty]) is None # When a trusted dir holds it, that path wins regardless of CWD. - assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(empty), str(trusted)]) == trusted_nvcc + assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [empty, trusted]) == trusted_nvcc def test_first_trusted_dir_wins(self, tmp_path): first = tmp_path / "a" @@ -327,30 +327,30 @@ def test_first_trusted_dir_wins(self, tmp_path): second.mkdir() first_nvcc = self._make_executable(first, "nvcc") self._make_executable(second, "nvcc") - assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(first), str(second)]) == first_nvcc + assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [first, second]) == first_nvcc def test_duplicate_dirs_skipped(self, tmp_path): present = tmp_path / "p" present.mkdir() nvcc = self._make_executable(present, "nvcc") - assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(present), str(present)]) == nvcc + assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [present, present]) == nvcc assert binary_finder_module._resolve_in_trusted_dirs("nvcc", []) is None def test_empty_dir_asserts(self): + # Path("") is Path("."), which would make the CWD a search dir. with pytest.raises(AssertionError): - binary_finder_module._resolve_in_trusted_dirs("nvcc", [""]) + binary_finder_module._resolve_in_trusted_dirs("nvcc", [Path("")]) @pytest.mark.skipif(binary_finder_module.IS_WINDOWS, reason="POSIX execute-bit semantics") def test_non_executable_file_rejected_on_posix(self, tmp_path): directory = tmp_path / "d" directory.mkdir() - path = os.path.join(str(directory), "nvcc") - with open(path, "w", encoding="utf-8") as handle: - handle.write("") - os.chmod(path, 0o644) - assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(directory)]) is None - os.chmod(path, 0o700) - assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(directory)]) == path + path = directory / "nvcc" + path.write_text("", encoding="utf-8") + path.chmod(0o644) + assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [directory]) is None + path.chmod(0o700) + assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [directory]) == path @pytest.mark.usefixtures("clear_find_binary_cache") @@ -380,8 +380,8 @@ def test_resolve_in_trusted_dirs_returns_absolute_path(tmp_path, monkeypatch, mo leaked a relative path that would re-resolve against a possibly different CWD at execution time. """ - rel_dir = os.path.join("some", "relative", "bin") - candidate = os.path.join(rel_dir, "nvcc") + rel_dir = Path("some", "relative", "bin") + candidate = rel_dir / "nvcc" mocker.patch.object( binary_finder_module, "_is_executable_candidate", @@ -392,5 +392,5 @@ def test_resolve_in_trusted_dirs_returns_absolute_path(tmp_path, monkeypatch, mo monkeypatch.chdir(tmp_path) result = binary_finder_module._resolve_in_trusted_dirs("nvcc", [rel_dir]) - assert os.path.isabs(result) - assert result == os.path.abspath(os.path.join(str(tmp_path), candidate)) + assert result.is_absolute() + assert result == tmp_path / candidate