From 826a69e7d12a5d9cca23008d5d811fa0fe81bb4b Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 13 Apr 2019 13:01:29 +0530 Subject: [PATCH 1/8] Initial commit for WaitableMock wait_until_called and wait_until_called_with are supported. This stores a dictionary with args as key and corresponding event object which is set once there is a call to it. In case of call not present we only want to know if the function was called and hence a per mock event object is also present which is set for no args case. --- Lib/unittest/mock.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 8684f1dfa5729f..969091a2ba3e21 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -28,6 +28,7 @@ import inspect import pprint import sys +import threading import builtins from types import ModuleType from unittest.util import safe_repr @@ -2482,6 +2483,37 @@ def __set__(self, obj, val): self(val) +class WaitableMock(Mock): + + def __init__(self, *args, **kwargs): + _safe_super(WaitableMock, self).__init__(*args, **kwargs) + self.event_class = kwargs.pop('event_class', threading.Event) + self.event = self.event_class() + self.expected_calls = {} + + def _mock_call(self, *args, **kwargs): + _safe_super(WaitableMock, self)._mock_call(*args, **kwargs) + for call in self._mock_mock_calls: + if (event := self.expected_calls.get(call.args)) and not event.is_set(): + event.set() + else: + self.event.set() + + def wait_until_called(self, timeout=1): + return self.event.wait(timeout=timeout) + + def wait_until_called_with(self, call=None, timeout=1): + if call: + if call.args not in self.expected_calls: + event = self.event_class() + self.expected_calls[call.args] = event + else: + event = self.expected_calls[call.args] + else: + event = self.event + + return event.is_set() or event.wait(timeout=timeout) + def seal(mock): """Disable the automatic generation of child mocks. From aa73fbc97d79ab8e95e20369e429417cc5563fd9 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 13 Apr 2019 20:53:37 +0530 Subject: [PATCH 2/8] Add tests and docs --- Lib/unittest/mock.py | 55 ++++--- .../test/testmock/testwaitablemock.py | 134 ++++++++++++++++++ 2 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 Lib/unittest/test/testmock/testwaitablemock.py diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 969091a2ba3e21..d2714d0bdd2bb4 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -18,6 +18,7 @@ 'NonCallableMagicMock', 'mock_open', 'PropertyMock', + 'WaitableMock', 'seal', ) @@ -2483,34 +2484,52 @@ def __set__(self, obj, val): self(val) -class WaitableMock(Mock): +class WaitableMock(MagicMock): + """ + A mock that can be used to wait until it was called. - def __init__(self, *args, **kwargs): + `event_class` - Class to be used to create event object. + Defaults to Threading.Event and can take values like multiprocessing.Event. + """ + + def __init__(self, *args, event_class=threading.Event, **kwargs): _safe_super(WaitableMock, self).__init__(*args, **kwargs) - self.event_class = kwargs.pop('event_class', threading.Event) - self.event = self.event_class() - self.expected_calls = {} + self._event = event_class() + self._expected_calls = {} def _mock_call(self, *args, **kwargs): - _safe_super(WaitableMock, self)._mock_call(*args, **kwargs) + ret_value = _safe_super(WaitableMock, self)._mock_call(*args, **kwargs) + for call in self._mock_mock_calls: - if (event := self.expected_calls.get(call.args)) and not event.is_set(): + event = self._expected_calls.get(call.args) + if event and not event.is_set(): event.set() - else: - self.event.set() - def wait_until_called(self, timeout=1): - return self.event.wait(timeout=timeout) + self._event.set() - def wait_until_called_with(self, call=None, timeout=1): - if call: - if call.args not in self.expected_calls: - event = self.event_class() - self.expected_calls[call.args] = event + return ret_value + + def wait_until_called(self, timeout=1.0): + """Wait until the mock object is called. + + `timeout` - time to wait for in seconds. Defaults to 1. + """ + return self._event.wait(timeout=timeout) + + def wait_until_called_with(self, *args, timeout=1.0): + """Wait until the mock object is called with given args. + If args is empty then it waits for the mock object to be called. + + `timeout` - time to wait for in seconds. Defaults to 1. + """ + if args: + if args not in self.expected_calls: + event = self._event_class() + self._expected_calls[args] = event else: - event = self.expected_calls[call.args] + event = self._expected_calls[args] else: - event = self.event + event = self._event return event.is_set() or event.wait(timeout=timeout) diff --git a/Lib/unittest/test/testmock/testwaitablemock.py b/Lib/unittest/test/testmock/testwaitablemock.py new file mode 100644 index 00000000000000..1bac75a8ab80cb --- /dev/null +++ b/Lib/unittest/test/testmock/testwaitablemock.py @@ -0,0 +1,134 @@ +import threading +import time +import unittest + +from test.support import start_threads +from unittest.mock import patch, WaitableMock, call + + +class Something: + + def method_1(self): + pass + + def method_2(self): + pass + + +class TestWaitableMock(unittest.TestCase): + + + def _call_after_delay(self, func, *args, delay): + time.sleep(delay) + func(*args) + + + def test_instance_check(self): + waitable_mock = WaitableMock(event_class=threading.Event) + + with patch(f'{__name__}.Something', waitable_mock): + something = Something() + + self.assertIsInstance(something.method_1, WaitableMock) + self.assertIsInstance( + something.method_1().method_2(), WaitableMock) + + + def test_side_effect(self): + waitable_mock = WaitableMock(event_class=threading.Event) + + with patch(f'{__name__}.Something', waitable_mock): + something = Something() + something.method_1.side_effect = [1] + + self.assertEqual(something.method_1(), 1) + + + def test_spec(self): + waitable_mock = WaitableMock( + event_class=threading.Event, spec=Something) + + with patch(f'{__name__}.Something', waitable_mock) as m: + something = m() + + self.assertIsInstance(something.method_1, WaitableMock) + self.assertIsInstance( + something.method_1().method_2(), WaitableMock) + + with self.assertRaises(AttributeError): + m.test + + + def test_wait_until_called(self): + waitable_mock = WaitableMock(event_class=threading.Event) + + with patch(f'{__name__}.Something', waitable_mock): + something = Something() + thread = threading.Thread(target=self._call_after_delay, + args=(something.method_1, ), + kwargs={'delay': 0.5}) + + with start_threads([thread]): + something.method_1.wait_until_called() + something.method_1.assert_called_once() + + + def test_wait_until_called_magic_method(self): + waitable_mock = WaitableMock(event_class=threading.Event) + + with patch(f'{__name__}.Something', waitable_mock): + something = Something() + thread = threading.Thread(target=self._call_after_delay, + args=(something.method_1.__str__, ), + kwargs={'delay': 0.5}) + + with start_threads([thread]): + something.method_1.__str__.wait_until_called() + something.method_1.__str__.assert_called_once() + + + def test_wait_until_called_timeout(self): + waitable_mock = WaitableMock(event_class=threading.Event) + + with patch(f'{__name__}.Something', waitable_mock): + something = Something() + thread = threading.Thread(target=self._call_after_delay, args=(something.method_1, ), + kwargs={'delay': 0.5}) + + with start_threads([thread]): + something.method_1.wait_until_called(timeout=0.1) + something.method_1.assert_not_called() + + time.sleep(0.5) + something.method_1.wait_until_called() + something.method_1.assert_called_once() + + + def test_wait_until_called_with(self): + waitable_mock = WaitableMock(event_class=threading.Event) + + with patch(f'{__name__}.Something', waitable_mock): + something = Something() + thread_1 = threading.Thread(target=self._call_after_delay, + args=(something.method_1, 1), + kwargs={'delay': 0.5}) + thread_2 = threading.Thread(target=self._call_after_delay, + args=(something.method_2, 1), + kwargs={'delay': 0.1}) + thread_3 = threading.Thread(target=self._call_after_delay, + args=(something.method_2, 2), + kwargs={'delay': 0.1}) + + with start_threads([thread_1, thread_2, thread_3]): + something.method_1.wait_until_called_with(1, timeout=0.1) + something.method_1.assert_not_called() + + time.sleep(0.5) + + something.method_1.assert_called_once_with(1) + something.method_2.assert_has_calls( + [call(1), call(2)], any_order=True) + + +if __name__ == "__main__": + unittest.main() From 0f0482ebb395c791a9e5ad7355a0f75e40c4c5ba Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 13 Apr 2019 20:56:24 +0530 Subject: [PATCH 3/8] Add NEWS entry --- .../next/Library/2019-04-13-20-56-09.bpo-17013.R_sgfy.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-04-13-20-56-09.bpo-17013.R_sgfy.rst diff --git a/Misc/NEWS.d/next/Library/2019-04-13-20-56-09.bpo-17013.R_sgfy.rst b/Misc/NEWS.d/next/Library/2019-04-13-20-56-09.bpo-17013.R_sgfy.rst new file mode 100644 index 00000000000000..7b5afc1dde588d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-04-13-20-56-09.bpo-17013.R_sgfy.rst @@ -0,0 +1,3 @@ +Add `WaitableMock` to :mod:`unittest.mock` that can be used to create Mock +objects that can wait until they are called. Patch by Karthikeyan +Singaravelan. From f51f5b74a34ddeb3fc3e7cb77bc72c2d7704b863 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 13 Apr 2019 21:40:01 +0530 Subject: [PATCH 4/8] Use defaultdict for event object creation --- Lib/unittest/mock.py | 9 +++++---- Lib/unittest/test/testmock/testwaitablemock.py | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index d2714d0bdd2bb4..00cc4859b5d904 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -26,6 +26,7 @@ __version__ = '1.0' +from collections import defaultdict import inspect import pprint import sys @@ -2495,15 +2496,14 @@ class WaitableMock(MagicMock): def __init__(self, *args, event_class=threading.Event, **kwargs): _safe_super(WaitableMock, self).__init__(*args, **kwargs) self._event = event_class() - self._expected_calls = {} + self._expected_calls = defaultdict(lambda: event_class()) def _mock_call(self, *args, **kwargs): ret_value = _safe_super(WaitableMock, self)._mock_call(*args, **kwargs) for call in self._mock_mock_calls: - event = self._expected_calls.get(call.args) - if event and not event.is_set(): - event.set() + event = self._expected_calls[call.args] + event.set() self._event.set() @@ -2533,6 +2533,7 @@ def wait_until_called_with(self, *args, timeout=1.0): return event.is_set() or event.wait(timeout=timeout) + def seal(mock): """Disable the automatic generation of child mocks. diff --git a/Lib/unittest/test/testmock/testwaitablemock.py b/Lib/unittest/test/testmock/testwaitablemock.py index 1bac75a8ab80cb..b5686216a95e2c 100644 --- a/Lib/unittest/test/testmock/testwaitablemock.py +++ b/Lib/unittest/test/testmock/testwaitablemock.py @@ -123,12 +123,22 @@ def test_wait_until_called_with(self): something.method_1.wait_until_called_with(1, timeout=0.1) something.method_1.assert_not_called() - time.sleep(0.5) - + something.method_1.wait_until_called(timeout=2.0) something.method_1.assert_called_once_with(1) something.method_2.assert_has_calls( [call(1), call(2)], any_order=True) + def test_wait_until_called_with_default_event(self): + waitable_mock = WaitableMock(event_class=threading.Event) + + with patch(f'{__name__}.Something', waitable_mock): + something = Something() + something.method_1(1) + + something.method_1.assert_called_once_with(1) + something.method_1.wait_until_called_with() + + if __name__ == "__main__": unittest.main() From a941ca1b223048b0a0a36617639d41dcecc1ebf7 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 13 Apr 2019 23:18:27 +0530 Subject: [PATCH 5/8] Use proper attributes and assert for mock_calls --- Lib/unittest/mock.py | 3 ++- Lib/unittest/test/testmock/testwaitablemock.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 00cc4859b5d904..63a4973e69b551 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2495,6 +2495,7 @@ class WaitableMock(MagicMock): def __init__(self, *args, event_class=threading.Event, **kwargs): _safe_super(WaitableMock, self).__init__(*args, **kwargs) + self._event_class = event_class self._event = event_class() self._expected_calls = defaultdict(lambda: event_class()) @@ -2523,7 +2524,7 @@ def wait_until_called_with(self, *args, timeout=1.0): `timeout` - time to wait for in seconds. Defaults to 1. """ if args: - if args not in self.expected_calls: + if args not in self._expected_calls: event = self._event_class() self._expected_calls[args] = event else: diff --git a/Lib/unittest/test/testmock/testwaitablemock.py b/Lib/unittest/test/testmock/testwaitablemock.py index b5686216a95e2c..a7b9ec6d1728f4 100644 --- a/Lib/unittest/test/testmock/testwaitablemock.py +++ b/Lib/unittest/test/testmock/testwaitablemock.py @@ -99,7 +99,6 @@ def test_wait_until_called_timeout(self): something.method_1.wait_until_called(timeout=0.1) something.method_1.assert_not_called() - time.sleep(0.5) something.method_1.wait_until_called() something.method_1.assert_called_once() @@ -125,6 +124,7 @@ def test_wait_until_called_with(self): something.method_1.wait_until_called(timeout=2.0) something.method_1.assert_called_once_with(1) + self.assertEqual(something.method_1.mock_calls, [call(1)]) something.method_2.assert_has_calls( [call(1), call(2)], any_order=True) From 3031310673497fa345b429765eaea652915653d8 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 13 Apr 2019 23:19:41 +0530 Subject: [PATCH 6/8] Remove unused lambda for defaultdict --- Lib/unittest/mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 63a4973e69b551..ce66d4b46d0044 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2497,7 +2497,7 @@ def __init__(self, *args, event_class=threading.Event, **kwargs): _safe_super(WaitableMock, self).__init__(*args, **kwargs) self._event_class = event_class self._event = event_class() - self._expected_calls = defaultdict(lambda: event_class()) + self._expected_calls = defaultdict(event_class) def _mock_call(self, *args, **kwargs): ret_value = _safe_super(WaitableMock, self)._mock_call(*args, **kwargs) From c67715b8ceaab0b537e7716f11d9fe326dbea2d1 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sun, 14 Apr 2019 00:24:55 +0530 Subject: [PATCH 7/8] Don't use default event for empty args and use helper for thread creation --- Lib/unittest/mock.py | 14 +++---- .../test/testmock/testwaitablemock.py | 38 +++++++++---------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index ce66d4b46d0044..24d16e789526a2 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2519,20 +2519,16 @@ def wait_until_called(self, timeout=1.0): def wait_until_called_with(self, *args, timeout=1.0): """Wait until the mock object is called with given args. - If args is empty then it waits for the mock object to be called. `timeout` - time to wait for in seconds. Defaults to 1. """ - if args: - if args not in self._expected_calls: - event = self._event_class() - self._expected_calls[args] = event - else: - event = self._expected_calls[args] + if args not in self._expected_calls: + event = self._event_class() + self._expected_calls[args] = event else: - event = self._event + event = self._expected_calls[args] - return event.is_set() or event.wait(timeout=timeout) + return event.wait(timeout=timeout) def seal(mock): diff --git a/Lib/unittest/test/testmock/testwaitablemock.py b/Lib/unittest/test/testmock/testwaitablemock.py index a7b9ec6d1728f4..e929c70fdee4ed 100644 --- a/Lib/unittest/test/testmock/testwaitablemock.py +++ b/Lib/unittest/test/testmock/testwaitablemock.py @@ -23,6 +23,12 @@ def _call_after_delay(self, func, *args, delay): func(*args) + def _create_thread(self, func, *args, **kwargs): + thread = threading.Thread(target=self._call_after_delay, + args=(func,) + args, kwargs=kwargs) + return thread + + def test_instance_check(self): waitable_mock = WaitableMock(event_class=threading.Event) @@ -64,9 +70,7 @@ def test_wait_until_called(self): with patch(f'{__name__}.Something', waitable_mock): something = Something() - thread = threading.Thread(target=self._call_after_delay, - args=(something.method_1, ), - kwargs={'delay': 0.5}) + thread = self._create_thread(something.method_1, delay=0.5) with start_threads([thread]): something.method_1.wait_until_called() @@ -78,9 +82,7 @@ def test_wait_until_called_magic_method(self): with patch(f'{__name__}.Something', waitable_mock): something = Something() - thread = threading.Thread(target=self._call_after_delay, - args=(something.method_1.__str__, ), - kwargs={'delay': 0.5}) + thread = self._create_thread(something.method_1.__str__, delay=0.5) with start_threads([thread]): something.method_1.__str__.wait_until_called() @@ -92,8 +94,7 @@ def test_wait_until_called_timeout(self): with patch(f'{__name__}.Something', waitable_mock): something = Something() - thread = threading.Thread(target=self._call_after_delay, args=(something.method_1, ), - kwargs={'delay': 0.5}) + thread = self._create_thread(something.method_1, delay=0.5) with start_threads([thread]): something.method_1.wait_until_called(timeout=0.1) @@ -108,15 +109,9 @@ def test_wait_until_called_with(self): with patch(f'{__name__}.Something', waitable_mock): something = Something() - thread_1 = threading.Thread(target=self._call_after_delay, - args=(something.method_1, 1), - kwargs={'delay': 0.5}) - thread_2 = threading.Thread(target=self._call_after_delay, - args=(something.method_2, 1), - kwargs={'delay': 0.1}) - thread_3 = threading.Thread(target=self._call_after_delay, - args=(something.method_2, 2), - kwargs={'delay': 0.1}) + thread_1 = self._create_thread(something.method_1, 1, delay=0.5) + thread_2 = self._create_thread(something.method_2, 1, delay=0.1) + thread_3 = self._create_thread(something.method_2, 2, delay=0.1) with start_threads([thread_1, thread_2, thread_3]): something.method_1.wait_until_called_with(1, timeout=0.1) @@ -129,7 +124,7 @@ def test_wait_until_called_with(self): [call(1), call(2)], any_order=True) - def test_wait_until_called_with_default_event(self): + def test_wait_until_called_with_no_argument(self): waitable_mock = WaitableMock(event_class=threading.Event) with patch(f'{__name__}.Something', waitable_mock): @@ -137,7 +132,12 @@ def test_wait_until_called_with_default_event(self): something.method_1(1) something.method_1.assert_called_once_with(1) - something.method_1.wait_until_called_with() + self.assertFalse( + something.method_1.wait_until_called_with(timeout=0.1)) + + thread_1 = self._create_thread(something.method_1, delay=0.1) + with start_threads([thread_1]): + self.assertTrue(something.method_1.wait_until_called_with()) if __name__ == "__main__": From 5b59f340d99d092a1cac973ccae8784a39682e4b Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sun, 14 Apr 2019 16:51:27 +0530 Subject: [PATCH 8/8] Allow defaultdict to take care of missing event objects --- Lib/unittest/mock.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 24d16e789526a2..9aac9a57da8b65 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2495,7 +2495,6 @@ class WaitableMock(MagicMock): def __init__(self, *args, event_class=threading.Event, **kwargs): _safe_super(WaitableMock, self).__init__(*args, **kwargs) - self._event_class = event_class self._event = event_class() self._expected_calls = defaultdict(event_class) @@ -2522,12 +2521,7 @@ def wait_until_called_with(self, *args, timeout=1.0): `timeout` - time to wait for in seconds. Defaults to 1. """ - if args not in self._expected_calls: - event = self._event_class() - self._expected_calls[args] = event - else: - event = self._expected_calls[args] - + event = self._expected_calls[args] return event.wait(timeout=timeout)