Skip to content

Commit 5e57f30

Browse files
committed
fix: add linter for thread-unsafe C API uses
1 parent 3989a6e commit 5e57f30

File tree

4 files changed

+100
-5
lines changed

4 files changed

+100
-5
lines changed

.github/workflows/mypy.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,21 @@ jobs:
7272
- name: Run Mypy
7373
run: |
7474
spin mypy
75+
# This job checks for suspicious C API usage that may break nogil compatibility.
76+
# It scans PR diffs for dangerous borrowed-referenced calls and fails if any are found.
77+
lint-capi:
78+
# To enable this workflow on a fork, comment out:
79+
if: github.repository == 'numpy/numpy'
80+
name: "C API Borrowed Ref Lint"
81+
runs-on: ubuntu-latest
82+
steps:
83+
- uses: actions/checkout@v3
84+
with:
85+
fetch-depth: 0
86+
submodules: recursive
87+
- name: Run API Borrowed Ref Linter
88+
run: |
89+
pip install -r requirements/build_requirements.txt
90+
pip install -r requirements/test_requirements.txt
91+
pip install ruff
92+
spin lint

numpy/_core/src/umath/ufunc_object.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,7 +1368,7 @@ _parse_axes_arg(PyUFuncObject *ufunc, int op_core_num_dims[], PyObject *axes,
13681368
* Get axes tuple for operand. If not a tuple already, make it one if
13691369
* there is only one axis (its content is checked later).
13701370
*/
1371-
op_axes_tuple = PyList_GET_ITEM(axes, iop);
1371+
op_axes_tuple = PyList_GET_ITEM(axes, iop); // noqa: borrowed-ref OK
13721372
if (PyTuple_Check(op_axes_tuple)) {
13731373
if (PyTuple_Size(op_axes_tuple) != op_ncore) {
13741374
/* must have been a tuple with too many entries. */
@@ -4939,7 +4939,7 @@ PyUFunc_RegisterLoopForDescr(PyUFuncObject *ufunc,
49394939
function, arg_typenums, data);
49404940

49414941
if (result == 0) {
4942-
cobj = PyDict_GetItemWithError(ufunc->userloops, key);
4942+
cobj = PyDict_GetItemWithError(ufunc->userloops, key); // noqa: borrowed-ref OK
49434943
if (cobj == NULL && PyErr_Occurred()) {
49444944
result = -1;
49454945
}
@@ -5070,7 +5070,7 @@ PyUFunc_RegisterLoopForType(PyUFuncObject *ufunc,
50705070
*/
50715071
int add_new_loop = 1;
50725072
for (Py_ssize_t j = 0; j < PyList_GET_SIZE(ufunc->_loops); j++) {
5073-
PyObject *item = PyList_GET_ITEM(ufunc->_loops, j);
5073+
PyObject *item = PyList_GET_ITEM(ufunc->_loops, j); // noqa: borrowed-ref OK
50745074
PyObject *existing_tuple = PyTuple_GET_ITEM(item, 0);
50755075

50765076
int cmp = PyObject_RichCompareBool(existing_tuple, signature_tuple, Py_EQ);
@@ -5112,7 +5112,7 @@ PyUFunc_RegisterLoopForType(PyUFuncObject *ufunc,
51125112
funcdata->nargs = 0;
51135113

51145114
/* Get entry for this user-defined type*/
5115-
cobj = PyDict_GetItemWithError(ufunc->userloops, key);
5115+
cobj = PyDict_GetItemWithError(ufunc->userloops, key); // noqa: borrowed-ref OK
51165116
if (cobj == NULL && PyErr_Occurred()) {
51175117
goto fail;
51185118
}

tools/ci/check_c_api_usage.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# List of suspicious function calls:
5+
SUSPICIOUS_FUNCS=(
6+
"PyList_GetItem"
7+
"PyList_GET_ITEM"
8+
"PyDict_GetItem"
9+
"PyDict_GetItemWithError"
10+
"PyDict_Next"
11+
"PyDict_GetItemString"
12+
"_PyDict_GetItemStringWithError"
13+
"PySequence_Fast"
14+
)
15+
16+
# Find all C/C++ source files in the repo
17+
# ALL_FILES=$(find . -type f \( -name "*.c" -o -name "*.h" -o -name "*.c.src" -o -name "*.cpp" \))
18+
ALL_FILES=$(find numpy -type f \( -name "*.c" -o -name "*.h" -o -name "*.c.src" -o -name "*.cpp" \))
19+
20+
# For debugging: print out file count
21+
echo "Scanning $(echo "$ALL_FILES" | wc -l) C/C++ source files..."
22+
23+
# Prepare a result file
24+
mkdir -p .tmp
25+
OUTPUT=$(mktemp .tmp/c_api_usage_report.XXXXXX.txt)
26+
echo -e "Running Suspicious C API usage report workflow...\n" > $OUTPUT
27+
28+
FAIL=0
29+
30+
# Scan each changed file
31+
for file in $ALL_FILES; do
32+
33+
for func in "${SUSPICIOUS_FUNCS[@]}"; do
34+
# -n : show line number
35+
# -P : perl-style boundaries
36+
# (?<!\w): check - no letter/number/underscore before
37+
# (?!\w) : check - no letter/number/underscore after
38+
matches=$(grep -n -P "(?<!\w)$func(?!\w)" "$file" || true)
39+
40+
# Check each match for 'noqa'
41+
if [[ -n "$matches" ]]; then
42+
while IFS= read -r line; do
43+
if [[ "$line" != *"noqa: borrowed-ref OK"* ]]; then
44+
echo "Found suspcious call to $func in file: $file" >> "$OUTPUT"
45+
echo " -> $line" >> "$OUTPUT"
46+
echo "Recommendation:" >> "$OUTPUT"
47+
echo "If this use is intentional and safe, add '// noqa: borrowed-ref OK' on the same line to silence this warning." >> "$OUTPUT"
48+
echo "Otherwise, consider replacing $func with a thread-safe API function." >> "$OUTPUT"
49+
echo "" >> "$OUTPUT"
50+
FAIL=1
51+
fi
52+
done <<< "$matches"
53+
fi
54+
done
55+
done
56+
57+
if [[ $FAIL -eq 1 ]]; then
58+
echo "C API borrow-ref linter found issues."
59+
else
60+
echo "C API borrow-ref linter found no issues."
61+
fi
62+
63+
cat "$OUTPUT"
64+
exit "$FAIL"

tools/linter.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,20 @@ def run_lint(self, fix: bool) -> None:
3636

3737
errors and print(errors)
3838

39-
sys.exit(retcode)
39+
# Running borrowed ref checker
40+
print("Running C API borrow-reference linter...")
41+
borrowed_ref_script = os.path.join(self.repository_root, "tools", "ci", "check_c_api_usage.sh")
42+
borrowed_res = subprocess.run(
43+
["bash", borrowed_ref_script],
44+
stdout=subprocess.PIPE,
45+
stderr=subprocess.STDOUT,
46+
encoding="utf-8",
47+
)
48+
print(borrowed_res.stdout)
49+
50+
# Exit with non-zero if either Ruff or C API check fails
51+
final_code = retcode or borrowed_res.returncode
52+
sys.exit(final_code)
4053

4154

4255
if __name__ == "__main__":

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy