There seems to be a difference in obtaining the attribute out of the __dict__ and getattr. patch uses __dict__ [0] to access the attribute to be patched and falls back to getattr. There is a difference in detecting normal attribute access to be a coroutine function versus the one obtained from __dict__ . We can perhaps make _is_async_obj [1] to see if the passed obj is a classmethod/staticmethod and to use __func__ to detect a coroutine function.
As a workaround for now you can pass AsyncMock explicitly to patch to return an AsyncMock. There was an open issue (issue36092) about patch and staticmethods/classmethods but not sure this is related to this.
Retagging it to remove asyncio. Thanks for the report!
# bpo39082.py
from unittest.mock import patch
import inspect
class Helper:
@classmethod
async def async_class_method(cls):
pass
@staticmethod
async def async_static_method(*args, **kwargs):
pass
print("Patching async static method")
async_patcher = patch("__main__.Helper.async_class_method")
print(f"{Helper.async_class_method = }")
print(f"{Helper.__dict__['async_class_method'] = }")
print(f"{inspect.iscoroutinefunction(Helper.async_class_method) = }")
print(f"{inspect.iscoroutinefunction(Helper.__dict__['async_class_method']) = }")
print(f"{inspect.iscoroutinefunction(getattr(Helper, 'async_class_method')) = }")
mock_ = async_patcher.start()
print(mock_)
print("\nPatching async static method")
async_patcher = patch("__main__.Helper.async_static_method")
print(f"{Helper.async_static_method = }")
print(f"{Helper.__dict__['async_static_method'] = }")
print(f"{inspect.iscoroutinefunction(Helper.async_static_method) = }")
print(f"{inspect.iscoroutinefunction(Helper.__dict__['async_static_method']) = }")
print(f"{inspect.iscoroutinefunction(getattr(Helper, 'async_static_method')) = }")
mock_ = async_patcher.start()
print(mock_)
$ python3.8 bpo39082.py
Patching async static method
Helper.async_class_method = <bound method Helper.async_class_method of <class '__main__.Helper'>>
Helper.__dict__['async_class_method'] = <classmethod object at 0x10de7bcd0>
inspect.iscoroutinefunction(Helper.async_class_method) = True
inspect.iscoroutinefunction(Helper.__dict__['async_class_method']) = False
inspect.iscoroutinefunction(getattr(Helper, 'async_class_method')) = True
<MagicMock name='async_class_method' id='4537489440'>
Patching async static method
Helper.async_static_method = <function Helper.async_static_method at 0x10e77baf0>
Helper.__dict__['async_static_method'] = <staticmethod object at 0x10de7bbb0>
inspect.iscoroutinefunction(Helper.async_static_method) = True
inspect.iscoroutinefunction(Helper.__dict__['async_static_method']) = False
inspect.iscoroutinefunction(getattr(Helper, 'async_static_method')) = True
<MagicMock name='async_static_method' id='4537741520'>
Detect __func__ to be used for iscoroutinefunction
diff --git Lib/unittest/mock.py Lib/unittest/mock.py
index cd5a2aeb60..572468ca8d 100644
--- Lib/unittest/mock.py
+++ Lib/unittest/mock.py
@@ -48,6 +48,8 @@ _safe_super = super
def _is_async_obj(obj):
if _is_instance_mock(obj) and not isinstance(obj, AsyncMock):
return False
+ if hasattr(obj, '__func__'):
+ obj = getattr(obj, '__func__')
return asyncio.iscoroutinefunction(obj) or inspect.isawaitable(obj)
[0] https://github.com/python/cpython/blob/50d4f12958bf806a4e1a1021d70cfd5d448c5cba/Lib/unittest/mock.py#L1387
[1] https://github.com/python/cpython/blob/50d4f12958bf806a4e1a1021d70cfd5d448c5cba/Lib/unittest/mock.py#L48
|