2010-03-06 42 views
12

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

+1

看起來好像在警告模塊中可能存在一些細微的錯誤。我在shell中玩弄你的代碼,並觀察到甚至在警告消息相同的地方聲明其他函數也會有相同的效果(沒有警告),而改變警告消息可以使其工作。 – 2010-03-06 00:59:12

回答

11

通過文檔和幾次閱讀和周圍的源和殼我想我已經想通了戳。文檔可能會改進,以更清晰地表明行爲。

警告模塊會在__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 

所以錯誤過濾器僅適用於警告註冊表中尚不存在的警告。要使代碼正常工作,在完成上下文管理器時,您需要清除警告註冊表中的相應條目(或者在使用忽略過濾器並希望使用先前使用的消息之後的任何時候被拾取錯誤過濾器)。似乎有點不直觀......

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) 
+0

我對這段代碼的使用很感興趣。它會尊重以前被忽視的警告嗎? – 2012-11-27 15:34:05

8

布賴恩·拉夫特是正確的約__warningregistry__是問題的原因。但我想澄清一件事:warnings模塊似乎工作的方式是它設置module.__warningregistry__每個模塊其中warn()被調用。更復雜的事情,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的issue 21724地址問題是resetwarnings()不明確的警示狀態。我附加了一個擴展的「上下文管理器」版本到這個問題,它可以從reset_warning_registry.py下載。

2

從禮柯林斯有幫助澄清繼,這裏是在進入上下文管理器時清空模塊的給定序列的警告註冊表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`` 
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只增加。