diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 882b05e87319fa..ebadbc215a0eed 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -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 ` 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 diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 5a98297d3f8847..d0af9212d55567 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -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__ ` + for any class. + (Contributed by Serhiy Storchaka in :gh:`41779`.) + New modules =========== diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 14f94285d3f3c2..82a48ad4d1aced 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -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__ diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-16-21-14-48.gh-issue-41779.rXIj5h.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-16-21-14-48.gh-issue-41779.rXIj5h.rst new file mode 100644 index 00000000000000..8ba3ca8df86ec8 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-16-21-14-48.gh-issue-41779.rXIj5h.rst @@ -0,0 +1,2 @@ +Allowed defining the *__dict__* and *__weakref__* :ref:`__slots__ ` +for any class. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index c99c6b3f6377b6..4c6ff51493f799 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -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;