Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 8 additions & 4 deletions Include/descrobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_descrobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ typedef propertyobject _PyPropertyObject;

extern PyTypeObject _PyMethodWrapper_Type;

extern void *_PyMember_GetOffset(PyObject *, PyMemberDef *);

#ifdef __cplusplus
}
#endif
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`).
14 changes: 11 additions & 3 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
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
22 changes: 16 additions & 6 deletions Python/structmember.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
{
Expand All @@ -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));
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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))
{
Expand Down
Loading