Skip to content

Commit 7397cda

Browse files
tirkarthicjw296
authored andcommitted
bpo-21478: Record calls to parent when autospecced objects are used as child with attach_mock (GH 14688)
* 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.
1 parent b530a44 commit 7397cda

File tree

3 files changed

+55
-11
lines changed

3 files changed

+55
-11
lines changed

Lib/unittest/mock.py

+16-11
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ def _is_exception(obj):
7272
)
7373

7474

75+
def _extract_mock(obj):
76+
# Autospecced functions will return a FunctionType with "mock" attribute
77+
# which is the actual mock object that needs to be used.
78+
if isinstance(obj, FunctionTypes) and hasattr(obj, 'mock'):
79+
return obj.mock
80+
else:
81+
return obj
82+
83+
7584
def _get_signature_object(func, as_instance, eat_self):
7685
"""
7786
Given an arbitrary, possibly callable object, try to create a suitable
@@ -346,13 +355,7 @@ def __repr__(self):
346355

347356

348357
def _check_and_set_parent(parent, value, name, new_name):
349-
# function passed to create_autospec will have mock
350-
# attribute attached to which parent must be set
351-
if isinstance(value, FunctionTypes):
352-
try:
353-
value = value.mock
354-
except AttributeError:
355-
pass
358+
value = _extract_mock(value)
356359

357360
if not _is_instance_mock(value):
358361
return False
@@ -467,10 +470,12 @@ def attach_mock(self, mock, attribute):
467470
Attach a mock as an attribute of this one, replacing its name and
468471
parent. Calls to the attached mock will be recorded in the
469472
`method_calls` and `mock_calls` attributes of this one."""
470-
mock._mock_parent = None
471-
mock._mock_new_parent = None
472-
mock._mock_name = ''
473-
mock._mock_new_name = None
473+
inner_mock = _extract_mock(mock)
474+
475+
inner_mock._mock_parent = None
476+
inner_mock._mock_new_parent = None
477+
inner_mock._mock_name = ''
478+
inner_mock._mock_new_name = None
474479

475480
setattr(self, attribute, mock)
476481

Lib/unittest/test/testmock/testmock.py

+37
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def cmeth(cls, a, b, c, d=None): pass
3737
def smeth(a, b, c, d=None): pass
3838

3939

40+
def something(a): pass
41+
42+
4043
class MockTest(unittest.TestCase):
4144

4245
def test_all(self):
@@ -1808,6 +1811,26 @@ def test_attach_mock_return_value(self):
18081811
self.assertEqual(m.mock_calls, call().foo().call_list())
18091812

18101813

1814+
def test_attach_mock_patch_autospec(self):
1815+
parent = Mock()
1816+
1817+
with mock.patch(f'{__name__}.something', autospec=True) as mock_func:
1818+
self.assertEqual(mock_func.mock._extract_mock_name(), 'something')
1819+
parent.attach_mock(mock_func, 'child')
1820+
parent.child(1)
1821+
something(2)
1822+
mock_func(3)
1823+
1824+
parent_calls = [call.child(1), call.child(2), call.child(3)]
1825+
child_calls = [call(1), call(2), call(3)]
1826+
self.assertEqual(parent.mock_calls, parent_calls)
1827+
self.assertEqual(parent.child.mock_calls, child_calls)
1828+
self.assertEqual(something.mock_calls, child_calls)
1829+
self.assertEqual(mock_func.mock_calls, child_calls)
1830+
self.assertIn('mock.child', repr(parent.child.mock))
1831+
self.assertEqual(mock_func.mock._extract_mock_name(), 'mock.child')
1832+
1833+
18111834
def test_attribute_deletion(self):
18121835
for mock in (Mock(), MagicMock(), NonCallableMagicMock(),
18131836
NonCallableMock()):
@@ -1891,6 +1914,20 @@ def foo(a, b): pass
18911914

18921915
self.assertRaises(TypeError, mock.child, 1)
18931916
self.assertEqual(mock.mock_calls, [call.child(1, 2)])
1917+
self.assertIn('mock.child', repr(mock.child.mock))
1918+
1919+
def test_parent_propagation_with_autospec_attach_mock(self):
1920+
1921+
def foo(a, b): pass
1922+
1923+
parent = Mock()
1924+
parent.attach_mock(create_autospec(foo, name='bar'), 'child')
1925+
parent.child(1, 2)
1926+
1927+
self.assertRaises(TypeError, parent.child, 1)
1928+
self.assertEqual(parent.child.mock_calls, [call.child(1, 2)])
1929+
self.assertIn('mock.child', repr(parent.child.mock))
1930+
18941931

18951932
def test_isinstance_under_settrace(self):
18961933
# bpo-36593 : __class__ is not set for a class that has __class__
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Record calls to parent when autospecced object is attached to a mock using
2+
:func:`unittest.mock.attach_mock`. Patch by Karthikeyan Singaravelan.

0 commit comments

Comments
 (0)