Skip to content

Commit d6a9d17

Browse files
ElizabethUpganssle
authored andcommitted
bpo-37555: Update _CallList.__contains__ to respect ANY (#14700)
* Flip equality to use mock calls' __eq__ * bpo-37555: Regression test demonstrating assert_has_calls not working with ANY and spec_set Co-authored-by: Neal Finne <neal@nealfinne.com> * Revert "Flip equality to use mock calls' __eq__" This reverts commit 94ddf54. * bpo-37555: Add regression tests for mock ANY ordering issues Add regression tests for whether __eq__ is order agnostic on _Call and _CallList, which is useful for comparisons involving ANY, especially if the ANY comparison is to a class not defaulting __eq__ to NotImplemented. Co-authored-by: Neal Finne <neal@nealfinne.com> * bpo-37555: Fix _CallList and _Call order sensitivity _Call and _CallList depend on ordering to correctly process that an object being compared to ANY with __eq__ should return True. This fix updates the comparison to check both a == b and b == a and return True if either condition is met, fixing situations from the tests in the previous two commits where assertEqual would not be commutative if checking _Call or _CallList objects. This seems like a reasonable fix considering that the Python data model specifies that if an object doesn't know how to compare itself to another object it should return NotImplemented, and that on getting NotImplemented from a == b, it should try b == a, implying that good behavior for __eq__ is commutative. This also flips the order of comparison in _CallList's __contains__ method, guaranteeing ANY will be on the left and have it's __eq__ called for equality checking, fixing the interaction between assert_has_calls and ANY. Co-author: Neal Finne <neal@neal.finne.com> * bpo-37555: Ensure _call_matcher returns _Call object * Adding ACK and news entry * bpo-37555: Replacing __eq__ with == to sidestep NotImplemented bool(NotImplemented) returns True, so it's necessary to use == instead of __eq__ in this comparison. * bpo-37555: cleaning up changes unnecessary to the final product * bpo-37555: Fixed call on bound arguments to respect args and kwargs * Revert "bpo-37555: Add regression tests for mock ANY ordering issues" This reverts commit 49c5310. * Revert "bpo-37555: cleaning up changes unnecessary to the final product" This reverts commit 18e964b. * Revert "bpo-37555: Replacing __eq__ with == to sidestep NotImplemented" This reverts commit f295eac. * Revert "bpo-37555: Fix _CallList and _Call order sensitivity" This reverts commit 874fb69. * Updated NEWS.d * bpo-37555: Add tests checking every function using _call_matcher both with and without spec * bpo-37555: Ensure all assert methods using _call_matcher are actually passing calls * Remove AnyCompare and use call objects everywhere. * Revert "Remove AnyCompare and use call objects everywhere." This reverts commit 24973c0. * Check for exception in assert_any_await
1 parent 81319a8 commit d6a9d17

File tree

5 files changed

+83
-13
lines changed

5 files changed

+83
-13
lines changed

Lib/unittest/mock.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,8 @@ def _call_matcher(self, _call):
852852
else:
853853
name, args, kwargs = _call
854854
try:
855-
return name, sig.bind(*args, **kwargs)
855+
bound_call = sig.bind(*args, **kwargs)
856+
return call(name, bound_call.args, bound_call.kwargs)
856857
except TypeError as e:
857858
return e.with_traceback(None)
858859
else:
@@ -901,9 +902,9 @@ def assert_called_with(self, /, *args, **kwargs):
901902
def _error_message():
902903
msg = self._format_mock_failure_message(args, kwargs)
903904
return msg
904-
expected = self._call_matcher((args, kwargs))
905+
expected = self._call_matcher(_Call((args, kwargs), two=True))
905906
actual = self._call_matcher(self.call_args)
906-
if expected != actual:
907+
if actual != expected:
907908
cause = expected if isinstance(expected, Exception) else None
908909
raise AssertionError(_error_message()) from cause
909910

@@ -963,10 +964,10 @@ def assert_any_call(self, /, *args, **kwargs):
963964
The assert passes if the mock has *ever* been called, unlike
964965
`assert_called_with` and `assert_called_once_with` that only pass if
965966
the call is the most recent one."""
966-
expected = self._call_matcher((args, kwargs))
967+
expected = self._call_matcher(_Call((args, kwargs), two=True))
968+
cause = expected if isinstance(expected, Exception) else None
967969
actual = [self._call_matcher(c) for c in self.call_args_list]
968-
if expected not in actual:
969-
cause = expected if isinstance(expected, Exception) else None
970+
if cause or expected not in _AnyComparer(actual):
970971
expected_string = self._format_mock_call_signature(args, kwargs)
971972
raise AssertionError(
972973
'%s call not found' % expected_string
@@ -1019,6 +1020,22 @@ def _calls_repr(self, prefix="Calls"):
10191020
return f"\n{prefix}: {safe_repr(self.mock_calls)}."
10201021

10211022

1023+
class _AnyComparer(list):
1024+
"""A list which checks if it contains a call which may have an
1025+
argument of ANY, flipping the components of item and self from
1026+
their traditional locations so that ANY is guaranteed to be on
1027+
the left."""
1028+
def __contains__(self, item):
1029+
for _call in self:
1030+
if len(item) != len(_call):
1031+
continue
1032+
if all([
1033+
expected == actual
1034+
for expected, actual in zip(item, _call)
1035+
]):
1036+
return True
1037+
return False
1038+
10221039

10231040
def _try_iter(obj):
10241041
if obj is None:
@@ -2171,9 +2188,9 @@ def _error_message():
21712188
msg = self._format_mock_failure_message(args, kwargs, action='await')
21722189
return msg
21732190

2174-
expected = self._call_matcher((args, kwargs))
2191+
expected = self._call_matcher(_Call((args, kwargs), two=True))
21752192
actual = self._call_matcher(self.await_args)
2176-
if expected != actual:
2193+
if actual != expected:
21772194
cause = expected if isinstance(expected, Exception) else None
21782195
raise AssertionError(_error_message()) from cause
21792196

@@ -2192,10 +2209,10 @@ def assert_any_await(self, /, *args, **kwargs):
21922209
"""
21932210
Assert the mock has ever been awaited with the specified arguments.
21942211
"""
2195-
expected = self._call_matcher((args, kwargs))
2212+
expected = self._call_matcher(_Call((args, kwargs), two=True))
2213+
cause = expected if isinstance(expected, Exception) else None
21962214
actual = [self._call_matcher(c) for c in self.await_args_list]
2197-
if expected not in actual:
2198-
cause = expected if isinstance(expected, Exception) else None
2215+
if cause or expected not in _AnyComparer(actual):
21992216
expected_string = self._format_mock_call_signature(args, kwargs)
22002217
raise AssertionError(
22012218
'%s await not found' % expected_string

Lib/unittest/test/testmock/testasync.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import inspect
33
import unittest
44

5-
from unittest.mock import (call, AsyncMock, patch, MagicMock, create_autospec,
6-
_AwaitEvent)
5+
from unittest.mock import (ANY, call, AsyncMock, patch, MagicMock,
6+
create_autospec, _AwaitEvent)
77

88

99
def tearDownModule():
@@ -192,6 +192,10 @@ async def main():
192192
spec.assert_awaited_with(1, 2, c=3)
193193
spec.assert_awaited()
194194

195+
with self.assertRaises(AssertionError):
196+
spec.assert_any_await(e=1)
197+
198+
195199
def test_patch_with_autospec(self):
196200

197201
async def test_async():
@@ -607,6 +611,30 @@ def test_assert_has_awaits_no_order(self):
607611
asyncio.run(self._runnable_test('SomethingElse'))
608612
self.mock.assert_has_awaits(calls)
609613

614+
def test_awaits_asserts_with_any(self):
615+
class Foo:
616+
def __eq__(self, other): pass
617+
618+
asyncio.run(self._runnable_test(Foo(), 1))
619+
620+
self.mock.assert_has_awaits([call(ANY, 1)])
621+
self.mock.assert_awaited_with(ANY, 1)
622+
self.mock.assert_any_await(ANY, 1)
623+
624+
def test_awaits_asserts_with_spec_and_any(self):
625+
class Foo:
626+
def __eq__(self, other): pass
627+
628+
mock_with_spec = AsyncMock(spec=Foo)
629+
630+
async def _custom_mock_runnable_test(*args):
631+
await mock_with_spec(*args)
632+
633+
asyncio.run(_custom_mock_runnable_test(Foo(), 1))
634+
mock_with_spec.assert_has_awaits([call(ANY, 1)])
635+
mock_with_spec.assert_awaited_with(ANY, 1)
636+
mock_with_spec.assert_any_await(ANY, 1)
637+
610638
def test_assert_has_awaits_ordered(self):
611639
calls = [call('NormalFoo'), call('baz')]
612640
with self.assertRaises(AssertionError):

Lib/unittest/test/testmock/testhelpers.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,28 @@ def __ne__(self, other): pass
6464
self.assertEqual(expected, mock.mock_calls)
6565
self.assertEqual(mock.mock_calls, expected)
6666

67+
def test_any_no_spec(self):
68+
# This is a regression test for bpo-37555
69+
class Foo:
70+
def __eq__(self, other): pass
71+
72+
mock = Mock()
73+
mock(Foo(), 1)
74+
mock.assert_has_calls([call(ANY, 1)])
75+
mock.assert_called_with(ANY, 1)
76+
mock.assert_any_call(ANY, 1)
77+
78+
def test_any_and_spec_set(self):
79+
# This is a regression test for bpo-37555
80+
class Foo:
81+
def __eq__(self, other): pass
82+
83+
mock = Mock(spec=Foo)
6784

85+
mock(Foo(), 1)
86+
mock.assert_has_calls([call(ANY, 1)])
87+
mock.assert_called_with(ANY, 1)
88+
mock.assert_any_call(ANY, 1)
6889

6990
class CallTest(unittest.TestCase):
7091

Misc/ACKS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ Tomer Filiba
503503
Segev Finer
504504
Jeffrey Finkelstein
505505
Russell Finn
506+
Neal Finne
506507
Dan Finnie
507508
Nils Fischbeck
508509
Frederik Fix
@@ -1714,6 +1715,7 @@ Roger Upole
17141715
Daniel Urban
17151716
Michael Urman
17161717
Hector Urtubia
1718+
Elizabeth Uselton
17171719
Lukas Vacek
17181720
Ville Vainio
17191721
Yann Vaginay
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix `NonCallableMock._call_matcher` returning tuple instead of `_Call` object
2+
when `self._spec_signature` exists. Patch by Elizabeth Uselton

0 commit comments

Comments
 (0)