如何禁用再重新启用警告?

13 投票
5 回答
7929 浏览
提问于 2025-04-15 20:04

我正在为一个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 个回答

6

布莱恩说得很对,关于 __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)
8

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下载。

11

我看了一些文档,翻了几遍源代码和命令行,觉得我大概明白了。文档可以改进一下,让人更清楚它的行为。

警告模块有一个叫做 __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

所以,错误过滤器只会对那些还没有在警告注册表中的警告起作用。为了让你的代码正常工作,你需要在使用完上下文管理器后,清除警告注册表中相应的条目(或者一般来说,在你使用了忽略过滤器后,想要让之前使用过的消息被错误过滤器捕捉时)。这看起来有点不太直观……

撰写回答