From d825cec5f8990a8b83a3fe6d3bbbaa7023fcf613 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Tue, 4 Aug 2026 13:13:36 -0700 Subject: [PATCH 1/2] 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/2] 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