123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- """
- Tests which scan for certain occurrences in the code, they may not find
- all of these occurrences but should catch almost all. This file was adapted
- from NumPy.
- """
- import os
- from pathlib import Path
- import ast
- import tokenize
- import scipy
- import pytest
- class ParseCall(ast.NodeVisitor):
- def __init__(self):
- self.ls = []
- def visit_Attribute(self, node):
- ast.NodeVisitor.generic_visit(self, node)
- self.ls.append(node.attr)
- def visit_Name(self, node):
- self.ls.append(node.id)
- class FindFuncs(ast.NodeVisitor):
- def __init__(self, filename):
- super().__init__()
- self.__filename = filename
- self.bad_filters = []
- self.bad_stacklevels = []
- def visit_Call(self, node):
- p = ParseCall()
- p.visit(node.func)
- ast.NodeVisitor.generic_visit(self, node)
- if p.ls[-1] == 'simplefilter' or p.ls[-1] == 'filterwarnings':
- if node.args[0].s == "ignore":
- self.bad_filters.append(
- "{}:{}".format(self.__filename, node.lineno))
- if p.ls[-1] == 'warn' and (
- len(p.ls) == 1 or p.ls[-2] == 'warnings'):
- if self.__filename == "_lib/tests/test_warnings.py":
- # This file
- return
- # See if stacklevel exists:
- if len(node.args) == 3:
- return
- args = {kw.arg for kw in node.keywords}
- if "stacklevel" not in args:
- self.bad_stacklevels.append(
- "{}:{}".format(self.__filename, node.lineno))
- @pytest.fixture(scope="session")
- def warning_calls():
- # combined "ignore" and stacklevel error
- base = Path(scipy.__file__).parent
- bad_filters = []
- bad_stacklevels = []
- for path in base.rglob("*.py"):
- # use tokenize to auto-detect encoding on systems where no
- # default encoding is defined (e.g., LANG='C')
- with tokenize.open(str(path)) as file:
- tree = ast.parse(file.read(), filename=str(path))
- finder = FindFuncs(path.relative_to(base))
- finder.visit(tree)
- bad_filters.extend(finder.bad_filters)
- bad_stacklevels.extend(finder.bad_stacklevels)
- return bad_filters, bad_stacklevels
- @pytest.mark.slow
- def test_warning_calls_filters(warning_calls):
- bad_filters, bad_stacklevels = warning_calls
- # We try not to add filters in the code base, because those filters aren't
- # thread-safe. We aim to only filter in tests with
- # np.testing.suppress_warnings. However, in some cases it may prove
- # necessary to filter out warnings, because we can't (easily) fix the root
- # cause for them and we don't want users to see some warnings when they use
- # SciPy correctly. So we list exceptions here. Add new entries only if
- # there's a good reason.
- allowed_filters = (
- os.path.join('datasets', '_fetchers.py'),
- os.path.join('datasets', '__init__.py'),
- os.path.join('optimize', '_optimize.py'),
- os.path.join('sparse', '__init__.py'), # np.matrix pending-deprecation
- os.path.join('stats', '_discrete_distns.py'), # gh-14901
- os.path.join('stats', '_continuous_distns.py'),
- )
- bad_filters = [item for item in bad_filters if item.split(':')[0] not in
- allowed_filters]
- if bad_filters:
- raise AssertionError(
- "warning ignore filter should not be used, instead, use\n"
- "numpy.testing.suppress_warnings (in tests only);\n"
- "found in:\n {}".format(
- "\n ".join(bad_filters)))
- @pytest.mark.slow
- @pytest.mark.xfail(reason="stacklevels currently missing")
- def test_warning_calls_stacklevels(warning_calls):
- bad_filters, bad_stacklevels = warning_calls
- msg = ""
- if bad_filters:
- msg += ("warning ignore filter should not be used, instead, use\n"
- "numpy.testing.suppress_warnings (in tests only);\n"
- "found in:\n {}".format("\n ".join(bad_filters)))
- msg += "\n\n"
- if bad_stacklevels:
- msg += "warnings should have an appropriate stacklevel:\n {}".format(
- "\n ".join(bad_stacklevels))
- if msg:
- raise AssertionError(msg)
|