我正在为Python库编写一些单元测试,并希望将某些警告作为异常引发,我可以使用simplefilter函数轻松完成。但是,对于一个测试,我想禁用警告,运行测试,然后重新启用警告。
我正在使用Python 2.6,所以我应该能够使用catch_warnings上下文管理器来做到这一点,但它似乎对我不起作用。即使失败了,我也应该能够拨打resetwarnings,然后重新设置我的过滤器。
这是一个说明问题的简单示例:
>>> import warnings
>>> warnings.simplefilter("error", UserWarning)
>>>
>>> def f():
... warnings.warn("Boo!", UserWarning)
...
>>>
>>> f() # raises UserWarning as an exception
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in f
UserWarning: Boo!
>>>
>>> f() # still raises the exception
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in f
UserWarning: Boo!
>>>
>>> with warnings.catch_warnings():
... warnings.simplefilter("ignore")
... f() # no warning is raised or printed
...
>>>
>>> f() # this should raise the warning as an exception, but doesn't
>>>
>>> warnings.resetwarnings()
>>> warnings.simplefilter("error", UserWarning)
>>>
>>> f() # even after resetting, I'm still getting nothing
>>>
有人可以解释我是如何做到这一点的吗?
编辑:显然这是一个已知的错误:http://bugs.python.org/issue4180
答案 0 :(得分:11)
通过文档阅读几次并在源和shell中查找,我想我已经弄明白了。文档可能会改进,以使行为更清楚。
警告模块在__warningsregistry__处保留一个注册表,以跟踪已显示的警告。如果在设置“错误”过滤器之前未在注册表中列出警告(消息),则对warn()的任何调用都不会导致将消息添加到注册表中。此外,警告注册表似乎在第一次警告呼叫之前未创建:
>>> import warnings
>>> __warningregistry__
------------------------------------------------------------
Traceback (most recent call last):
File "<ipython console>", line 1, in <module>
NameError: name '__warningregistry__' is not defined
>>> warnings.simplefilter('error')
>>> __warningregistry__
------------------------------------------------------------
Traceback (most recent call last):
File "<ipython console>", line 1, in <module>
NameError: name '__warningregistry__' is not defined
>>> warnings.warn('asdf')
------------------------------------------------------------
Traceback (most recent call last):
File "<ipython console>", line 1, in <module>
UserWarning: asdf
>>> __warningregistry__
{}
现在,如果我们忽略警告,它们将被添加到警告注册表中:
>>> warnings.simplefilter("ignore")
>>> warnings.warn('asdf')
>>> __warningregistry__
{('asdf', <type 'exceptions.UserWarning'>, 1): True}
>>> warnings.simplefilter("error")
>>> warnings.warn('asdf')
>>> warnings.warn('qwerty')
------------------------------------------------------------
Traceback (most recent call last):
File "<ipython console>", line 1, in <module>
UserWarning: qwerty
因此,错误过滤器仅适用于警告注册表中尚未包含的警告。为了使您的代码工作,您需要在完成上下文管理器后清除警告注册表中的相应条目(或者通常在您使用忽略过滤器之后的任何时候,并希望使用prev。used消息被拿起错误过滤器)。似乎有点不直观......
答案 1 :(得分:8)
Brian Luft关于__warningregistry__
导致问题的原因是正确的。但我想澄清一件事:warnings
模块似乎工作的方式是它为每个模块设置module.__warningregistry__
,其中warn()
被调用。更复杂的是,警告的stacklevel
选项导致为模块设置属性,警告是以“名称”发出的,不一定是warn()
被调用的那个......这取决于警告发出时的调用堆栈。
这意味着您可能有许多不同的模块,其中存在__warningregistry__
属性,并且根据您的应用程序,它们可能都需要清除才能再次看到警告。我一直依赖以下代码片段来完成这个...它清除了所有名称与regexp匹配的模块的warnings注册表(默认为所有内容):
def reset_warning_registry(pattern=".*"):
"clear warning registry for all match modules"
import re
import sys
key = "__warningregistry__"
for mod in sys.modules.values():
if hasattr(mod, key) and re.match(pattern, mod.__name__):
getattr(mod, key).clear()
更新:CPython issue 21724解决了resetwarnings()未清除警告状态的问题。我在此问题上附加了扩展的“上下文管理器”版本,可以从reset_warning_registry.py下载。
答案 2 :(得分:6)
Brian关于__warningregistry__
的观点。因此,您需要扩展catch_warnings
以保存/恢复全局__warningregistry__
这样的事可能有效
class catch_warnings_plus(warnings.catch_warnings):
def __enter__(self):
super(catch_warnings_plus,self).__enter__()
self._warningregistry=dict(globals.get('__warningregistry__',{}))
def __exit__(self, *exc_info):
super(catch_warnings_plus,self).__exit__(*exc_info)
__warningregistry__.clear()
__warningregistry__.update(self._warningregistry)
答案 3 :(得分:2)
来自Eli Collins&#39;有用的说明,这里是catch_warnings
上下文管理器的修改版本,它在进入上下文管理器时清除给定模块序列中的警告注册表,并在退出时恢复注册表:
from warnings import catch_warnings
class catch_warn_reset(catch_warnings):
""" Version of ``catch_warnings`` class that resets warning registry
"""
def __init__(self, *args, **kwargs):
self.modules = kwargs.pop('modules', [])
self._warnreg_copies = {}
super(catch_warn_reset, self).__init__(*args, **kwargs)
def __enter__(self):
for mod in self.modules:
if hasattr(mod, '__warningregistry__'):
mod_reg = mod.__warningregistry__
self._warnreg_copies[mod] = mod_reg.copy()
mod_reg.clear()
return super(catch_warn_reset, self).__enter__()
def __exit__(self, *exc_info):
super(catch_warn_reset, self).__exit__(*exc_info)
for mod in self.modules:
if hasattr(mod, '__warningregistry__'):
mod.__warningregistry__.clear()
if mod in self._warnreg_copies:
mod.__warningregistry__.update(self._warnreg_copies[mod])
使用类似的东西:
import my_module_raising_warnings
with catch_warn_reset(modules=[my_module_raising_warnings]):
# Whatever you'd normally do inside ``catch_warnings``
答案 4 :(得分:0)
我遇到了同样的问题,虽然所有其他答案都有效但我选择了不同的路线。我不想测试警告模块,也不知道它的内部工作原理。所以我只是嘲笑它:
import warnings
import unittest
from unittest.mock import patch
from unittest.mock import call
class WarningTest(unittest.TestCase):
@patch('warnings.warn')
def test_warnings(self, fake_warn):
warn_once()
warn_twice()
fake_warn.assert_has_calls(
[call("You've been warned."),
call("This is your second warning.")])
def warn_once():
warnings.warn("You've been warned.")
def warn_twice():
warnings.warn("This is your second warning.")
if __name__ == '__main__':
__main__=unittest.main()
此代码为Python 3,对于2.6,您需要使用外部模拟库作为unittest.mock仅在2.7中添加。