]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-21478: Record calls to parent when autospecced objects are used as child with...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Mon, 22 Jul 2019 08:04:07 +0000 (01:04 -0700)
committerChris Withers <chris@withers.org>
Mon, 22 Jul 2019 08:04:07 +0000 (09:04 +0100)
* Clear name and parent of mock in autospecced objects used with attach_mock

* Add NEWS entry

* Fix reversed order of comparison

* Test child and standalone function calls

* Use a helper function extracting mock to avoid code duplication and refactor tests.
(cherry picked from commit 7397cda99795a4a8d96193d710105e77a07b7411)

Co-authored-by: Xtreak <tir.karthi@gmail.com>
Lib/unittest/mock.py
Lib/unittest/test/testmock/testmock.py
Misc/NEWS.d/next/Library/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst [new file with mode: 0644]

index 569a5146c8c12915fa4cefa54083a91fbd9cffc2..ee291fc89a1060378b86e869a5a4fe9e14890696 100644 (file)
@@ -62,6 +62,15 @@ def _is_exception(obj):
     )
 
 
+def _extract_mock(obj):
+    # Autospecced functions will return a FunctionType with "mock" attribute
+    # which is the actual mock object that needs to be used.
+    if isinstance(obj, FunctionTypes) and hasattr(obj, 'mock'):
+        return obj.mock
+    else:
+        return obj
+
+
 def _get_signature_object(func, as_instance, eat_self):
     """
     Given an arbitrary, possibly callable object, try to create a suitable
@@ -323,13 +332,7 @@ class _CallList(list):
 
 
 def _check_and_set_parent(parent, value, name, new_name):
-    # function passed to create_autospec will have mock
-    # attribute attached to which parent must be set
-    if isinstance(value, FunctionTypes):
-        try:
-            value = value.mock
-        except AttributeError:
-            pass
+    value = _extract_mock(value)
 
     if not _is_instance_mock(value):
         return False
@@ -433,10 +436,12 @@ class NonCallableMock(Base):
         Attach a mock as an attribute of this one, replacing its name and
         parent. Calls to the attached mock will be recorded in the
         `method_calls` and `mock_calls` attributes of this one."""
-        mock._mock_parent = None
-        mock._mock_new_parent = None
-        mock._mock_name = ''
-        mock._mock_new_name = None
+        inner_mock = _extract_mock(mock)
+
+        inner_mock._mock_parent = None
+        inner_mock._mock_new_parent = None
+        inner_mock._mock_name = ''
+        inner_mock._mock_new_name = None
 
         setattr(self, attribute, mock)
 
index f92b921fe664aaa202ec1c378d6b7318b4208d23..8329e5f5021788ce71f1b6fc61dd6b5643c9cee1 100644 (file)
@@ -39,6 +39,9 @@ class Something(object):
         pass
 
 
+def something(a): pass
+
+
 class MockTest(unittest.TestCase):
 
     def test_all(self):
@@ -1765,6 +1768,26 @@ class MockTest(unittest.TestCase):
                 self.assertEqual(m.mock_calls, call().foo().call_list())
 
 
+    def test_attach_mock_patch_autospec(self):
+        parent = Mock()
+
+        with mock.patch(f'{__name__}.something', autospec=True) as mock_func:
+            self.assertEqual(mock_func.mock._extract_mock_name(), 'something')
+            parent.attach_mock(mock_func, 'child')
+            parent.child(1)
+            something(2)
+            mock_func(3)
+
+            parent_calls = [call.child(1), call.child(2), call.child(3)]
+            child_calls = [call(1), call(2), call(3)]
+            self.assertEqual(parent.mock_calls, parent_calls)
+            self.assertEqual(parent.child.mock_calls, child_calls)
+            self.assertEqual(something.mock_calls, child_calls)
+            self.assertEqual(mock_func.mock_calls, child_calls)
+            self.assertIn('mock.child', repr(parent.child.mock))
+            self.assertEqual(mock_func.mock._extract_mock_name(), 'mock.child')
+
+
     def test_attribute_deletion(self):
         for mock in (Mock(), MagicMock(), NonCallableMagicMock(),
                      NonCallableMock()):
@@ -1849,6 +1872,20 @@ class MockTest(unittest.TestCase):
 
         self.assertRaises(TypeError, mock.child, 1)
         self.assertEqual(mock.mock_calls, [call.child(1, 2)])
+        self.assertIn('mock.child', repr(mock.child.mock))
+
+    def test_parent_propagation_with_autospec_attach_mock(self):
+
+        def foo(a, b): pass
+
+        parent = Mock()
+        parent.attach_mock(create_autospec(foo, name='bar'), 'child')
+        parent.child(1, 2)
+
+        self.assertRaises(TypeError, parent.child, 1)
+        self.assertEqual(parent.child.mock_calls, [call.child(1, 2)])
+        self.assertIn('mock.child', repr(parent.child.mock))
+
 
     def test_isinstance_under_settrace(self):
         # bpo-36593 : __class__ is not set for a class that has __class__
diff --git a/Misc/NEWS.d/next/Library/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst b/Misc/NEWS.d/next/Library/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst
new file mode 100644 (file)
index 0000000..0ac9b8e
--- /dev/null
@@ -0,0 +1,2 @@
+Record calls to parent when autospecced object is attached to a mock using
+:func:`unittest.mock.attach_mock`. Patch by Karthikeyan Singaravelan.