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 c96127821ebda50760e788b1213975a0d5bea37f)
Co-authored-by: Xtreak <tir.karthi@gmail.com>
diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py
index 3faeb4c..7ab6812 100644
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -804,6 +804,35 @@
return message % (action, expected_string, actual_string)
+ def _get_call_signature_from_name(self, name):
+ """
+ * If call objects are asserted against a method/function like obj.meth1
+ then there could be no name for the call object to lookup. Hence just
+ return the spec_signature of the method/function being asserted against.
+ * If the name is not empty then remove () and split by '.' to get
+ list of names to iterate through the children until a potential
+ match is found. A child mock is created only during attribute access
+ so if we get a _SpecState then no attributes of the spec were accessed
+ and can be safely exited.
+ """
+ if not name:
+ return self._spec_signature
+
+ sig = None
+ names = name.replace('()', '').split('.')
+ children = self._mock_children
+
+ for name in names:
+ child = children.get(name)
+ if child is None or isinstance(child, _SpecState):
+ break
+ else:
+ children = child._mock_children
+ sig = child._spec_signature
+
+ return sig
+
+
def _call_matcher(self, _call):
"""
Given a call (or simply an (args, kwargs) tuple), return a
@@ -811,7 +840,12 @@
This is a best effort method which relies on the spec's signature,
if available, or falls back on the arguments themselves.
"""
- sig = self._spec_signature
+
+ if isinstance(_call, tuple) and len(_call) > 2:
+ sig = self._get_call_signature_from_name(_call[0])
+ else:
+ sig = self._spec_signature
+
if sig is not None:
if len(_call) == 2:
name = ''
diff --git a/Lib/unittest/test/testmock/testmock.py b/Lib/unittest/test/testmock/testmock.py
index 090da45..d3a1e89 100644
--- a/Lib/unittest/test/testmock/testmock.py
+++ b/Lib/unittest/test/testmock/testmock.py
@@ -1339,6 +1339,54 @@
)
+ def test_assert_has_calls_nested_spec(self):
+ class Something:
+
+ def __init__(self): pass
+ def meth(self, a, b, c, d=None): pass
+
+ class Foo:
+
+ def __init__(self, a): pass
+ def meth1(self, a, b): pass
+
+ mock_class = create_autospec(Something)
+
+ for m in [mock_class, mock_class()]:
+ m.meth(1, 2, 3, d=1)
+ m.assert_has_calls([call.meth(1, 2, 3, d=1)])
+ m.assert_has_calls([call.meth(1, 2, 3, 1)])
+
+ mock_class.reset_mock()
+
+ for m in [mock_class, mock_class()]:
+ self.assertRaises(AssertionError, m.assert_has_calls, [call.Foo()])
+ m.Foo(1).meth1(1, 2)
+ m.assert_has_calls([call.Foo(1), call.Foo(1).meth1(1, 2)])
+ m.Foo.assert_has_calls([call(1), call().meth1(1, 2)])
+
+ mock_class.reset_mock()
+
+ invalid_calls = [call.meth(1),
+ call.non_existent(1),
+ call.Foo().non_existent(1),
+ call.Foo().meth(1, 2, 3, 4)]
+
+ for kall in invalid_calls:
+ self.assertRaises(AssertionError,
+ mock_class.assert_has_calls,
+ [kall]
+ )
+
+
+ def test_assert_has_calls_nested_without_spec(self):
+ m = MagicMock()
+ m().foo().bar().baz()
+ m.one().two().three()
+ calls = call.one().two().three().call_list()
+ m.assert_has_calls(calls)
+
+
def test_assert_has_calls_with_function_spec(self):
def f(a, b, c, d=None): pass