Skip to content

Commit 4d21297

Browse files
gh-41779: Allow defining any __slots__ for a class derived from tuple (GH-141763)
1 parent d6f77e6 commit 4d21297

File tree

9 files changed

+70
-14
lines changed

9 files changed

+70
-14
lines changed

Doc/reference/datamodel.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2617,7 +2617,7 @@ Notes on using *__slots__*:
26172617
* :exc:`TypeError` will be raised if *__slots__* other than *__dict__* and
26182618
*__weakref__* are defined for a class derived from a
26192619
:c:member:`"variable-length" built-in type <PyTypeObject.tp_itemsize>` such as
2620-
:class:`int`, :class:`bytes`, and :class:`tuple`.
2620+
:class:`int`, :class:`bytes`, and :class:`type`, except :class:`tuple`.
26212621

26222622
* Any non-string :term:`iterable` may be assigned to *__slots__*.
26232623

@@ -2642,6 +2642,7 @@ Notes on using *__slots__*:
26422642

26432643
.. versionchanged:: 3.15
26442644
Allowed defining the *__dict__* and *__weakref__* *__slots__* for any class.
2645+
Allowed defining any *__slots__* for a class derived from :class:`tuple`.
26452646

26462647

26472648
.. _class-customization:

Doc/whatsnew/3.15.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,10 @@ Other language changes
398398
for any class.
399399
(Contributed by Serhiy Storchaka in :gh:`41779`.)
400400

401+
* Allowed defining any :ref:`__slots__ <slots>` for a class derived from
402+
:class:`tuple` (including classes created by :func:`collections.namedtuple`).
403+
(Contributed by Serhiy Storchaka in :gh:`41779`.)
404+
401405

402406
New modules
403407
===========

Include/descrobject.h

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,14 @@ struct PyMemberDef {
8080
#define _Py_T_NONE 20 // Deprecated. Value is always None.
8181

8282
/* Flags */
83-
#define Py_READONLY 1
84-
#define Py_AUDIT_READ 2 // Added in 3.10, harmless no-op before that
85-
#define _Py_WRITE_RESTRICTED 4 // Deprecated, no-op. Do not reuse the value.
86-
#define Py_RELATIVE_OFFSET 8
83+
#define Py_READONLY (1 << 0)
84+
#define Py_AUDIT_READ (1 << 1) // Added in 3.10, harmless no-op before that
85+
#define _Py_WRITE_RESTRICTED (1 << 2) // Deprecated, no-op. Do not reuse the value.
86+
#define Py_RELATIVE_OFFSET (1 << 3)
87+
88+
#ifndef Py_LIMITED_API
89+
# define _Py_AFTER_ITEMS (1 << 4) // For internal use.
90+
#endif
8791

8892
PyAPI_FUNC(PyObject *) PyMember_GetOne(const char *, PyMemberDef *);
8993
PyAPI_FUNC(int) PyMember_SetOne(char *, PyMemberDef *, PyObject *);

Include/internal/pycore_descrobject.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ typedef propertyobject _PyPropertyObject;
2222

2323
extern PyTypeObject _PyMethodWrapper_Type;
2424

25+
extern void *_PyMember_GetOffset(PyObject *, PyMemberDef *);
26+
2527
#ifdef __cplusplus
2628
}
2729
#endif

Lib/test/test_descr.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,18 @@ class X(object):
13201320
with self.assertRaisesRegex(AttributeError, "'X' object has no attribute 'a'"):
13211321
X().a
13221322

1323+
def test_slots_after_items(self):
1324+
class C(tuple):
1325+
__slots__ = ['a']
1326+
x = C((1, 2, 3))
1327+
self.assertNotHasAttr(x, "__dict__")
1328+
self.assertNotHasAttr(x, "a")
1329+
x.a = 42
1330+
self.assertEqual(x.a, 42)
1331+
del x.a
1332+
self.assertNotHasAttr(x, "a")
1333+
self.assertEqual(x, (1, 2, 3))
1334+
13231335
def test_slots_special(self):
13241336
# Testing __dict__ and __weakref__ in __slots__...
13251337
class D(object):
@@ -1422,6 +1434,9 @@ class W(base):
14221434
self.assertIs(weakref.ref(a)(), a)
14231435
self.assertEqual(a, base(arg))
14241436

