From 9d02192aea6a01576a563f7938f6ad8b19829dd5 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 11 Nov 2025 17:36:13 +0100 Subject: [PATCH 1/3] Add regression test for PySet_Contains As discussed in gh-141183, the test suite did not test that PySet_Contains does not convert unhashable key into a frozenset. This commit adds a regression test for this behavior, to ensure that any behavior change is caught by the test suite. --- Modules/_testlimitedcapi/set.c | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Modules/_testlimitedcapi/set.c b/Modules/_testlimitedcapi/set.c index 35da5fa5f008e1..21c5e9d7f88f11 100644 --- a/Modules/_testlimitedcapi/set.c +++ b/Modules/_testlimitedcapi/set.c @@ -155,6 +155,67 @@ test_frozenset_add_in_capi(PyObject *self, PyObject *Py_UNUSED(obj)) return NULL; } +static PyObject * +test_set_contains_does_not_convert_unhashable_key(PyObject *self, PyObject *Py_UNUSED(obj)) +{ + // The documentation of PySet_Contains state: + // + // int PySet_Contains(PyObject *anyset, PyObject *key) + // + // Part of the Stable ABI. + // + // ... Unlike the Python __contains__() method, this function does not + // automatically convert unhashable sets [key] into temporary frozensets. + // Raise a TypeError if the key is unhashable. + // + // That is to say {2,3} in {1, 2, frozenset({2,3})} + // ^_ will be converted in a frozenset in Python code. + // But not if using PySet_Contains(..., key) + // + // We test that this behavior is unchanged as this is a stable API. + + PyObject *outer_set = PySet_New(NULL); + + PyObject *needle = PySet_New(NULL); + if (needle == NULL) { + Py_DECREF(outer_set); + return NULL; + } + + PyObject *num = PyLong_FromLong(42); + if (num == NULL) { + Py_DECREF(outer_set); + Py_DECREF(needle); + return NULL; + } + + // Add an element to needle to make it {42} + if (PySet_Add(needle, num) < 0) { + Py_DECREF(outer_set); + Py_DECREF(needle); + Py_DECREF(num); + return NULL; + } + + int result = PySet_Contains(outer_set, needle); + + Py_DECREF(num); + Py_DECREF(needle); + Py_DECREF(outer_set); + + if (result < 0) { + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + Py_RETURN_NONE; + } + return NULL; + } + + PyErr_SetString(PyExc_AssertionError, + "PySet_Contains should have raised TypeError for unhashable key"); + return NULL; +} + static PyMethodDef test_methods[] = { {"set_check", set_check, METH_O}, {"set_checkexact", set_checkexact, METH_O}, @@ -174,6 +235,8 @@ static PyMethodDef test_methods[] = { {"set_clear", set_clear, METH_O}, {"test_frozenset_add_in_capi", test_frozenset_add_in_capi, METH_NOARGS}, + {"test_set_contains_does_not_convert_unhashable_key", + test_set_contains_does_not_convert_unhashable_key, METH_NOARGS}, {NULL}, }; From fa94d3491b45e60779b223f2a66f71b4743ea364 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 12 Nov 2025 09:29:41 +0100 Subject: [PATCH 2/3] Reduce comments in test functions --- Modules/_testlimitedcapi/set.c | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/Modules/_testlimitedcapi/set.c b/Modules/_testlimitedcapi/set.c index 21c5e9d7f88f11..93d69b5e1dd28d 100644 --- a/Modules/_testlimitedcapi/set.c +++ b/Modules/_testlimitedcapi/set.c @@ -158,22 +158,7 @@ test_frozenset_add_in_capi(PyObject *self, PyObject *Py_UNUSED(obj)) static PyObject * test_set_contains_does_not_convert_unhashable_key(PyObject *self, PyObject *Py_UNUSED(obj)) { - // The documentation of PySet_Contains state: - // - // int PySet_Contains(PyObject *anyset, PyObject *key) - // - // Part of the Stable ABI. - // - // ... Unlike the Python __contains__() method, this function does not - // automatically convert unhashable sets [key] into temporary frozensets. - // Raise a TypeError if the key is unhashable. - // - // That is to say {2,3} in {1, 2, frozenset({2,3})} - // ^_ will be converted in a frozenset in Python code. - // But not if using PySet_Contains(..., key) - // - // We test that this behavior is unchanged as this is a stable API. - + // see documentation of int PySet_Contains in c-api/set.rst PyObject *outer_set = PySet_New(NULL); PyObject *needle = PySet_New(NULL); @@ -189,7 +174,6 @@ test_set_contains_does_not_convert_unhashable_key(PyObject *self, PyObject *Py_U return NULL; } - // Add an element to needle to make it {42} if (PySet_Add(needle, num) < 0) { Py_DECREF(outer_set); Py_DECREF(needle); From 4845b0e61e4f4be0b8657ec25f61df1024846036 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 12 Nov 2025 15:06:52 +0100 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Kumar Aditya --- Modules/_testlimitedcapi/set.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_testlimitedcapi/set.c b/Modules/_testlimitedcapi/set.c index 93d69b5e1dd28d..34ed6b1d60b5a4 100644 --- a/Modules/_testlimitedcapi/set.c +++ b/Modules/_testlimitedcapi/set.c @@ -158,7 +158,7 @@ test_frozenset_add_in_capi(PyObject *self, PyObject *Py_UNUSED(obj)) static PyObject * test_set_contains_does_not_convert_unhashable_key(PyObject *self, PyObject *Py_UNUSED(obj)) { - // see documentation of int PySet_Contains in c-api/set.rst + // See https://docs.python.org/3/c-api/set.html#c.PySet_Contains PyObject *outer_set = PySet_New(NULL); PyObject *needle = PySet_New(NULL);