test_warnings.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. """
  2. Tests which scan for certain occurrences in the code, they may not find
  3. all of these occurrences but should catch almost all. This file was adapted
  4. from NumPy.
  5. """
  6. import os
  7. from pathlib import Path
  8. import ast
  9. import tokenize
  10. import scipy
  11. import pytest
  12. class ParseCall(ast.NodeVisitor):
  13. def __init__(self):
  14. self.ls = []
  15. def visit_Attribute(self, node):
  16. ast.NodeVisitor.generic_visit(self, node)
  17. self.ls.append(node.attr)
  18. def visit_Name(self, node):
  19. self.ls.append(node.id)
  20. class FindFuncs(ast.NodeVisitor):
  21. def __init__(self, filename):
  22. super().__init__()
  23. self.__filename = filename
  24. self.bad_filters = []
  25. self.bad_stacklevels = []
  26. def visit_Call(self, node):
  27. p = ParseCall()
  28. p.visit(node.func)
  29. ast.NodeVisitor.generic_visit(self, node)
  30. if p.ls[-1] == 'simplefilter' or p.ls[-1] == 'filterwarnings':
  31. if node.args[0].s == "ignore":
  32. self.bad_filters.append(
  33. "{}:{}".format(self.__filename, node.lineno))
  34. if p.ls[-1] == 'warn' and (
  35. len(p.ls) == 1 or p.ls[-2] == 'warnings'):
  36. if self.__filename == "_lib/tests/test_warnings.py":
  37. # This file
  38. return
  39. # See if stacklevel exists:
  40. if len(node.args) == 3:
  41. return
  42. args = {kw.arg for kw in node.keywords}
  43. if "stacklevel" not in args:
  44. self.bad_stacklevels.append(
  45. "{}:{}".format(self.__filename, node.lineno))
  46. @pytest.fixture(scope="session")
  47. def warning_calls():
  48. # combined "ignore" and stacklevel error
  49. base = Path(scipy.__file__).parent
  50. bad_filters = []
  51. bad_stacklevels = []
  52. for path in base.rglob("*.py"):
  53. # use tokenize to auto-detect encoding on systems where no
  54. # default encoding is defined (e.g., LANG='C')
  55. with tokenize.open(str(path)) as file:
  56. tree = ast.parse(file.read(), filename=str(path))
  57. finder = FindFuncs(path.relative_to(base))
  58. finder.visit(tree)
  59. bad_filters.extend(finder.bad_filters)
  60. bad_stacklevels.extend(finder.bad_stacklevels)
  61. return bad_filters, bad_stacklevels
  62. @pytest.mark.slow
  63. def test_warning_calls_filters(warning_calls):
  64. bad_filters, bad_stacklevels = warning_calls
  65. # We try not to add filters in the code base, because those filters aren't
  66. # thread-safe. We aim to only filter in tests with
  67. # np.testing.suppress_warnings. However, in some cases it may prove
  68. # necessary to filter out warnings, because we can't (easily) fix the root
  69. # cause for them and we don't want users to see some warnings when they use
  70. # SciPy correctly. So we list exceptions here. Add new entries only if
  71. # there's a good reason.
  72. allowed_filters = (
  73. os.path.join('datasets', '_fetchers.py'),
  74. os.path.join('datasets', '__init__.py'),
  75. os.path.join('optimize', '_optimize.py'),
  76. os.path.join('sparse', '__init__.py'), # np.matrix pending-deprecation
  77. os.path.join('stats', '_discrete_distns.py'), # gh-14901
  78. os.path.join('stats', '_continuous_distns.py'),
  79. )
  80. bad_filters = [item for item in bad_filters if item.split(':')[0] not in
  81. allowed_filters]
  82. if bad_filters:
  83. raise AssertionError(
  84. "warning ignore filter should not be used, instead, use\n"
  85. "numpy.testing.suppress_warnings (in tests only);\n"
  86. "found in:\n {}".format(
  87. "\n ".join(bad_filters)))
  88. @pytest.mark.slow
  89. @pytest.mark.xfail(reason="stacklevels currently missing")
  90. def test_warning_calls_stacklevels(warning_calls):
  91. bad_filters, bad_stacklevels = warning_calls
  92. msg = ""
  93. if bad_filters:
  94. msg += ("warning ignore filter should not be used, instead, use\n"
  95. "numpy.testing.suppress_warnings (in tests only);\n"
  96. "found in:\n {}".format("\n ".join(bad_filters)))
  97. msg += "\n\n"
  98. if bad_stacklevels:
  99. msg += "warnings should have an appropriate stacklevel:\n {}".format(
  100. "\n ".join(bad_stacklevels))
  101. if msg:
  102. raise AssertionError(msg)