gh-41779: Allow defining the __dict__ and __weakref__ __slots__ for any class (GH-141755)

This commit is contained in:
Serhiy Storchaka 2025-11-19 19:11:37 +02:00 committed by GitHub
parent 598d4c64de
commit 4bcab461c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 104 additions and 20 deletions

View file

@ -2630,8 +2630,8 @@ Notes on using *__slots__*:
descriptor directly from the base class). This renders the meaning of the
program undefined. In the future, a check may be added to prevent this.
* :exc:`TypeError` will be raised if nonempty *__slots__* are defined for a
class derived from a
* :exc:`TypeError` will be raised if *__slots__* other than *__dict__* and
*__weakref__* are defined for a class derived from a
:c:member:`"variable-length" built-in type <PyTypeObject.tp_itemsize>` such as
:class:`int`, :class:`bytes`, and :class:`tuple`.
@ -2656,6 +2656,10 @@ Notes on using *__slots__*:
of the iterator's values. However, the *__slots__* attribute will be an empty
iterator.
.. versionchanged:: next
Allowed defining the *__dict__* and *__weakref__* *__slots__* for any class.
.. _class-customization:
Customizing class creation

View file

@ -394,6 +394,10 @@ Other language changes
syntax warnings by module name.
(Contributed by Serhiy Storchaka in :gh:`135801`.)
* Allowed defining the *__dict__* and *__weakref__* :ref:`__slots__ <slots>`
for any class.
(Contributed by Serhiy Storchaka in :gh:`41779`.)
New modules
===========

View file

@ -1329,18 +1329,17 @@ class D(object):
self.assertNotHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
with self.assertRaises(TypeError):
weakref.ref(a)
class W(object):
__slots__ = ["__weakref__"]
a = W()
self.assertHasAttr(a, "__weakref__")
self.assertNotHasAttr(a, "__dict__")
try:
with self.assertRaises(AttributeError):
a.foo = 42
except AttributeError:
pass
else:
self.fail("shouldn't be allowed to set a.foo")
self.assertIs(weakref.ref(a)(), a)
class C1(W, D):
__slots__ = []
@ -1349,6 +1348,7 @@ class C1(W, D):
self.assertHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
self.assertIs(weakref.ref(a)(), a)
class C2(D, W):
__slots__ = []
@ -1357,6 +1357,77 @@ class C2(D, W):
self.assertHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
self.assertIs(weakref.ref(a)(), a)
@unittest.skipIf(_testcapi is None, 'need the _testcapi module')
def test_slots_special_before_items(self):
class D(_testcapi.HeapCCollection):
__slots__ = ["__dict__"]
a = D(1, 2, 3)
self.assertHasAttr(a, "__dict__")
self.assertNotHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
with self.assertRaises(TypeError):
weakref.ref(a)
del a.__dict__
self.assertNotHasAttr(a, "foo")
self.assertEqual(a.__dict__, {})
self.assertEqual(list(a), [1, 2, 3])
class W(_testcapi.HeapCCollection):
__slots__ = ["__weakref__"]
a = W(1, 2, 3)
self.assertHasAttr(a, "__weakref__")
self.assertNotHasAttr(a, "__dict__")
with self.assertRaises(AttributeError):
a.foo = 42
self.assertIs(weakref.ref(a)(), a)
with self.assertRaises(TypeError):
class X(_testcapi.HeapCCollection):
__slots__ = ['x']
with self.assertRaises(TypeError):
class X(_testcapi.HeapCCollection):
__slots__ = ['__dict__', 'x']
@support.subTests(('base', 'arg'), [
(tuple, (1, 2, 3)),
(int, 9876543210**2),
(bytes, b'ab'),
])
def test_slots_special_after_items(self, base, arg):
class D(base):
__slots__ = ["__dict__"]
a = D(arg)
self.assertHasAttr(a, "__dict__")
self.assertNotHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
with self.assertRaises(TypeError):
weakref.ref(a)
del a.__dict__
self.assertNotHasAttr(a, "foo")
self.assertEqual(a.__dict__, {})
self.assertEqual(a, base(arg))
class W(base):
__slots__ = ["__weakref__"]
a = W(arg)
self.assertHasAttr(a, "__weakref__")
self.assertNotHasAttr(a, "__dict__")
with self.assertRaises(AttributeError):
a.foo = 42
self.assertIs(weakref.ref(a)(), a)
self.assertEqual(a, base(arg))
with self.assertRaises(TypeError):
class X(base):
__slots__ = ['x']
with self.assertRaises(TypeError):
class X(base):
__slots__ = ['__dict__', 'x']
def test_slots_special2(self):
# Testing __qualname__ and __classcell__ in __slots__

View file

@ -0,0 +1,2 @@
Allowed defining the *__dict__* and *__weakref__* :ref:`__slots__ <slots>`
for any class.

View file

@ -4343,14 +4343,6 @@ type_new_slots_bases(type_new_ctx *ctx)
static int
type_new_slots_impl(type_new_ctx *ctx, PyObject *dict)
{
/* Are slots allowed? */
if (ctx->nslot > 0 && ctx->base->tp_itemsize != 0) {
PyErr_Format(PyExc_TypeError,
"nonempty __slots__ not supported for subtype of '%s'",
ctx->base->tp_name);
return -1;
}
if (type_new_visit_slots(ctx) < 0) {
return -1;
}
@ -4377,14 +4369,13 @@ type_new_slots(type_new_ctx *ctx, PyObject *dict)
ctx->add_dict = 0;
ctx->add_weak = 0;
ctx->may_add_dict = (ctx->base->tp_dictoffset == 0);
ctx->may_add_weak = (ctx->base->tp_weaklistoffset == 0
&& ctx->base->tp_itemsize == 0);
ctx->may_add_weak = (ctx->base->tp_weaklistoffset == 0);
if (ctx->slots == NULL) {
if (ctx->may_add_dict) {
ctx->add_dict++;
}
if (ctx->may_add_weak) {
if (ctx->may_add_weak && ctx->base->tp_itemsize == 0) {
ctx->add_weak++;
}
}
@ -4650,6 +4641,12 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type, PyObject *dict
if (et->ht_slots != NULL) {
PyMemberDef *mp = _PyHeapType_GET_MEMBERS(et);
Py_ssize_t nslot = PyTuple_GET_SIZE(et->ht_slots);
if (ctx->base->tp_itemsize != 0) {
PyErr_Format(PyExc_TypeError,
"arbitrary __slots__ not supported for subtype of '%s'",
ctx->base->tp_name);
return -1;
}
for (Py_ssize_t i = 0; i < nslot; i++, mp++) {
mp->name = PyUnicode_AsUTF8(
PyTuple_GET_ITEM(et->ht_slots, i));
@ -4889,8 +4886,14 @@ type_new_init(type_new_ctx *ctx)
set_tp_dict(type, dict);
PyHeapTypeObject *et = (PyHeapTypeObject*)type;
et->ht_slots = ctx->slots;
ctx->slots = NULL;
if (ctx->slots && PyTuple_GET_SIZE(ctx->slots)) {
et->ht_slots = ctx->slots;
ctx->slots = NULL;
}
else {
et->ht_slots = NULL;
Py_CLEAR(ctx->slots);
}
return type;