Skip to content

Commit 8df4566

Browse files
eendebakptclaudeseberg
authored
TST: Add tests for PyUFunc_ReplaceLoopBySignature (numpy#31097)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net>
1 parent 563c604 commit 8df4566

3 files changed

Lines changed: 96 additions & 0 deletions

File tree

numpy/_core/_umath_tests.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ def test_signature(
3030
# undocumented
3131
def test_dispatch() -> _TestDispatchResult: ...
3232

33+
# undocumented test helpers for PyUFunc_ReplaceLoopBySignature
34+
def replace_loop(ufunc: np.ufunc, /) -> object: ...
35+
def restore_loop(ufunc: np.ufunc, capsule: object, /) -> None: ...
36+
3337
# undocumented ufuncs and gufuncs
3438
always_error: Final[_UFunc_Nin2_Nout1[L["always_error"], L[1], None]] = ...
3539
always_error_unary: Final[_UFunc_Nin1_Nout1[L["always_error_unary"], L[1], None]] = ...

numpy/_core/src/umath/_umath_tests.c.src

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,72 @@ static void *const conv1d_full_data[] = {NULL};
857857
static const char conv1d_full_typecodes[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE};
858858

859859

860+
/*
861+
* Test helpers for PyUFunc_ReplaceLoopBySignature.
862+
*
863+
* _constant42_loop: unary float64 loop that always writes 42.0.
864+
*/
865+
static void
866+
_constant42_loop(char **args, npy_intp const *dimensions,
867+
npy_intp const *steps, void *NPY_UNUSED(data))
868+
{
869+
npy_intp n = dimensions[0];
870+
char *out = args[1];
871+
npy_intp out_step = steps[1];
872+
for (npy_intp i = 0; i < n; i++) {
873+
*(double *)out = 42.0;
874+
out += out_step;
875+
}
876+
}
877+
878+
/*
879+
* replace_loop(ufunc): Replace the dd->d loop with _constant42_loop.
880+
* Only works for unary ufuncs. Returns a capsule holding the old loop.
881+
*/
882+
static PyObject *
883+
UMath_Tests_replace_loop(PyObject *NPY_UNUSED(dummy), PyObject *args)
884+
{
885+
PyUFuncObject *ufunc;
886+
if (!PyArg_ParseTuple(args, "O!", &PyUFunc_Type, &ufunc)) {
887+
return NULL;
888+
}
889+
if (ufunc->nin != 1 || ufunc->nout != 1) {
890+
PyErr_SetString(PyExc_ValueError,
891+
"replace_loop only supports unary ufuncs");
892+
return NULL;
893+
}
894+
int signature[2] = {NPY_DOUBLE, NPY_DOUBLE};
895+
PyUFuncGenericFunction oldfunc = NULL;
896+
if (PyUFunc_ReplaceLoopBySignature(
897+
ufunc, _constant42_loop, signature, &oldfunc) < 0) {
898+
PyErr_SetString(PyExc_RuntimeError,
899+
"failed to find a float64 loop");
900+
return NULL;
901+
}
902+
return PyCapsule_New((void *)oldfunc, "oldfunc", NULL);
903+
}
904+
905+
/*
906+
* restore_loop(ufunc, capsule): Restore the loop saved by replace_loop.
907+
*/
908+
static PyObject *
909+
UMath_Tests_restore_loop(PyObject *NPY_UNUSED(dummy), PyObject *args)
910+
{
911+
PyUFuncObject *ufunc;
912+
PyObject *capsule;
913+
if (!PyArg_ParseTuple(args, "O!O", &PyUFunc_Type, &ufunc, &capsule)) {
914+
return NULL;
915+
}
916+
PyUFuncGenericFunction oldfunc = (PyUFuncGenericFunction)
917+
PyCapsule_GetPointer(capsule, "oldfunc");
918+
if (oldfunc == NULL) {
919+
return NULL;
920+
}
921+
int signature[2] = {NPY_DOUBLE, NPY_DOUBLE};
922+
PyUFunc_ReplaceLoopBySignature(ufunc, oldfunc, signature, NULL);
923+
Py_RETURN_NONE;
924+
}
925+
860926
static PyMethodDef UMath_TestsMethods[] = {
861927
{"test_signature", UMath_Tests_test_signature, METH_VARARGS,
862928
"Test signature parsing of ufunc. \n"
@@ -865,6 +931,12 @@ static PyMethodDef UMath_TestsMethods[] = {
865931
"internals. \n",
866932
},
867933
{"test_dispatch", UMath_Tests_test_dispatch, METH_NOARGS, NULL},
934+
{"replace_loop", UMath_Tests_replace_loop, METH_VARARGS,
935+
"Replace the float64 loop of a ufunc with one that outputs 42.0.\n"
936+
"Returns a capsule holding the old loop for restore_loop().\n"},
937+
{"restore_loop", UMath_Tests_restore_loop, METH_VARARGS,
938+
"Restore a ufunc loop previously replaced by replace_loop().\n"
939+
"Arguments: ufunc, capsule\n"},
868940
{NULL, NULL, 0, NULL} /* Sentinel */
869941
};
870942

numpy/_core/tests/test_umath.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5111,6 +5111,26 @@ def test_bad_legacy_gufunc_silent_errors(x1):
51115111
ncu_tests.always_error_gufunc(x1, 0.0)
51125112

51135113

5114+
class TestReplaceLoopBySignature:
5115+
"""Tests for PyUFunc_ReplaceLoopBySignature C API."""
5116+
5117+
@pytest.mark.thread_unsafe(reason="modifies ufunc within test")
5118+
def test_replace_loop(self):
5119+
# Call the ufunc first to populate any internal dispatch caches,
5120+
# then replace the float64 loop with one that outputs 42.0,
5121+
# verify the replacement is used, and restore the original.
5122+
a = np.array([1.0, 2.0, 3.0])
5123+
assert_array_equal(np.negative(a), [-1.0, -2.0, -3.0])
5124+
5125+
saved = ncu_tests.replace_loop(np.negative)
5126+
try:
5127+
assert_array_equal(np.negative(a), [42.0, 42.0, 42.0])
5128+
finally:
5129+
ncu_tests.restore_loop(np.negative, saved)
5130+
5131+
assert_array_equal(np.negative(a), [-1.0, -2.0, -3.0])
5132+
5133+
51145134
class TestAddDocstring:
51155135
@pytest.mark.skipif(sys.flags.optimize == 2, reason="Python running -OO")
51165136
def test_add_same_docstring(self):

0 commit comments

Comments
 (0)