Skip to content

Commit 0728cf6

Browse files
committed
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).
1 parent 3bd069a commit 0728cf6

3 files changed

Lines changed: 68 additions & 2 deletions

File tree

.github/workflows/ci-nightly.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
run: |
4747
python -m pip install pytest
4848
# Standalone CI tool tests; skip repo-root conftest.py (imports cuda.pathfinder).
49-
python -m pytest -v --noconftest ci/tools/tests
49+
python -m pytest -v --noconftest ci/tools/tests toolshed/tests
5050
5151
find-wheels:
5252
runs-on: ubuntu-latest

toolshed/run_stubgen_pyx.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@ def _normalize_stub_headers(root: pathlib.Path) -> None:
3030
for stub in root.rglob("*.pyi"):
3131
data = stub.read_bytes()
3232
newline = data.find(b"\n")
33+
# ``find`` returns -1 when the stub is a single line with no trailing
34+
# newline. Both slices must honour that sentinel: ``data[-1:]`` is the
35+
# file's last byte, so the rest-of-file slice would append a duplicate
36+
# of it to the rewritten header.
3337
first_line = data[:newline] if newline != -1 else data
38+
rest = data[newline:] if newline != -1 else b""
3439
if not first_line.startswith(_HEADER_PREFIX) or b"\\" not in first_line:
3540
continue
36-
stub.write_bytes(first_line.replace(b"\\", b"/") + data[newline:])
41+
stub.write_bytes(first_line.replace(b"\\", b"/") + rest)
3742

3843

3944
def main() -> int:
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from __future__ import annotations
5+
6+
import os
7+
import sys
8+
9+
import pytest
10+
11+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
12+
from run_stubgen_pyx import _HEADER_PREFIX, _normalize_stub_headers
13+
14+
WINDOWS_HEADER = _HEADER_PREFIX + b" from cuda_core\\cuda\\core\\_device.pyx"
15+
POSIX_HEADER = _HEADER_PREFIX + b" from cuda_core/cuda/core/_device.pyx"
16+
17+
18+
def write_stub(tmp_path, content):
19+
stub = tmp_path / "_device.pyi"
20+
stub.write_bytes(content)
21+
return stub
22+
23+
24+
@pytest.mark.agent_authored(model="claude-opus-5")
25+
@pytest.mark.parametrize(
26+
("body", "note"),
27+
[
28+
pytest.param(b"\n\nclass Device: ...\n", "header, blank line, body", id="body-with-trailing-newline"),
29+
pytest.param(b"\nclass Device: ...", "body without a trailing newline", id="body-no-trailing-newline"),
30+
pytest.param(b"\n", "header line only", id="header-only-with-newline"),
31+
# `.pyi` is excluded from the end-of-file-fixer hook, so a stub with no
32+
# trailing newline at all is a shape this repo tolerates -- and it is
33+
# the one the -1 sentinel from `bytes.find` corrupted: `data[-1:]`
34+
# appended a duplicate of the file's last byte to the header.
35+
pytest.param(b"", "no trailing newline anywhere", id="header-only-no-newline"),
36+
],
37+
)
38+
def test_separator_is_rewritten_without_touching_anything_else(tmp_path, body, note):
39+
stub = write_stub(tmp_path, WINDOWS_HEADER + body)
40+
41+
_normalize_stub_headers(tmp_path)
42+
43+
assert stub.read_bytes() == POSIX_HEADER + body, note
44+
45+
46+
@pytest.mark.agent_authored(model="claude-opus-5")
47+
@pytest.mark.parametrize(
48+
"content",
49+
[
50+
pytest.param(POSIX_HEADER, id="already-posix-no-newline"),
51+
pytest.param(POSIX_HEADER + b"\nclass Device: ...\n", id="already-posix"),
52+
pytest.param(b"# hand-written stub\\with\\backslashes", id="not-a-generated-header"),
53+
pytest.param(b"", id="empty-file"),
54+
],
55+
)
56+
def test_files_that_need_no_rewrite_are_left_byte_identical(tmp_path, content):
57+
stub = write_stub(tmp_path, content)
58+
59+
_normalize_stub_headers(tmp_path)
60+
61+
assert stub.read_bytes() == content

0 commit comments

Comments
 (0)