classification
Title: mock calls don't propagate to parent (autospec)
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.9, Python 3.8, Python 3.7
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: michael.foord Nosy List: Caris Moses, Claudiu.Popa, and, cjw296, iforapsy, kushal.das, mariocj89, michael.foord, xtreak
Priority: normal Keywords: patch

Created on 2014-05-12 12:56 by and, last changed 2019-10-17 02:09 by xtreak. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 11273 merged xtreak, 2018-12-21 06:45
PR 12039 merged miss-islington, 2019-03-03 17:14
PR 14688 merged xtreak, 2019-07-10 18:32
PR 14902 merged miss-islington, 2019-07-22 07:38
PR 14903 merged miss-islington, 2019-07-22 07:39
Messages (22)
msg218321 - (view) Author: Dmitry Andreychuk (and) Date: 2014-05-12 12:56
Calls to autospecced mock functions are not recorded to mock_calls list of parent mock. This only happens if autospec is used and the original object is a function.

Example:

import unittest.mock as mock

def foo():
    pass

parent = mock.Mock()
parent.child = mock.create_autospec(foo)
parent.child()
print(parent.mock_calls)


Output:
[]

Expected output:
[call.child()]

It works fine if foo function is substituted with a class.

Initially I came across this problem with patch() and attach_mock() but I simplified it for the demonstration.
msg218362 - (view) Author: Michael Foord (michael.foord) * (Python committer) Date: 2014-05-12 20:48
Mock objects detect when another mock is added as a "child", but they don't currently detect that a function created by autospec has been added. It should be a fairly easy fix.
msg332252 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2018-12-20 19:45
I think this should be handled in _check_and_set_parent where if value's type is FunctionType then value.mock should be used against which parent and name should be set since create_autospec returns function with mock attached to 'mock' attribute and all helper methods attached. There could be a case where a function is directly attached to mock where value will be of FunctionType but value.mock will not be set since it's not created with create_autospec and the AttributeError has to silenced. I think the below patch will handle both scenarios. This doesn't cause any test failure and hence it will be good to convert the original report as a unit test. Feedback welcome on the approach. I will raise a PR with tests and I am updating the relevant versions where this fix can be applied.

Sample program

from unittest import mock

def foo(a, b):
    pass

parent = mock.Mock()
a = mock.create_autospec(foo)
parent.child = a # 'a' is FunctionType and has a.mock attribute set (create_autospec -> _set_signature -> _setup_func)

parent.child_func = foo # 'foo' is FunctionType with no mock attribute set that could cause AttributeError

parent.child(1, 2) # Recorded
parent.child_func(2, 3) # Not recorded since it's actual call to child_func and has no parent set due to AttributeError
print(parent.method_calls) # [call.child(1, 2)]

Patch : 

diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py
index 38189c9aec..143263722d 100644
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -321,6 +321,12 @@ class _CallList(list):


 def _check_and_set_parent(parent, value, name, new_name):
