Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2633,7 +2633,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__*.

Expand All @@ -2658,6 +2658,7 @@ Notes on using *__slots__*:

.. versionchanged:: next
Allowed defining the *__dict__* and *__weakref__* *__slots__* for any class.
Allowed defining any *__slots__* for a class derived from :class:`tuple`.


.. _class-customization:
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========
Expand Down
1 change: 1 addition & 0 deletions Include/descrobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ struct PyMemberDef {
#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_AFTER_ITEMS 16

PyAPI_FUNC(PyObject *) PyMember_GetOne(const char *, PyMemberDef *);
PyAPI_FUNC(int) PyMember_SetOne(char *, PyMemberDef *, PyObject *);
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,18 @@ class X(object):
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):
Expand Down Expand Up @@ -1422,6 +1434,9 @@ class W(base):
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']
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Allowed defining any :ref:`__slots__ <slots>` for a class derived from
:class:`tuple` (including classes created by
:func:`collections.namedtuple`).
15 changes: 12 additions & 3 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2568,6 +2568,8 @@ _PyHeapType_GET_MEMBERS(PyHeapTypeObject* type)
return PyObject_GetItemData((PyObject *)type);
}

void *_PyMember_GetOffset(PyObject *obj, PyMemberDef *l);

static int
traverse_slots(PyTypeObject *type, PyObject *self, visitproc visit, void *arg)
{
Expand All @@ -2578,7 +2580,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);
Expand Down Expand Up @@ -2653,7 +2655,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;
Expand Down Expand Up @@ -4641,7 +4643,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);
Expand All @@ -4655,6 +4661,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);
Expand Down
9 changes: 9 additions & 0 deletions Python/specialize.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,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 */

Expand Down Expand Up @@ -810,6 +811,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;
}
Comment on lines +814 to +817
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks OK but I don't know enough about the specializer to review properly.

if (dmem->flags & Py_AUDIT_READ) {
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_AUDITED_SLOT);
return -1;
Expand Down Expand Up @@ -1004,6 +1009,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;
Expand Down
21 changes: 15 additions & 6 deletions Python/structmember.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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)
{
Expand All @@ -31,7 +42,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));
Expand Down Expand Up @@ -80,7 +91,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;
}
Expand Down Expand Up @@ -151,10 +162,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))
{
Expand Down
Loading