From d825cec5f8990a8b83a3fe6d3bbbaa7023fcf613 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Tue, 4 Aug 2026 13:13:36 -0700 Subject: [PATCH 1/3] Migrate toolshed and ci helper scripts from os.path to pathlib Part 7 of the series proposed in #2410. Path joining and filesystem predicates in the toolshed and ci/tools helper scripts now go through pathlib. glob.glob in dump_cutile_b64.py becomes Path.glob, with the mtime key reading Path.stat(). Kept on os.path, with a comment where it is not obvious: - os.path.abspath in build_static_bitcode_input.py, since sys.path wants a str and Path.absolute() does not normalize. - os.path.isfile in check_generated_file_seals.py. That guard exists to skip anything that is not a readable regular file, and Path.is_file() is not a drop-in: it propagates OSError for errnos outside pathlib's ignore list (EACCES, ENAMETOOLONG) where os.path.isfile returns False. - os.path.normpath in check_spdx.py, which already carries its own comment. The plan on #2410 also listed a root conftest.py; there is no such file. The three conftest.py files live under cuda_pathfinder, cuda_core and cuda_bindings, and none of them use os.path. Verified locally: ci/tools/tests/test_check_release_notes.py passes (42 tests), and check_spdx.py and check_generated_file_seals.py produce output identical to the pre-change scripts when run over every tracked .py file. Signed-off-by: LeSingh1 --- ci/tools/check_release_notes.py | 11 ++++++----- ci/tools/tests/test_check_release_notes.py | 4 ++-- toolshed/build_static_bitcode_input.py | 7 +++---- toolshed/check_generated_file_seals.py | 2 ++ toolshed/check_spdx.py | 5 ++--- toolshed/dump_cutile_b64.py | 6 +++--- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/ci/tools/check_release_notes.py b/ci/tools/check_release_notes.py index 75d2c9871f0..53b1bf29799 100644 --- a/ci/tools/check_release_notes.py +++ b/ci/tools/check_release_notes.py @@ -18,6 +18,7 @@ import os import re import sys +from pathlib import Path COMPONENT_TO_PACKAGE: dict[str, str] = { "cuda-core": "cuda_core", @@ -63,7 +64,7 @@ def is_post_release(version: str) -> bool: def load_backport_branch(repo_root: str = ".") -> str | None: - path = os.path.join(repo_root, "ci", "versions.yml") + path = Path(repo_root, "ci", "versions.yml") try: with open(path, encoding="utf-8") as f: for line in f: @@ -85,7 +86,7 @@ def is_backport_version(version: str, backport_branch: str) -> bool: def notes_path(package: str, version: str) -> str: - return os.path.join(package, "docs", "source", "release", f"{version}-notes.rst") + return str(Path(package, "docs", "source", "release", f"{version}-notes.rst")) def check_release_notes(git_tag: str, component: str, repo_root: str = ".") -> list[tuple[str, str]]: @@ -105,10 +106,10 @@ def check_release_notes(git_tag: str, component: str, repo_root: str = ".") -> l return [] path = notes_path(COMPONENT_TO_PACKAGE[component], version) - full = os.path.join(repo_root, path) - if not os.path.isfile(full): + full = Path(repo_root, path) + if not full.is_file(): return [(path, "missing")] - if os.path.getsize(full) == 0: + if full.stat().st_size == 0: return [(path, "empty")] return [] diff --git a/ci/tools/tests/test_check_release_notes.py b/ci/tools/tests/test_check_release_notes.py index 4f65404eed5..fc2124c680b 100644 --- a/ci/tools/tests/test_check_release_notes.py +++ b/ci/tools/tests/test_check_release_notes.py @@ -3,10 +3,10 @@ from __future__ import annotations -import os import sys +from pathlib import Path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +sys.path.insert(0, str(Path(__file__).parent.parent)) from check_release_notes import ( check_release_notes, is_post_release, diff --git a/toolshed/build_static_bitcode_input.py b/toolshed/build_static_bitcode_input.py index e2400100dde..603394c2b5f 100755 --- a/toolshed/build_static_bitcode_input.py +++ b/toolshed/build_static_bitcode_input.py @@ -17,6 +17,7 @@ import os import sys import textwrap +from pathlib import Path import llvmlite.binding # HINT: pip install llvmlite @@ -24,10 +25,8 @@ def get_minimal_nvvmir_txt_template(): - cuda_bindings_tests_dir = os.path.normpath("cuda_bindings/tests") - assert os.path.isdir(cuda_bindings_tests_dir), ( - "Please run this helper script from the cuda-python top-level directory." - ) + cuda_bindings_tests_dir = Path("cuda_bindings/tests") + assert cuda_bindings_tests_dir.is_dir(), "Please run this helper script from the cuda-python top-level directory." sys.path.insert(0, os.path.abspath(cuda_bindings_tests_dir)) import test_nvvm diff --git a/toolshed/check_generated_file_seals.py b/toolshed/check_generated_file_seals.py index 1a9c45de61b..32892fed09c 100644 --- a/toolshed/check_generated_file_seals.py +++ b/toolshed/check_generated_file_seals.py @@ -142,6 +142,8 @@ def main(args): returncode = 0 for filepath in args: + # os.path.isfile, not Path.is_file: this skips anything that is not a + # readable regular file, and Path.is_file raises on e.g. EACCES. if not os.path.isfile(filepath): continue if not validate_generated_file_seal(filepath, previously_sealed_paths): diff --git a/toolshed/check_spdx.py b/toolshed/check_spdx.py index a2d0c041546..710aa14ae1b 100644 --- a/toolshed/check_spdx.py +++ b/toolshed/check_spdx.py @@ -2,11 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 import datetime -import os import re import subprocess import sys -from pathlib import PureWindowsPath +from pathlib import Path, PureWindowsPath import pathspec @@ -39,7 +38,7 @@ def load_spdx_ignore(): - if os.path.exists(SPDX_IGNORE_FILENAME): + if Path(SPDX_IGNORE_FILENAME).exists(): with open(SPDX_IGNORE_FILENAME, encoding="utf-8") as f: lines = f.readlines() else: diff --git a/toolshed/dump_cutile_b64.py b/toolshed/dump_cutile_b64.py index 422bf95232b..8e58e452e02 100644 --- a/toolshed/dump_cutile_b64.py +++ b/toolshed/dump_cutile_b64.py @@ -9,9 +9,9 @@ """ import base64 -import glob import os import sys +from pathlib import Path import cupy @@ -54,13 +54,13 @@ def main(): raise # Find the .cutile file in current directory - cutile_files = glob.glob("./*.cutile") + cutile_files = list(Path().glob("*.cutile")) if not cutile_files: print("No .cutile file found in current directory", file=sys.stderr) sys.exit(1) # Use the most recently modified one if multiple exist - cutile_path = max(cutile_files, key=os.path.getmtime) + cutile_path = max(cutile_files, key=lambda path: path.stat().st_mtime) # Read the binary content with open(cutile_path, "rb") as f: From 438575714f890b6bda695b1e5d6905aca6001133 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Fri, 7 Aug 2026 16:06:09 -0700 Subject: [PATCH 2/3] Return Path from notes_path; use Path.is_file in seal checker Per review: treat these helper scripts as private, so notes_path can return Path and drop the str/Path round-trip at its call site. Accept the behavioral change from os.path.isfile to Path.is_file in check_generated_file_seals. --- ci/tools/check_release_notes.py | 15 +++++++++------ toolshed/check_generated_file_seals.py | 5 +---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ci/tools/check_release_notes.py b/ci/tools/check_release_notes.py index 53b1bf29799..3d6bd9f8477 100644 --- a/ci/tools/check_release_notes.py +++ b/ci/tools/check_release_notes.py @@ -85,13 +85,16 @@ def is_backport_version(version: str, backport_branch: str) -> bool: return version == backport_branch -def notes_path(package: str, version: str) -> str: - return str(Path(package, "docs", "source", "release", f"{version}-notes.rst")) +def notes_path(package: str, version: str) -> Path: + return Path(package, "docs", "source", "release", f"{version}-notes.rst") -def check_release_notes(git_tag: str, component: str, repo_root: str = ".") -> list[tuple[str, str]]: +def check_release_notes(git_tag: str, component: str, repo_root: str = ".") -> list[tuple[str | Path, str]]: """Return a list of (path, reason) for missing or empty release notes. + ``path`` is the repo-relative notes path, or a ```` naming the + offending argument when the tag or component itself is the problem. + Returns an empty list when notes are present and non-empty, or when the tag is a .post release (no new notes required). """ @@ -106,7 +109,7 @@ def check_release_notes(git_tag: str, component: str, repo_root: str = ".") -> l return [] path = notes_path(COMPONENT_TO_PACKAGE[component], version) - full = Path(repo_root, path) + full = Path(repo_root) / path if not full.is_file(): return [(path, "missing")] if full.stat().st_size == 0: @@ -124,7 +127,7 @@ def write_step_summary(message: str) -> None: f.write("\n") -def warn_missing_backport_notes(git_tag: str, component: str, problems: list[tuple[str, str]]) -> None: +def warn_missing_backport_notes(git_tag: str, component: str, problems: list[tuple[str | Path, str]]) -> None: print(f"WARNING: missing or empty release notes for backport tag {git_tag}:") summary_lines = [ "## Release Notes Reminder", @@ -149,7 +152,7 @@ def validate_backport_decision( backport_git_tag: str, backport_branch: str | None, repo_root: str, -) -> tuple[int | None, list[tuple[str, str]]]: +) -> tuple[int | None, list[tuple[str | Path, str]]]: if component not in BACKPORT_PLANNING_COMPONENTS or is_post_release(version): return None, [] diff --git a/toolshed/check_generated_file_seals.py b/toolshed/check_generated_file_seals.py index 32892fed09c..71fc066af33 100644 --- a/toolshed/check_generated_file_seals.py +++ b/toolshed/check_generated_file_seals.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 import hashlib -import os import re import subprocess import sys @@ -142,9 +141,7 @@ def main(args): returncode = 0 for filepath in args: - # os.path.isfile, not Path.is_file: this skips anything that is not a - # readable regular file, and Path.is_file raises on e.g. EACCES. - if not os.path.isfile(filepath): + if not Path(filepath).is_file(): continue if not validate_generated_file_seal(filepath, previously_sealed_paths): returncode = 1 From ba3ce084984402532aa634ac7db4e3826e2bbdc5 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sun, 9 Aug 2026 14:25:21 -0700 Subject: [PATCH 3/3] Review: thread Path through check_release_notes, drop remaining os.path Follow-up to mdboom's review. - repo_root is now a Path end to end: load_backport_branch, check_release_notes and validate_backport_decision take Path, and --repo-root parses with type=Path. That removes the Path(repo_root) re-wrap inside the functions and the 19 str(tmp_path) conversions the tests needed to call them. The five main() argv lists keep str(): those are command-line strings, which argparse then turns back into a Path. - build_static_bitcode_input: the last os.path use (os.path.abspath) becomes Path.resolve(); the os import is now unused and is dropped. --- ci/tools/check_release_notes.py | 12 ++++++------ ci/tools/tests/test_check_release_notes.py | 22 +++++++++++----------- toolshed/build_static_bitcode_input.py | 3 +-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/ci/tools/check_release_notes.py b/ci/tools/check_release_notes.py index 3d6bd9f8477..1c99ddb019a 100644 --- a/ci/tools/check_release_notes.py +++ b/ci/tools/check_release_notes.py @@ -63,8 +63,8 @@ def is_post_release(version: str) -> bool: return ".post" in version -def load_backport_branch(repo_root: str = ".") -> str | None: - path = Path(repo_root, "ci", "versions.yml") +def load_backport_branch(repo_root: Path = Path(".")) -> str | None: + path = repo_root / "ci" / "versions.yml" try: with open(path, encoding="utf-8") as f: for line in f: @@ -89,7 +89,7 @@ def notes_path(package: str, version: str) -> Path: return Path(package, "docs", "source", "release", f"{version}-notes.rst") -def check_release_notes(git_tag: str, component: str, repo_root: str = ".") -> list[tuple[str | Path, str]]: +def check_release_notes(git_tag: str, component: str, repo_root: Path = Path(".")) -> list[tuple[str | Path, str]]: """Return a list of (path, reason) for missing or empty release notes. ``path`` is the repo-relative notes path, or a ```` naming the @@ -109,7 +109,7 @@ def check_release_notes(git_tag: str, component: str, repo_root: str = ".") -> l return [] path = notes_path(COMPONENT_TO_PACKAGE[component], version) - full = Path(repo_root) / path + full = repo_root / path if not full.is_file(): return [(path, "missing")] if full.stat().st_size == 0: @@ -151,7 +151,7 @@ def validate_backport_decision( version: str, backport_git_tag: str, backport_branch: str | None, - repo_root: str, + repo_root: Path, ) -> tuple[int | None, list[tuple[str | Path, str]]]: if component not in BACKPORT_PLANNING_COMPONENTS or is_post_release(version): return None, [] @@ -209,7 +209,7 @@ def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--git-tag", required=True) parser.add_argument("--component", required=True, choices=list(COMPONENT_TO_PACKAGE)) - parser.add_argument("--repo-root", default=".") + parser.add_argument("--repo-root", default=Path("."), type=Path) parser.add_argument("--backport-git-tag", default="") parser.add_argument("--backport-branch", default="") args = parser.parse_args(argv) diff --git a/ci/tools/tests/test_check_release_notes.py b/ci/tools/tests/test_check_release_notes.py index fc2124c680b..e08eac6610d 100644 --- a/ci/tools/tests/test_check_release_notes.py +++ b/ci/tools/tests/test_check_release_notes.py @@ -87,43 +87,43 @@ def _make_notes(self, tmp_path, pkg, version, content="Release notes."): def test_present_and_nonempty(self, tmp_path): self._make_notes(tmp_path, "cuda_core", "0.7.0") - problems = check_release_notes("cuda-core-v0.7.0", "cuda-core", str(tmp_path)) + problems = check_release_notes("cuda-core-v0.7.0", "cuda-core", tmp_path) assert problems == [] def test_missing(self, tmp_path): - problems = check_release_notes("cuda-core-v0.7.0", "cuda-core", str(tmp_path)) + problems = check_release_notes("cuda-core-v0.7.0", "cuda-core", tmp_path) assert len(problems) == 1 assert problems[0][1] == "missing" def test_empty(self, tmp_path): self._make_notes(tmp_path, "cuda_core", "0.7.0", content="") - problems = check_release_notes("cuda-core-v0.7.0", "cuda-core", str(tmp_path)) + problems = check_release_notes("cuda-core-v0.7.0", "cuda-core", tmp_path) assert len(problems) == 1 assert problems[0][1] == "empty" def test_post_release_skipped(self, tmp_path): - problems = check_release_notes("v12.6.2.post1", "cuda-bindings", str(tmp_path)) + problems = check_release_notes("v12.6.2.post1", "cuda-bindings", tmp_path) assert problems == [] def test_invalid_tag(self, tmp_path): - problems = check_release_notes("not-a-tag", "cuda-core", str(tmp_path)) + problems = check_release_notes("not-a-tag", "cuda-core", tmp_path) assert len(problems) == 1 assert "cannot parse" in problems[0][1] def test_component_prefix_mismatch(self, tmp_path): # Pass a cuda-core tag with component=cuda-pathfinder; must be rejected. - problems = check_release_notes("cuda-core-v0.7.0", "cuda-pathfinder", str(tmp_path)) + problems = check_release_notes("cuda-core-v0.7.0", "cuda-pathfinder", tmp_path) assert len(problems) == 1 assert "cannot parse" in problems[0][1] def test_unknown_component(self, tmp_path): - problems = check_release_notes("v13.1.0", "bogus", str(tmp_path)) + problems = check_release_notes("v13.1.0", "bogus", tmp_path) assert len(problems) == 1 assert "unknown component" in problems[0][1] def test_plain_v_tag(self, tmp_path): self._make_notes(tmp_path, "cuda_python", "13.1.0") - problems = check_release_notes("v13.1.0", "cuda-python", str(tmp_path)) + problems = check_release_notes("v13.1.0", "cuda-python", tmp_path) assert problems == [] @@ -133,17 +133,17 @@ def test_from_versions_yml(self, tmp_path): d.mkdir(parents=True) (d / "versions.yml").write_text('backport_branch: "12.9.x"\n') - assert load_backport_branch(str(tmp_path)) == "12.9.x" + assert load_backport_branch(tmp_path) == "12.9.x" def test_from_github_ref_name_for_legacy_backport_branch(self, tmp_path, monkeypatch): monkeypatch.setenv("GITHUB_REF_NAME", "12.9.x") - assert load_backport_branch(str(tmp_path)) == "12.9.x" + assert load_backport_branch(tmp_path) == "12.9.x" def test_ignores_non_backport_github_ref_name(self, tmp_path, monkeypatch): monkeypatch.setenv("GITHUB_REF_NAME", "main") - assert load_backport_branch(str(tmp_path)) is None + assert load_backport_branch(tmp_path) is None class TestMain: diff --git a/toolshed/build_static_bitcode_input.py b/toolshed/build_static_bitcode_input.py index 603394c2b5f..622cba64e8c 100755 --- a/toolshed/build_static_bitcode_input.py +++ b/toolshed/build_static_bitcode_input.py @@ -14,7 +14,6 @@ """ import binascii -import os import sys import textwrap from pathlib import Path @@ -27,7 +26,7 @@ def get_minimal_nvvmir_txt_template(): cuda_bindings_tests_dir = Path("cuda_bindings/tests") assert cuda_bindings_tests_dir.is_dir(), "Please run this helper script from the cuda-python top-level directory." - sys.path.insert(0, os.path.abspath(cuda_bindings_tests_dir)) + sys.path.insert(0, str(cuda_bindings_tests_dir.resolve())) import test_nvvm return test_nvvm.MINIMAL_NVVMIR_TXT_TEMPLATE