+    if isinstance(value, FunctionTypes):
+        try:
+            value = value.mock
+        except AttributeError:
+            pass
+
     if not _is_instance_mock(value):
         return False
     if ((value._mock_name or value._mock_new_name) or
msg347592 - (view) Author: Jack Wong (iforapsy) Date: 2019-07-10 01:50
Can we reopen this bug? Karthikeyan's PR works for Dmitry's toy example, but it does not work in the usual case where patch() and attach_mock() are used. I encountered this bug on Python 3.7.3, which includes the PR.

Non-toy example:
    import unittest.mock as mock

    def foo():
        pass

    parent = mock.Mock()

    with mock.patch('__main__.foo', autospec=True) as mock_foo:
        parent.attach_mock(mock_foo, 'child')
        parent.child()
        print(parent.mock_calls)

Actual output:
[]

Expected output:
[call.child()]

The reason why Karthikeyan's PR works on the toy example is that that mock's name is not set. In the usual case, the function mock's name will be set so this "if" block in _check_and_set_parent will return immediately.
    if ((value._mock_name or value._mock_new_name) or
        (value._mock_parent is not None) or
        (value._mock_new_parent is not None)):
        return False

I think a possible fix is to move the inner mock extraction out to the attach_mock function as that function contains code to clear the mock's parent and name attributes. Downside is that that would make it fail on Dmitry's toy example.
msg347593 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-07-10 02:22
Thanks Jack for the report. I am reopening this issue. I will use your example as a unit test. I will try to look into it. If you have more cases or examples related to the issue feel free to add them.
msg347631 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-07-10 18:38
> I think a possible fix is to move the inner mock extraction out to the attach_mock function as that function contains code to clear the mock's parent and name attributes. Downside is that that would make it fail on Dmitry's toy example.

Moving the inner mock check would cause the original report to fail. But the same logic can be copied to attach_mock so that name and parent for inner mock set in "mock" attribute is cleared. So _check_and_set_parent code path is hit the mock attribute without name and parent would behave as expected. Jack, would appreciate your review of the PR. I have added your report as a unittest along with directly using create_autospec with attach_mock that behaves like patch and autospec=True. I have also asserted that name is reset to child as per the docs at https://docs.python.org/3/library/unittest.mock.html#attaching-mocks-as-attributes

Thanks
msg347705 - (view) Author: Jack Wong (iforapsy) Date: 2019-07-11 20:20
Thanks, Karthikeyan! I applied your change to Lib/unittest/mock.py, and it fixed the bug for me. I confirmed that calls to the child mock now appear in the parent's mock_calls in my test suite. Your PR looks good to me.
msg348285 - (view) Author: Chris Withers (cjw296) * (Python committer) Date: 2019-07-22 07:38
New changeset 7397cda99795a4a8d96193d710105e77a07b7411 by Chris Withers (Xtreak) in branch 'master':
bpo-21478: Record calls to parent when autospecced objects are used as child with attach_mock (GH 14688)
https://github.com/python/cpython/commit/7397cda99795a4a8d96193d710105e77a07b7411
msg348288 - (view) Author: Chris Withers (cjw296) * (Python committer) Date: 2019-07-22 07:59
New changeset 22fd679dc363bfcbda336775da16aff4d6fcb33f by Chris Withers (Miss Islington (bot)) in branch '3.8':
bpo-21478: Record calls to parent when autospecced objects are used as child with attach_mock (GH 14688) (GH-14902)
https://github.com/python/cpython/commit/22fd679dc363bfcbda336775da16aff4d6fcb33f
msg348289 - (view) Author: Chris Withers (cjw296) * (Python committer) Date: 2019-07-22 08:04
New changeset e9b187a2bfbb0586fc5d554ce745b7fe04e0b9a8 by Chris Withers (Miss Islington (bot)) in branch '3.7':
bpo-21478: Record calls to parent when autospecced objects are used as child with attach_mock (GH 14688) (GH-14903)
https://github.com/python/cpython/commit/e9b187a2bfbb0586fc5d554ce745b7fe04e0b9a8
msg348291 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-07-22 08:40
Thanks Jack for the report. Thanks Mario and Chris for reviews. I am closing this as resolved.
msg354489 - (view) Author: Caris Moses (Caris Moses) Date: 2019-10-11 20:13
Hello,
I am still running into this issue. I have tested the following code with Python 3.7.4, 3.7.5rc1 , and 3.8.0rc1.

from unittest import TestCase
from unittest.mock import patch, Mock, call

class MyObject:
    def __init__(self):
        self.foo = 0
        self.bar = 0

    def set_foo(self, value):
        self.foo = value

    def set_bar(self, value):
        self.bar = value

def do_something():
    o = MyObject()
    o.set_foo(3)
    o.set_bar(4)
    return 'something unrelated'

class MyObjectTest(TestCase):

    @patch('test_mock.MyObject.set_bar', autospec=True)
    @patch('test_mock.MyObject.set_foo', autospec=True)
    def test_do_something(self, mock_set_foo, mock_set_bar):
        manager = Mock()
        manager.attach_mock(mock_set_foo, 'set_foo_func')
        manager.attach_mock(mock_set_bar, 'set_bar_func')
        do_something()
        assert manager.mock_calls == [call.set_foo_func(3), call.set_bar_func(4)]
msg354491 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-10-11 20:35
Oh well :( My initial guess is that the report is for methods. The other reports were for functions. So I am wondering if the check for FunctionType is successful and if I need to handle something more. I haven't tried it yet.
msg354496 - (view) Author: Caris Moses (Caris Moses) Date: 2019-10-11 21:09
I tried it with mocked functions instead of methods and got the same result, so I dug into this a little deeper. It seems as though the issue it how the mocked functions are called. If I replace the do_something() line with the following then it works.

#do_something()
manager.set_foo_func(3)
manager.set_bar_func(4)

I am a beginner with unittest so please let me know if I am just using this incorrectly. However in the original code I posted, if I print(manager.set_foo_func.mock_calls, manager.set_bar_func.mock_calls) I get the calls made in do_something(), however print(manager.mock_calls) returns an empty list which leads me to believe something else is wrong.
msg354642 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-10-14 17:40
I tried your example as below using __name__. I received an AttributeError for which issue38473 in 3.7.5RC1 and 3.8.0RC1 and opened issue38473 and I am running my below code under that issue PR. For 3.7.4, I received manager.mock_calls to be an empty list since it doesn't contain the patch for this issue. Can you please confirm my results too.

➜  cpython git:(bpo38473) cat ../backups/bpo21478.py
import unittest
from unittest import TestCase
from unittest.mock import patch, Mock, call, ANY


class MyObject:
    def __init__(self):
        self.foo = 0
        self.bar = 0

    def set_foo(self, value):
        self.foo = value

    def set_bar(self, value):
        self.bar = value


def do_something():
    o = MyObject()
    o.set_foo(3)
    o.set_bar(4)
    return "something unrelated"


class MyObjectTest(TestCase):
    @patch(f"{__name__}.MyObject.set_bar", autospec=True)
    @patch(f"{__name__}.MyObject.set_foo", autospec=True)
    def test_do_something(self, mock_set_foo, mock_set_bar):
        manager = Mock()
        manager.attach_mock(mock_set_foo, "set_foo_func")
        manager.attach_mock(mock_set_bar, "set_bar_func")
        do_something()
        assert manager.mock_calls == [
            call.set_foo_func(ANY, 3),
            call.set_bar_func(ANY, 4),
        ]
        manager.assert_has_calls([call.set_foo_func(ANY, 3), call.set_bar_func(ANY, 4)])


if __name__ == "__main__":
    unittest.main()


➜  cpython git:(bpo38473) ./python ../backups/bpo21478.py
.
----------------------------------------------------------------------
Ran 1 test in 0.006s

OK
msg354735 - (view) Author: Caris Moses (Caris Moses) Date: 2019-10-15 15:50
I am having some trouble figuring out how to use CPython to get the exact PR you tested on since I've never used CPython before. However, when I just install Python 3.7.5RC1 and 3.8.0RC1 from the binaries and run your code I do get the AttributeError. And when I run your code with Python3.7.4 from binaries I get an empty list.

Can you point me to a good source that will tell me how to get the patch for issue38473? I built and ran cpython from source on my Mac successfully. However when I checkout a different branch or version, rebuild, and run ./python.exe I always get the same Python version.
msg354737 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-10-15 16:00
Thanks for the confirmation. You can download the patch for the PR by appending .diff/.patch to the PR URL. The patch can be applied to your source locally with "git apply patch_file" to run my example. Reverting patch would cause AttributeError

Diff URL : https://github.com/python/cpython/pull/16784.diff

The PR also includes regression test so reverting the patch to mock.py and running below command would cause error in test.

./python -m test test_unittest

Checking out a different branch and doing below command should do a clean rebuild with a different version of python. Also for reference : https://devguide.python.org/

git clean -xdf && ./configure && make -j4

Hope it helps
msg354748 - (view) Author: Caris Moses (Caris Moses) Date: 2019-10-15 17:15
Great, thanks so much. It works with the patch. So will this patch be included in the next released version of Python?
msg354749 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-10-15 17:23
It needs to approved and merged by a core dev so that it will be available by 3.7.6 and 3.8.1. To clarify the calls are recorded in 3.7.5 and 3.8.0 in mock_calls. It's a problem with assert_has_calls and autospec. As a workaround you can turn off autospec or use mock_calls to assert for calls instead of assert_has_calls. Sorry for the inconvenience.
msg354750 - (view) Author: Caris Moses (Caris Moses) Date: 2019-10-15 17:44
I see. Thanks for your help!
msg354815 - (view) Author: Caris Moses (Caris Moses) Date: 2019-10-16 20:16
I believe I have found another bug related to this issue. I can start a new issue if necessary. When I use some_mock.attach_mock(...) and make calls, the resulting some_mock.call_args is None while the some_mock.mock_calls list is not empty.

The code below shows this in Python 3.7.5:

from unittest import TestCase
from unittest.mock import patch, Mock

def foo(value):
    return value

class MyObjectTest(TestCase):

    @patch(f'{__name__}.foo')
    def test_do_something(self, mock_foo):
        manager = Mock()
        manager.attach_mock(mock_foo, 'foo_func')
        foo(3)
        print(manager.mock_calls)
        print(manager.call_args)

if __name__ == "__main__":
    unittest.main()

The print statements return:
[call.foo_func(3)]
None

While the code below (without attach_mock) works fine:
from unittest import TestCase
from unittest.mock import patch, Mock

def foo(value):
    return value

class MyObjectTest(TestCase):

    @patch(f'{__name__}.foo')
    def test_do_something(self, mock_foo):
        foo(3)
        print(mock_foo.mock_calls)
        print(mock_foo.call_args)

if __name__ == "__main__":
    unittest.main()

Print statements correctly return:
[call(3)]
call(3)

for completeness the call_args_list also returns [] when using attach_mock. I also tested in Python 3.8.0 and got the same result.
msg354827 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-10-17 02:09
Please open a new issue. It's getting little hard to track on this since it was closed and has 2 PRs merged.
History
Date User Action Args
2019-10-17 02:09:03xtreaksetmessages: + msg354827
2019-10-16 20:16:21Caris Mosessetmessages: + msg354815
2019-10-15 17:44:09Caris Mosessetmessages: + msg354750
2019-10-15 17:23:00xtreaksetmessages: + msg354749
2019-10-15 17:15:58Caris Mosessetmessages: + msg354748
2019-10-15 16:00:48xtreaksetmessages: + msg354737
2019-10-15 15:50:02Caris Mosessetmessages: + msg354735
2019-10-14 17:40:46xtreaksetmessages: + msg354642
2019-10-11 21:09:27Caris Mosessetmessages: + msg354496
2019-10-11 20:35:09xtreaksetmessages: + msg354491
2019-10-11 20:13:43Caris Mosessetnosy: + Caris Moses
messages: + msg354489
2019-09-09 15:40:25xtreaklinkissue28569 superseder
2019-07-22 08:40:25xtreaksetstatus: open -> closed
resolution: fixed
messages: + msg348291

stage: patch review -> resolved
2019-07-22 08:04:13cjw296setmessages: + msg348289
2019-07-22 07:59:14cjw296setmessages: + msg348288
2019-07-22 07:39:06miss-islingtonsetpull_requests: + pull_request14681
2019-07-22 07:38:59miss-islingtonsetpull_requests: + pull_request14680
2019-07-22 07:38:39cjw296setmessages: + msg348285
2019-07-11 20:20:01iforapsysetmessages: + msg347705
2019-07-10 18:38:18xtreaksetmessages: + msg347631
2019-07-10 18:32:26xtreaksetstage: patch review
pull_requests: + pull_request14493
2019-07-10 02:22:05xtreaksetstatus: closed -> open
versions: + Python 3.9
messages: + msg347593

resolution: fixed -> (no value)
stage: resolved -> (no value)
2019-07-10 01:50:13iforapsysetnosy: + iforapsy
messages: + msg347592
2019-03-03 17:31:37xtreaksetstatus: open -> closed
resolution: fixed
stage: patch review -> resolved
2019-03-03 17:14:28miss-islingtonsetpull_requests: + pull_request12147
2018-12-21 06:45:10xtreaksetkeywords: + patch
stage: needs patch -> patch review
pull_requests: + pull_request10508
2018-12-20 19:45:55xtreaksetnosy: + cjw296, mariocj89, xtreak

messages: + msg332252
versions: + Python 3.7, Python 3.8, - Python 3.3, Python 3.4, Python 3.5
2015-03-09 14:44:35Claudiu.Popasetnosy: + Claudiu.Popa
2014-05-12 20:49:31michael.foordsetnosy: + kushal.das
stage: needs patch

versions: + Python 3.5
2014-05-12 20:48:16michael.foordsetassignee: michael.foord
messages: + msg218362
2014-05-12 20:46:38ned.deilysetnosy: + michael.foord
2014-05-12 12:56:34andcreate