From 850f95f6f64a55920cbb91b022b70b736bd20ed8 Mon Sep 17 00:00:00 2001 From: chaope Date: Mon, 15 Dec 2025 06:43:15 -0500 Subject: [PATCH] gh-142651: make `Mock.call_count` thread-safe (#142656) --- .../testmock/testthreadingmock.py | 24 ++++++++++++++++++- Lib/unittest/mock.py | 2 +- ...-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst | 3 +++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index a02b532ed447..3603995b090a 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -1,8 +1,10 @@ +import sys import time import unittest +import threading import concurrent.futures -from test.support import threading_helper +from test.support import setswitchinterval, threading_helper from unittest.mock import patch, ThreadingMock @@ -196,6 +198,26 @@ class TestThreadingMock(unittest.TestCase): m.wait_until_any_call_with() m.assert_called_once() + def test_call_count_thread_safe(self): + # See https://github.com/python/cpython/issues/142651. + m = ThreadingMock() + LOOPS = 100 + THREADS = 10 + def test_function(): + for _ in range(LOOPS): + m() + + oldswitchinterval = sys.getswitchinterval() + setswitchinterval(1e-6) + try: + threads = [threading.Thread(target=test_function) for _ in range(THREADS)] + with threading_helper.start_threads(threads): + pass + finally: + sys.setswitchinterval(oldswitchinterval) + + self.assertEqual(m.call_count, LOOPS * THREADS) + if __name__ == "__main__": unittest.main() diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 0bb675065538..34fd49bf56fb 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1180,7 +1180,6 @@ class CallableMixin(Base): def _increment_mock_call(self, /, *args, **kwargs): self.called = True - self.call_count += 1 # handle call_args # needs to be set here so assertions on call arguments pass before @@ -1188,6 +1187,7 @@ class CallableMixin(Base): _call = _Call((args, kwargs), two=True) self.call_args = _call self.call_args_list.append(_call) + self.call_count = len(self.call_args_list) # initial stuff for method_calls: do_method_calls = self._mock_parent is not None diff --git a/Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst b/Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst new file mode 100644 index 000000000000..236900bac5d6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst @@ -0,0 +1,3 @@ +:mod:`unittest.mock`: fix a thread safety issue where :attr:`Mock.call_count +` may return inaccurate values when the mock +is called concurrently from multiple threads. -- 2.47.3