Подтвердить что ты не робот

Как отключить, а затем снова включить предупреждение?

Я пишу некоторые модульные тесты для библиотеки 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

4b9b3361

Ответ 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.