Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8ca0fa9

Browse files
authored
bpo-35226: Fix equality for nested unittest.mock.call objects. (#10555)
Also refactor the call recording imolementation and add some notes about its limitations.
1 parent 3bc0eba commit 8ca0fa9

6 files changed

Lines changed: 124 additions & 23 deletions

File tree

Doc/library/unittest.mock-examples.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,15 @@ You use the :data:`call` object to construct lists for comparing with
166166
>>> mock.mock_calls == expected
167167
True
168168

169+
However, parameters to calls that return mocks are not recorded, which means it is not
170+
possible to track nested calls where the parameters used to create ancestors are important:
171+
172+
>>> m = Mock()
173+
>>> m.factory(important=True).deliver()
174+
<Mock name='mock.factory().deliver()' id='...'>
175+
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
176+
True
177+
169178

170179
Setting Return Values and Attributes
171180
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Doc/library/unittest.mock.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,19 @@ the *new_callable* argument to :func:`patch`.
702702
unpacked as tuples to get at the individual arguments. See
703703
:ref:`calls as tuples <calls-as-tuples>`.
704704

705+
.. note::
706+
707+
The way :attr:`mock_calls` are recorded means that where nested
708+
calls are made, the parameters of ancestor calls are not recorded
709+
and so will always compare equal:
710+
711+
>>> mock = MagicMock()
712+
>>> mock.top(a=3).bottom()
713+
<MagicMock name='mock.top().bottom()' id='...'>
714+
>>> mock.mock_calls
715+
[call.top(a=3), call.top().bottom()]
716+
>>> mock.mock_calls[-1] == call.top(a=-1).bottom()
717+
True
705718

706719
.. attribute:: __class__
707720

Lib/unittest/mock.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -977,46 +977,51 @@ def _mock_call(_mock_self, *args, **kwargs):
977977
self = _mock_self
978978
self.called = True
979979
self.call_count += 1
980-
_new_name = self._mock_new_name
981-
_new_parent = self._mock_new_parent
982980

981+
# handle call_args
983982
_call = _Call((args, kwargs), two=True)
984983
self.call_args = _call
985984
self.call_args_list.append(_call)
986-
self.mock_calls.append(_Call(('', args, kwargs)))
987985

988986
seen = set()
989-
skip_next_dot = _new_name == '()'
987+
988+
# initial stuff for method_calls:
990989
do_method_calls = self._mock_parent is not None
991-
name = self._mock_name
992-
while _new_parent is not None:
993-
this_mock_call = _Call((_new_name, args, kwargs))
994-
if _new_parent._mock_new_name:
995-
dot = '.'
996-
if skip_next_dot:
997-
dot = ''
990+
method_call_name = self._mock_name
998991

999-
skip_next_dot = False
1000-
if _new_parent._mock_new_name == '()':
1001-
skip_next_dot = True
992+
# initial stuff for mock_calls:
993+
mock_call_name = self._mock_new_name
994+
is_a_call = mock_call_name == '()'
995+
self.mock_calls.append(_Call(('', args, kwargs)))
1002996

1003-
_new_name = _new_parent._mock_new_name + dot + _new_name
997+
# follow up the chain of mocks:
998+
_new_parent = self._mock_new_parent
999+
while _new_parent is not None:
10041000

1001+
# handle method_calls:
10051002
if do_method_calls:
1006-
if _new_name == name:
1007-
this_method_call = this_mock_call
1008-
else:
1009-
this_method_call = _Call((name, args, kwargs))
1010-
_new_parent.method_calls.append(this_method_call)
1011-
1003+
_new_parent.method_calls.append(_Call((method_call_name, args, kwargs)))
10121004
do_method_calls = _new_parent._mock_parent is not None
10131005
if do_method_calls:
1014-
name = _new_parent._mock_name + '.' + name
1006+
method_call_name = _new_parent._mock_name + '.' + method_call_name
10151007

1008+
# handle mock_calls:
1009+
this_mock_call = _Call((mock_call_name, args, kwargs))
10161010
_new_parent.mock_calls.append(this_mock_call)
1011+
1012+
if _new_parent._mock_new_name:
1013+
if is_a_call:
1014+
dot = ''
1015+
else:
1016+
dot = '.'
1017+
is_a_call = _new_parent._mock_new_name == '()'
1018+
mock_call_name = _new_parent._mock_new_name + dot + mock_call_name
1019+
1020+
# follow the parental chain:
10171021
_new_parent = _new_parent._mock_new_parent
10181022

1019-
# use ids here so as not to call __hash__ on the mocks
1023+
# check we're not in an infinite loop:
1024+
# ( use ids here so as not to call __hash__ on the mocks)
10201025
_new_parent_id = id(_new_parent)
10211026
if _new_parent_id in seen:
10221027
break
@@ -2054,6 +2059,10 @@ def __eq__(self, other):
20542059
else:
20552060
self_name, self_args, self_kwargs = self
20562061

2062+
if (getattr(self, 'parent', None) and getattr(other, 'parent', None)
2063+
and self.parent != other.parent):
2064+
return False
2065+
20572066
other_name = ''
20582067
if len_other == 0:
20592068
other_args, other_kwargs = (), {}

Lib/unittest/test/testmock/testhelpers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,22 @@ def test_extended_call(self):
270270
self.assertEqual(mock.mock_calls, last_call.call_list())
271271

272272

273+
def test_extended_not_equal(self):
274+
a = call(x=1).foo
275+
b = call(x=2).foo
276+
self.assertEqual(a, a)
277+
self.assertEqual(b, b)
278+
self.assertNotEqual(a, b)
279+
280+
281+
def test_nested_calls_not_equal(self):
282+
a = call(x=1).foo().bar
283+
b = call(x=2).foo().bar
284+
self.assertEqual(a, a)
285+
self.assertEqual(b, b)
286+
self.assertNotEqual(a, b)
287+
288+
273289
def test_call_list(self):
274290
mock = MagicMock()
275291
mock(1)

Lib/unittest/test/testmock/testmock.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,57 @@ def test_mock_calls(self):
925925
call().__int__().call_list())
926926

