Skip to content

Commit 38d311d

Browse files
miss-islingtontirkarthi
authored andcommitted
bpo-36871: Ensure method signature is used when asserting mock calls to a method (GH15578)
* Fix call_matcher for mock when using methods * Add NEWS entry * Use None check and convert doctest to unittest * Use better name for mock in tests. Handle _SpecState when the attribute was not accessed and add tests. * Use reset_mock instead of reinitialization. Change inner class constructor signature for check * Reword comment regarding call object lookup logic (cherry picked from commit c961278) Co-authored-by: Xtreak <tir.karthi@gmail.com>
1 parent 612d393 commit 38d311d

File tree

3 files changed

+86
-1
lines changed

3 files changed

+86
-1
lines changed

Lib/unittest/mock.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,14 +804,48 @@ def _format_mock_failure_message(self, args, kwargs, action='call'):
804804
return message % (action, expected_string, actual_string)
805805

806806

807+
def _get_call_signature_from_name(self, name):
808+
"""
809+
* If call objects are asserted against a method/function like obj.meth1
810+
then there could be no name for the call object to lookup. Hence just
811+
return the spec_signature of the method/function being asserted against.
812+
* If the name is not empty then remove () and split by '.' to get
813+
list of names to iterate through the children until a potential
814+
match is found. A child mock is created only during attribute access
815+
so if we get a _SpecState then no attributes of the spec were accessed
816+
and can be safely exited.
817+
"""
818+
if not name:
819+
return self._spec_signature
820+
821+
sig = None
822+
names = name.replace('()', '').split('.')
823+
children = self._mock_children
824+
825+
for name in names:
826+
child = children.get(name)
827+
if child is None or isinstance(child, _SpecState):
828+
break
829+
else:
830+
children = child._mock_children
831+
sig = child._spec_signature
832+
833+
return sig
834+
835+
807836
def _call_matcher(self, _call):
808837
"""
809838
Given a call (or simply an (args, kwargs) tuple), return a
810839
comparison key suitable for matching with other calls.
811840
This is a best effort method which relies on the spec's signature,
812841
if available, or falls back on the arguments themselves.
813842
"""
814-
sig = self._spec_signature
843+
844+
if isinstance(_call, tuple) and len(_call) > 2:
845+
sig = self._get_call_signature_from_name(_call[0])
846+
else:
847+
sig = self._spec_signature
848+
815849
if sig is not None:
816850
if len(_call) == 2:
817851
name = ''

Lib/unittest/test/testmock/testmock.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,54 @@ def test_assert_has_calls(self):
13391339
)
13401340

13411341

1342+
def test_assert_has_calls_nested_spec(self):
1343+
class Something:
1344+
1345+
def __init__(self): pass
1346+
def meth(self, a, b, c, d=None): pass
1347+
1348+
class Foo:
1349+
1350+
def __init__(self, a): pass
1351+
def meth1(self, a, b): pass
1352+
1353+
mock_class = create_autospec(Something)
1354+
1355+
for m in [mock_class, mock_class()]:
1356+
m.meth(1, 2, 3, d=1)
1357+
m.assert_has_calls([call.meth(1, 2, 3, d=1)])
1358+
m.assert_has_calls([call.meth(1, 2, 3, 1)])
1359+
1360+
mock_class.reset_mock()
1361+
1362+
for m in [mock_class, mock_class()]:
1363+
self.assertRaises(AssertionError, m.assert_has_calls, [call.Foo()])
1364+
m.Foo(1).meth1(1, 2)
1365+
m.assert_has_calls([call.Foo(1), call.Foo(1).meth1(1, 2)])
1366+
m.Foo.assert_has_calls([call(1), call().meth1(1, 2)])
1367+
1368+
mock_class.reset_mock()
1369+
1370+
invalid_calls = [call.meth(1),
1371+
call.non_existent(1),
1372+
call.Foo().non_existent(1),
1373+
call.Foo().meth(1, 2, 3, 4)]
1374+
1375+
for kall in invalid_calls:
1376+
self.assertRaises(AssertionError,
1377+
mock_class.assert_has_calls,
1378+
[kall]
1379+
)
1380+
1381+
1382+
def test_assert_has_calls_nested_without_spec(self):
1383+
m = MagicMock()
1384+
m().foo().bar().baz()
1385+
m.one().two().three()
1386+
calls = call.one().two().three().call_list()
1387+
m.assert_has_calls(calls)
1388+
1389+
13421390
def test_assert_has_calls_with_function_spec(self):
13431391
def f(a, b, c, d=None): pass
13441392

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Ensure method signature is used instead of constructor signature of a class
2+
while asserting mock object against method calls. Patch by Karthikeyan
3+
Singaravelan.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy