test_warnings.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  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. # get first argument of the `args` node of the filter call
  32. match node.args[0]:
  33. case ast.Constant() as c:
  34. argtext = c.value
  35. case ast.JoinedStr() as js:
  36. # if we get an f-string, discard the templated pieces, which
  37. # are likely the type or specific message; we're interested
  38. # in the action, which is less likely to use a template
  39. argtext = "".join(
  40. x.value for x in js.values if isinstance(x, ast.Constant)
  41. )
  42. case _:
  43. raise ValueError("unknown ast node type")
  44. # check if filter is set to ignore outside of test code
  45. if argtext == "ignore" and "tests" not in self.__filename.parts:
  46. self.bad_filters.append(
  47. f"{self.__filename}:{node.lineno}")
  48. if p.ls[-1] == 'warn' and (
  49. len(p.ls) == 1 or p.ls[-2] == 'warnings'):
  50. if self.__filename == "_lib/tests/test_warnings.py":
  51. # This file
  52. return
  53. # See if stacklevel exists:
  54. if len(node.args) == 3:
  55. return
  56. args = {kw.arg for kw in node.keywords}
  57. if "stacklevel" not in args:
  58. self.bad_stacklevels.append(
  59. f"{self.__filename}:{node.lineno}")
  60. @pytest.fixture(scope="session")
  61. def warning_calls():
  62. # combined "ignore" and stacklevel error
  63. base = Path(scipy.__file__).parent
  64. bad_filters = []
  65. bad_stacklevels = []
  66. for path in base.rglob("*.py"):
  67. # use tokenize to auto-detect encoding on systems where no
  68. # default encoding is defined (e.g., LANG='C')
  69. with tokenize.open(str(path)) as file:
  70. tree = ast.parse(file.read(), filename=str(path))
  71. finder = FindFuncs(path.relative_to(base))
  72. finder.visit(tree)
  73. bad_filters.extend(finder.bad_filters)
  74. bad_stacklevels.extend(finder.bad_stacklevels)
  75. return bad_filters, bad_stacklevels
  76. @pytest.mark.fail_slow(40)
  77. @pytest.mark.slow
  78. def test_warning_calls_filters(warning_calls):
  79. bad_filters, bad_stacklevels = warning_calls
  80. # We try not to add filters in the code base, because those filters aren't
  81. # thread-safe. We aim to only filter in tests with
  82. # warnings.catch_warnings. However, in some cases it may prove
  83. # necessary to filter out warnings, because we can't (easily) fix the root
  84. # cause for them and we don't want users to see some warnings when they use
  85. # SciPy correctly. So we list exceptions here. Add new entries only if
  86. # there's a good reason.
  87. allowed_filters = (
  88. os.path.join('datasets', '_fetchers.py'),
  89. os.path.join('datasets', '__init__.py'),
  90. os.path.join('optimize', '_optimize.py'),
  91. os.path.join('optimize', '_constraints.py'),
  92. os.path.join('optimize', '_nnls.py'),
  93. os.path.join('signal', '_ltisys.py'),
  94. os.path.join('sparse', '__init__.py'), # np.matrix pending-deprecation
  95. os.path.join('special', '_basic.py'), # gh-21801
  96. os.path.join('stats', '_discrete_distns.py'), # gh-14901
  97. os.path.join('stats', '_continuous_distns.py'),
  98. os.path.join('stats', '_binned_statistic.py'), # gh-19345
  99. os.path.join('stats', '_stats_py.py'), # gh-20743
  100. os.path.join('stats', '_variation.py'), # gh-22827
  101. os.path.join('stats', 'tests', 'test_axis_nan_policy.py'), # gh-20694
  102. os.path.join('_lib', '_util.py'), # gh-19341
  103. os.path.join('sparse', 'linalg', '_dsolve', 'linsolve.py'), # gh-17924
  104. "conftest.py",
  105. )
  106. bad_filters = [item for item in bad_filters if item.split(':')[0] not in
  107. allowed_filters]
  108. if bad_filters:
  109. raise AssertionError(
  110. "Warning ignore filters should not be used outside of tests.\n"
  111. "Found in:\n {}".format(
  112. "\n ".join(bad_filters)))