927927

928+
def test_child_mock_call_equal(self):
929+
m = Mock()
930+
result = m()
931+
result.wibble()
932+
# parent looks like this:
933+
self.assertEqual(m.mock_calls, [call(), call().wibble()])
934+
# but child should look like this:
935+
self.assertEqual(result.mock_calls, [call.wibble()])
936+
937+
938+
def test_mock_call_not_equal_leaf(self):
939+
m = Mock()
940+
m.foo().something()
941+
self.assertNotEqual(m.mock_calls[1], call.foo().different())
942+
self.assertEqual(m.mock_calls[0], call.foo())
943+
944+
945+
def test_mock_call_not_equal_non_leaf(self):
946+
m = Mock()
947+
m.foo().bar()
948+
self.assertNotEqual(m.mock_calls[1], call.baz().bar())
949+
self.assertNotEqual(m.mock_calls[0], call.baz())
950+
951+
952+
def test_mock_call_not_equal_non_leaf_params_different(self):
953+
m = Mock()
954+
m.foo(x=1).bar()
955+
# This isn't ideal, but there's no way to fix it without breaking backwards compatibility:
956+
self.assertEqual(m.mock_calls[1], call.foo(x=2).bar())
957+
958+
959+
def test_mock_call_not_equal_non_leaf_attr(self):
960+
m = Mock()
961+
m.foo.bar()
962+
self.assertNotEqual(m.mock_calls[0], call.baz.bar())
963+
964+
965+
def test_mock_call_not_equal_non_leaf_call_versus_attr(self):
966+
m = Mock()
967+
m.foo.bar()
968+
self.assertNotEqual(m.mock_calls[0], call.foo().bar())
969+
970+
971+
def test_mock_call_repr(self):
972+
m = Mock()
973+
m.foo().bar().baz.bob()
974+
self.assertEqual(repr(m.mock_calls[0]), 'call.foo()')
975+
self.assertEqual(repr(m.mock_calls[1]), 'call.foo().bar()')
976+
self.assertEqual(repr(m.mock_calls[2]), 'call.foo().bar().baz.bob()')
977+
978+
928979
def test_subclassing(self):
929980
class Subclass(Mock):
930981
pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Recursively check arguments when testing for equality of
2+
:class:`unittest.mock.call` objects and add note that tracking of parameters
3+
used to create ancestors of mocks in ``mock_calls`` is not possible.

0 commit comments

Comments
 (0)