When I define a function and patch it using the with
statement it runs fine.
def some_func(): print('this is some_func') with patch('__main__.some_func', return_value='this is patched some_func'): some_func()
Output:
this is patched some_func
My understanding is that using the with
statement would cause the __enter__
and __exit__
methods to be called on the patch object. So I thought that would be equivalent to doing this:
patched_some_func = patch('__main__.some_func', return_value='this is patched some_func') patched_some_func.__enter__() some_func() patched_some_func.__exit__()
The output from the some_func
call is the same in this case:
this is patched some_func
But I get an IndexError
when I call the __exit__
method:
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
Why am I getting an IndexError
in the second case but not the first where I use with
?
(An additional note, I get this error on python 3.7.10 but not on python 3.7.3)
Advertisement
Answer
Your understanding is incorrect; on exit the context manager __exit__
method is passed None, None, None
:
patched_some_func.__exit__(None, None, None)
The arguments are always passed in, either the exception information, or None
for each of the three arguments. See the documentation on object.__exit__()
:
If the context was exited without an exception, all three arguments will be
None
.
Also see the 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 ofBLOCK
or through a non-local goto (abreak
,continue
orreturn
statement inBLOCK
),mgr.__exit__()
is called with threeNone
arguments. If the finally-suite was reached through an exception raised inBLOCK
,mgr.__exit__()
is called with three arguments representing the exception type, value, and traceback.
As to why you get an index error in 3.7.10: the mock.patch()
implementation in older releases used 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, the patcher __exit__()
method also uses a catchall *args
tuple, but otherwise doesn’t try to index into this tuple.