Skip to content

Commit 540c085

Browse files
committed
Revise backport of GH-159972.
Avoid changing the default build behaviour. The following changes are now only for the free-threaded build: - fresh __bases__ tuple copying - type_new_copy_dict() and namespace-key normalization - addition of the new exact-Unicode dictionary helpers In the default build, you can still create a type with string-subclass keys.
1 parent 15bc382 commit 540c085

5 files changed

Lines changed: 55 additions & 11 deletions

File tree

‎Include/internal/pycore_dict.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,12 @@ PyAPI_FUNC(int) _PyDict_SetItem_KnownHash_LockHeld(PyDictObject *mp, PyObject *k
169169
PyAPI_FUNC(int) _PyDict_GetItemRef_KnownHash_LockHeld(PyDictObject *op, PyObject *key, Py_hash_t hash, PyObject **result);
170170
PyAPI_FUNC(int) _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject *key, Py_hash_t hash, PyObject **result);
171171
extern int _PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject **result);
172+
#ifdef Py_GIL_DISABLED
172173
extern PyObject *_PyDict_GetItem_ExactUnicode(PyDictObject *mp, PyObject *key);
173174
extern int _PyDict_GetItemStackRef_ExactUnicode(PyDictObject *mp,
174175
PyObject *key,
175176
_PyStackRef *value_addr);
177+
#endif
176178
PyAPI_FUNC(int) _PyObjectDict_SetItem(PyTypeObject *tp, PyObject *obj, PyObject **dictptr, PyObject *name, PyObject *value);
177179

178180
extern int _PyDict_Pop_KnownHash(

‎Lib/test/test_descr.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4371,6 +4371,22 @@ class B_tuple(tuple):
43714371
B_tuple.__bases__ = (tuple,)
43724372
self.assertEqual(repr(b), '(1, 2)')
43734373

4374+
@support.cpython_only
4375+
def test_assign_bases_tuple_identity(self):
4376+
class A:
4377+
pass
4378+
4379+
class B(A):
4380+
pass
4381+
4382+
bases = (A,)
4383+
B.__bases__ = bases
4384+
self.assertEqual(B.__bases__, bases)
4385+
if support.Py_GIL_DISABLED:
4386+
self.assertIsNot(B.__bases__, bases)
4387+
else:
4388+
self.assertIs(B.__bases__, bases)
4389+
43744390
def test_assign_bases_many_subclasses(self):
43754391
# This is intended to check that typeobject.c:queue_slot_update() can
43764392
# handle updating many subclasses when a slot method is re-assigned.
@@ -5098,6 +5114,22 @@ def __new__(mcls, name, bases, ns):
50985114
with self.assertWarnsRegex(RuntimeWarning, 'MyClass'):
50995115
MyClass = meta('MyClass', (), {})
51005116

5117+
@support.cpython_only
5118+
def test_str_subclass_namespace_key_identity(self):
5119+
class Key(str):
5120+
pass
5121+
5122+
key = Key('other')
5123+
C = type('C', (), {key: 1})
5124+
stored_key = next(iter(C.__dict__))
5125+
self.assertEqual(stored_key, key)
5126+
self.assertEqual(C.other, 1)
5127+
if support.Py_GIL_DISABLED:
5128+
self.assertIs(type(stored_key), str)
5129+
else:
5130+
self.assertIs(stored_key, key)
5131+
5132+
@unittest.skipUnless(support.Py_GIL_DISABLED, 'needs a free-threaded build')
51015133
def test_str_subclass_namespace_keys(self):
51025134
# Keys of the class namespace that are str subclasses are copied
51035135
# to exact str in the type's __dict__, like type.__setattr__ does.

‎Lib/test/test_free_threading/test_type.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
from threading import Thread
77
from unittest import TestCase
88

9-
from test.support import threading_helper
9+
from test.support import import_helper, threading_helper
10+
11+
_testinternalcapi = import_helper.import_module("_testinternalcapi")
1012

1113

1214

‎Objects/dictobject.c‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2580,6 +2580,7 @@ PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result)
25802580
return _PyDict_GetItemRef_KnownHash((PyDictObject *)op, key, hash, result);
25812581
}
25822582

