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
24 changes: 24 additions & 0 deletions Lib/test/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,30 @@ def test_kwargs_order_preserved_in_c_functions(self):
self.assertEqual(list(collections.OrderedDict(b=1, a=2, c=3)),
['b', 'a', 'c'])

def test_kwargs_unpacking_mutation_isolated(self):
# gh-86199: a callee mutating its **kwargs must never affect the
# caller's dict.
def fn(**kw):
kw['injected'] = None
d = {'a': 1}
fn(**d)
self.assertEqual(d, {'a': 1})
e = {}
fn(**e)
self.assertEqual(e, {})

@cpython_only
@unittest.skipIf(_testcapi is None, "requires _testcapi")
def test_kwargs_unpacking_not_aliased_in_tp_call(self):
# gh-86199: a tp_call callee receives a private copy of the kwargs
# dict, never the caller's dict.
get_kwargs = _testcapi.get_kwargs # METH_VARARGS | METH_KEYWORDS
d = {'a': 1}
self.assertIsNot(get_kwargs(**d), d)
self.assertEqual(get_kwargs(**d), d)
e = {}
self.assertIsNot(get_kwargs(**e), e)

def test_frames_are_popped_after_failed_calls(self):
# GH-93252: stuff blows up if we don't pop the new frame after
# recovering from failed calls:
Expand Down
45 changes: 45 additions & 0 deletions Lib/test/test_free_threading/test_kwargs_unpack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import unittest
from unittest import TestCase

from test.support import threading_helper, import_helper

_testcapi = import_helper.import_module("_testcapi")

threading_helper.requires_working_threading(module=True)


class TestKwargsUnpackRace(TestCase):
def test_mutate_kwargs_during_unpack(self):
# gh-86199: unpacking a shared kwargs dict must tolerate another
# thread resizing it.
num_mutators, num_callers = 2, 6
iters = 1000
min_keys, max_keys = 4, 3000

fastcalldict = _testcapi.pyobject_fastcalldict

def target(**kwargs):
return len(kwargs)

shared = {f"k{i}": i for i in range(min_keys)}

def resize_kwargs():
for _ in range(iters):
for i in range(min_keys, max_keys):
shared[f"k{i}"] = i
for i in range(max_keys - 1, min_keys - 1, -1):
shared.pop(f"k{i}", None)

def call_target():
for _ in range(iters):
try:
fastcalldict(target, (), shared)
except Exception:
pass

threading_helper.run_concurrently(
[resize_kwargs] * num_mutators + [call_target] * num_callers)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Speed up calls of the form ``f(**kwargs)`` by no longer always copying the
keyword arguments before the call; such calls are now up to 1.4x faster.
This also fixes :c:func:`PyObject_Call` to copy the keyword arguments before
passing them to a callee that would otherwise receive the caller's dict, so a
called object can no longer mutate the caller's dict (:gh:`86795`), and makes
unpacking a shared keyword-arguments dict safe on the free-threaded build.
176 changes: 172 additions & 4 deletions Modules/_testinternalcapi/test_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading