如何禁用再重新启用警告?
我正在为一个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
>>>
有人能解释一下我该怎么做吗?
编辑:显然这是一个已知的bug:http://bugs.python.org/issue4180
5 个回答
布莱恩说得很对,关于 __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)
Brian Luft说得对,__warningregistry__
确实是问题的根源。但我想澄清一点:warnings
模块的工作方式是,它会为每个调用了warn()
的模块设置module.__warningregistry__
。更复杂的是,stacklevel
这个选项会导致警告的属性被设置在发出警告的“名义上”的模块,而不一定是实际调用warn()
的那个模块……这取决于发出警告时的调用栈。
这意味着你可能会有很多不同的模块都有__warningregistry__
这个属性,而根据你的应用程序,它们可能都需要被清除,才能再次看到警告。我一直依赖以下这段代码来实现这个功能……它会清除所有模块的警告注册表,前提是模块名符合正则表达式(默认是所有模块):
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的问题 21724解决了resetwarnings()无法清除警告状态的问题。我在这个问题上附上了一个扩展的“上下文管理器”版本,可以从reset_warning_registry.py下载。
我看了一些文档,翻了几遍源代码和命令行,觉得我大概明白了。文档可以改进一下,让人更清楚它的行为。
警告模块有一个叫做 __warningsregistry__ 的注册表,用来记录哪些警告已经显示过。如果一个警告(消息)在设置“错误”过滤器之前没有被列在注册表里,那么调用 warn() 时,这个消息就不会被添加到注册表中。而且,警告注册表似乎是在第一次调用 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
所以,错误过滤器只会对那些还没有在警告注册表中的警告起作用。为了让你的代码正常工作,你需要在使用完上下文管理器后,清除警告注册表中相应的条目(或者一般来说,在你使用了忽略过滤器后,想要让之前使用过的消息被错误过滤器捕捉时)。这看起来有点不太直观……