Как отключить, а затем снова включить предупреждение?
Я пишу некоторые модульные тесты для библиотеки 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
>>>
Может кто-нибудь объяснить, как я могу это сделать?
EDIT: По-видимому, это известная ошибка: http://bugs.python.org/issue4180
Ответы
Ответ 1
Чтение через документы и несколько раз и выталкивание вокруг источника и оболочки, я думаю, что я понял это. Документы, вероятно, могли бы улучшить, чтобы яснее было понять, что такое поведение.
Модуль предупреждений хранит реестр в __warningsregistry__, чтобы отслеживать, какие предупреждения были показаны. Если предупреждение (сообщение) не указано в реестре до того, как установлен фильтр "error", любые вызовы 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
Таким образом, фильтр ошибок будет применяться только к предупреждениям, которые еще не находятся в реестре предупреждений. Чтобы сделать работу с вашим кодом, вам нужно очистить соответствующие записи из реестра предупреждений, когда вы закончите с менеджером контекста (или вообще в любое время после того, как вы использовали фильтр игнорирования и хотите использовать предыдущее сообщение для возьмите фильтр ошибок). Кажется немного неинтуитивным...
Ответ 2
Брайан Люфт правильно относится к __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.
Ответ 3
Брайан находится примерно на __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)
Ответ 4
Следуя полезному пояснению Эли Коллинза, здесь приведена измененная версия менеджера контекста 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``
Ответ 5
Я столкнулся с теми же проблемами, и, хотя все остальные ответы действительны, я выбираю другой маршрут. Я не хочу проверять модуль предупреждений и не знаю о его внутренних действиях. Поэтому я просто издевался над этим:
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 вам нужна внешняя mocking-библиотека, поскольку unittest.mock был добавлен только в 2.7.