145 lines
5 KiB
Python
145 lines
5 KiB
Python
from contextlib import contextmanager
|
|
import sys
|
|
import warnings
|
|
import re
|
|
import functools
|
|
import os
|
|
|
|
__all__ = ['all_warnings', 'expected_warnings', 'warn']
|
|
|
|
|
|
# A version of `warnings.warn` with a default stacklevel of 2.
|
|
# functool is used so as not to increase the call stack accidentally
|
|
warn = functools.partial(warnings.warn, stacklevel=2)
|
|
|
|
|
|
@contextmanager
|
|
def all_warnings():
|
|
"""
|
|
Context for use in testing to ensure that all warnings are raised.
|
|
|
|
Examples
|
|
--------
|
|
>>> import warnings
|
|
>>> def foo():
|
|
... warnings.warn(RuntimeWarning("bar"), stacklevel=2)
|
|
|
|
We raise the warning once, while the warning filter is set to "once".
|
|
Hereafter, the warning is invisible, even with custom filters:
|
|
|
|
>>> with warnings.catch_warnings():
|
|
... warnings.simplefilter('once')
|
|
... foo() # doctest: +SKIP
|
|
|
|
We can now run ``foo()`` without a warning being raised:
|
|
|
|
>>> from numpy.testing import assert_warns
|
|
>>> foo() # doctest: +SKIP
|
|
|
|
To catch the warning, we call in the help of ``all_warnings``:
|
|
|
|
>>> with all_warnings():
|
|
... assert_warns(RuntimeWarning, foo)
|
|
"""
|
|
# _warnings.py is on the critical import path.
|
|
# Since this is a testing only function, we lazy import inspect.
|
|
import inspect
|
|
# Whenever a warning is triggered, Python adds a __warningregistry__
|
|
# member to the *calling* module. The exercize here is to find
|
|
# and eradicate all those breadcrumbs that were left lying around.
|
|
#
|
|
# We proceed by first searching all parent calling frames and explicitly
|
|
# clearing their warning registries (necessary for the doctests above to
|
|
# pass). Then, we search for all submodules of skimage and clear theirs
|
|
# as well (necessary for the skimage test suite to pass).
|
|
|
|
frame = inspect.currentframe()
|
|
if frame:
|
|
for f in inspect.getouterframes(frame):
|
|
f[0].f_locals['__warningregistry__'] = {}
|
|
del frame
|
|
|
|
for mod_name, mod in list(sys.modules.items()):
|
|
try:
|
|
mod.__warningregistry__.clear()
|
|
except AttributeError:
|
|
pass
|
|
|
|
with warnings.catch_warnings(record=True) as w:
|
|
warnings.simplefilter("always")
|
|
yield w
|
|
|
|
|
|
@contextmanager
|
|
def expected_warnings(matching):
|
|
r"""Context for use in testing to catch known warnings matching regexes
|
|
|
|
Parameters
|
|
----------
|
|
matching : None or a list of strings or compiled regexes
|
|
Regexes for the desired warning to catch
|
|
If matching is None, this behaves as a no-op.
|
|
|
|
Examples
|
|
--------
|
|
>>> import numpy as np
|
|
>>> image = np.random.randint(0, 2**16, size=(100, 100), dtype=np.uint16)
|
|
>>> # rank filters are slow when bit-depth exceeds 10 bits
|
|
>>> from skimage import filters
|
|
>>> with expected_warnings(['Bad rank filter performance']):
|
|
... median_filtered = filters.rank.median(image)
|
|
|
|
Notes
|
|
-----
|
|
Uses `all_warnings` to ensure all warnings are raised.
|
|
Upon exiting, it checks the recorded warnings for the desired matching
|
|
pattern(s).
|
|
Raises a ValueError if any match was not found or an unexpected
|
|
warning was raised.
|
|
Allows for three types of behaviors: `and`, `or`, and `optional` matches.
|
|
This is done to accommodate different build environments or loop conditions
|
|
that may produce different warnings. The behaviors can be combined.
|
|
If you pass multiple patterns, you get an orderless `and`, where all of the
|
|
warnings must be raised.
|
|
If you use the `|` operator in a pattern, you can catch one of several
|
|
warnings.
|
|
Finally, you can use `|\A\Z` in a pattern to signify it as optional.
|
|
|
|
"""
|
|
if isinstance(matching, str):
|
|
raise ValueError('``matching`` should be a list of strings and not '
|
|
'a string itself.')
|
|
|
|
# Special case for disabling the context manager
|
|
if matching is None:
|
|
yield None
|
|
return
|
|
|
|
strict_warnings = os.environ.get('SKIMAGE_TEST_STRICT_WARNINGS', '1')
|
|
if strict_warnings.lower() == 'true':
|
|
strict_warnings = True
|
|
elif strict_warnings.lower() == 'false':
|
|
strict_warnings = False
|
|
else:
|
|
strict_warnings = bool(int(strict_warnings))
|
|
|
|
with all_warnings() as w:
|
|
# enter context
|
|
yield w
|
|
# exited user context, check the recorded warnings
|
|
# Allow users to provide None
|
|
while None in matching:
|
|
matching.remove(None)
|
|
remaining = [m for m in matching if r'\A\Z' not in m.split('|')]
|
|
for warn in w:
|
|
found = False
|
|
for match in matching:
|
|
if re.search(match, str(warn.message)) is not None:
|
|
found = True
|
|
if match in remaining:
|
|
remaining.remove(match)
|
|
if strict_warnings and not found:
|
|
raise ValueError('Unexpected warning: %s' % str(warn.message))
|
|
if strict_warnings and (len(remaining) > 0):
|
|
msg = 'No warning raised matching:\n%s' % '\n'.join(remaining)
|
|
raise ValueError(msg)
|