diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py index 53446107da3..4d41484c5c9 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py @@ -183,7 +183,7 @@ def _load_lib_no_cache(libname: str) -> LoadedDL: # Phase 3: Load from found path, or fall back to system search + late find. if find is not None: - return LOADER.load_with_abs_path(desc, find.abs_path, find.found_via) + return LOADER.load_with_abs_path(desc, str(find.abs_path), find.found_via) loaded = LOADER.load_with_system_search(desc) if loaded is not None: @@ -191,7 +191,7 @@ def _load_lib_no_cache(libname: str) -> LoadedDL: find = run_find_steps(ctx, LATE_FIND_STEPS) if find is not None: - return LOADER.load_with_abs_path(desc, find.abs_path, find.found_via) + return LOADER.load_with_abs_path(desc, str(find.abs_path), find.found_via) if desc.ctk_root_canary_anchor_libnames: canary_abs_path = _try_ctk_root_canary(ctx) diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py index 2d6a5f016a7..200916beccd 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py @@ -10,11 +10,9 @@ from __future__ import annotations -import glob -import os from collections.abc import Sequence from dataclasses import dataclass -from pathlib import PurePath +from pathlib import Path, PurePath from typing import Protocol, cast from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor @@ -30,7 +28,7 @@ def _no_such_file_in_sub_dirs( error_messages.append(f"No such file: {file_wild}") for sub_dir in find_sub_dirs_all_sitepackages(sub_dirs): attachments.append(f' listdir("{sub_dir}"):') - for node in sorted(os.listdir(sub_dir)): + for node in sorted(entry.name for entry in Path(sub_dir).iterdir()): attachments.append(f" {node}") @@ -39,7 +37,7 @@ def _find_so_in_rel_dirs( so_basename: str, error_messages: list[str], attachments: list[str], -) -> str | None: +) -> Path | None: sub_dirs_searched: list[tuple[str, ...]] = [] file_wild = so_basename + "*" for rel_dir in rel_dirs: @@ -51,25 +49,27 @@ def _find_so_in_rel_dirs( # multiple coexist, matching the newest-first bias elsewhere in pathfinder # (see LinuxSearchPlatform.find_in_lib_dir and load_dl_linux._candidate_sonames). # Issue #1732 tracks the deferred question of raising on true ambiguity. - so_name = os.path.join(abs_dir, so_basename) - if os.path.isfile(so_name): - return so_name - for so_name in sorted(glob.glob(os.path.join(abs_dir, file_wild)), reverse=True): - if os.path.isfile(so_name): - return so_name + abs_dir_path = Path(abs_dir) + so_path = abs_dir_path / so_basename + if so_path.is_file(): + return so_path + for so_path in sorted(abs_dir_path.glob(file_wild), reverse=True): + if so_path.is_file(): + return so_path sub_dirs_searched.append(sub_dir) for sub_dir in sub_dirs_searched: _no_such_file_in_sub_dirs(sub_dir, file_wild, error_messages, attachments) return None -def _find_dll_under_dir(dirpath: str, file_wild: str, target_arch: str | None = None) -> str | None: - for path in sorted(glob.glob(os.path.join(dirpath, file_wild))): - if not os.path.isfile(path): +def _find_dll_under_dir(dirpath: Path, file_wild: str, target_arch: str | None = None) -> Path | None: + for path in sorted(dirpath.glob(file_wild)): + if not path.is_file(): continue - if is_suppressed_dll_file(os.path.basename(path)): + if is_suppressed_dll_file(path.name): continue - if target_arch is not None and not windows_pe_matches_arch(path, target_arch): + # windows_pe_matches_arch() lives in _utils and is still str-typed. + if target_arch is not None and not windows_pe_matches_arch(str(path), target_arch): continue return path return None @@ -80,14 +80,14 @@ def _find_dll_in_rel_dirs( lib_searched_for: str, error_messages: list[str], attachments: list[str], -) -> str | None: +) -> Path | None: sub_dirs_searched: list[tuple[str, ...]] = [] for rel_dir in rel_dirs: sub_dir = PurePath(rel_dir).parts for abs_dir in find_sub_dirs_all_sitepackages(sub_dir): - dll_name = _find_dll_under_dir(abs_dir, lib_searched_for) - if dll_name is not None: - return dll_name + dll_path = _find_dll_under_dir(Path(abs_dir), lib_searched_for) + if dll_path is not None: + return dll_path sub_dirs_searched.append(sub_dir) for sub_dir in sub_dirs_searched: _no_such_file_in_sub_dirs(sub_dir, lib_searched_for, error_messages, attachments) @@ -99,7 +99,7 @@ def lib_searched_for(self, libname: str) -> str: ... def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: ... - def conda_anchor_point(self, conda_prefix: str) -> str: ... + def conda_anchor_point(self, conda_prefix: str) -> Path: ... def anchor_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: ... @@ -109,16 +109,16 @@ def find_in_site_packages( lib_searched_for: str, error_messages: list[str], attachments: list[str], - ) -> str | None: ... + ) -> Path | None: ... def find_in_lib_dir( self, - lib_dir: str, + lib_dir: Path, desc: LibDescriptor, lib_searched_for: str, error_messages: list[str], attachments: list[str], - ) -> str | None: ... + ) -> Path | None: ... @dataclass(frozen=True, slots=True) @@ -129,8 +129,8 @@ def lib_searched_for(self, libname: str) -> str: def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: return cast(tuple[str, ...], desc.site_packages_linux) - def conda_anchor_point(self, conda_prefix: str) -> str: - return conda_prefix + def conda_anchor_point(self, conda_prefix: str) -> Path: + return Path(conda_prefix) def anchor_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: return cast(tuple[str, ...], desc.anchor_rel_dirs_linux) @@ -141,21 +141,21 @@ def find_in_site_packages( lib_searched_for: str, error_messages: list[str], attachments: list[str], - ) -> str | None: + ) -> Path | None: return _find_so_in_rel_dirs(rel_dirs, lib_searched_for, error_messages, attachments) def find_in_lib_dir( self, - lib_dir: str, + lib_dir: Path, _desc: LibDescriptor, lib_searched_for: str, error_messages: list[str], attachments: list[str], - ) -> str | None: + ) -> Path | None: # Most libraries have both unversioned and versioned files/symlinks (exact match first) - so_name = os.path.join(lib_dir, lib_searched_for) - if os.path.isfile(so_name): - return so_name + so_path = lib_dir / lib_searched_for + if so_path.is_file(): + return so_path # Some libraries only exist as versioned files (e.g., libcupti.so.13 in conda), # so the glob fallback is needed file_wild = lib_searched_for + "*" @@ -163,15 +163,15 @@ def find_in_lib_dir( # situations, and to be internally consistent, we sort in reverse order with the # intent to return the newest version first. Issue #1732 tracks the deferred # question of raising on true ambiguity. - for so_name in sorted(glob.glob(os.path.join(lib_dir, file_wild)), reverse=True): - if os.path.isfile(so_name): - return so_name + for so_path in sorted(lib_dir.glob(file_wild), reverse=True): + if so_path.is_file(): + return so_path error_messages.append(f"No such file: {file_wild}") attachments.append(f' listdir("{lib_dir}"):') - if not os.path.isdir(lib_dir): + if not lib_dir.is_dir(): attachments.append(" DIRECTORY DOES NOT EXIST") else: - for node in sorted(os.listdir(lib_dir)): + for node in sorted(entry.name for entry in lib_dir.iterdir()): attachments.append(f" {node}") return None @@ -186,8 +186,8 @@ def lib_searched_for(self, libname: str) -> str: def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: return cast(tuple[str, ...], desc.site_packages_windows.for_arch(self.target_arch)) - def conda_anchor_point(self, conda_prefix: str) -> str: - return os.path.join(conda_prefix, "Library") + def conda_anchor_point(self, conda_prefix: str) -> Path: + return Path(conda_prefix, "Library") def anchor_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: return cast(tuple[str, ...], desc.anchor_rel_dirs_windows.for_arch(self.target_arch)) @@ -198,31 +198,31 @@ def find_in_site_packages( lib_searched_for: str, error_messages: list[str], attachments: list[str], - ) -> str | None: + ) -> Path | None: return _find_dll_in_rel_dirs(rel_dirs, lib_searched_for, error_messages, attachments) def find_in_lib_dir( self, - lib_dir: str, + lib_dir: Path, desc: LibDescriptor, _lib_searched_for: str, error_messages: list[str], attachments: list[str], - ) -> str | None: + ) -> Path | None: file_wild = desc.name + "*.dll" target_arch = self.target_arch if desc.requires_windows_binary_arch_check else None - dll_name = _find_dll_under_dir(lib_dir, file_wild, target_arch) - if dll_name is not None: - return dll_name + dll_path = _find_dll_under_dir(lib_dir, file_wild, target_arch) + if dll_path is not None: + return dll_path if target_arch is None: error_messages.append(f"No such file: {file_wild}") else: error_messages.append(f"No {target_arch}-compatible PE file: {file_wild}") attachments.append(f' listdir("{lib_dir}"):') - if not os.path.isdir(lib_dir): + if not lib_dir.is_dir(): attachments.append(" DIRECTORY DOES NOT EXIST") else: - for node in sorted(os.listdir(lib_dir)): + for node in sorted(entry.name for entry in lib_dir.iterdir()): attachments.append(f" {node}") return None diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py index 5901094fcaa..17704372a6b 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py @@ -19,10 +19,10 @@ :data:`~cuda.pathfinder._dynamic_libs.search_platform.PLATFORM` instance. """ -import glob import os from collections.abc import Callable from dataclasses import dataclass, field +from pathlib import Path, PurePath from typing import NoReturn, cast from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor @@ -39,7 +39,7 @@ class FindResult: """A library file located on disk (not yet loaded).""" - abs_path: str + abs_path: Path found_via: str @@ -70,22 +70,26 @@ def raise_not_found(self) -> NoReturn: FindStep = Callable[[SearchContext], FindResult | None] -def _find_lib_dir_using_anchor(desc: LibDescriptor, platform: SearchPlatform, anchor_point: str) -> str | None: +def _find_lib_dir_using_anchor(desc: LibDescriptor, platform: SearchPlatform, anchor_point: Path) -> Path | None: """Find the library directory under *anchor_point* using the descriptor's relative paths.""" rel_dirs = platform.anchor_rel_dirs(desc) for rel_path in rel_dirs: - for dirname in sorted(glob.glob(os.path.join(anchor_point, rel_path))): - if os.path.isdir(dirname): - return os.path.normpath(dirname) + for match in sorted(anchor_point.glob(rel_path)): + if match.is_dir(): + # os.path.normpath has no pathlib equivalent: PurePath deliberately does + # not collapse "..", and Path.resolve() would also follow symlinks. Keeping + # normpath preserves the exact path reported for an anchor such as a + # CUDA_PATH containing "..". + return Path(os.path.normpath(match)) return None -def _find_using_lib_dir(ctx: SearchContext, lib_dir: str | None) -> str | None: +def _find_using_lib_dir(ctx: SearchContext, lib_dir: Path | None) -> Path | None: """Find a library file in a resolved lib directory.""" if lib_dir is None: return None return cast( - str | None, + Path | None, ctx.platform.find_in_lib_dir( lib_dir, ctx.desc, @@ -104,15 +108,17 @@ def _derive_ctk_root_linux(resolved_lib_path: str) -> str | None: - ``$CTK_ROOT/lib/libfoo.so.*`` - ``$CTK_ROOT/targets//lib64/libfoo.so.*`` - ``$CTK_ROOT/targets//lib/libfoo.so.*`` + + Uses the host path flavour (``PurePath``); the Windows canary layouts are + handled by :func:`_derive_ctk_root_windows`. """ - lib_dir = os.path.dirname(resolved_lib_path) - basename = os.path.basename(lib_dir) - if basename in ("lib64", "lib"): - parent = os.path.dirname(lib_dir) - grandparent = os.path.dirname(parent) - if os.path.basename(grandparent) == "targets": - return os.path.dirname(grandparent) - return parent + lib_dir = PurePath(resolved_lib_path).parent + if lib_dir.name in ("lib64", "lib"): + parent = lib_dir.parent + grandparent = parent.parent + if grandparent.name == "targets": + return str(grandparent.parent) + return str(parent) return None @@ -123,6 +129,12 @@ def _derive_ctk_root_windows(resolved_lib_path: str) -> str | None: - ``$CTK_ROOT/bin/x64/foo.dll`` (CTK 13 style) - ``$CTK_ROOT/bin/arm64/foo.dll`` (Windows on Arm CTK 13 style) - ``$CTK_ROOT/bin/foo.dll`` (CTK 12 style) + + Uses ``ntpath`` rather than ``PureWindowsPath``: ``ntpath.dirname`` slices the + input and keeps whichever separator the caller used, while ``PureWindowsPath`` + rewrites them to backslashes. :func:`derive_ctk_root` also reaches this function + on Linux, where rewriting would yield an unusable root for a POSIX path whose + parent directory happens to be named ``bin``. """ import ntpath @@ -147,7 +159,7 @@ def derive_ctk_root(resolved_lib_path: str) -> str | None: def find_via_ctk_root(ctx: SearchContext, ctk_root: str) -> FindResult | None: """Find a library under a previously derived CTK root.""" - lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, ctk_root) + lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, Path(ctk_root)) abs_path = _find_using_lib_dir(ctx, lib_dir) if abs_path is None: return None @@ -197,7 +209,7 @@ def find_in_cuda_path(ctx: SearchContext) -> FindResult | None: cuda_home = get_cuda_path_or_home() if cuda_home is None: return None - lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, cuda_home) + lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, Path(cuda_home)) abs_path = _find_using_lib_dir(ctx, lib_dir) if abs_path is not None: return FindResult(abs_path, "CUDA_PATH") diff --git a/cuda_pathfinder/tests/test_ctk_root_discovery.py b/cuda_pathfinder/tests/test_ctk_root_discovery.py index b4aa33d6a74..2196c4876c3 100644 --- a/cuda_pathfinder/tests/test_ctk_root_discovery.py +++ b/cuda_pathfinder/tests/test_ctk_root_discovery.py @@ -6,6 +6,7 @@ import subprocess import sys import textwrap +from pathlib import Path import pytest @@ -184,7 +185,7 @@ def test_try_via_ctk_root_finds_nvvm(tmp_path): result = find_via_ctk_root(_ctx("nvvm"), str(ctk_root)) assert result is not None - assert result.abs_path == str(nvvm_lib) + assert result.abs_path == nvvm_lib assert result.found_via == "system-ctk-root" @@ -201,7 +202,7 @@ def test_try_via_ctk_root_regular_lib(tmp_path): result = find_via_ctk_root(_ctx("cudart"), str(ctk_root)) assert result is not None - assert result.abs_path == str(cudart_lib) + assert result.abs_path == cudart_lib assert result.found_via == "system-ctk-root" @@ -219,7 +220,7 @@ def test_try_via_ctk_root_windows_arm64_prefers_arch_dir(tmp_path): ctx = SearchContext(LIB_DESCRIPTORS["cudart"], platform=WindowsSearchPlatform(target_arch="arm64")) result = find_via_ctk_root(ctx, str(ctk_root)) assert result is not None - assert result.abs_path == str(arm64_lib) + assert result.abs_path == arm64_lib assert result.found_via == "system-ctk-root" @@ -427,7 +428,7 @@ def test_resolve_ctk_root_via_canary_none_when_probe_fails(mocker): def test_resolve_ctk_root_via_canary_none_when_unrecognized(mocker): mocker.patch( f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess", - return_value=os.path.join(os.sep, "weird", "path", "libcudart.so.13"), + return_value=str(Path(os.sep, "weird", "path", "libcudart.so.13")), ) assert resolve_ctk_root_via_canary("cudart") is None diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index 54136dc34e1..c8145115d20 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -6,6 +6,7 @@ from __future__ import annotations import os +from pathlib import Path import pytest @@ -43,10 +44,10 @@ def _make_desc(name: str = "cudart", **overrides) -> LibDescriptor: "packaged_with": "ctk", "linux_sonames": ("libcudart.so",), "windows_dlls": ("cudart64_12.dll",), - "site_packages_linux": (os.path.join("nvidia", "cuda_runtime", "lib"),), + "site_packages_linux": ("nvidia/cuda_runtime/lib",), "site_packages_windows": WindowsSearchDirs( - x64=(os.path.join("nvidia", "cuda_runtime", "bin"),), - arm64=(os.path.join("nvidia", "cuda_runtime", "bin"),), + x64=("nvidia/cuda_runtime/bin",), + arm64=("nvidia/cuda_runtime/bin",), ), } defaults.update(overrides) @@ -195,11 +196,11 @@ def test_found_linux(self, mocker, tmp_path): ) desc = _make_desc( - site_packages_linux=(os.path.join("nvidia", "cuda_runtime", "lib"),), + site_packages_linux=("nvidia/cuda_runtime/lib",), ) result = find_in_site_packages(_ctx(desc, platform=LinuxSearchPlatform())) assert result is not None - assert result.abs_path == str(so_file) + assert result.abs_path == so_file assert result.found_via == "site-packages" def test_found_windows(self, mocker, tmp_path): @@ -217,13 +218,13 @@ def test_found_windows(self, mocker, tmp_path): desc = _make_desc( name="cudart", site_packages_windows=WindowsSearchDirs( - x64=(os.path.join("nvidia", "cuda_runtime", "bin"),), - arm64=(os.path.join("nvidia", "cuda_runtime", "bin"),), + x64=("nvidia/cuda_runtime/bin",), + arm64=("nvidia/cuda_runtime/bin",), ), ) result = find_in_site_packages(_ctx(desc, platform=WindowsSearchPlatform(target_arch="x64"))) assert result is not None - assert result.abs_path == str(dll) + assert result.abs_path == dll assert result.found_via == "site-packages" @pytest.mark.agent_authored(model="gpt-5") @@ -245,7 +246,7 @@ def test_found_windows_arm64_prefers_cuda13_arch_dir_to_cuda12(self, mocker, tmp desc = LIB_DESCRIPTORS["cudart"] result = find_in_site_packages(_ctx(desc, platform=WindowsSearchPlatform(target_arch="arm64"))) assert result is not None - assert result.abs_path == str(arm64_dll) + assert result.abs_path == arm64_dll assert result.found_via == "site-packages" @pytest.mark.agent_authored(model="gpt-5") @@ -267,7 +268,7 @@ def test_found_windows_x64_prefers_cuda13_arch_dir_to_cuda12(self, mocker, tmp_p desc = LIB_DESCRIPTORS["cudart"] result = find_in_site_packages(_ctx(desc, platform=WindowsSearchPlatform(target_arch="x64"))) assert result is not None - assert result.abs_path == str(x86_64_dll) + assert result.abs_path == x86_64_dll assert result.found_via == "site-packages" @pytest.mark.agent_authored(model="gpt-5") @@ -283,7 +284,7 @@ def test_found_windows_x64_uses_cuda12_when_cuda13_is_absent(self, mocker, tmp_p desc = LIB_DESCRIPTORS["cudart"] result = find_in_site_packages(_ctx(desc, platform=WindowsSearchPlatform(target_arch="x64"))) assert result is not None - assert result.abs_path == str(cuda12_dll) + assert result.abs_path == cuda12_dll assert result.found_via == "site-packages" @pytest.mark.agent_authored(model="gpt-5") @@ -338,7 +339,7 @@ def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path): result = find_in_site_packages(_ctx(platform=LinuxSearchPlatform())) assert result is not None - assert result.abs_path == str(versioned) + assert result.abs_path == versioned assert result.found_via == "site-packages" def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path): @@ -356,7 +357,7 @@ def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path result = find_in_site_packages(_ctx(platform=LinuxSearchPlatform())) assert result is not None - assert result.abs_path == str(newer) + assert result.abs_path == newer assert result.found_via == "site-packages" def test_glob_fallback_zero_matches_returns_none(self, mocker, tmp_path): @@ -399,7 +400,7 @@ def test_found_linux(self, mocker, tmp_path): result = find_in_conda(_ctx(platform=LinuxSearchPlatform())) assert result is not None - assert result.abs_path == str(so_file) + assert result.abs_path == so_file assert result.found_via == "conda" def test_found_windows(self, mocker, tmp_path): @@ -412,7 +413,7 @@ def test_found_windows(self, mocker, tmp_path): result = find_in_conda(_ctx(platform=WindowsSearchPlatform(target_arch="x64"))) assert result is not None - assert result.abs_path == str(dll) + assert result.abs_path == dll assert result.found_via == "conda" @pytest.mark.agent_authored(model="gpt-5") @@ -429,7 +430,7 @@ def test_found_windows_arm64_prefers_arch_dir(self, mocker, tmp_path): result = find_in_conda(_ctx(platform=WindowsSearchPlatform(target_arch="arm64"))) assert result is not None - assert result.abs_path == str(arm64_dll) + assert result.abs_path == arm64_dll assert result.found_via == "conda" # The next three tests cover the Linux glob fallback in @@ -450,7 +451,7 @@ def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path): result = find_in_conda(_ctx(platform=LinuxSearchPlatform())) assert result is not None - assert result.abs_path == str(versioned) + assert result.abs_path == versioned assert result.found_via == "conda" def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path): @@ -465,7 +466,7 @@ def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path result = find_in_conda(_ctx(platform=LinuxSearchPlatform())) assert result is not None - assert result.abs_path == str(newer) + assert result.abs_path == newer assert result.found_via == "conda" def test_glob_fallback_zero_matches_returns_none(self, mocker, tmp_path): @@ -501,7 +502,7 @@ def test_found_linux(self, mocker, tmp_path): result = find_in_cuda_path(_ctx(platform=LinuxSearchPlatform())) assert result is not None - assert result.abs_path == str(so_file) + assert result.abs_path == so_file assert result.found_via == "CUDA_PATH" def test_found_windows(self, mocker, tmp_path): @@ -514,7 +515,7 @@ def test_found_windows(self, mocker, tmp_path): result = find_in_cuda_path(_ctx(platform=WindowsSearchPlatform(target_arch="x64"))) assert result is not None - assert result.abs_path == str(dll) + assert result.abs_path == dll assert result.found_via == "CUDA_PATH" @pytest.mark.agent_authored(model="gpt-5") @@ -531,7 +532,7 @@ def test_found_windows_arm64_prefers_arch_dir(self, mocker, tmp_path): result = find_in_cuda_path(_ctx(platform=WindowsSearchPlatform(target_arch="arm64"))) assert result is not None - assert result.abs_path == str(arm64_dll) + assert result.abs_path == arm64_dll assert result.found_via == "CUDA_PATH" @pytest.mark.parametrize( @@ -558,7 +559,7 @@ def test_nvvm_windows_checks_binary_arch(self, mocker, tmp_path, target_arch, ma assert (result is not None) is expected_found if expected_found: assert result is not None - assert result.abs_path == str(dll) + assert result.abs_path == dll assert result.found_via == "CUDA_PATH" else: assert any(f"No {target_arch}-compatible PE file" in message for message in ctx.error_messages) @@ -571,7 +572,7 @@ def test_nvvm_windows_checks_binary_arch(self, mocker, tmp_path, target_arch, ma class TestRunFindSteps: def test_returns_first_hit(self): - hit = FindResult("/path/to/lib.so", "step-a") + hit = FindResult(Path("/path/to/lib.so"), "step-a") def step_a(_ctx): return hit @@ -590,7 +591,7 @@ def test_empty_steps(self): assert run_find_steps(_ctx(), ()) is None def test_skips_nones_returns_later_hit(self): - hit = FindResult("/later/lib.so", "step-c") + hit = FindResult(Path("/later/lib.so"), "step-c") result = run_find_steps(_ctx(), (lambda _: None, lambda _: hit)) assert result is hit @@ -682,9 +683,8 @@ def test_find_lib_dir_uses_descriptor_linux(self, tmp_path): (tmp_path / "nvvm" / "lib64").mkdir(parents=True) desc = _make_desc(name="nvvm", anchor_rel_dirs_linux=("nvvm/lib64",)) - result = _find_lib_dir_using_anchor(desc, LinuxSearchPlatform(), str(tmp_path)) - assert result is not None - assert result.endswith(os.path.join("nvvm", "lib64")) + result = _find_lib_dir_using_anchor(desc, LinuxSearchPlatform(), tmp_path) + assert result == tmp_path / "nvvm" / "lib64" def test_find_lib_dir_uses_descriptor_windows(self, tmp_path): (tmp_path / "nvvm" / "bin").mkdir(parents=True) @@ -696,9 +696,8 @@ def test_find_lib_dir_uses_descriptor_windows(self, tmp_path): arm64=("nvvm/bin/arm64", "nvvm/bin"), ), ) - result = _find_lib_dir_using_anchor(desc, WindowsSearchPlatform(target_arch="x64"), str(tmp_path)) - assert result is not None - assert result.endswith(os.path.join("nvvm", "bin")) + result = _find_lib_dir_using_anchor(desc, WindowsSearchPlatform(target_arch="x64"), tmp_path) + assert result == tmp_path / "nvvm" / "bin" @pytest.mark.agent_authored(model="gpt-5") def test_find_lib_dir_windows_arm64_uses_arm64_anchor(self, tmp_path): @@ -712,13 +711,12 @@ def test_find_lib_dir_windows_arm64_uses_arm64_anchor(self, tmp_path): arm64=("bin/arm64",), ), ) - result = _find_lib_dir_using_anchor(desc, WindowsSearchPlatform(target_arch="arm64"), str(tmp_path)) - assert result is not None - assert result.endswith(os.path.join("bin", "arm64")) + result = _find_lib_dir_using_anchor(desc, WindowsSearchPlatform(target_arch="arm64"), tmp_path) + assert result == tmp_path / "bin" / "arm64" def test_find_lib_dir_returns_none_when_no_match(self, tmp_path): desc = _make_desc(anchor_rel_dirs_linux=("nonexistent",)) - assert _find_lib_dir_using_anchor(desc, LinuxSearchPlatform(), str(tmp_path)) is None + assert _find_lib_dir_using_anchor(desc, LinuxSearchPlatform(), tmp_path) is None def test_nvvm_cuda_home_linux(self, mocker, tmp_path): """End-to-end: find_in_cuda_path resolves nvvm under its custom subdir.""" @@ -736,5 +734,5 @@ def test_nvvm_cuda_home_linux(self, mocker, tmp_path): ) result = find_in_cuda_path(_ctx(desc, platform=LinuxSearchPlatform())) assert result is not None - assert result.abs_path == str(so_file) + assert result.abs_path == so_file assert result.found_via == "CUDA_PATH"