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) * |
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) * |
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) * |
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) * |
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) * |
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) * |
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) * |
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) * |
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) * |
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) * |
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) * |
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) * |
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) * |
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.
|
|
Date |
User |
Action |
Args |
2022-04-11 14:58:03 | admin | set | github: 65677 |
2019-10-17 02:09:03 | xtreak | set | messages:
+ msg354827 |
2019-10-16 20:16:21 | Caris Moses | set | messages:
+ msg354815 |
2019-10-15 17:44:09 | Caris Moses | set | messages:
+ msg354750 |
2019-10-15 17:23:00 | xtreak | set | messages:
+ msg354749 |
2019-10-15 17:15:58 | Caris Moses | set | messages:
+ msg354748 |
2019-10-15 16:00:48 | xtreak | set | messages:
+ msg354737 |
2019-10-15 15:50:02 | Caris Moses | set | messages:
+ msg354735 |
2019-10-14 17:40:46 | xtreak | set | messages:
+ msg354642 |
2019-10-11 21:09:27 | Caris Moses | set | messages:
+ msg354496 |
2019-10-11 20:35:09 | xtreak | set | messages:
+ msg354491 |
2019-10-11 20:13:43 | Caris Moses | set | nosy:
+ Caris Moses messages:
+ msg354489
|
2019-09-09 15:40:25 | xtreak | link | issue28569 superseder |
2019-07-22 08:40:25 | xtreak | set | status: open -> closed resolution: fixed messages:
+ msg348291
stage: patch review -> resolved |
2019-07-22 08:04:13 | cjw296 | set | messages:
+ msg348289 |
2019-07-22 07:59:14 | cjw296 | set | messages:
+ msg348288 |
2019-07-22 07:39:06 | miss-islington | set | pull_requests:
+ pull_request14681 |
2019-07-22 07:38:59 | miss-islington | set | pull_requests:
+ pull_request14680 |
2019-07-22 07:38:39 | cjw296 | set | messages:
+ msg348285 |
2019-07-11 20:20:01 | iforapsy | set | messages:
+ msg347705 |
2019-07-10 18:38:18 | xtreak | set | messages:
+ msg347631 |
2019-07-10 18:32:26 | xtreak | set | stage: patch review pull_requests:
+ pull_request14493 |
2019-07-10 02:22:05 | xtreak | set | status: closed -> open versions:
+ Python 3.9 messages:
+ msg347593
resolution: fixed -> (no value) stage: resolved -> (no value) |
2019-07-10 01:50:13 | iforapsy | set | nosy:
+ iforapsy messages:
+ msg347592
|
2019-03-03 17:31:37 | xtreak | set | status: open -> closed resolution: fixed stage: patch review -> resolved |
2019-03-03 17:14:28 | miss-islington | set | pull_requests:
+ pull_request12147 |
2018-12-21 06:45:10 | xtreak | set | keywords:
+ patch stage: needs patch -> patch review pull_requests:
+ pull_request10508 |
2018-12-20 19:45:55 | xtreak | set | nosy:
+ 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:35 | Claudiu.Popa | set | nosy:
+ Claudiu.Popa
|
2014-05-12 20:49:31 | michael.foord | set | nosy:
+ kushal.das stage: needs patch
versions:
+ Python 3.5 |
2014-05-12 20:48:16 | michael.foord | set | assignee: michael.foord messages:
+ msg218362 |
2014-05-12 20:46:38 | ned.deily | set | nosy:
+ michael.foord
|
2014-05-12 12:56:34 | and | create | |