diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index a98decc2c9f..513cd5c924d 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -852,7 +852,8 @@ class NonCallableMock(Base): else: name, args, kwargs = _call try: - return name, sig.bind(*args, **kwargs) + bound_call = sig.bind(*args, **kwargs) + return call(name, bound_call.args, bound_call.kwargs) except TypeError as e: return e.with_traceback(None) else: @@ -901,9 +902,9 @@ class NonCallableMock(Base): def _error_message(): msg = self._format_mock_failure_message(args, kwargs) return msg - expected = self._call_matcher((args, kwargs)) + expected = self._call_matcher(_Call((args, kwargs), two=True)) actual = self._call_matcher(self.call_args) - if expected != actual: + if actual != expected: cause = expected if isinstance(expected, Exception) else None raise AssertionError(_error_message()) from cause @@ -963,10 +964,10 @@ class NonCallableMock(Base): The assert passes if the mock has *ever* been called, unlike `assert_called_with` and `assert_called_once_with` that only pass if the call is the most recent one.""" - expected = self._call_matcher((args, kwargs)) + expected = self._call_matcher(_Call((args, kwargs), two=True)) + cause = expected if isinstance(expected, Exception) else None actual = [self._call_matcher(c) for c in self.call_args_list] - if expected not in actual: - cause = expected if isinstance(expected, Exception) else None + if cause or expected not in _AnyComparer(actual): expected_string = self._format_mock_call_signature(args, kwargs) raise AssertionError( '%s call not found' % expected_string @@ -1019,6 +1020,22 @@ class NonCallableMock(Base): return f"\n{prefix}: {safe_repr(self.mock_calls)}." +class _AnyComparer(list): + """A list which checks if it contains a call which may have an + argument of ANY, flipping the components of item and self from + their traditional locations so that ANY is guaranteed to be on + the left.""" + def __contains__(self, item): + for _call in self: + if len(item) != len(_call): + continue + if all([ + expected == actual + for expected, actual in zip(item, _call) + ]): + return True + return False + def _try_iter(obj): if obj is None: @@ -2171,9 +2188,9 @@ class AsyncMockMixin(Base): msg = self._format_mock_failure_message(args, kwargs, action='await') return msg - expected = self._call_matcher((args, kwargs)) + expected = self._call_matcher(_Call((args, kwargs), two=True)) actual = self._call_matcher(self.await_args) - if expected != actual: + if actual != expected: cause = expected if isinstance(expected, Exception) else None raise AssertionError(_error_message()) from cause @@ -2192,10 +2209,10 @@ class AsyncMockMixin(Base): """ Assert the mock has ever been awaited with the specified arguments. """ - expected = self._call_matcher((args, kwargs)) + expected = self._call_matcher(_Call((args, kwargs), two=True)) + cause = expected if isinstance(expected, Exception) else None actual = [self._call_matcher(c) for c in self.await_args_list] - if expected not in actual: - cause = expected if isinstance(expected, Exception) else None + if cause or expected not in _AnyComparer(actual): expected_string = self._format_mock_call_signature(args, kwargs) raise AssertionError( '%s await not found' % expected_string diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 550234223cc..87f3cfc8fa4 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -2,8 +2,8 @@ import asyncio import inspect import unittest -from unittest.mock import (call, AsyncMock, patch, MagicMock, create_autospec, - _AwaitEvent) +from unittest.mock import (ANY, call, AsyncMock, patch, MagicMock, + create_autospec, _AwaitEvent) def tearDownModule(): @@ -192,6 +192,10 @@ class AsyncAutospecTest(unittest.TestCase): spec.assert_awaited_with(1, 2, c=3) spec.assert_awaited() + with self.assertRaises(AssertionError): + spec.assert_any_await(e=1) + + def test_patch_with_autospec(self): async def test_async(): @@ -607,6 +611,30 @@ class AsyncMockAssert(unittest.TestCase): asyncio.run(self._runnable_test('SomethingElse')) self.mock.assert_has_awaits(calls) + def test_awaits_asserts_with_any(self): + class Foo: + def __eq__(self, other): pass + + asyncio.run(self._runnable_test(Foo(), 1)) + + self.mock.assert_has_awaits([call(ANY, 1)]) + self.mock.assert_awaited_with(ANY, 1) + self.mock.assert_any_await(ANY, 1) + + def test_awaits_asserts_with_spec_and_any(self): + class Foo: + def __eq__(self, other): pass + + mock_with_spec = AsyncMock(spec=Foo) + + async def _custom_mock_runnable_test(*args): + await mock_with_spec(*args) + + asyncio.run(_custom_mock_runnable_test(Foo(), 1)) + mock_with_spec.assert_has_awaits([call(ANY, 1)]) + mock_with_spec.assert_awaited_with(ANY, 1) + mock_with_spec.assert_any_await(ANY, 1) + def test_assert_has_awaits_ordered(self): calls = [call('NormalFoo'), call('baz')] with self.assertRaises(AssertionError): diff --git a/Lib/unittest/test/testmock/testhelpers.py b/Lib/unittest/test/testmock/testhelpers.py index f3c7acb98c2..9e7ec5d62d5 100644 --- a/Lib/unittest/test/testmock/testhelpers.py +++ b/Lib/unittest/test/testmock/testhelpers.py @@ -64,7 +64,28 @@ class AnyTest(unittest.TestCase): self.assertEqual(expected, mock.mock_calls) self.assertEqual(mock.mock_calls, expected) + def test_any_no_spec(self): + # This is a regression test for bpo-37555 + class Foo: + def __eq__(self, other): pass + mock = Mock() + mock(Foo(), 1) + mock.assert_has_calls([call(ANY, 1)]) + mock.assert_called_with(ANY, 1) + mock.assert_any_call(ANY, 1) + + def test_any_and_spec_set(self): + # This is a regression test for bpo-37555 + class Foo: + def __eq__(self, other): pass + + mock = Mock(spec=Foo) + + mock(Foo(), 1) + mock.assert_has_calls([call(ANY, 1)]) + mock.assert_called_with(ANY, 1) + mock.assert_any_call(ANY, 1) class CallTest(unittest.TestCase): diff --git a/Misc/ACKS b/Misc/ACKS index f321131b2ab..71e61c3db38 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -503,6 +503,7 @@ Tomer Filiba Segev Finer Jeffrey Finkelstein Russell Finn +Neal Finne Dan Finnie Nils Fischbeck Frederik Fix @@ -1714,6 +1715,7 @@ Roger Upole Daniel Urban Michael Urman Hector Urtubia +Elizabeth Uselton Lukas Vacek Ville Vainio Yann Vaginay diff --git a/Misc/NEWS.d/next/Library/2019-07-19-20-13-48.bpo-37555.S5am28.rst b/Misc/NEWS.d/next/Library/2019-07-19-20-13-48.bpo-37555.S5am28.rst new file mode 100644 index 00000000000..16d1d62d309 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-07-19-20-13-48.bpo-37555.S5am28.rst @@ -0,0 +1,2 @@ +Fix `NonCallableMock._call_matcher` returning tuple instead of `_Call` object +when `self._spec_signature` exists. Patch by Elizabeth Uselton \ No newline at end of file