2583+
#ifdef Py_GIL_DISABLED
25832584
static inline int
25842585
compare_generic_exact_unicode(PyDictObject *mp, PyDictKeysObject *dk,
25852586
void *ep0, Py_ssize_t ix, PyObject *key,
@@ -2632,7 +2633,6 @@ _PyDict_GetItemStackRef_ExactUnicode(PyDictObject *mp, PyObject *key,
26322633
{
26332634
assert(PyUnicode_CheckExact(key));
26342635
PyObject *value;
2635-
#ifdef Py_GIL_DISABLED
26362636
ensure_shared_on_read(mp);
26372637
PyDictKeysObject *dk = _Py_atomic_load_ptr_acquire(&mp->ma_keys);
26382638
if (dk->dk_kind == DICT_KEYS_UNICODE) {
@@ -2648,12 +2648,9 @@ _PyDict_GetItemStackRef_ExactUnicode(PyDictObject *mp, PyObject *key,
26482648
value = _PyDict_GetItem_ExactUnicode(mp, key);
26492649
*value_addr = value ? PyStackRef_FromPyObjectNew(value) : PyStackRef_NULL;
26502650
Py_END_CRITICAL_SECTION();
2651-
#else
2652-
value = _PyDict_GetItem_ExactUnicode(mp, key);
2653-
*value_addr = value ? PyStackRef_FromPyObjectNew(value) : PyStackRef_NULL;
2654-
#endif
26552651
return value != NULL;
26562652
}
2653+
#endif
26572654

26582655
int
26592656
_PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject **result)

‎Objects/typeobject.c‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,6 +2116,7 @@ type_set_bases(PyObject *tp, PyObject *new_bases, void *Py_UNUSED(closure))
21162116
if (type_check_new_bases(type, new_bases) < 0) {
21172117
return -1;
21182118
}
2119+
#ifdef Py_GIL_DISABLED
21192120
// Create a fresh bases tuple to store. Its identity tells whether
21202121
// tp_bases was replaced again while TYPE_LOCK was released or due to
21212122
// re-entrancy from the mro() call.
@@ -2124,6 +2125,9 @@ type_set_bases(PyObject *tp, PyObject *new_bases, void *Py_UNUSED(closure))
21242125
if (bases == NULL) {
21252126
return -1;
21262127
}
2128+
#else
2129+
PyObject *bases = Py_NewRef(new_bases);
2130+
#endif
21272131
BEGIN_TYPE_LOCK();
21282132
res = type_set_bases_unlocked(type, bases);
21292133
END_TYPE_LOCK();
@@ -4902,6 +4906,7 @@ type_new_get_slots(type_new_ctx *ctx, PyObject *dict)
49024906
}
49034907

49044908

4909+
#ifdef Py_GIL_DISABLED
49054910
/* Copy the class namespace. Keys that are str subclasses are replaced by
49064911
exact str copies so that the type dict only holds exact str keys (see
49074912
type_setattro() which does the same for assigned names). Lookups in a
@@ -4963,11 +4968,16 @@ type_new_copy_dict(PyObject *orig_dict)
49634968
Py_DECREF(newdict);
49644969
return NULL;
49654970
}
4971+
#endif
49664972

49674973
static PyTypeObject*
49684974
type_new_init(type_new_ctx *ctx)
49694975
{
4976+
#ifdef Py_GIL_DISABLED
49704977
PyObject *dict = type_new_copy_dict(ctx->orig_dict);
4978+
#else
4979+
PyObject *dict = _PyDict_CopyAsDict(ctx->orig_dict);
4980+
#endif
49714981
if (dict == NULL) {
49724982
goto error;
49734983
}
@@ -6797,11 +6807,12 @@ type_update_dict(PyTypeObject *type, PyDictObject *dict, PyObject *name,
67976807
// We don't want any re-entrancy between when we update the dict
67986808
// and call type_modified_unlocked, including running the destructor
67996809
// of the current value as it can observe the cache in an inconsistent
6800-
// state. The name is an exact unicode (see type_setattro()) and
6801-
// str subclass keys are copied to exact str by type_new_copy_dict()
6802-
// and type_setattro(). The type dict can still hold non-str keys
6803-
// (type_new_impl() only warns about them) and comparing with one of
6804-
// those can run user code that releases the locks.
6810+
// state. The name is an exact unicode (see type_setattro()). In the
6811+
// free-threaded build, str subclass keys are copied to exact str by
6812+
// type_new_copy_dict(). The type dict can still hold non-str keys
6813+
// (type_new_impl() only warns about them), or str subclass keys in the
6814+
// default build. Comparing with those can run user code that releases
6815+
// the locks.
68056816
if (_PyDict_GetItemRef_Unicode_LockHeld(dict, name, old_value) < 0) {
68066817
return -1;
68076818
}

0 commit comments

Comments
 (0)