Как отключить, а затем снова включить предупреждение? - PullRequest
12 голосов
/ 06 марта 2010

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

11 голосов
/ 06 марта 2010

Прочитав документы и несколько раз и покопавшись в источнике и оболочке, я думаю, я понял это. Документы, вероятно, могли бы улучшить, чтобы прояснить, каково поведение.

Модуль предупреждений ведет реестр на __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

Таким образом, фильтр ошибок будет применяться только к предупреждениям, которых еще нет в реестре предупреждений. Чтобы ваш код работал, вам нужно будет удалить соответствующие записи из реестра предупреждений, когда вы закончите с менеджером контекста (или вообще в любое время после того, как вы использовали фильтр игнорирования и хотите, чтобы предыдущее использованное сообщение подобрать фильтр ошибок). Кажется немного не интуитивным ...

8 голосов
/ 12 марта 2012

Брайан Люфт прав, когда __warningregistry__ является причиной проблемы. Но я хотел уточнить одну вещь: способ, которым модуль warnings работает, заключается в том, что он устанавливает module.__warningregistry__ для каждого модуля , где вызывается warn(). Ситуация усложняется еще и тем, что опция stacklevel для предупреждений приводит к установке атрибута для модуля, для которого было выдано предупреждение «от имени», а не обязательно к тому, где warn() вызывался ... стек вызовов в момент выдачи предупреждения.

Это означает, что у вас может быть много разных модулей, в которых присутствует атрибут __warningregistry__, и в зависимости от вашего приложения все они могут нуждаться в очистке, прежде чем вы снова увидите предупреждения. Для этого я использовал следующий фрагмент кода ... он очищает реестр предупреждений для всех модулей, чье имя совпадает с регулярным выражением (по умолчанию everything ):

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 .

6 голосов
/ 06 марта 2010

Брайан находится на отметке __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)
2 голосов
/ 13 октября 2014

Вслед за полезным разъяснением Эли Коллинза приведена модифицированная версия диспетчера контекста 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 голосов
/ 02 декабря 2016

Я столкнулся с теми же проблемами, и, хотя все остальные ответы верны, я выбираю другой маршрут. Я не хочу тестировать модуль предупреждений и не знать о его внутренней работе. Поэтому я просто высмеял это вместо:

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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...