test_code_quality.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. # coding=utf-8
  2. from os import walk, sep, pardir
  3. from os.path import split, join, abspath, exists, isfile
  4. from glob import glob
  5. import re
  6. import random
  7. import ast
  8. from sympy.testing.pytest import raises
  9. from sympy.testing.quality_unicode import _test_this_file_encoding
  10. # System path separator (usually slash or backslash) to be
  11. # used with excluded files, e.g.
  12. # exclude = set([
  13. # "%(sep)smpmath%(sep)s" % sepd,
  14. # ])
  15. sepd = {"sep": sep}
  16. # path and sympy_path
  17. SYMPY_PATH = abspath(join(split(__file__)[0], pardir, pardir)) # go to sympy/
  18. assert exists(SYMPY_PATH)
  19. TOP_PATH = abspath(join(SYMPY_PATH, pardir))
  20. BIN_PATH = join(TOP_PATH, "bin")
  21. EXAMPLES_PATH = join(TOP_PATH, "examples")
  22. # Error messages
  23. message_space = "File contains trailing whitespace: %s, line %s."
  24. message_implicit = "File contains an implicit import: %s, line %s."
  25. message_tabs = "File contains tabs instead of spaces: %s, line %s."
  26. message_carriage = "File contains carriage returns at end of line: %s, line %s"
  27. message_str_raise = "File contains string exception: %s, line %s"
  28. message_gen_raise = "File contains generic exception: %s, line %s"
  29. message_old_raise = "File contains old-style raise statement: %s, line %s, \"%s\""
  30. message_eof = "File does not end with a newline: %s, line %s"
  31. message_multi_eof = "File ends with more than 1 newline: %s, line %s"
  32. message_test_suite_def = "Function should start with 'test_' or '_': %s, line %s"
  33. message_duplicate_test = "This is a duplicate test function: %s, line %s"
  34. message_self_assignments = "File contains assignments to self/cls: %s, line %s."
  35. message_func_is = "File contains '.func is': %s, line %s."
  36. message_bare_expr = "File contains bare expression: %s, line %s."
  37. implicit_test_re = re.compile(r'^\s*(>>> )?(\.\.\. )?from .* import .*\*')
  38. str_raise_re = re.compile(
  39. r'^\s*(>>> )?(\.\.\. )?raise(\s+(\'|\")|\s*(\(\s*)+(\'|\"))')
  40. gen_raise_re = re.compile(
  41. r'^\s*(>>> )?(\.\.\. )?raise(\s+Exception|\s*(\(\s*)+Exception)')
  42. old_raise_re = re.compile(r'^\s*(>>> )?(\.\.\. )?raise((\s*\(\s*)|\s+)\w+\s*,')
  43. test_suite_def_re = re.compile(r'^def\s+(?!(_|test))[^(]*\(\s*\)\s*:$')
  44. test_ok_def_re = re.compile(r'^def\s+test_.*:$')
  45. test_file_re = re.compile(r'.*[/\\]test_.*\.py$')
  46. func_is_re = re.compile(r'\.\s*func\s+is')
  47. def tab_in_leading(s):
  48. """Returns True if there are tabs in the leading whitespace of a line,
  49. including the whitespace of docstring code samples."""
  50. n = len(s) - len(s.lstrip())
  51. if not s[n:n + 3] in ['...', '>>>']:
  52. check = s[:n]
  53. else:
  54. smore = s[n + 3:]
  55. check = s[:n] + smore[:len(smore) - len(smore.lstrip())]
  56. return not (check.expandtabs() == check)
  57. def find_self_assignments(s):
  58. """Returns a list of "bad" assignments: if there are instances
  59. of assigning to the first argument of the class method (except
  60. for staticmethod's).
  61. """
  62. t = [n for n in ast.parse(s).body if isinstance(n, ast.ClassDef)]
  63. bad = []
  64. for c in t:
  65. for n in c.body:
  66. if not isinstance(n, ast.FunctionDef):
  67. continue
  68. if any(d.id == 'staticmethod'
  69. for d in n.decorator_list if isinstance(d, ast.Name)):
  70. continue
  71. if n.name == '__new__':
  72. continue
  73. if not n.args.args:
  74. continue
  75. first_arg = n.args.args[0].arg
  76. for m in ast.walk(n):
  77. if isinstance(m, ast.Assign):
  78. for a in m.targets:
  79. if isinstance(a, ast.Name) and a.id == first_arg:
  80. bad.append(m)
  81. elif (isinstance(a, ast.Tuple) and
  82. any(q.id == first_arg for q in a.elts
  83. if isinstance(q, ast.Name))):
  84. bad.append(m)
  85. return bad
  86. def check_directory_tree(base_path, file_check, exclusions=set(), pattern="*.py"):
  87. """
  88. Checks all files in the directory tree (with base_path as starting point)
  89. with the file_check function provided, skipping files that contain
  90. any of the strings in the set provided by exclusions.
  91. """
  92. if not base_path:
  93. return
  94. for root, dirs, files in walk(base_path):
  95. check_files(glob(join(root, pattern)), file_check, exclusions)
  96. def check_files(files, file_check, exclusions=set(), pattern=None):
  97. """
  98. Checks all files with the file_check function provided, skipping files
  99. that contain any of the strings in the set provided by exclusions.
  100. """
  101. if not files:
  102. return
  103. for fname in files:
  104. if not exists(fname) or not isfile(fname):
  105. continue
  106. if any(ex in fname for ex in exclusions):
  107. continue
  108. if pattern is None or re.match(pattern, fname):
  109. file_check(fname)
  110. class _Visit(ast.NodeVisitor):
  111. """return the line number corresponding to the
  112. line on which a bare expression appears if it is a binary op
  113. or a comparison that is not in a with block.
  114. EXAMPLES
  115. ========
  116. >>> import ast
  117. >>> class _Visit(ast.NodeVisitor):
  118. ... def visit_Expr(self, node):
  119. ... if isinstance(node.value, (ast.BinOp, ast.Compare)):
  120. ... print(node.lineno)
  121. ... def visit_With(self, node):
  122. ... pass # no checking there
  123. ...
  124. >>> code='''x = 1 # line 1
  125. ... for i in range(3):
  126. ... x == 2 # <-- 3
  127. ... if x == 2:
  128. ... x == 3 # <-- 5
  129. ... x + 1 # <-- 6
  130. ... x = 1
  131. ... if x == 1:
  132. ... print(1)
  133. ... while x != 1:
  134. ... x == 1 # <-- 11
  135. ... with raises(TypeError):
  136. ... c == 1
  137. ... raise TypeError
  138. ... assert x == 1
  139. ... '''
  140. >>> _Visit().visit(ast.parse(code))
  141. 3
  142. 5
  143. 6
  144. 11
  145. """
  146. def visit_Expr(self, node):
  147. if isinstance(node.value, (ast.BinOp, ast.Compare)):
  148. assert None, message_bare_expr % ('', node.lineno)
  149. def visit_With(self, node):
  150. pass
  151. BareExpr = _Visit()
  152. def line_with_bare_expr(code):
  153. """return None or else 0-based line number of code on which
  154. a bare expression appeared.
  155. """
  156. tree = ast.parse(code)
  157. try:
  158. BareExpr.visit(tree)
  159. except AssertionError as msg:
  160. assert msg.args
  161. msg = msg.args[0]
  162. assert msg.startswith(message_bare_expr.split(':', 1)[0])
  163. return int(msg.rsplit(' ', 1)[1].rstrip('.')) # the line number
  164. def test_files():
  165. """
  166. This test tests all files in SymPy and checks that:
  167. o no lines contains a trailing whitespace
  168. o no lines end with \r\n
  169. o no line uses tabs instead of spaces
  170. o that the file ends with a single newline
  171. o there are no general or string exceptions
  172. o there are no old style raise statements
  173. o name of arg-less test suite functions start with _ or test_
  174. o no duplicate function names that start with test_
  175. o no assignments to self variable in class methods
  176. o no lines contain ".func is" except in the test suite
  177. o there is no do-nothing expression like `a == b` or `x + 1`
  178. """
  179. def test(fname):
  180. with open(fname, encoding="utf8") as test_file:
  181. test_this_file(fname, test_file)
  182. with open(fname, encoding='utf8') as test_file:
  183. _test_this_file_encoding(fname, test_file)
  184. def test_this_file(fname, test_file):
  185. idx = None
  186. code = test_file.read()
  187. test_file.seek(0) # restore reader to head
  188. py = fname if sep not in fname else fname.rsplit(sep, 1)[-1]
  189. if py.startswith('test_'):
  190. idx = line_with_bare_expr(code)
  191. if idx is not None:
  192. assert False, message_bare_expr % (fname, idx + 1)
  193. line = None # to flag the case where there were no lines in file
  194. tests = 0
  195. test_set = set()
  196. for idx, line in enumerate(test_file):
  197. if test_file_re.match(fname):
  198. if test_suite_def_re.match(line):
  199. assert False, message_test_suite_def % (fname, idx + 1)
  200. if test_ok_def_re.match(line):
  201. tests += 1
  202. test_set.add(line[3:].split('(')[0].strip())
  203. if len(test_set) != tests:
  204. assert False, message_duplicate_test % (fname, idx + 1)
  205. if line.endswith(" \n") or line.endswith("\t\n"):
  206. assert False, message_space % (fname, idx + 1)
  207. if line.endswith("\r\n"):
  208. assert False, message_carriage % (fname, idx + 1)
  209. if tab_in_leading(line):
  210. assert False, message_tabs % (fname, idx + 1)
  211. if str_raise_re.search(line):
  212. assert False, message_str_raise % (fname, idx + 1)
  213. if gen_raise_re.search(line):
  214. assert False, message_gen_raise % (fname, idx + 1)
  215. if (implicit_test_re.search(line) and
  216. not list(filter(lambda ex: ex in fname, import_exclude))):
  217. assert False, message_implicit % (fname, idx + 1)
  218. if func_is_re.search(line) and not test_file_re.search(fname):
  219. assert False, message_func_is % (fname, idx + 1)
  220. result = old_raise_re.search(line)
  221. if result is not None:
  222. assert False, message_old_raise % (
  223. fname, idx + 1, result.group(2))
  224. if line is not None:
  225. if line == '\n' and idx > 0:
  226. assert False, message_multi_eof % (fname, idx + 1)
  227. elif not line.endswith('\n'):
  228. # eof newline check
  229. assert False, message_eof % (fname, idx + 1)
  230. # Files to test at top level
  231. top_level_files = [join(TOP_PATH, file) for file in [
  232. "isympy.py",
  233. "build.py",
  234. "setup.py",
  235. ]]
  236. # Files to exclude from all tests
  237. exclude = {
  238. "%(sep)ssympy%(sep)sparsing%(sep)sautolev%(sep)s_antlr%(sep)sautolevparser.py" % sepd,
  239. "%(sep)ssympy%(sep)sparsing%(sep)sautolev%(sep)s_antlr%(sep)sautolevlexer.py" % sepd,
  240. "%(sep)ssympy%(sep)sparsing%(sep)sautolev%(sep)s_antlr%(sep)sautolevlistener.py" % sepd,
  241. "%(sep)ssympy%(sep)sparsing%(sep)slatex%(sep)s_antlr%(sep)slatexparser.py" % sepd,
  242. "%(sep)ssympy%(sep)sparsing%(sep)slatex%(sep)s_antlr%(sep)slatexlexer.py" % sepd,
  243. }
  244. # Files to exclude from the implicit import test
  245. import_exclude = {
  246. # glob imports are allowed in top-level __init__.py:
  247. "%(sep)ssympy%(sep)s__init__.py" % sepd,
  248. # these __init__.py should be fixed:
  249. # XXX: not really, they use useful import pattern (DRY)
  250. "%(sep)svector%(sep)s__init__.py" % sepd,
  251. "%(sep)smechanics%(sep)s__init__.py" % sepd,
  252. "%(sep)squantum%(sep)s__init__.py" % sepd,
  253. "%(sep)spolys%(sep)s__init__.py" % sepd,
  254. "%(sep)spolys%(sep)sdomains%(sep)s__init__.py" % sepd,
  255. # interactive SymPy executes ``from sympy import *``:
  256. "%(sep)sinteractive%(sep)ssession.py" % sepd,
  257. # isympy.py executes ``from sympy import *``:
  258. "%(sep)sisympy.py" % sepd,
  259. # these two are import timing tests:
  260. "%(sep)sbin%(sep)ssympy_time.py" % sepd,
  261. "%(sep)sbin%(sep)ssympy_time_cache.py" % sepd,
  262. # Taken from Python stdlib:
  263. "%(sep)sparsing%(sep)ssympy_tokenize.py" % sepd,
  264. # this one should be fixed:
  265. "%(sep)splotting%(sep)spygletplot%(sep)s" % sepd,
  266. # False positive in the docstring
  267. "%(sep)sbin%(sep)stest_external_imports.py" % sepd,
  268. "%(sep)sbin%(sep)stest_submodule_imports.py" % sepd,
  269. # These are deprecated stubs that can be removed at some point:
  270. "%(sep)sutilities%(sep)sruntests.py" % sepd,
  271. "%(sep)sutilities%(sep)spytest.py" % sepd,
  272. "%(sep)sutilities%(sep)srandtest.py" % sepd,
  273. "%(sep)sutilities%(sep)stmpfiles.py" % sepd,
  274. "%(sep)sutilities%(sep)squality_unicode.py" % sepd,
  275. }
  276. check_files(top_level_files, test)
  277. check_directory_tree(BIN_PATH, test, {"~", ".pyc", ".sh", ".mjs"}, "*")
  278. check_directory_tree(SYMPY_PATH, test, exclude)
  279. check_directory_tree(EXAMPLES_PATH, test, exclude)
  280. def _with_space(c):
  281. # return c with a random amount of leading space
  282. return random.randint(0, 10)*' ' + c
  283. def test_raise_statement_regular_expression():
  284. candidates_ok = [
  285. "some text # raise Exception, 'text'",
  286. "raise ValueError('text') # raise Exception, 'text'",
  287. "raise ValueError('text')",
  288. "raise ValueError",
  289. "raise ValueError('text')",
  290. "raise ValueError('text') #,",
  291. # Talking about an exception in a docstring
  292. ''''"""This function will raise ValueError, except when it doesn't"""''',
  293. "raise (ValueError('text')",
  294. ]
  295. str_candidates_fail = [
  296. "raise 'exception'",
  297. "raise 'Exception'",
  298. 'raise "exception"',
  299. 'raise "Exception"',
  300. "raise 'ValueError'",
  301. ]
  302. gen_candidates_fail = [
  303. "raise Exception('text') # raise Exception, 'text'",
  304. "raise Exception('text')",
  305. "raise Exception",
  306. "raise Exception('text')",
  307. "raise Exception('text') #,",
  308. "raise Exception, 'text'",
  309. "raise Exception, 'text' # raise Exception('text')",
  310. "raise Exception, 'text' # raise Exception, 'text'",
  311. ">>> raise Exception, 'text'",
  312. ">>> raise Exception, 'text' # raise Exception('text')",
  313. ">>> raise Exception, 'text' # raise Exception, 'text'",
  314. ]
  315. old_candidates_fail = [
  316. "raise Exception, 'text'",
  317. "raise Exception, 'text' # raise Exception('text')",
  318. "raise Exception, 'text' # raise Exception, 'text'",
  319. ">>> raise Exception, 'text'",
  320. ">>> raise Exception, 'text' # raise Exception('text')",
  321. ">>> raise Exception, 'text' # raise Exception, 'text'",
  322. "raise ValueError, 'text'",
  323. "raise ValueError, 'text' # raise Exception('text')",
  324. "raise ValueError, 'text' # raise Exception, 'text'",
  325. ">>> raise ValueError, 'text'",
  326. ">>> raise ValueError, 'text' # raise Exception('text')",
  327. ">>> raise ValueError, 'text' # raise Exception, 'text'",
  328. "raise(ValueError,",
  329. "raise (ValueError,",
  330. "raise( ValueError,",
  331. "raise ( ValueError,",
  332. "raise(ValueError ,",
  333. "raise (ValueError ,",
  334. "raise( ValueError ,",
  335. "raise ( ValueError ,",
  336. ]
  337. for c in candidates_ok:
  338. assert str_raise_re.search(_with_space(c)) is None, c
  339. assert gen_raise_re.search(_with_space(c)) is None, c
  340. assert old_raise_re.search(_with_space(c)) is None, c
  341. for c in str_candidates_fail:
  342. assert str_raise_re.search(_with_space(c)) is not None, c
  343. for c in gen_candidates_fail:
  344. assert gen_raise_re.search(_with_space(c)) is not None, c
  345. for c in old_candidates_fail:
  346. assert old_raise_re.search(_with_space(c)) is not None, c
  347. def test_implicit_imports_regular_expression():
  348. candidates_ok = [
  349. "from sympy import something",
  350. ">>> from sympy import something",
  351. "from sympy.somewhere import something",
  352. ">>> from sympy.somewhere import something",
  353. "import sympy",
  354. ">>> import sympy",
  355. "import sympy.something.something",
  356. "... import sympy",
  357. "... import sympy.something.something",
  358. "... from sympy import something",
  359. "... from sympy.somewhere import something",
  360. ">> from sympy import *", # To allow 'fake' docstrings
  361. "# from sympy import *",
  362. "some text # from sympy import *",
  363. ]
  364. candidates_fail = [
  365. "from sympy import *",
  366. ">>> from sympy import *",
  367. "from sympy.somewhere import *",
  368. ">>> from sympy.somewhere import *",
  369. "... from sympy import *",
  370. "... from sympy.somewhere import *",
  371. ]
  372. for c in candidates_ok:
  373. assert implicit_test_re.search(_with_space(c)) is None, c
  374. for c in candidates_fail:
  375. assert implicit_test_re.search(_with_space(c)) is not None, c
  376. def test_test_suite_defs():
  377. candidates_ok = [
  378. " def foo():\n",
  379. "def foo(arg):\n",
  380. "def _foo():\n",
  381. "def test_foo():\n",
  382. ]
  383. candidates_fail = [
  384. "def foo():\n",
  385. "def foo() :\n",
  386. "def foo( ):\n",
  387. "def foo():\n",
  388. ]
  389. for c in candidates_ok:
  390. assert test_suite_def_re.search(c) is None, c
  391. for c in candidates_fail:
  392. assert test_suite_def_re.search(c) is not None, c
  393. def test_test_duplicate_defs():
  394. candidates_ok = [
  395. "def foo():\ndef foo():\n",
  396. "def test():\ndef test_():\n",
  397. "def test_():\ndef test__():\n",
  398. ]
  399. candidates_fail = [
  400. "def test_():\ndef test_ ():\n",
  401. "def test_1():\ndef test_1():\n",
  402. ]
  403. ok = (None, 'check')
  404. def check(file):
  405. tests = 0
  406. test_set = set()
  407. for idx, line in enumerate(file.splitlines()):
  408. if test_ok_def_re.match(line):
  409. tests += 1
  410. test_set.add(line[3:].split('(')[0].strip())
  411. if len(test_set) != tests:
  412. return False, message_duplicate_test % ('check', idx + 1)
  413. return None, 'check'
  414. for c in candidates_ok:
  415. assert check(c) == ok
  416. for c in candidates_fail:
  417. assert check(c) != ok
  418. def test_find_self_assignments():
  419. candidates_ok = [
  420. "class A(object):\n def foo(self, arg): arg = self\n",
  421. "class A(object):\n def foo(self, arg): self.prop = arg\n",
  422. "class A(object):\n def foo(self, arg): obj, obj2 = arg, self\n",
  423. "class A(object):\n @classmethod\n def bar(cls, arg): arg = cls\n",
  424. "class A(object):\n def foo(var, arg): arg = var\n",
  425. ]
  426. candidates_fail = [
  427. "class A(object):\n def foo(self, arg): self = arg\n",
  428. "class A(object):\n def foo(self, arg): obj, self = arg, arg\n",
  429. "class A(object):\n def foo(self, arg):\n if arg: self = arg",
  430. "class A(object):\n @classmethod\n def foo(cls, arg): cls = arg\n",
  431. "class A(object):\n def foo(var, arg): var = arg\n",
  432. ]
  433. for c in candidates_ok:
  434. assert find_self_assignments(c) == []
  435. for c in candidates_fail:
  436. assert find_self_assignments(c) != []
  437. def test_test_unicode_encoding():
  438. unicode_whitelist = ['foo']
  439. unicode_strict_whitelist = ['bar']
  440. fname = 'abc'
  441. test_file = ['α']
  442. raises(AssertionError, lambda: _test_this_file_encoding(
  443. fname, test_file, unicode_whitelist, unicode_strict_whitelist))
  444. fname = 'abc'
  445. test_file = ['abc']
  446. _test_this_file_encoding(
  447. fname, test_file, unicode_whitelist, unicode_strict_whitelist)
  448. fname = 'foo'
  449. test_file = ['abc']
  450. raises(AssertionError, lambda: _test_this_file_encoding(
  451. fname, test_file, unicode_whitelist, unicode_strict_whitelist))
  452. fname = 'bar'
  453. test_file = ['abc']
  454. _test_this_file_encoding(
  455. fname, test_file, unicode_whitelist, unicode_strict_whitelist)