From d6f77e6a3fa46fa31694d288a3081d5ee76e9d07 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 6 Jan 2026 12:24:02 +0530 Subject: [PATCH 1/3] gh-116738: make entering of `contextvars.Context` thread safe (#143074) --- ...2025-12-22-16-22-02.gh-issue-116738.caQuq_.rst | 1 + Python/context.c | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-22-16-22-02.gh-issue-116738.caQuq_.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-22-16-22-02.gh-issue-116738.caQuq_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-22-16-22-02.gh-issue-116738.caQuq_.rst new file mode 100644 index 00000000000000..9fa2039ca81d35 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-22-16-22-02.gh-issue-116738.caQuq_.rst @@ -0,0 +1 @@ +Fix thread safety of :func:`contextvars.Context.run`. diff --git a/Python/context.c b/Python/context.c index 606ce4b1c8f60a..62b582f271ffe5 100644 --- a/Python/context.c +++ b/Python/context.c @@ -195,16 +195,20 @@ _PyContext_Enter(PyThreadState *ts, PyObject *octx) { ENSURE_Context(octx, -1) PyContext *ctx = (PyContext *)octx; +#ifdef Py_GIL_DISABLED + int already_entered = _Py_atomic_exchange_int(&ctx->ctx_entered, 1); +#else + int already_entered = ctx->ctx_entered; + ctx->ctx_entered = 1; +#endif - if (ctx->ctx_entered) { + if (already_entered) { _PyErr_Format(ts, PyExc_RuntimeError, "cannot enter context: %R is already entered", ctx); return -1; } ctx->ctx_prev = (PyContext *)ts->context; /* borrow */ - ctx->ctx_entered = 1; - ts->context = Py_NewRef(ctx); context_switched(ts); return 0; @@ -225,8 +229,9 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx) { ENSURE_Context(octx, -1) PyContext *ctx = (PyContext *)octx; + int already_entered = FT_ATOMIC_LOAD_INT_RELAXED(ctx->ctx_entered); - if (!ctx->ctx_entered) { + if (!already_entered) { PyErr_Format(PyExc_RuntimeError, "cannot exit context: %R has not been entered", ctx); return -1; @@ -243,7 +248,7 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx) Py_SETREF(ts->context, (PyObject *)ctx->ctx_prev); ctx->ctx_prev = NULL; - ctx->ctx_entered = 0; + FT_ATOMIC_STORE_INT(ctx->ctx_entered, 0); context_switched(ts); return 0; } From 4d21297d288f17c0db775605e2543749840e46b2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 6 Jan 2026 11:36:00 +0200 Subject: [PATCH 2/3] gh-41779: Allow defining any __slots__ for a class derived from tuple (GH-141763) --- Doc/reference/datamodel.rst | 3 ++- Doc/whatsnew/3.15.rst | 4 ++++ Include/descrobject.h | 12 ++++++---- Include/internal/pycore_descrobject.h | 2 ++ Lib/test/test_descr.py | 15 +++++++++++++ ...5-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst | 3 +++ Objects/typeobject.c | 14 +++++++++--- Python/specialize.c | 9 ++++++++ Python/structmember.c | 22 ++++++++++++++----- 9 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index d92972117a31f1..488fbc6b1f68cd 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -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 ` 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: diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d5a2e04958e8a8..39d6fb6572c834 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -398,6 +398,10 @@ Other language changes for any class. (Contributed by Serhiy Storchaka in :gh:`41779`.) +* Allowed defining any :ref:`__slots__ ` for a class derived from + :class:`tuple` (including classes created by :func:`collections.namedtuple`). + (Contributed by Serhiy Storchaka in :gh:`41779`.) + New modules =========== diff --git a/Include/descrobject.h b/Include/descrobject.h index fd66d17b497a31..340de4e0e1e6ff 100644 --- a/Include/descrobject.h +++ b/Include/descrobject.h @@ -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 *); diff --git a/Include/internal/pycore_descrobject.h b/Include/internal/pycore_descrobject.h index 3cec59a68a3d2b..6143f82176a1f2 100644 --- a/Include/internal/pycore_descrobject.h +++ b/Include/internal/pycore_descrobject.h @@ -22,6 +22,8 @@ typedef propertyobject _PyPropertyObject; extern PyTypeObject _PyMethodWrapper_Type; +extern void *_PyMember_GetOffset(PyObject *, PyMemberDef *); + #ifdef __cplusplus } #endif diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 82a48ad4d1aced..0dc61ca7fb0da3 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -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): @@ -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'] 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 index 00000000000000..16e546890c085b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst @@ -0,0 +1,3 @@ +Allowed defining any :ref:`__slots__ ` for a class derived from +:class:`tuple` (including classes created by +:func:`collections.namedtuple`). diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 7f5149aeece12b..77e5c3e9f9ec95 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -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); diff --git a/Python/specialize.c b/Python/specialize.c index 31b7fd364cd8be..62f0373a4c274d 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -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; diff --git a/Python/structmember.c b/Python/structmember.c index 574acf296157f3..b88e13ac0462b8 100644 --- a/Python/structmember.c +++ b/Python/structmember.c @@ -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)) { From 7dae1077cd18c1ddc50b130335936bc71e1c4ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 6 Jan 2026 11:10:18 +0100 Subject: [PATCH 3/3] gh-143394: On macOS, run main PyREPL tests as "Apple Terminal" as well (GH-143461) --- Lib/test/test_pyrepl/test_pyrepl.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 00dcbdc562ae64..35a1733787e7a2 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -2012,6 +2012,17 @@ def test_no_newline(self): self.assertIn(expected_output_sequence, cleaned_output) +@skipUnless(sys.platform == "darwin", "macOS only") +class TestMainAppleTerminal(TestMain): + """Test the REPL with Apple Terminal's TERM_PROGRAM set.""" + + def run_repl(self, repl_input, env=None, **kwargs): + if env is None: + env = os.environ.copy() + env["TERM_PROGRAM"] = "Apple_Terminal" + return super().run_repl(repl_input, env=env, **kwargs) + + class TestPyReplCtrlD(TestCase): """Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.