Skip to content

Commit 4df0561

Browse files
authored
Merge pull request numpy#27735 from mtsokol/ufunc-object-dict
ENH: Add a `__dict__` to ufunc objects and allow overriding `__doc__`
2 parents 7bfdc8c + 42b44cc commit 4df0561

File tree

10 files changed

+101
-6
lines changed

10 files changed

+101
-6
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* ``_add_newdoc_ufunc`` is now deprecated. ``ufunc.__doc__ = newdoc`` should
2+
be used instead.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
* UFuncs now support ``__dict__`` attribute and allow overriding ``__doc__``
2+
(either directly or via ``ufunc.__dict__["__doc__"]``). ``__dict__`` can be
3+
used to also override other properties, such as ``__module__`` or
4+
``__qualname__``.

numpy/_core/include/numpy/ufuncobject.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,10 @@ typedef struct _tagPyUFuncObject {
170170
* with the dtypes for the inputs and outputs.
171171
*/
172172
PyUFunc_TypeResolutionFunc *type_resolver;
173-
/* Was the legacy loop resolver */
174-
void *reserved2;
173+
174+
/* A dictionary to monkeypatch ufuncs */
175+
PyObject *dict;
176+
175177
/*
176178
* This was blocked off to be the "new" inner loop selector in 1.7,
177179
* but this was never implemented. (This is also why the above

numpy/_core/src/multiarray/npy_static_data.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ intern_strings(void)
6363
INTERN_STRING(__dlpack__, "__dlpack__");
6464
INTERN_STRING(pyvals_name, "UFUNC_PYVALS_NAME");
6565
INTERN_STRING(legacy, "legacy");
66+
INTERN_STRING(__doc__, "__doc__");
6667
return 0;
6768
}
6869

numpy/_core/src/multiarray/npy_static_data.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ typedef struct npy_interned_str_struct {
3838
PyObject *__dlpack__;
3939
PyObject *pyvals_name;
4040
PyObject *legacy;
41+
PyObject *__doc__;
4142
} npy_interned_str_struct;
4243

4344
/*

numpy/_core/src/umath/_struct_ufunc_tests.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ PyMODINIT_FUNC PyInit__struct_ufunc_tests(void)
133133
import_umath();
134134

135135
add_triplet = PyUFunc_FromFuncAndData(NULL, NULL, NULL, 0, 2, 1,
136-
PyUFunc_None, "add_triplet",
137-
"add_triplet_docstring", 0);
136+
PyUFunc_None, "add_triplet",
137+
NULL, 0);
138138

139139
dtype_dict = Py_BuildValue("[(s, s), (s, s), (s, s)]",
140140
"f0", "u8", "f1", "u8", "f2", "u8");

numpy/_core/src/umath/ufunc_object.c

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4687,6 +4687,7 @@ PyUFunc_FromFuncAndDataAndSignatureAndIdentity(PyUFuncGenericFunction *func, voi
46874687
ufunc->core_signature = NULL;
46884688
ufunc->core_enabled = 0;
46894689
ufunc->obj = NULL;
4690+
ufunc->dict = NULL;
46904691
ufunc->core_num_dims = NULL;
46914692
ufunc->core_num_dim_ix = 0;
46924693
ufunc->core_offsets = NULL;
@@ -4771,6 +4772,11 @@ PyUFunc_FromFuncAndDataAndSignatureAndIdentity(PyUFuncGenericFunction *func, voi
47714772
return NULL;
47724773
}
47734774
}
4775+
ufunc->dict = PyDict_New();
4776+
if (ufunc->dict == NULL) {
4777+
Py_DECREF(ufunc);
4778+
return NULL;
4779+
}
47744780
/*
47754781
* TODO: I tried adding a default promoter here (either all object for
47764782
* some special cases, or all homogeneous). Those are reasonable
@@ -5177,6 +5183,7 @@ ufunc_dealloc(PyUFuncObject *ufunc)
51775183
Py_DECREF(ufunc->identity_value);
51785184
}
51795185
Py_XDECREF(ufunc->obj);
5186+
Py_XDECREF(ufunc->dict);
51805187
Py_XDECREF(ufunc->_loops);
51815188
if (ufunc->_dispatch_cache != NULL) {
51825189
PyArrayIdentityHash_Dealloc(ufunc->_dispatch_cache);
@@ -5197,6 +5204,7 @@ ufunc_traverse(PyUFuncObject *self, visitproc visit, void *arg)
51975204
if (self->identity == PyUFunc_IdentityValue) {
51985205
Py_VISIT(self->identity_value);
51995206
}
5207+
Py_VISIT(self->dict);
52005208
return 0;
52015209
}
52025210

@@ -6411,6 +6419,15 @@ ufunc_get_doc(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
64116419
{
64126420
PyObject *doc;
64136421

6422+
// If there is a __doc__ in the instance __dict__, use it.
6423+
int result = PyDict_GetItemRef(ufunc->dict, npy_interned_str.__doc__, &doc);
6424+
if (result == -1) {
6425+
return NULL;
6426+
}
6427+
else if (result == 1) {
6428+
return doc;
6429+
}
6430+
64146431
if (npy_cache_import_runtime(
64156432
"numpy._core._internal", "_ufunc_doc_signature_formatter",
64166433
&npy_runtime_imports._ufunc_doc_signature_formatter) == -1) {
@@ -6434,6 +6451,15 @@ ufunc_get_doc(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
64346451
return doc;
64356452
}
64366453

6454+
static int
6455+
ufunc_set_doc(PyUFuncObject *ufunc, PyObject *doc, void *NPY_UNUSED(ignored))
6456+
{
6457+
if (doc == NULL) {
6458+
return PyDict_DelItem(ufunc->dict, npy_interned_str.__doc__);
6459+
} else {
6460+
return PyDict_SetItem(ufunc->dict, npy_interned_str.__doc__, doc);
6461+
}
6462+
}
64376463

64386464
static PyObject *
64396465
ufunc_get_nin(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
@@ -6519,8 +6545,8 @@ ufunc_get_signature(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored))
65196545

65206546
static PyGetSetDef ufunc_getset[] = {
65216547
{"__doc__",
6522-
(getter)ufunc_get_doc,
6523-
NULL, NULL, NULL},
6548+
(getter)ufunc_get_doc, (setter)ufunc_set_doc,
6549+
NULL, NULL},
65246550
{"nin",
65256551
(getter)ufunc_get_nin,
65266552
NULL, NULL, NULL},
@@ -6549,6 +6575,17 @@ static PyGetSetDef ufunc_getset[] = {
65496575
};
65506576

65516577

6578+
/******************************************************************************
6579+
*** UFUNC MEMBERS ***
6580+
*****************************************************************************/
6581+
6582+
static PyMemberDef ufunc_members[] = {
6583+
{"__dict__", T_OBJECT, offsetof(PyUFuncObject, dict),
6584+
READONLY},
6585+
{NULL},
6586+
};
6587+
6588+
65526589
/******************************************************************************
65536590
*** UFUNC TYPE OBJECT ***
65546591
*****************************************************************************/
@@ -6568,6 +6605,12 @@ NPY_NO_EXPORT PyTypeObject PyUFunc_Type = {
65686605
.tp_traverse = (traverseproc)ufunc_traverse,
65696606
.tp_methods = ufunc_methods,
65706607
.tp_getset = ufunc_getset,
6608+
.tp_getattro = PyObject_GenericGetAttr,
6609+
.tp_setattro = PyObject_GenericSetAttr,
6610+
// TODO when Python 3.12 is the minimum supported version,
6611+
// use Py_TPFLAGS_MANAGED_DICT
6612+
.tp_members = ufunc_members,
6613+
.tp_dictoffset = offsetof(PyUFuncObject, dict),
65716614
};
65726615

65736616
/* End of code for ufunc objects */

numpy/_core/src/umath/umathmodule.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,13 @@ ufunc_frompyfunc(PyObject *NPY_UNUSED(dummy), PyObject *args, PyObject *kwds) {
167167
PyObject *
168168
add_newdoc_ufunc(PyObject *NPY_UNUSED(dummy), PyObject *args)
169169
{
170+
171+
/* 2024-11-12, NumPy 2.2 */
172+
if (DEPRECATE("_add_newdoc_ufunc is deprecated. "
173+
"Use `ufunc.__doc__ = newdoc` instead.") < 0) {
174+
return NULL;
175+
}
176+
170177
PyUFuncObject *ufunc;
171178
PyObject *str;
172179
if (!PyArg_ParseTuple(args, "O!O!:_add_newdoc_ufunc", &PyUFunc_Type, &ufunc,

numpy/_core/tests/test_deprecations.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616

1717
from numpy._core._multiarray_tests import fromstring_null_term_c_api
18+
import numpy._core._struct_ufunc_tests as struct_ufunc
1819

1920
try:
2021
import pytz
@@ -732,3 +733,13 @@ def test_deprecated(self):
732733
self.assert_deprecated(np.save, args=sample_args,
733734
kwargs={'allow_pickle': allow_pickle,
734735
'fix_imports': False})
736+
737+
738+
class TestAddNewdocUFunc(_DeprecationTestCase):
739+
# Deprecated in Numpy 2.2, 2024-11
740+
def test_deprecated(self):
741+
self.assert_deprecated(
742+
lambda: np._core.umath._add_newdoc_ufunc(
743+
struct_ufunc.add_triplet, "new docs"
744+
)
745+
)

numpy/_core/tests/test_umath.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4016,6 +4016,28 @@ def test_array_ufunc_direct_call(self):
40164016
res = a.__array_ufunc__(np.add, "__call__", a, a)
40174017
assert_array_equal(res, a + a)
40184018

4019+
def test_ufunc_docstring(self):
4020+
original_doc = np.add.__doc__
4021+
new_doc = "new docs"
4022+
4023+
np.add.__doc__ = new_doc
4024+
assert np.add.__doc__ == new_doc
4025+
assert np.add.__dict__["__doc__"] == new_doc
4026+
4027+
del np.add.__doc__
4028+
assert np.add.__doc__ == original_doc
4029+
assert np.add.__dict__ == {}
4030+
4031+
np.add.__dict__["other"] = 1
4032+
np.add.__dict__["__doc__"] = new_doc
4033+
assert np.add.__doc__ == new_doc
4034+
4035+
del np.add.__dict__["__doc__"]
4036+
assert np.add.__doc__ == original_doc
4037+
del np.add.__dict__["other"]
4038+
assert np.add.__dict__ == {}
4039+
4040+
40194041
class TestChoose:
40204042
def test_mixed(self):
40214043
c = np.array([True, True])
@@ -4862,9 +4884,11 @@ def func():
48624884

48634885

48644886
class TestAdd_newdoc_ufunc:
4887+
@pytest.mark.filterwarnings("ignore:_add_newdoc_ufunc:DeprecationWarning")
48654888
def test_ufunc_arg(self):
48664889
assert_raises(TypeError, ncu._add_newdoc_ufunc, 2, "blah")
48674890
assert_raises(ValueError, ncu._add_newdoc_ufunc, np.add, "blah")
48684891

4892+
@pytest.mark.filterwarnings("ignore:_add_newdoc_ufunc:DeprecationWarning")
48694893
def test_string_arg(self):
48704894
assert_raises(TypeError, ncu._add_newdoc_ufunc, np.add, 3)

0 commit comments

Comments
 (0)