Issue 36834: mock.patch.object does not persist module name for functions (original) (raw)
The expectation is that the module attribute for a patched function should persist after patching.
Minimal test case is attached. Simply run pytest in a venv with the files.
Output: def test_zxc(): with mock.patch.object(mymodule, 'asd', side_effect=mymodule.asd, autospec=True) as spy_asd:
assert spy_asd.__module__ == 'mymodule'
E AssertionError: assert None == 'mymodule' E + where None = <function asd at 0x7fe4cd6fd620>.module
test_mymodule.py:8: AssertionError
Originally reported at https://github.com/pytest-dev/pytest-mock/issues/146 before it was determined this was a unittest.mock issue.
Happens on both Python 2.7 and 3.7. Probably not really tied to a specific Python version and more of mock library issue.
My local venv: Python 3.7.2 pytest 4.4.1
I guess the problem is that to patch 'bar' in foo.bar the signature of bar is taken and then it's evaluated using exec [1] where the function body has _check_sig that checks for the signature during function call. _check_sig has attributes of func copied. Since it's evaluated using exec the returned funcopy variable might not have module set as per the execution context. One possible approach would be to _copy_func_details(func, funcopy) so that the module and other attributes are copied.
This produces a test failure where in case of lambdas the name is which is not a valid identifier and hence funcopy is used. Then name is set as '' by my patch. Hence, 'funcopy' is replaced by '' causing below test failure. This could be resolved by setting name again to the expected value. This was newly added for test coverage with adbf178e49113b2de0042e86a1228560475a65c5.
My main worry is that it's used in create_autospec hence do we really want to copy module and others to the patched function for places with create_autospec also with functions supplying autospec=True. This is a user expectation but I would like to know maintainers opinion as well of this change.
Reproducer
cat /tmp/foo.py
def bar(a: int): pass
/tmp/bar.py
from unittest import mock
import foo
with mock.patch.object(foo, 'bar', autospec=True) as mocked_attribute: assert mocked_attribute.module == 'foo'
Python 3.7 module is None
➜ cpython git:(master) ✗ python3.7 /tmp/bar.py Traceback (most recent call last): File "/tmp/bar.py", line 6, in assert mocked_attribute.module == 'foo', f'module is {mocked_attribute.module}' AssertionError: module is None
With patch
➜ cpython git:(master) ✗ ./python.exe /tmp/bar.py # No failure
Patch
diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 1e8057d5f5..a7006fcf7d 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -165,6 +165,8 @@ def _set_signature(mock, original, instance=False): exec (src, context) funcopy = context[name] _setup_func(funcopy, mock, sig)
- _copy_func_details(func, funcopy)
- funcopy.name = name return funcopy
Test failure without funcopy.name = name in my patch where I don't restore the 'funcopy' for lambdas. But this can be fixed.
====================================================================== FAIL: test_spec_function_no_name (unittest.test.testmock.testhelpers.SpecSignatureTest)
Traceback (most recent call last): File "/Users/karthikeyansingaravelan/stuff/python/cpython/Lib/unittest/test/testmock/testhelpers.py", line 946, in test_spec_function_no_name self.assertEqual(mock.name, 'funcopy') AssertionError: '' != 'funcopy'
- funcopy
Ran 386 tests in 2.911s