]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-41779: Allow defining any __slots__ for a class derived from tuple (GH-141763)
authorSerhiy Storchaka <storchaka@gmail.com>
Tue, 6 Jan 2026 09:36:00 +0000 (11:36 +0200)
committerGitHub <noreply@github.com>
Tue, 6 Jan 2026 09:36:00 +0000 (11:36 +0200)
Doc/reference/datamodel.rst
Doc/whatsnew/3.15.rst
Include/descrobject.h
Include/internal/pycore_descrobject.h
Lib/test/test_descr.py
Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst [new file with mode: 0644]
Objects/typeobject.c
Python/specialize.c
Python/structmember.c

index d92972117a31f131ad0f827cf246126859d5d2c1..488fbc6b1f68cd3d5c9d112d8b0f7122b61cb8c3 100644 (file)
@@ -2617,7 +2617,7 @@ Notes on using *__slots__*:
 * :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`.
+  :class:`int`, :class:`bytes`, and :class:`type`, except :class:`tuple`.
 
 * Any non-string :term:`iterable` may be assigned to *__slots__*.
 
@@ -2642,6 +2642,7 @@ Notes on using *__slots__*:
 
 .. versionchanged:: 3.15
    Allowed defining the *__dict__* and *__weakref__* *__slots__* for any class.
+   Allowed defining any *__slots__* for a class derived from :class:`tuple`.
 
 
 .. _class-customization:
index d5a2e04958e8a800776469eb5bca007e9dc9b602..39d6fb6572c834b0dca08b231be75a9ac5080d2b 100644 (file)
@@ -398,6 +398,10 @@ Other language changes
   for any class.
   (Contributed by Serhiy Storchaka in :gh:`41779`.)
 
+* Allowed defining any :ref:`__slots__ <slots>` for a class derived from
+  :class:`tuple` (including classes created by :func:`collections.namedtuple`).
+  (Contributed by Serhiy Storchaka in :gh:`41779`.)
+
 
 New modules
 ===========
index fd66d17b497a31f295d5791c49fb879fad8883cf..340de4e0e1e6ffc88b079e26830094ebc2a6fb28 100644 (file)
@@ -80,10 +80,14 @@ struct PyMemberDef {
 #define _Py_T_NONE     20 // Deprecated. Value is always None.
 
 /* Flags */
-#define Py_READONLY            1
-#define Py_AUDIT_READ          2 // Added in 3.10, harmless no-op before that
-#define _Py_WRITE_RESTRICTED   4 // Deprecated, no-op. Do not reuse the value.
-#define Py_RELATIVE_OFFSET     8
+#define Py_READONLY            (1 << 0)
+#define Py_AUDIT_READ          (1 << 1) // Added in 3.10, harmless no-op before that
+#define _Py_WRITE_RESTRICTED   (1 << 2) // Deprecated, no-op. Do not reuse the value.
+#define Py_RELATIVE_OFFSET     (1 << 3)
+
+#ifndef Py_LIMITED_API
+#  define _Py_AFTER_ITEMS      (1 << 4) // For internal use.
+#endif
 
 PyAPI_FUNC(PyObject *) PyMember_GetOne(const char *, PyMemberDef *);
 PyAPI_FUNC(int) PyMember_SetOne(char *, PyMemberDef *, PyObject *);
index 3cec59a68a3d2b2af5320fe1435c13a4aa66886c..6143f82176a1f2f9892788b70306bb9376c682e8 100644 (file)
@@ -22,6 +22,8 @@ typedef propertyobject _PyPropertyObject;
 
 extern PyTypeObject _PyMethodWrapper_Type;
 
+extern void *_PyMember_GetOffset(PyObject *, PyMemberDef *);
+
 #ifdef __cplusplus
 }
 #endif
index 82a48ad4d1aced7a557c947d2d00d1312a778f94..0dc61ca7fb0da3ab66f67534f9c1f968c4746227 100644 (file)
@@ -1320,6 +1320,18 @@ class ClassPropertiesAndMethods(unittest.TestCase):
         with self.assertRaisesRegex(AttributeError, "'X' object has no attribute 'a'"):
             X().a
 
+    def test_slots_after_items(self):
+        class C(tuple):
+            __slots__ = ['a']
+        x = C((1, 2, 3))
+        self.assertNotHasAttr(x, "__dict__")
+        self.assertNotHasAttr(x, "a")
+        x.a = 42
+        self.assertEqual(x.a, 42)
+        del x.a
+        self.assertNotHasAttr(x, "a")
+        self.assertEqual(x, (1, 2, 3))
+
     def test_slots_special(self):
         # Testing __dict__ and __weakref__ in __slots__...
         class D(object):
@@ -1422,6 +1434,9 @@ class ClassPropertiesAndMethods(unittest.TestCase):
         self.assertIs(weakref.ref(a)(), a)
         self.assertEqual(a, base(arg))
 
+    @support.subTests('base', [int, bytes] +
+                      ([_testcapi.HeapCCollection] if _testcapi else []))
+    def test_unsupported_slots(self, base):
         with self.assertRaises(TypeError):
             class X(base):
                 __slots__ = ['x']
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst
new file mode 100644 (file)
index 0000000..16e5468
--- /dev/null
@@ -0,0 +1,3 @@
+Allowed defining any :ref:`__slots__ <slots>` for a class derived from
+:class:`tuple` (including classes created by
+:func:`collections.namedtuple`).
index 7f5149aeece12b65066ea0118f12cd0280cd23d9..77e5c3e9f9ec958557111f74145df6017ea9a463 100644 (file)
@@ -4,6 +4,7 @@
 #include "pycore_abstract.h"      // _PySequence_IterSearch()
 #include "pycore_call.h"          // _PyObject_VectorcallTstate()
 #include "pycore_code.h"          // CO_FAST_FREE
+#include "pycore_descrobject.h"   // _PyMember_GetOffset()
 #include "pycore_dict.h"          // _PyDict_KeysSize()
 #include "pycore_function.h"      // _PyFunction_GetVersionForCurrentState()
 #include "pycore_interpframe.h"   // _PyInterpreterFrame
@@ -2578,7 +2579,7 @@ traverse_slots(PyTypeObject *type, PyObject *self, visitproc visit, void *arg)
     mp = _PyHeapType_GET_MEMBERS((PyHeapTypeObject *)type);
     for (i = 0; i < n; i++, mp++) {
         if (mp->type == Py_T_OBJECT_EX) {
-            char *addr = (char *)self + mp->offset;
+            void *addr = _PyMember_GetOffset(self, mp);
             PyObject *obj = *(PyObject **)addr;
             if (obj != NULL) {
                 int err = visit(obj, arg);
@@ -2653,7 +2654,7 @@ clear_slots(PyTypeObject *type, PyObject *self)
     mp = _PyHeapType_GET_MEMBERS((PyHeapTypeObject *)type);
     for (i = 0; i < n; i++, mp++) {
         if (mp->type == Py_T_OBJECT_EX && !(mp->flags & Py_READONLY)) {
-            char *addr = (char *)self + mp->offset;
+            void *addr = _PyMember_GetOffset(self, mp);
             PyObject *obj = *(PyObject **)addr;
             if (obj != NULL) {
                 *(PyObject **)addr = NULL;
@@ -4641,7 +4642,11 @@ 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) {
+        int after_items = (ctx->base->tp_itemsize != 0 &&
+                           !(ctx->base->tp_flags & Py_TPFLAGS_ITEMS_AT_END));
+        if (ctx->base->tp_itemsize != 0 &&
+            !(ctx->base->tp_flags & Py_TPFLAGS_TUPLE_SUBCLASS))
+        {
             PyErr_Format(PyExc_TypeError,
                          "arbitrary __slots__ not supported for subtype of '%s'",
                          ctx->base->tp_name);
@@ -4655,6 +4660,9 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type, PyObject *dict
             }
             mp->type = Py_T_OBJECT_EX;
             mp->offset = slotoffset;
+            if (after_items) {
+                mp->flags |= _Py_AFTER_ITEMS;
+            }
 
             /* __dict__ and __weakref__ are already filtered out */
             assert(strcmp(mp->name, "__dict__") != 0);
index 31b7fd364cd8be356d0ca261a9dd6e9148fa056c..62f0373a4c274d609966f2dc35bce87a75b35964 100644 (file)
@@ -141,6 +141,7 @@ _PyCode_Quicken(_Py_CODEUNIT *instructions, Py_ssize_t size, int enable_counters
 #define SPEC_FAIL_ATTR_METACLASS_OVERRIDDEN 34
 #define SPEC_FAIL_ATTR_SPLIT_DICT 35
 #define SPEC_FAIL_ATTR_DESCR_NOT_DEFERRED 36
+#define SPEC_FAIL_ATTR_SLOT_AFTER_ITEMS 37
 
 /* Binary subscr and store subscr */
 
@@ -812,6 +813,10 @@ do_specialize_instance_load_attr(PyObject* owner, _Py_CODEUNIT* instr, PyObject*
                 SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_EXPECTED_ERROR);
                 return -1;
             }
+            if (dmem->flags & _Py_AFTER_ITEMS) {
+                SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_SLOT_AFTER_ITEMS);
+                return -1;
+            }
             if (dmem->flags & Py_AUDIT_READ) {
                 SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_AUDITED_SLOT);
                 return -1;
@@ -1006,6 +1011,10 @@ _Py_Specialize_StoreAttr(_PyStackRef owner_st, _Py_CODEUNIT *instr, PyObject *na
                 SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_EXPECTED_ERROR);
                 goto fail;
             }
+            if (dmem->flags & _Py_AFTER_ITEMS) {
+                SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_SLOT_AFTER_ITEMS);
+                goto fail;
+            }
             if (dmem->flags & Py_READONLY) {
                 SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_ATTR_READ_ONLY);
                 goto fail;
index 574acf296157f3ae275c3b811ed555248727351e..b88e13ac0462b80c27fc2f2d2ba8c36bb3709fad 100644 (file)
@@ -3,6 +3,7 @@
 
 #include "Python.h"
 #include "pycore_abstract.h"      // _PyNumber_Index()
+#include "pycore_descrobject.h"   // _PyMember_GetOffset()
 #include "pycore_long.h"          // _PyLong_IsNegative()
 #include "pycore_object.h"        // _Py_TryIncrefCompare(), FT_ATOMIC_*()
 #include "pycore_critical_section.h"
@@ -20,6 +21,17 @@ member_get_object(const char *addr, const char *obj_addr, PyMemberDef *l)
     return v;
 }
 
+void *
+_PyMember_GetOffset(PyObject *obj, PyMemberDef *mp)
+{
+    unsigned char *addr = (unsigned char *)obj + mp->offset;
+    if (mp->flags & _Py_AFTER_ITEMS) {
+        PyTypeObject *type = Py_TYPE(obj);
+        addr += _Py_SIZE_ROUND_UP(Py_SIZE(obj) * type->tp_itemsize, SIZEOF_VOID_P);
+    }
+    return addr;
+}
+
 PyObject *
 PyMember_GetOne(const char *obj_addr, PyMemberDef *l)
 {
@@ -31,7 +43,7 @@ PyMember_GetOne(const char *obj_addr, PyMemberDef *l)
         return NULL;
     }
 
-    const char* addr = obj_addr + l->offset;
+    const void *addr = _PyMember_GetOffset((PyObject *)obj_addr, l);
     switch (l->type) {
     case Py_T_BOOL:
         v = PyBool_FromLong(FT_ATOMIC_LOAD_CHAR_RELAXED(*(char*)addr));
@@ -80,7 +92,7 @@ PyMember_GetOne(const char *obj_addr, PyMemberDef *l)
         v = PyUnicode_FromString((char*)addr);
         break;
     case Py_T_CHAR: {
-        char char_val = FT_ATOMIC_LOAD_CHAR_RELAXED(*addr);
+        char char_val = FT_ATOMIC_LOAD_CHAR_RELAXED(*(char*)addr);
         v = PyUnicode_FromStringAndSize(&char_val, 1);
         break;
     }
@@ -151,10 +163,8 @@ PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
         return -1;
     }
 
-#ifdef Py_GIL_DISABLED
-    PyObject *obj = (PyObject *) addr;
-#endif
-    addr += l->offset;
+    PyObject *obj = (PyObject *)addr;
+    addr = _PyMember_GetOffset(obj, l);
 
     if ((l->flags & Py_READONLY))
     {