<p>有两件事会影响对<code>__del__</code>的模拟:</p>
<ol>
<li><p>一旦测试函数结束,方法的修补就会恢复</p>
<p>这在<a href="https://docs.pytest.org/en/stable/monkeypatch.html" rel="nofollow noreferrer">monkeypatch API reference</a>中提到,也可以从<code>monkeypatch</code>夹具本身的代码中看到,它在这里调用<a href="https://docs.pytest.org/en/stable/reference.html#pytest.MonkeyPatch.undo" rel="nofollow noreferrer">^{<cd3>}</a>方法:</p>
<pre class="lang-py prettyprint-override"><code>@fixture
def monkeypatch() -> Generator["MonkeyPatch", None, None]:
"""A convenient fixture for monkey-patching.
...
All modifications will be undone after the requesting test function or
fixture has finished. ...
"""
mpatch = MonkeyPatch()
yield mpatch
mpatch.undo() # <
</code></pre>
</li>
<li><p>如<a href="https://stackoverflow.com/a/66824996/2745495">this other answer</a>中所述,调用<code>__del__</code>的时间(即,当对象被销毁和垃圾收集时)不是您可以保证或期望在测试函数引发<code>AssertionError</code>时发生的事情。只有在没有更多引用时才会调用它:</p>
<blockquote>
<p><strong>CPython implementation detail</strong>: It is possible for a reference cycle to prevent the reference count of an object from going to zero. In this case, the cycle will be later detected and deleted by the cyclic garbage collector. A common cause of reference cycles is when an exception has been caught in a local variable. The frame’s locals then reference the exception, which references its own traceback, which references the locals of all frames caught in the traceback.</p>
</blockquote>
</li>
</ol>
<p>考虑到这两件事,发生的情况是<code>__del__</code>的monkeypatching在<code>MyClass</code>对象<code>c</code>最终被删除之前(当调用其<code>__del__</code>函数时)被撤消或恢复。由于我们在这里处理一个异常,很可能对异常周围的局部变量的引用仍然存储在<em>某处</em>,因此<code>c</code>实例的引用计数在其<code>__del__</code>仍然被修补时没有变为零</p>
<p>我试图通过使用<code> full-trace</code>和<code> showlocals</code>选项运行测试来验证这一点。您将看到,在一个名为<code>_multicall</code>的函数中,运行测试函数并捕获异常:</p>
<pre class="lang-py prettyprint-override"><code>$ pytest tests/1.py setup-show full-trace showlocals
# ...lots of logs...
hook_impls = [<HookImpl plugin_name='python', plugin=<module '_pytest.python' from '/path/to//lib/python3.8/site-packages/_pytest/python.py'>>]
caller_kwargs = {'pyfuncitem': <Function test_PatchWithFailure>}, firstresult = True
def _multicall(hook_impls, caller_kwargs, firstresult=False):
# ...other parts of function...
else:
res = hook_impl.function(*args)
if res is not None:
results.append(res)
if firstresult: # halt further impl calls
break
except BaseException:
excinfo = sys.exc_info()
finally:
# ...other parts of function...
> return outcome.get_result()
args = [<Function test_PatchWithFailure>]
caller_kwargs = {'pyfuncitem': <Function test_PatchWithFailure>}
excinfo = (<class 'AssertionError'>, AssertionError('assert False'), <traceback object at 0x1108ea800>)
firstresult = True
# ...lots of logs...
../../path/to/lib/python3.8/site-packages/pluggy/callers.py:197:
</code></pre>
<p>据我所知,函数是在<code>hook_impl.function</code>中调用的(请参见传递的<code>args</code>),然后发生<code>assert False</code>,它被<code>except</code>块捕获,异常信息存储在<code>excinfo</code>中。然后,此异常信息将对<code>c</code>实例的引用存储在其<code>traceback object</code>中</p>
<pre class="lang-py prettyprint-override"><code># In test_PatchWithFailure
print('>>>> NOW IN test_PatchWithFailure')
c = MyClass()
print(c)
# Logs:
# <1.MyClass object at 0x10de152e0> # < SAME as BELOW
# In _multicall
except BaseException:
print('>>>> NOW IN _multicall')
excinfo = sys.exc_info()
import inspect
# print(inspect.trace()[-1]) # The last entry is where the exception was raised
# print(inspect.trace()[-1][0]) # The frame object
# print(inspect.trace()[-1][0].f_locals) # local vars
print(f'{inspect.trace()[-1].lineno}, {inspect.trace()[-1].code_context}')
print(f'Is "c" in here?: {"c" in inspect.trace()[-1][0].f_locals}')
print(inspect.trace()[-1][0].f_locals['c'])
# Logs:
# 42, [' assert False\n']
# Is "c" in here?: True
# <1.MyClass object at 0x10de152e0> # < SAME as ABOVE
</code></pre>
<p><em>我不确定我上面所做的是否正确,但我认为:</p>
<ol>
<li>当pytest捕获AssertionError时,它有效地添加了对本地<code>c</code>对象的引用,防止在函数结束时对其进行<code>__del__</code>-ed</li>
<li>然后,monkeypatching被撤消/恢复,而<code>c</code>仍然没有被垃圾收集</li>
<li>然后,pytest在所有测试函数运行后,最终报告它收集的所有错误(这将释放对<code>c</code>实例的引用,使其能够<code>__del__</code>)</li>
<li>最后<code>c</code>被<code>__del__</code>-ed,但是monkeypatch已经不存在了</li>
</ol>
<pre class="lang-none prettyprint-override"><code> - Captured stdout call
>>>> NOW IN test_PatchWithFailure
<1.MyClass object at 0x10de152e0>
>>>> NOW IN _multicall
42, [' assert False\n']
Is "c" in here?: True
<1.MyClass object at 0x10de152e0>
>>>> NOW IN _multicall
42, [' assert False\n']
Is "c" in here?: True
<1.MyClass object at 0x10de152e0>
- Captured stdout teardown
>>>> NOW returned from yield MonkeyPatch, calling undo()
>>>> UNDOING <class '1.MyClass'> __del__ <function MyClass.__del__ at 0x10bf04e50>
>>>> UNDOING <class '1.MyClass'> __init__ <function MyClass.__init__ at 0x10bf04d30>
================================================================ short test summary info ================================================================
FAILED tests/1.py::test_PatchWithFailure - assert False
=================================================================== 1 failed in 0.16s ===================================================================
Destroying MyClass
</code></pre>
<hr/>
<p>现在</p>
<blockquote>
<p>How could I work around this issue?</p>
</blockquote>
<p>与其依赖于最终删除对象的时间和<code>monkeypatch</code>-ing <code>__del__</code>,解决方法是将<code>MyClass</code>子类化,然后完全覆盖/替换<code>__init__</code>和<code>__del__</code>:</p>
<pre><code>def test_PatchWithFailure():
class MockedMyClass(MyClass):
def __init__(self):
print('Calling mocked __init__')
super().__init__()
def __del__(self):
print('Calling mocked __del__')
c = MockedMyClass()
assert False
</code></pre>
<p>见<a href="https://stackoverflow.com/q/41919677/2745495">Overriding destructors without calling their parents</a>。由于派生类不调用父类“<code>__del__</code>,因此在测试期间不会调用它。它类似于monkeypatching,用其他东西替换该方法,但在这里<code>__del__</code>的定义在整个测试期间仍然是模拟的。<code>MyClass</code>的所有其他功能应该仍然可以从<code>MockedMyClass</code>使用/测试</p>
<pre class="lang-none prettyprint-override"><code> c = MockedMyClass()
> assert False
E assert False
tests/1.py:59: AssertionError
- Captured stdout call
Calling mocked __init__
Constructing MyClass
================================================================ short test summary info ================================================================
FAILED tests/1.py::test_PatchWithFailure - assert False
=================================================================== 1 failed in 0.13s ===================================================================
Calling mocked __del__
</code></pre>
<p>在这里,我们看到销毁<code>c</code>只调用模拟的<code>__del__</code>(实际上在这里什么都不做)。没有更多的“破坏我的类”,希望能解决你的问题。创建一个提供<code>MockedMyClass</code>实例的fixture应该很简单</p>
<pre><code>@pytest.fixture
def mocked_myclass():
class MockedMyClass(MyClass):
def __init__(self):
print('Calling mocked __init__')
super().__init__()
def __del__(self):
print('Calling mocked __del__')
return MockedMyClass()
def test_PatchWithFailure(mocked_myclass):
c = mocked_myclass
assert False
</code></pre>