Spaces:
Running
Running
# 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", "\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) | |