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
34 changes: 34 additions & 0 deletions Lib/test/test_dict_mappingproxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Tests for dict() / update / unpacking of types.MappingProxyType.

gh-157217: merging from a mappingproxy wrapping a dict should take the
locked dict-to-dict path instead of iterating the proxy unlocked.
"""

import collections
import types
import unittest


class MappingProxyDictMergeTests(unittest.TestCase):
def test_update_from_mappingproxy_dict(self):
d = {}
d.update(types.MappingProxyType({1: 1, 2: 2, 3: 3}))
self.assertEqual(d, {1: 1, 2: 2, 3: 3})

def test_dict_constructor_from_mappingproxy(self):
view = types.MappingProxyType({'a': 1, 'b': 2})
self.assertEqual(dict(view), {'a': 1, 'b': 2})
self.assertEqual({**view}, {'a': 1, 'b': 2})
dest = {'z': 0}
dest.update(view)
self.assertEqual(dest, {'z': 0, 'a': 1, 'b': 2})

def test_dict_constructor_from_mappingproxy_userdict(self):
self.assertEqual(
dict(types.MappingProxyType(collections.UserDict(x=1))),
{'x': 1},
)


if __name__ == '__main__':
unittest.main()
46 changes: 44 additions & 2 deletions Lib/test/test_free_threading/test_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from threading import Barrier, Thread
from unittest import TestCase
import sys
from test.support import import_helper, threading_helper
from test.support import import_helper, threading_helper, Py_GIL_DISABLED

_testinternalcapi = import_helper.import_module("_testinternalcapi")

Expand Down Expand Up @@ -145,6 +145,7 @@ class ClassB(Base):
# keep reference to __dict__
d = obj.__dict__
obj.__class__ = ClassB
self.assertEqual(d['attr'], 123)


def test_name_change(self):
Expand Down Expand Up @@ -359,7 +360,8 @@ class Base:
pass

def setter():
func = lambda self: "x"
def func(self):
return "x"
barrier.wait()
while not done:
Base.__repr__ = func
Expand Down Expand Up @@ -401,6 +403,46 @@ class B(A):
with threading_helper.start_threads(threads):
pass

@unittest.skipUnless(Py_GIL_DISABLED,
"race only occurs on the free-threaded build")
def test_dir_racing_class_dict_insert(self):
# gh-157217: dir() iterated a mappingproxy of the class dict without
# holding that dict's critical section. A concurrent insert into the
# class dict (for example a lazy __annotations_cache__) then raised
# RuntimeError: dictionary changed size during iteration.
errors = []

class C:
x: int

for i in range(200):
setattr(C, f'attr_{i}', i)

def reader():
barrier.wait()
for _ in range(400):
try:
dir(C)
dict(vars(C))
{**vars(C)}
except RuntimeError as exc:
errors.append(exc)

def writer():
barrier.wait()
# First access stores __annotations_cache__ on the class.
C.__annotations__
for i in range(200):
setattr(C, f'extra_{i}', i)

n_readers = 4
barrier = threading.Barrier(n_readers + 1)
threads = [Thread(target=reader) for _ in range(n_readers)]
threads.append(Thread(target=writer))
with threading_helper.start_threads(threads):
pass
self.assertEqual(errors, [])


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :func:`dir` raising :exc:`RuntimeError` on the free-threaded build
when another thread concurrently inserts into a class ``__dict__``.
``dict.update()`` and related merges now take the locked dict-to-dict path
when the source is a :class:`types.MappingProxyType` wrapping a dict.
20 changes: 17 additions & 3 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -4303,11 +4303,25 @@ dict_merge(PyObject *a, PyObject *b, int override, PyObject **dupkey)

PyDictObject *mp = _PyAnyDict_CAST(a);

/* Mapping proxies (including type.__dict__) wrap a real dict. Unwrap
* so we take the locked dict-to-dict path instead of iterating the
* proxy without holding the underlying dict's critical section.
* Layout must match mappingproxyobject in descrobject.c. See gh-157217.
*/
typedef struct {
PyObject_HEAD
PyObject *mapping;
} mappingproxyobject;
PyObject *source = b;
if (Py_IS_TYPE(b, &PyDictProxy_Type)) {
source = ((mappingproxyobject *)b)->mapping;
}

int res = 0;
if (PyAnyDict_Check(b) && (Py_TYPE(b)->tp_iter == dict_iter)) {
PyDictObject *other = (PyDictObject*)b;
if (PyAnyDict_Check(source) && (Py_TYPE(source)->tp_iter == dict_iter)) {
PyDictObject *other = (PyDictObject*)source;
int res;
Py_BEGIN_CRITICAL_SECTION2(a, b);
Py_BEGIN_CRITICAL_SECTION2(a, source);
assert(can_modify_dict(mp));
res = dict_dict_merge((PyDictObject *)a, other, override, dupkey);
ASSERT_CONSISTENT(a);
Expand Down
Loading