为什么在 unittest.mock._patch 上调用 __exit__ 方法会抛出 IndexError?

Why does calling the __exit__ method on unittest.mock._patch throw an IndexError?

当我定义一个函数并使用 with 语句对其进行修补时,它运行良好。

def some_func():
  print('this is some_func')

with patch('__main__.some_func', return_value='this is patched some_func'):
  some_func()

输出:

this is patched some_func

我的理解是,使用 with 语句会导致在补丁对象上调用 __enter____exit__ 方法。所以我认为这相当于这样做:

patched_some_func = patch('__main__.some_func', return_value='this is patched some_func')
patched_some_func.__enter__()
some_func()
patched_some_func.__exit__()

在这种情况下,some_func 调用的输出是相同的:

this is patched some_func

但是当我调用 __exit__ 方法时我得到一个 IndexError:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib64/python3.7/unittest/mock.py", line 1437, in __exit__
    return exit_stack.__exit__(*exc_info)
  File "/usr/lib64/python3.7/contextlib.py", line 482, in __exit__
    received_exc = exc_details[0] is not None
IndexError: tuple index out of range

为什么我在第二种情况下得到 IndexError 而不是第一种情况,我使用 with? (另外请注意,我在 python 3.7.10 上收到此错误,但在 python 3.7.3 上没有收到此错误)

您的理解有误;退出时上下文管理器 __exit__ 方法被传递 None, None, None:

patched_some_func.__exit__(None, None, None)

参数总是传入,或者是异常信息,或者是None三个参数中的每一个。见 documentation on object.__exit__():

If the context was exited without an exception, all three arguments will be None.

另见 PEP 343 – The “with” Statement specification section:

The calling convention for mgr.__exit__() is as follows. If the finally-suite was reached through normal completion of BLOCK or through a non-local goto (a break, continue or return statement in BLOCK), mgr.__exit__() is called with three None arguments. If the finally-suite was reached through an exception raised in BLOCK, mgr.__exit__() is called with three arguments representing the exception type, value, and traceback.

关于您在 3.7.10 中出现索引错误的原因:旧版本中的 mock.patch() 实现使用 an incorrect implementation that could result in an unexpected exception, and the fix for this was to use an contextlib.ExitStack() instance. The ExitStack context manager uses def __exit__(self, *exc_details): as the method signature and expects the exc_details tuple to have at least 1 element in it, which normally is either None or an exception object. The bug fix is part of Python 3.7.8, which is why you don't see the same context manager in 3.7.3 as you see in 3.7.10. In the older version of the code,修补程序 __exit__() 方法也使用 catchall *args 元组,否则不会尝试索引到该元组中。