Skip to content

Migrate cuda_pathfinder/_dynamic_libs search modules from os.path to pathlib - #2489

Open
LeSingh1 wants to merge 2 commits into
NVIDIA:mainfrom
LeSingh1:pathlib/dynamic-libs
Open

Migrate cuda_pathfinder/_dynamic_libs search modules from os.path to pathlib#2489
LeSingh1 wants to merge 2 commits into
NVIDIA:mainfrom
LeSingh1:pathlib/dynamic-libs

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Part 1 of the series proposed in #2410: cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py and search_steps.py. Deliberately small so the conversion style and the compatibility approach can be reviewed before the remaining six parts follow the same template.

What changed

Path construction, joining, and filesystem predicates now go through Path/PurePath instead of os.path string manipulation. search_platform.py no longer imports os at all.

Compatibility

Treated as a hard constraint, per the discussion on the issue. Every entry point still accepts str, and every function that documents or returns str still returns strPath is used strictly as the internal representation and converted back with str() at each return. SearchPlatform, FindResult.abs_path, and the LoadedDL.abs_path that reaches users are unchanged in both type and value. No signature changes.

Deliberately not converted

Rather than folding these in silently, both are left alone with a comment saying why:

  • _find_lib_dir_using_anchor keeps os.path.normpath. pathlib has no equivalent because PurePath intentionally does not collapse .., so converting would change the path reported for an anchor such as a CUDA_PATH containing ...
  • _derive_ctk_root_windows keeps ntpath. ntpath.dirname slices its input and preserves the caller's separators; PureWindowsPath rewrites them to backslashes. derive_ctk_root also reaches this function on Linux, so converting would return an unusable root (\opt\foo) for a POSIX path whose parent directory happens to be named bin.

glob.glob is also retained where wildcard expansion is needed — Path.glob orders results differently, and the reverse-sorted newest-first policy from #1732 depends on the current string sort.

Happy to revisit any of these if you'd prefer them handled differently; that decision will set the pattern for the rest of the series.

Verification