1437+
@support.subTests('base', [int, bytes] +
1438+
([_testcapi.HeapCCollection] if _testcapi else []))
1439+
def test_unsupported_slots(self, base):
14251440
with self.assertRaises(TypeError):
14261441
class X(base):
14271442
__slots__ = ['x']
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Allowed defining any :ref:`__slots__ <slots>` for a class derived from
2+
:class:`tuple` (including classes created by
3+
:func:`collections.namedtuple`).

Objects/typeobject.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "pycore_abstract.h" // _PySequence_IterSearch()
55
#include "pycore_call.h" // _PyObject_VectorcallTstate()
66
#include "pycore_code.h" // CO_FAST_FREE
7+
#include "pycore_descrobject.h" // _PyMember_GetOffset()
78
#include "pycore_dict.h" // _PyDict_KeysSize()
89
#include "pycore_function.h" // _PyFunction_GetVersionForCurrentState()
910
#include "pycore_interpframe.h" // _PyInterpreterFrame
@@ -2578,7 +2579,7 @@ traverse_slots(PyTypeObject *type, PyObject *self, visitproc visit, void *arg)
25782579
mp = _PyHeapType_GET_MEMBERS((PyHeapTypeObject *)type);
25792580
for (i = 0; i < n; i++, mp++) {
25802581
if (mp->type == Py_T_OBJECT_EX) {
2581-
char *addr = (char *)self + mp->offset;
2582+
void *addr = _PyMember_GetOffset(self, mp);
25822583
PyObject *obj = *(PyObject **)addr;
25832584
if (obj != NULL) {
25842585
int err = visit(obj, arg);
@@ -2653,7 +2654,7 @@ clear_slots(PyTypeObject *type, PyObject *self)
26532654
mp = _PyHeapType_GET_MEMBERS((PyHeapTypeObject *)type);
26542655
for (i = 0; i < n; i++, mp++) {
26552656
if (mp->type == Py_T_OBJECT_EX && !(mp->flags & Py_READONLY)) {
2656-
char *addr = (char *)self + mp->offset;
2657+
void *addr = _PyMember_GetOffset(self, mp);
26572658
PyObject *obj = *(PyObject **)addr;
26582659
if (obj != NULL) {
26592660
*(PyObject **)addr = NULL;
@@ -4641,7 +4642,11 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type, PyObject *dict
46414642
if (et->ht_slots != NULL) {
46424643
PyMemberDef *mp = _PyHeapType_GET_MEMBERS(et);
46434644
Py_ssize_t nslot = PyTuple_GET_SIZE(et->ht_slots);
4644-
if (ctx->base->tp_itemsize != 0) {
4645+
int after_items = (ctx->base->tp_itemsize != 0 &&
4646+
!(ctx->base->tp_flags & Py_TPFLAGS_ITEMS_AT_END));
4647+
if (ctx->base->tp_itemsize != 0 &&
4648+
!(ctx->base->tp_flags & Py_TPFLAGS_TUPLE_SUBCLASS))
4649+
{
46454650
PyErr_Format(PyExc_TypeError,
46464651
"arbitrary __slots__ not supported for subtype of '%s'",
46474652
ctx->base->tp_name);
@@ -4655,6 +4660,9 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type, PyObject *dict
46554660
}
46564661
mp->type = Py_T_OBJECT_EX;
46574662
mp->offset = slotoffset;
4663+
if (after_items) {
4664+
mp->flags |= _Py_AFTER_ITEMS;
4665+
}
46584666

46594667
/* __dict__ and __weakref__ are already filtered out */
46604668
assert(strcmp(mp->name, "__dict__") != 0);

Python/specialize.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ _PyCode_Quicken(_Py_CODEUNIT *instructions, Py_ssize_t size, int enable_counters
141141
#define SPEC_FAIL_ATTR_METACLASS_OVERRIDDEN 34
142142
#define SPEC_FAIL_ATTR_SPLIT_DICT 35
143143
#define SPEC_FAIL_ATTR_DESCR_NOT_DEFERRED 36
144+
#define SPEC_FAIL_ATTR_SLOT_AFTER_ITEMS 37
144145

145146
/* Binary subscr and store subscr */
146147

@@ -812,6 +813,10 @@ do_specialize_instance_load_attr(PyObject* owner, _Py_CODEUNIT* instr, PyObject*
812813
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_EXPECTED_ERROR);
813814
return -1;
814815
}
816+
if (dmem->flags & _Py_AFTER_ITEMS) {
817+
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_SLOT_AFTER_ITEMS);
818+
return -1;
819+
}
815820
if (dmem->flags & Py_AUDIT_READ) {
816821
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_AUDITED_SLOT);
817822
return -1;
@@ -1006,6 +1011,10 @@ _Py_Specialize_StoreAttr(_PyStackRef owner_st, _Py_CODEUNIT *instr, PyObject *na
10061011
SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_EXPECTED_ERROR);
10071012
goto fail;
10081013
}
1014+
if (dmem->flags & _Py_AFTER_ITEMS) {
1015+
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_SLOT_AFTER_ITEMS);
1016+
goto fail;
1017+
}
10091018
if (dmem->flags & Py_READONLY) {
10101019
SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_ATTR_READ_ONLY);
10111020
goto fail;

Python/structmember.c

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "Python.h"
55
#include "pycore_abstract.h" // _PyNumber_Index()
6+
#include "pycore_descrobject.h" // _PyMember_GetOffset()
67
#include "pycore_long.h" // _PyLong_IsNegative()
78
#include "pycore_object.h" // _Py_TryIncrefCompare(), FT_ATOMIC_*()
89
#include "pycore_critical_section.h"
@@ -20,6 +21,17 @@ member_get_object(const char *addr, const char *obj_addr, PyMemberDef *l)
2021
return v;
2122
}
2223

24+
void *
25+
_PyMember_GetOffset(PyObject *obj, PyMemberDef *mp)
26+
{
27+
unsigned char *addr = (unsigned char *)obj + mp->offset;
28+
if (mp->flags & _Py_AFTER_ITEMS) {
29+
PyTypeObject *type = Py_TYPE(obj);
30+
addr += _Py_SIZE_ROUND_UP(Py_SIZE(obj) * type->tp_itemsize, SIZEOF_VOID_P);
31+
}
32+
return addr;
33+
}
34+
2335
PyObject *
2436
PyMember_GetOne(const char *obj_addr, PyMemberDef *l)
2537
{
@@ -31,7 +43,7 @@ PyMember_GetOne(const char *obj_addr, PyMemberDef *l)
3143
return NULL;
3244
}
3345

34-
const char* addr = obj_addr + l->offset;
46+
const void *addr = _PyMember_GetOffset((PyObject *)obj_addr, l);
3547
switch (l->type) {
3648
case Py_T_BOOL:
3749
v = PyBool_FromLong(FT_ATOMIC_LOAD_CHAR_RELAXED(*(char*)addr));
@@ -80,7 +92,7 @@ PyMember_GetOne(const char *obj_addr, PyMemberDef *l)
8092
v = PyUnicode_FromString((char*)addr);
8193
break;
8294
case Py_T_CHAR: {
83-
char char_val = FT_ATOMIC_LOAD_CHAR_RELAXED(*addr);
95+
char char_val = FT_ATOMIC_LOAD_CHAR_RELAXED(*(char*)addr);
8496
v = PyUnicode_FromStringAndSize(&char_val, 1);
8597
break;
8698
}
@@ -151,10 +163,8 @@ PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
151163
return -1;
152164
}
153165

154-
#ifdef Py_GIL_DISABLED
155-
PyObject *obj = (PyObject *) addr;
156-
#endif
157-
addr += l->offset;
166+
PyObject *obj = (PyObject *)addr;
167+
addr = _PyMember_GetOffset(obj, l);
158168

159169
if ((l->flags & Py_READONLY))
160170
{

0 commit comments

Comments
 (0)