如何禁用然后重新启用警告?

时间:2010-03-06 00:03:18

标签: python warnings

我正在为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

5 个答案:

答案 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中添加。