No behavior change intended. Beyond the test suite, I fuzzed the old and new implementations against each other: 200k randomized path shapes for the derive_ctk_root helpers, and 3000 randomized real directory trees (including trailing-slash, // and /./ spellings) for the search helpers. The only string-level divergence is that a redundant . component in an input directory is now collapsed — the new string is the normalized form of the old and names the same file — and that input cannot occur, since lib_dir always arrives already normpath-ed.

pre-commit run passes every hook at the pinned versions; mypy is clean over cuda_pathfinder/cuda.

Since I don't have CUDA hardware, I verified the suite on Linux CI rather than locally. Against upstream/main the full pytest output is byte-identical apart from elapsed time — same 1188 passed, 4 skipped, and the same 88 Not found errors for absent NVIDIA libraries, in the same order.

os.path is soft-deprecated in favor of the type-safe pathlib API (NVIDIA#2410).
Convert the two filesystem-search modules under _dynamic_libs so path
construction, joining, and predicates go through Path/PurePath instead of
os.path string manipulation, which makes it explicit which values are paths
and which are the plain strings the search cascade accumulates for error
reporting.

Public compatibility is treated as a hard constraint: every entry point still
accepts str, and every function that documents or returns str still returns
str. Path is used strictly as the internal representation and converted back
with str() at each return, so SearchPlatform, FindResult.abs_path, and the
LoadedDL.abs_path that reaches users are all unchanged in type and value.

Two sites are deliberately left on the old API rather than converted silently,
each with a comment explaining why:

- _find_lib_dir_using_anchor keeps os.path.normpath. pathlib has no equivalent
  because PurePath intentionally does not collapse "..", so converting would
  change the path reported for an anchor such as a CUDA_PATH containing "..".

- _derive_ctk_root_windows keeps ntpath. ntpath.dirname slices its input and
  preserves the caller's separators, while PureWindowsPath rewrites them to
  backslashes. derive_ctk_root also reaches this function on Linux, so
  converting would return an unusable root for a POSIX path whose parent
  directory is named "bin".

glob.glob is also kept where wildcard expansion is needed; Path.glob differs in
result ordering, and the reverse-sorted newest-first policy from NVIDIA#1732 depends
on the current string sort.

No behavior change is intended. The only string-level difference is that
redundant "." components in an input directory are now collapsed, which cannot
occur for the values these functions actually receive.

Signed-off-by: LeSingh1 <sshaurya914@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the cuda.pathfinder Everything related to the cuda.pathfinder module label Aug 4, 2026
@LeSingh1

LeSingh1 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@mdboom — this is the part 1 I offered on #2410, sent as a small sample so the conversion style and the compatibility approach can be checked before the remaining six parts follow the same template. Happy to hold the rest until you've had a look, or to drop this entirely if you'd rather assign it elsewhere.

Two notes for whoever picks it up:

pr-metadata-check needs a milestone and I can't set one as an outside contributor — the cuda.pathfinder label applied but the milestone didn't. cuda.pathfinder next looks like the right one.

On verification: I don't have CUDA hardware, so I ran the suite on Linux CI rather than locally. Rather than just matching the pass count, I diffed the full pytest output against upstream/main with timestamps stripped — it's byte-identical apart from elapsed time, including the same 88 Not found errors for absent NVIDIA libraries in the same order. I also fuzzed the old and new implementations against each other (200k randomized path shapes for the derive_ctk_root helpers, 3000 randomized directory trees for the search helpers), which is what surfaced the two sites I left on os.path/ntpath and documented in the description — the ntpath one is a genuine functional regression on Linux, not a style preference.

@mdboom

mdboom commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

A few implementation comments:

Treated as a hard constraint, per the discussion on the issue. Every entry point still accepts str, and every function that documents or returns str still returns str — Path is used strictly as the internal representation and converted back with str() at each return. SearchPlatform, FindResult.abs_path, and the LoadedDL.abs_path that reaches users are unchanged in both type and value. No signature changes.

This only needs to apply to functions in the public API. We are free to change the implementation details of internal function calls as we see fit, so we should prefer pathlib.Path wherever feasible.

glob.glob is also retained where wildcard expansion is needed — Path.glob orders results differently, and the reverse-sorted newest-first policy from #1732 depends on the current string sort.

All uses of glob.glob are passed to sorted anyway, so why does the sorting order matter? In the changes here we are paying a penalty of converting between Path and str objects, so it would be preferable to stay in the pathlib.Path space if possible.

Follow-up to review feedback on NVIDIA#2489.

Narrow the str-compatibility constraint to the public API. The internal
helpers and the SearchPlatform protocol now pass and return Path; str()
is applied only where the value leaves this module:

- FindResult.abs_path, which reaches users unchanged as LoadedDL.abs_path
- derive_ctk_root(), whose result is consumed by load_nvidia_dynamic_lib
  and the header search

Replace all four glob.glob() call sites with Path.glob(). Sorting is done
on the string form via a small sorted_glob() helper: PurePath ordering is
case-insensitive on Windows, so plain sorted() over Path objects would
reorder mixed-case filenames relative to the byte-wise ordering these call
sites have always used, and the newest-first policy from NVIDIA#1732 rides on
that ordering.

Two sites deliberately stay on os.path:

- _find_lib_dir_using_anchor() keeps os.path.normpath(): PurePath does not
  collapse "..", and Path.resolve() would additionally follow symlinks.
- _derive_ctk_root_windows() keeps ntpath. derive_ctk_root() also reaches
  it on Linux, where PureWindowsPath would rewrite "/opt/foo/bin/libx.so"
  to "\opt\foo" instead of "/opt/foo".

Differential fuzzing against the previous revision (400k randomized path
shapes, 600 randomized directory trees) shows one intended change:
LinuxSearchPlatform.conda_anchor_point() now returns a normalized Path
rather than $CONDA_PREFIX verbatim. That is not observable downstream,
because _find_lib_dir_using_anchor() already normpath'd its result;
resolved paths, error messages and listdir attachments are byte-identical.

Signed-off-by: LeSingh1 <sshaurya914@gmail.com>
@LeSingh1

LeSingh1 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Done in 5d5f21a.

Internals and the SearchPlatform protocol now pass and return Path. str() is applied only where a value leaves these modules: FindResult.abs_path (which surfaces unchanged as the public LoadedDL.abs_path) and derive_ctk_root(). All four glob.glob sites are now Path.glob — you were right that the sorted() wrapper makes the ordering argument moot.

One thing worth flagging: sorted() over Path compares PurePath._str_normcase, which is lowercased on Windows, so it isn't the same order as sorted() over str for mixed-case names. I kept the existing order by sorting on the string form in a small sorted_glob() helper, rather than change Windows behaviour in a PR that otherwise changes nothing — and I have no Windows machine to check it on. Happy to drop the key for simpler code if you'd prefer.

os.path.normpath in _find_lib_dir_using_anchor stays: no pathlib equivalent, since PurePath doesn't collapse .. and resolve() would also follow symlinks. _derive_ctk_root_windows stays on ntpath — I re-checked, and it still matters because derive_ctk_root reaches it on Linux, where PureWindowsPath turns /opt/foo/bin/libx.so into \opt\foo.

A correction to my earlier comment. I said the CI baseline was "1188 passed and 88 Not found errors for absent NVIDIA libraries". That was wrong, and I should have looked harder before asserting it. The 88 were fixture 'mocker' not found — my fork-side test job was missing pytest-mock, so those 88 tests never ran. The Not found: libname= lines I took for the cause were unrelated INFO logging. I've fixed the job to install the test dependency group and pinned -p no:randomly, and the real numbers are 1276 passed, 4 skipped, 0 errors, identical on the PR's base commit and on this revision — full pytest output byte-identical apart from elapsed time.

Differential fuzzing against the previous revision (400k randomized path shapes, 600 randomized directory trees) shows one intended difference: conda_anchor_point() returns a normalized Path rather than $CONDA_PREFIX verbatim. Not observable downstream, since _find_lib_dir_using_anchor already normpath'd its result.

LeSingh1 added a commit to LeSingh1/cuda-python that referenced this pull request Aug 4, 2026
Follow-up to the review feedback on NVIDIA#2489: the str-compatibility constraint
applies only to the public API.

The try_* methods and _no_such_file_in_dir now work in Path throughout. str()
is applied once, where abs_path is stored on the public LocatedStaticLib and
LocatedBitcodeLib. The relative-path constants go from os.path.join(...) to
forward-slash literals, matching how site_packages_dirs is already written in
the same dicts; Path normalizes the separator on Windows.

One behavior change: a CUDA_PATH or CONDA_PREFIX containing redundant
separators ("//", "/.") now produces a normalized abs_path, because Path
collapses them. Differential fuzzing against the pre-revision code (16k lookups
over randomized trees, comparing located paths and full error text) shows no
other difference, and none at all when those variables are free of redundant
separators.

Signed-off-by: LeSingh1 <sshaurya914@gmail.com>
Comment on lines +42 to +44
``abs_path`` is a ``str``: it is handed to the platform loaders and surfaces
unchanged as the public ``LoadedDL.abs_path``. Everything upstream of this
dataclass works with ``Path``.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary comment.

Comment on lines +159 to +161

Returns a ``str`` because the result crosses into ``load_nvidia_dynamic_lib``
and the header search, which are outside this module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary comment

Comment on lines 47 to 48
abs_path: str
found_via: str

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's store these as Path with a custom field converter. That way all users of this class don't have to convert their paths back to strings and back again.

Suggested change
abs_path: str
found_via: str
abs_path: Path = field(converter=Path)
found_via: str

if abs_path is None:
return None
return FindResult(abs_path, "system-ctk-root")
return FindResult(str(abs_path), "system-ctk-root")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return FindResult(str(abs_path), "system-ctk-root")
return FindResult(abs_path, "system-ctk-root")

abs_path = ctx.platform.find_in_site_packages(rel_dirs, ctx.lib_searched_for, ctx.error_messages, ctx.attachments)
if abs_path is not None:
return FindResult(abs_path, "site-packages")
return FindResult(str(abs_path), "site-packages")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return FindResult(str(abs_path), "site-packages")
return FindResult(abs_path, "site-packages")

abs_path = _find_using_lib_dir(ctx, lib_dir)
if abs_path is not None:
return FindResult(abs_path, "conda")
return FindResult(str(abs_path), "conda")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return FindResult(str(abs_path), "conda")
return FindResult(abs_path, "conda")

abs_path = _find_using_lib_dir(ctx, lib_dir)
if abs_path is not None:
return FindResult(abs_path, "CUDA_PATH")
return FindResult(str(abs_path), "CUDA_PATH")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return FindResult(str(abs_path), "CUDA_PATH")
return FindResult(abs_path, "CUDA_PATH")

result = _find_lib_dir_using_anchor(desc, LinuxSearchPlatform(), tmp_path)
assert result is not None
assert result.endswith(os.path.join("nvvm", "lib64"))
assert str(result).endswith(os.path.join("nvvm", "lib64"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also update the uses of os.path in test files.

Comment on lines +25 to +41
def sorted_glob(directory: Path, pattern: str, *, reverse: bool = False) -> list[Path]:
"""Return ``directory.glob(pattern)`` matches in a deterministic order.

The ordering is deliberately taken from the string form rather than from
``Path`` comparison: ``PurePath`` ordering is case-insensitive on Windows,
so plain ``sorted()`` over ``Path`` objects would reorder mixed-case
filenames relative to the byte-wise ordering used up to now. Issue #1732
tracks the newest-first policy that rides on this ordering, so it is kept
unchanged on both platforms.
"""
return sorted(directory.glob(pattern), key=str, reverse=reverse)


def _sorted_dir_entry_names(directory: Path) -> list[str]:
return sorted(entry.name for entry in directory.iterdir())


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing worth flagging: sorted() over Path compares PurePath._str_normcase, which is lowercased on Windows, so it isn't the same order as sorted() over str for mixed-case names. I kept the existing order by sorting on the string form in a small sorted_glob() helper, rather than change Windows behaviour in a PR that otherwise changes nothing — and I have no Windows machine to check it on. Happy to drop the key for simpler code if you'd prefer.

Yes, let's switch to the simpler code. I would consider this a fix of a latest Windows bug because it makes sorting behave the same way as it normally does on that platform.

So let's remove these helpers, take the change in Windows sorting behavior as desired (one of the reasons to move to Path is to get these sorts of bugfixes from upstream Python), and update all uses of these helpers to the simpler form.

mdboom pushed a commit that referenced this pull request Aug 7, 2026
Part 4 of the series proposed in #2410.

Filesystem predicates and path joining in the pathfinder tests now go through
pathlib: os.path.isfile/isdir become Path.is_file()/is_dir(), os.path.basename
becomes Path.name, os.path.join becomes Path joining, and the site-packages
check uses Path.parts instead of splitting on os.path.sep.

site_pkg_rel.replace("/", os.sep) is dropped in test_find_static_lib.py: Path
already accepts forward slashes on Windows.

Two files are left out on purpose. test_find_nvidia_binaries.py moves with
part 3, whose signature changes it depends on. test_search_steps.py is being
edited by #2489 (part 1), so converting it here would only create a conflict.

Left on the stdlib modules: glob.glob in test_find_nvidia_headers.py, which
expands an absolute pattern from the header catalog (Path.glob needs a base
dir, and the wildcard is not pinned to the last component); os.pathsep in
test_ctk_root_discovery.py, which builds PYTHONPATH, not a path; and os.sep in
test_utils_env_vars.py, which builds a trailing separator on purpose.

Signed-off-by: LeSingh1 <sshaurya914@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cuda.pathfinder Everything related to the cuda.pathfinder module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants