511 lines
19 KiB
Python
511 lines
19 KiB
Python
# coding=utf-8
|
||
from os import walk, sep, pardir
|
||
from os.path import split, join, abspath, exists, isfile
|
||
from glob import glob
|
||
import re
|
||
import random
|
||
import ast
|
||
|
||
from sympy.testing.pytest import raises
|
||
from sympy.testing.quality_unicode import _test_this_file_encoding
|
||
|
||
# System path separator (usually slash or backslash) to be
|
||
# used with excluded files, e.g.
|
||
# exclude = set([
|
||
# "%(sep)smpmath%(sep)s" % sepd,
|
||
# ])
|
||
sepd = {"sep": sep}
|
||
|
||
# path and sympy_path
|
||
SYMPY_PATH = abspath(join(split(__file__)[0], pardir, pardir)) # go to sympy/
|
||
assert exists(SYMPY_PATH)
|
||
|
||
TOP_PATH = abspath(join(SYMPY_PATH, pardir))
|
||
BIN_PATH = join(TOP_PATH, "bin")
|
||
EXAMPLES_PATH = join(TOP_PATH, "examples")
|
||
|
||
# Error messages
|
||
message_space = "File contains trailing whitespace: %s, line %s."
|
||
message_implicit = "File contains an implicit import: %s, line %s."
|
||
message_tabs = "File contains tabs instead of spaces: %s, line %s."
|
||
message_carriage = "File contains carriage returns at end of line: %s, line %s"
|
||
message_str_raise = "File contains string exception: %s, line %s"
|
||
message_gen_raise = "File contains generic exception: %s, line %s"
|
||
message_old_raise = "File contains old-style raise statement: %s, line %s, \"%s\""
|
||
message_eof = "File does not end with a newline: %s, line %s"
|
||
message_multi_eof = "File ends with more than 1 newline: %s, line %s"
|
||
message_test_suite_def = "Function should start with 'test_' or '_': %s, line %s"
|
||
message_duplicate_test = "This is a duplicate test function: %s, line %s"
|
||
message_self_assignments = "File contains assignments to self/cls: %s, line %s."
|
||
message_func_is = "File contains '.func is': %s, line %s."
|
||
message_bare_expr = "File contains bare expression: %s, line %s."
|
||
|
||
implicit_test_re = re.compile(r'^\s*(>>> )?(\.\.\. )?from .* import .*\*')
|
||
str_raise_re = re.compile(
|
||
r'^\s*(>>> )?(\.\.\. )?raise(\s+(\'|\")|\s*(\(\s*)+(\'|\"))')
|
||
gen_raise_re = re.compile(
|
||
r'^\s*(>>> )?(\.\.\. )?raise(\s+Exception|\s*(\(\s*)+Exception)')
|
||
old_raise_re = re.compile(r'^\s*(>>> )?(\.\.\. )?raise((\s*\(\s*)|\s+)\w+\s*,')
|
||
test_suite_def_re = re.compile(r'^def\s+(?!(_|test))[^(]*\(\s*\)\s*:$')
|
||
test_ok_def_re = re.compile(r'^def\s+test_.*:$')
|
||
test_file_re = re.compile(r'.*[/\\]test_.*\.py$')
|
||
func_is_re = re.compile(r'\.\s*func\s+is')
|
||
|
||
|
||
def tab_in_leading(s):
|
||
"""Returns True if there are tabs in the leading whitespace of a line,
|
||
including the whitespace of docstring code samples."""
|
||
n = len(s) - len(s.lstrip())
|
||
if not s[n:n + 3] in ['...', '>>>']:
|
||
check = s[:n]
|
||
else:
|
||
smore = s[n + 3:]
|
||
check = s[:n] + smore[:len(smore) - len(smore.lstrip())]
|
||
return not (check.expandtabs() == check)
|
||
|
||
|
||
def find_self_assignments(s):
|
||
"""Returns a list of "bad" assignments: if there are instances
|
||
of assigning to the first argument of the class method (except
|
||
for staticmethod's).
|
||
"""
|
||
t = [n for n in ast.parse(s).body if isinstance(n, ast.ClassDef)]
|
||
|
||
bad = []
|
||
for c in t:
|
||
for n in c.body:
|
||
if not isinstance(n, ast.FunctionDef):
|
||
continue
|
||
if any(d.id == 'staticmethod'
|
||
for d in n.decorator_list if isinstance(d, ast.Name)):
|
||
continue
|
||
if n.name == '__new__':
|
||
continue
|
||
if not n.args.args:
|
||
continue
|
||
first_arg = n.args.args[0].arg
|
||
|
||
for m in ast.walk(n):
|
||
if isinstance(m, ast.Assign):
|
||
for a in m.targets:
|
||
if isinstance(a, ast.Name) and a.id == first_arg:
|
||
bad.append(m)
|
||
elif (isinstance(a, ast.Tuple) and
|
||
any(q.id == first_arg for q in a.elts
|
||
if isinstance(q, ast.Name))):
|
||
bad.append(m)
|
||
|
||
return bad
|
||
|
||
|
||
def check_directory_tree(base_path, file_check, exclusions=set(), pattern="*.py"):
|
||
"""
|
||
Checks all files in the directory tree (with base_path as starting point)
|
||
with the file_check function provided, skipping files that contain
|
||
any of the strings in the set provided by exclusions.
|
||
"""
|
||
if not base_path:
|
||
return
|
||
for root, dirs, files in walk(base_path):
|
||
check_files(glob(join(root, pattern)), file_check, exclusions)
|
||
|
||
|
||
def check_files(files, file_check, exclusions=set(), pattern=None):
|
||
"""
|
||
Checks all files with the file_check function provided, skipping files
|
||
that contain any of the strings in the set provided by exclusions.
|
||
"""
|
||
if not files:
|
||
return
|
||
for fname in files:
|
||
if not exists(fname) or not isfile(fname):
|
||
continue
|
||
if any(ex in fname for ex in exclusions):
|
||
continue
|
||
if pattern is None or re.match(pattern, fname):
|
||
file_check(fname)
|
||
|
||
|
||
class _Visit(ast.NodeVisitor):
|
||
"""return the line number corresponding to the
|
||
line on which a bare expression appears if it is a binary op
|
||
or a comparison that is not in a with block.
|
||
|
||
EXAMPLES
|
||
========
|
||
|
||
>>> import ast
|
||
>>> class _Visit(ast.NodeVisitor):
|
||
... def visit_Expr(self, node):
|
||
... if isinstance(node.value, (ast.BinOp, ast.Compare)):
|
||
... print(node.lineno)
|
||
... def visit_With(self, node):
|
||
... pass # no checking there
|
||
...
|
||
>>> code='''x = 1 # line 1
|
||
... for i in range(3):
|
||
... x == 2 # <-- 3
|
||
... if x == 2:
|
||
... x == 3 # <-- 5
|
||
... x + 1 # <-- 6
|
||
... x = 1
|
||
... if x == 1:
|
||
... print(1)
|
||
... while x != 1:
|
||
... x == 1 # <-- 11
|
||
... with raises(TypeError):
|
||
... c == 1
|
||
... raise TypeError
|
||
... assert x == 1
|
||
... '''
|
||
>>> _Visit().visit(ast.parse(code))
|
||
3
|
||
5
|
||
6
|
||
11
|
||
"""
|
||
def visit_Expr(self, node):
|
||
if isinstance(node.value, (ast.BinOp, ast.Compare)):
|
||
assert None, message_bare_expr % ('', node.lineno)
|
||
def visit_With(self, node):
|
||
pass
|
||
|
||
|
||
BareExpr = _Visit()
|
||
|
||
|
||
def line_with_bare_expr(code):
|
||
"""return None or else 0-based line number of code on which
|
||
a bare expression appeared.
|
||
"""
|
||
tree = ast.parse(code)
|
||
try:
|
||
BareExpr.visit(tree)
|
||
except AssertionError as msg:
|
||
assert msg.args
|
||
msg = msg.args[0]
|
||
assert msg.startswith(message_bare_expr.split(':', 1)[0])
|
||
return int(msg.rsplit(' ', 1)[1].rstrip('.')) # the line number
|
||
|
||
|
||
def test_files():
|
||
"""
|
||
This test tests all files in SymPy and checks that:
|
||
o no lines contains a trailing whitespace
|
||
o no lines end with \r\n
|
||
o no line uses tabs instead of spaces
|
||
o that the file ends with a single newline
|
||
o there are no general or string exceptions
|
||
o there are no old style raise statements
|
||
o name of arg-less test suite functions start with _ or test_
|
||
o no duplicate function names that start with test_
|
||
o no assignments to self variable in class methods
|
||
o no lines contain ".func is" except in the test suite
|
||
o there is no do-nothing expression like `a == b` or `x + 1`
|
||
"""
|
||
|
||
def test(fname):
|
||
with open(fname, encoding="utf8") as test_file:
|
||
test_this_file(fname, test_file)
|
||
with open(fname, encoding='utf8') as test_file:
|
||
_test_this_file_encoding(fname, test_file)
|
||
|
||
def test_this_file(fname, test_file):
|
||
idx = None
|
||
code = test_file.read()
|
||
test_file.seek(0) # restore reader to head
|
||
py = fname if sep not in fname else fname.rsplit(sep, 1)[-1]
|
||
if py.startswith('test_'):
|
||
idx = line_with_bare_expr(code)
|
||
if idx is not None:
|
||
assert False, message_bare_expr % (fname, idx + 1)
|
||
|
||
line = None # to flag the case where there were no lines in file
|
||
tests = 0
|
||
test_set = set()
|
||
for idx, line in enumerate(test_file):
|
||
if test_file_re.match(fname):
|
||
if test_suite_def_re.match(line):
|
||
assert False, message_test_suite_def % (fname, idx + 1)
|
||
if test_ok_def_re.match(line):
|
||
tests += 1
|
||
test_set.add(line[3:].split('(')[0].strip())
|
||
if len(test_set) != tests:
|
||
assert False, message_duplicate_test % (fname, idx + 1)
|
||
if line.endswith(" \n") or line.endswith("\t\n"):
|
||
assert False, message_space % (fname, idx + 1)
|
||
if line.endswith("\r\n"):
|
||
assert False, message_carriage % (fname, idx + 1)
|
||
if tab_in_leading(line):
|
||
assert False, message_tabs % (fname, idx + 1)
|
||
if str_raise_re.search(line):
|
||
assert False, message_str_raise % (fname, idx + 1)
|
||
if gen_raise_re.search(line):
|
||
assert False, message_gen_raise % (fname, idx + 1)
|
||
if (implicit_test_re.search(line) and
|
||
not list(filter(lambda ex: ex in fname, import_exclude))):
|
||
assert False, message_implicit % (fname, idx + 1)
|
||
if func_is_re.search(line) and not test_file_re.search(fname):
|
||
assert False, message_func_is % (fname, idx + 1)
|
||
|
||
result = old_raise_re.search(line)
|
||
|
||
if result is not None:
|
||
assert False, message_old_raise % (
|
||
fname, idx + 1, result.group(2))
|
||
|
||
if line is not None:
|
||
if line == '\n' and idx > 0:
|
||
assert False, message_multi_eof % (fname, idx + 1)
|
||
elif not line.endswith('\n'):
|
||
# eof newline check
|
||
assert False, message_eof % (fname, idx + 1)
|
||
|
||
|
||
# Files to test at top level
|
||
top_level_files = [join(TOP_PATH, file) for file in [
|
||
"isympy.py",
|
||
"build.py",
|
||
"setup.py",
|
||
]]
|
||
# Files to exclude from all tests
|
||
exclude = {
|
||
"%(sep)ssympy%(sep)sparsing%(sep)sautolev%(sep)s_antlr%(sep)sautolevparser.py" % sepd,
|
||
"%(sep)ssympy%(sep)sparsing%(sep)sautolev%(sep)s_antlr%(sep)sautolevlexer.py" % sepd,
|
||
"%(sep)ssympy%(sep)sparsing%(sep)sautolev%(sep)s_antlr%(sep)sautolevlistener.py" % sepd,
|
||
"%(sep)ssympy%(sep)sparsing%(sep)slatex%(sep)s_antlr%(sep)slatexparser.py" % sepd,
|
||
"%(sep)ssympy%(sep)sparsing%(sep)slatex%(sep)s_antlr%(sep)slatexlexer.py" % sepd,
|
||
}
|
||
# Files to exclude from the implicit import test
|
||
import_exclude = {
|
||
# glob imports are allowed in top-level __init__.py:
|
||
"%(sep)ssympy%(sep)s__init__.py" % sepd,
|
||
# these __init__.py should be fixed:
|
||
# XXX: not really, they use useful import pattern (DRY)
|
||
"%(sep)svector%(sep)s__init__.py" % sepd,
|
||
"%(sep)smechanics%(sep)s__init__.py" % sepd,
|
||
"%(sep)squantum%(sep)s__init__.py" % sepd,
|
||
"%(sep)spolys%(sep)s__init__.py" % sepd,
|
||
"%(sep)spolys%(sep)sdomains%(sep)s__init__.py" % sepd,
|
||
# interactive SymPy executes ``from sympy import *``:
|
||
"%(sep)sinteractive%(sep)ssession.py" % sepd,
|
||
# isympy.py executes ``from sympy import *``:
|
||
"%(sep)sisympy.py" % sepd,
|
||
# these two are import timing tests:
|
||
"%(sep)sbin%(sep)ssympy_time.py" % sepd,
|
||
"%(sep)sbin%(sep)ssympy_time_cache.py" % sepd,
|
||
# Taken from Python stdlib:
|
||
"%(sep)sparsing%(sep)ssympy_tokenize.py" % sepd,
|
||
# this one should be fixed:
|
||
"%(sep)splotting%(sep)spygletplot%(sep)s" % sepd,
|
||
# False positive in the docstring
|
||
"%(sep)sbin%(sep)stest_external_imports.py" % sepd,
|
||
"%(sep)sbin%(sep)stest_submodule_imports.py" % sepd,
|
||
# These are deprecated stubs that can be removed at some point:
|
||
"%(sep)sutilities%(sep)sruntests.py" % sepd,
|
||
"%(sep)sutilities%(sep)spytest.py" % sepd,
|
||
"%(sep)sutilities%(sep)srandtest.py" % sepd,
|
||
"%(sep)sutilities%(sep)stmpfiles.py" % sepd,
|
||
"%(sep)sutilities%(sep)squality_unicode.py" % sepd,
|
||
}
|
||
check_files(top_level_files, test)
|
||
check_directory_tree(BIN_PATH, test, {"~", ".pyc", ".sh", ".mjs"}, "*")
|
||
check_directory_tree(SYMPY_PATH, test, exclude)
|
||
check_directory_tree(EXAMPLES_PATH, test, exclude)
|
||
|
||
|
||
def _with_space(c):
|
||
# return c with a random amount of leading space
|
||
return random.randint(0, 10)*' ' + c
|
||
|
||
|
||
def test_raise_statement_regular_expression():
|
||
candidates_ok = [
|
||
"some text # raise Exception, 'text'",
|
||
"raise ValueError('text') # raise Exception, 'text'",
|
||
"raise ValueError('text')",
|
||
"raise ValueError",
|
||
"raise ValueError('text')",
|
||
"raise ValueError('text') #,",
|
||
# Talking about an exception in a docstring
|
||
''''"""This function will raise ValueError, except when it doesn't"""''',
|
||
"raise (ValueError('text')",
|
||
]
|
||
str_candidates_fail = [
|
||
"raise 'exception'",
|
||
"raise 'Exception'",
|
||
'raise "exception"',
|
||
'raise "Exception"',
|
||
"raise 'ValueError'",
|
||
]
|
||
gen_candidates_fail = [
|
||
"raise Exception('text') # raise Exception, 'text'",
|
||
"raise Exception('text')",
|
||
"raise Exception",
|
||
"raise Exception('text')",
|
||
"raise Exception('text') #,",
|
||
"raise Exception, 'text'",
|
||
"raise Exception, 'text' # raise Exception('text')",
|
||
"raise Exception, 'text' # raise Exception, 'text'",
|
||
">>> raise Exception, 'text'",
|
||
">>> raise Exception, 'text' # raise Exception('text')",
|
||
">>> raise Exception, 'text' # raise Exception, 'text'",
|
||
]
|
||
old_candidates_fail = [
|
||
"raise Exception, 'text'",
|
||
"raise Exception, 'text' # raise Exception('text')",
|
||
"raise Exception, 'text' # raise Exception, 'text'",
|
||
">>> raise Exception, 'text'",
|
||
">>> raise Exception, 'text' # raise Exception('text')",
|
||
">>> raise Exception, 'text' # raise Exception, 'text'",
|
||
"raise ValueError, 'text'",
|
||
"raise ValueError, 'text' # raise Exception('text')",
|
||
"raise ValueError, 'text' # raise Exception, 'text'",
|
||
">>> raise ValueError, 'text'",
|
||
">>> raise ValueError, 'text' # raise Exception('text')",
|
||
">>> raise ValueError, 'text' # raise Exception, 'text'",
|
||
"raise(ValueError,",
|
||
"raise (ValueError,",
|
||
"raise( ValueError,",
|
||
"raise ( ValueError,",
|
||
"raise(ValueError ,",
|
||
"raise (ValueError ,",
|
||
"raise( ValueError ,",
|
||
"raise ( ValueError ,",
|
||
]
|
||
|
||
for c in candidates_ok:
|
||
assert str_raise_re.search(_with_space(c)) is None, c
|
||
assert gen_raise_re.search(_with_space(c)) is None, c
|
||
assert old_raise_re.search(_with_space(c)) is None, c
|
||
for c in str_candidates_fail:
|
||
assert str_raise_re.search(_with_space(c)) is not None, c
|
||
for c in gen_candidates_fail:
|
||
assert gen_raise_re.search(_with_space(c)) is not None, c
|
||
for c in old_candidates_fail:
|
||
assert old_raise_re.search(_with_space(c)) is not None, c
|
||
|
||
|
||
def test_implicit_imports_regular_expression():
|
||
candidates_ok = [
|
||
"from sympy import something",
|
||
">>> from sympy import something",
|
||
"from sympy.somewhere import something",
|
||
">>> from sympy.somewhere import something",
|
||
"import sympy",
|
||
">>> import sympy",
|
||
"import sympy.something.something",
|
||
"... import sympy",
|
||
"... import sympy.something.something",
|
||
"... from sympy import something",
|
||
"... from sympy.somewhere import something",
|
||
">> from sympy import *", # To allow 'fake' docstrings
|
||
"# from sympy import *",
|
||
"some text # from sympy import *",
|
||
]
|
||
candidates_fail = [
|
||
"from sympy import *",
|
||
">>> from sympy import *",
|
||
"from sympy.somewhere import *",
|
||
">>> from sympy.somewhere import *",
|
||
"... from sympy import *",
|
||
"... from sympy.somewhere import *",
|
||
]
|
||
for c in candidates_ok:
|
||
assert implicit_test_re.search(_with_space(c)) is None, c
|
||
for c in candidates_fail:
|
||
assert implicit_test_re.search(_with_space(c)) is not None, c
|
||
|
||
|
||
def test_test_suite_defs():
|
||
candidates_ok = [
|
||
" def foo():\n",
|
||
"def foo(arg):\n",
|
||
"def _foo():\n",
|
||
"def test_foo():\n",
|
||
]
|
||
candidates_fail = [
|
||
"def foo():\n",
|
||
"def foo() :\n",
|
||
"def foo( ):\n",
|
||
"def foo():\n",
|
||
]
|
||
for c in candidates_ok:
|
||
assert test_suite_def_re.search(c) is None, c
|
||
for c in candidates_fail:
|
||
assert test_suite_def_re.search(c) is not None, c
|
||
|
||
|
||
def test_test_duplicate_defs():
|
||
candidates_ok = [
|
||
"def foo():\ndef foo():\n",
|
||
"def test():\ndef test_():\n",
|
||
"def test_():\ndef test__():\n",
|
||
]
|
||
candidates_fail = [
|
||
"def test_():\ndef test_ ():\n",
|
||
"def test_1():\ndef test_1():\n",
|
||
]
|
||
ok = (None, 'check')
|
||
def check(file):
|
||
tests = 0
|
||
test_set = set()
|
||
for idx, line in enumerate(file.splitlines()):
|
||
if test_ok_def_re.match(line):
|
||
tests += 1
|
||
test_set.add(line[3:].split('(')[0].strip())
|
||
if len(test_set) != tests:
|
||
return False, message_duplicate_test % ('check', idx + 1)
|
||
return None, 'check'
|
||
for c in candidates_ok:
|
||
assert check(c) == ok
|
||
for c in candidates_fail:
|
||
assert check(c) != ok
|
||
|
||
|
||
def test_find_self_assignments():
|
||
candidates_ok = [
|
||
"class A(object):\n def foo(self, arg): arg = self\n",
|
||
"class A(object):\n def foo(self, arg): self.prop = arg\n",
|
||
"class A(object):\n def foo(self, arg): obj, obj2 = arg, self\n",
|
||
"class A(object):\n @classmethod\n def bar(cls, arg): arg = cls\n",
|
||
"class A(object):\n def foo(var, arg): arg = var\n",
|
||
]
|
||
candidates_fail = [
|
||
"class A(object):\n def foo(self, arg): self = arg\n",
|
||
"class A(object):\n def foo(self, arg): obj, self = arg, arg\n",
|
||
"class A(object):\n def foo(self, arg):\n if arg: self = arg",
|
||
"class A(object):\n @classmethod\n def foo(cls, arg): cls = arg\n",
|
||
"class A(object):\n def foo(var, arg): var = arg\n",
|
||
]
|
||
|
||
for c in candidates_ok:
|
||
assert find_self_assignments(c) == []
|
||
for c in candidates_fail:
|
||
assert find_self_assignments(c) != []
|
||
|
||
|
||
def test_test_unicode_encoding():
|
||
unicode_whitelist = ['foo']
|
||
unicode_strict_whitelist = ['bar']
|
||
|
||
fname = 'abc'
|
||
test_file = ['α']
|
||
raises(AssertionError, lambda: _test_this_file_encoding(
|
||
fname, test_file, unicode_whitelist, unicode_strict_whitelist))
|
||
|
||
fname = 'abc'
|
||
test_file = ['abc']
|
||
_test_this_file_encoding(
|
||
fname, test_file, unicode_whitelist, unicode_strict_whitelist)
|
||
|
||
fname = 'foo'
|
||
test_file = ['abc']
|
||
raises(AssertionError, lambda: _test_this_file_encoding(
|
||
fname, test_file, unicode_whitelist, unicode_strict_whitelist))
|
||
|
||
fname = 'bar'
|
||
test_file = ['abc']
|
||
_test_this_file_encoding(
|
||
fname, test_file, unicode_whitelist, unicode_strict_whitelist)
|