_warnings.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. from contextlib import contextmanager
  2. import sys
  3. import warnings
  4. import re
  5. import functools
  6. import os
  7. __all__ = ['all_warnings', 'expected_warnings', 'warn']
  8. # A version of `warnings.warn` with a default stacklevel of 2.
  9. # functool is used so as not to increase the call stack accidentally
  10. warn = functools.partial(warnings.warn, stacklevel=2)
  11. @contextmanager
  12. def all_warnings():
  13. """
  14. Context for use in testing to ensure that all warnings are raised.
  15. Examples
  16. --------
  17. >>> import warnings
  18. >>> def foo():
  19. ... warnings.warn(RuntimeWarning("bar"), stacklevel=2)
  20. We raise the warning once, while the warning filter is set to "once".
  21. Hereafter, the warning is invisible, even with custom filters:
  22. >>> with warnings.catch_warnings():
  23. ... warnings.simplefilter('once')
  24. ... foo() # doctest: +SKIP
  25. We can now run ``foo()`` without a warning being raised:
  26. >>> from numpy.testing import assert_warns
  27. >>> foo() # doctest: +SKIP
  28. To catch the warning, we call in the help of ``all_warnings``:
  29. >>> with all_warnings():
  30. ... assert_warns(RuntimeWarning, foo)
  31. """
  32. # _warnings.py is on the critical import path.
  33. # Since this is a testing only function, we lazy import inspect.
  34. import inspect
  35. # Whenever a warning is triggered, Python adds a __warningregistry__
  36. # member to the *calling* module. The exercise here is to find
  37. # and eradicate all those breadcrumbs that were left lying around.
  38. #
  39. # We proceed by first searching all parent calling frames and explicitly
  40. # clearing their warning registries (necessary for the doctests above to
  41. # pass). Then, we search for all submodules of skimage and clear theirs
  42. # as well (necessary for the skimage test suite to pass).
  43. frame = inspect.currentframe()
  44. if frame:
  45. for f in inspect.getouterframes(frame):
  46. f[0].f_locals['__warningregistry__'] = {}
  47. del frame
  48. for mod_name, mod in list(sys.modules.items()):
  49. try:
  50. mod.__warningregistry__.clear()
  51. except AttributeError:
  52. pass
  53. with warnings.catch_warnings(record=True) as w:
  54. warnings.simplefilter("always")
  55. yield w
  56. @contextmanager
  57. def expected_warnings(matching):
  58. r"""Context for use in testing to catch known warnings matching regexes
  59. Parameters
  60. ----------
  61. matching : None or a list of strings or compiled regexes
  62. Regexes for the desired warning to catch
  63. If matching is None, this behaves as a no-op.
  64. Examples
  65. --------
  66. >>> import numpy as np
  67. >>> rng = np.random.default_rng()
  68. >>> image = rng.integers(0, 2**16, size=(100, 100), dtype=np.uint16)
  69. >>> # rank filters are slow when bit-depth exceeds 10 bits
  70. >>> from skimage import filters
  71. >>> with expected_warnings(['Bad rank filter performance']):
  72. ... median_filtered = filters.rank.median(image)
  73. Notes
  74. -----
  75. Uses `all_warnings` to ensure all warnings are raised.
  76. Upon exiting, it checks the recorded warnings for the desired matching
  77. pattern(s).
  78. Raises a ValueError if any match was not found or an unexpected
  79. warning was raised.
  80. Allows for three types of behaviors: `and`, `or`, and `optional` matches.
  81. This is done to accommodate different build environments or loop conditions
  82. that may produce different warnings. The behaviors can be combined.
  83. If you pass multiple patterns, you get an orderless `and`, where all of the
  84. warnings must be raised.
  85. If you use the `|` operator in a pattern, you can catch one of several
  86. warnings.
  87. Finally, you can use `|\A\Z` in a pattern to signify it as optional.
  88. """
  89. if isinstance(matching, str):
  90. raise ValueError(
  91. '``matching`` should be a list of strings and not a string itself.'
  92. )
  93. # Special case for disabling the context manager
  94. if matching is None:
  95. yield None
  96. return
  97. strict_warnings = os.environ.get('SKIMAGE_TEST_STRICT_WARNINGS', '1')
  98. if strict_warnings.lower() == 'true':
  99. strict_warnings = True
  100. elif strict_warnings.lower() == 'false':
  101. strict_warnings = False
  102. else:
  103. strict_warnings = bool(int(strict_warnings))
  104. with all_warnings() as w:
  105. # enter context
  106. yield w
  107. # exited user context, check the recorded warnings
  108. # Allow users to provide None
  109. while None in matching:
  110. matching.remove(None)
  111. remaining = [m for m in matching if r'\A\Z' not in m.split('|')]
  112. for warn in w:
  113. found = False
  114. for match in matching:
  115. if re.search(match, str(warn.message)) is not None:
  116. found = True
  117. if match in remaining:
  118. remaining.remove(match)
  119. if strict_warnings and not found:
  120. raise ValueError(f'Unexpected warning: {str(warn.message)}')
  121. if strict_warnings and (len(remaining) > 0):
  122. newline = "\n"
  123. msg = f"No warning raised matching:{newline}{newline.join(remaining)}"
  124. raise ValueError(msg)