From 0728cf66c0d23b957251b17d5cc6ba87cdbbdbe8 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sat, 8 Aug 2026 18:50:59 -0700 Subject: [PATCH] fix(toolshed): honour the find() -1 sentinel when rewriting stub headers `_normalize_stub_headers` rewrites the OS path separator in a generated stub's first-line comment. It splits the file on the first newline: newline = data.find(b"\n") first_line = data[:newline] if newline != -1 else data ... stub.write_bytes(first_line.replace(b"\\", b"/") + data[newline:]) The first slice special-cases the -1 that `bytes.find` returns when there is no newline; the second does not. `data[-1:]` is the file's last byte, so for a stub that is a single line with no trailing newline the rewritten header gets a duplicate of its own final character appended: in : b'...stubgen-pyx from cuda_core\\cuda\\core\\x.pyx' out: b'...stubgen-pyx from cuda_core/cuda/core/x.pyxx' A `.pyi` with no trailing newline is a shape this repo tolerates on purpose: `.pyi` is excluded from the end-of-file-fixer hook in .pre-commit-config.yaml, so nothing adds one. And because this wrapper runs as a pre-commit *fixer* (the stubgen-pyx-cuda-core hook), the corrupted byte is written back into the working tree and committed. Slice the remainder with the same sentinel check. Adds tests under toolshed/tests/ covering the rewrite with and without a trailing newline, header-only stubs, and the cases that must be left byte-identical (already-POSIX headers, hand-written stubs containing backslashes, and empty files). --- .github/workflows/ci-nightly.yml | 2 +- toolshed/run_stubgen_pyx.py | 7 ++- toolshed/tests/test_run_stubgen_pyx.py | 61 ++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 toolshed/tests/test_run_stubgen_pyx.py diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 0188ebf2524..bf9d42e9f56 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -46,7 +46,7 @@ jobs: run: | python -m pip install pytest # Standalone CI tool tests; skip repo-root conftest.py (imports cuda.pathfinder). - python -m pytest -v --noconftest ci/tools/tests + python -m pytest -v --noconftest ci/tools/tests toolshed/tests find-wheels: runs-on: ubuntu-latest diff --git a/toolshed/run_stubgen_pyx.py b/toolshed/run_stubgen_pyx.py index 1a163ff0778..e758ba370cb 100644 --- a/toolshed/run_stubgen_pyx.py +++ b/toolshed/run_stubgen_pyx.py @@ -30,10 +30,15 @@ def _normalize_stub_headers(root: pathlib.Path) -> None: for stub in root.rglob("*.pyi"): data = stub.read_bytes() newline = data.find(b"\n") + # ``find`` returns -1 when the stub is a single line with no trailing + # newline. Both slices must honour that sentinel: ``data[-1:]`` is the + # file's last byte, so the rest-of-file slice would append a duplicate + # of it to the rewritten header. first_line = data[:newline] if newline != -1 else data + rest = data[newline:] if newline != -1 else b"" if not first_line.startswith(_HEADER_PREFIX) or b"\\" not in first_line: continue - stub.write_bytes(first_line.replace(b"\\", b"/") + data[newline:]) + stub.write_bytes(first_line.replace(b"\\", b"/") + rest) def main() -> int: diff --git a/toolshed/tests/test_run_stubgen_pyx.py b/toolshed/tests/test_run_stubgen_pyx.py new file mode 100644 index 00000000000..f61ac82b654 --- /dev/null +++ b/toolshed/tests/test_run_stubgen_pyx.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from run_stubgen_pyx import _HEADER_PREFIX, _normalize_stub_headers + +WINDOWS_HEADER = _HEADER_PREFIX + b" from cuda_core\\cuda\\core\\_device.pyx" +POSIX_HEADER = _HEADER_PREFIX + b" from cuda_core/cuda/core/_device.pyx" + + +def write_stub(tmp_path, content): + stub = tmp_path / "_device.pyi" + stub.write_bytes(content) + return stub + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize( + ("body", "note"), + [ + pytest.param(b"\n\nclass Device: ...\n", "header, blank line, body", id="body-with-trailing-newline"), + pytest.param(b"\nclass Device: ...", "body without a trailing newline", id="body-no-trailing-newline"), + pytest.param(b"\n", "header line only", id="header-only-with-newline"), + # `.pyi` is excluded from the end-of-file-fixer hook, so a stub with no + # trailing newline at all is a shape this repo tolerates -- and it is + # the one the -1 sentinel from `bytes.find` corrupted: `data[-1:]` + # appended a duplicate of the file's last byte to the header. + pytest.param(b"", "no trailing newline anywhere", id="header-only-no-newline"), + ], +) +def test_separator_is_rewritten_without_touching_anything_else(tmp_path, body, note): + stub = write_stub(tmp_path, WINDOWS_HEADER + body) + + _normalize_stub_headers(tmp_path) + + assert stub.read_bytes() == POSIX_HEADER + body, note + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize( + "content", + [ + pytest.param(POSIX_HEADER, id="already-posix-no-newline"), + pytest.param(POSIX_HEADER + b"\nclass Device: ...\n", id="already-posix"), + pytest.param(b"# hand-written stub\\with\\backslashes", id="not-a-generated-header"), + pytest.param(b"", id="empty-file"), + ], +) +def test_files_that_need_no_rewrite_are_left_byte_identical(tmp_path, content): + stub = write_stub(tmp_path, content) + + _normalize_stub_headers(tmp_path) + + assert stub.read_bytes() == content