I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,218 @@
#!/usr/bin/env python
"""
Import diagnostics. Run bin/diagnose_imports.py --help for details.
"""
from __future__ import annotations
if __name__ == "__main__":
import sys
import inspect
import builtins
import optparse
from os.path import abspath, dirname, join, normpath
this_file = abspath(__file__)
sympy_dir = join(dirname(this_file), '..', '..', '..')
sympy_dir = normpath(sympy_dir)
sys.path.insert(0, sympy_dir)
option_parser = optparse.OptionParser(
usage=
"Usage: %prog option [options]\n"
"\n"
"Import analysis for imports between SymPy modules.")
option_group = optparse.OptionGroup(
option_parser,
'Analysis options',
'Options that define what to do. Exactly one of these must be given.')
option_group.add_option(
'--problems',
help=
'Print all import problems, that is: '
'If an import pulls in a package instead of a module '
'(e.g. sympy.core instead of sympy.core.add); ' # see ##PACKAGE##
'if it imports a symbol that is already present; ' # see ##DUPLICATE##
'if it imports a symbol '
'from somewhere other than the defining module.', # see ##ORIGIN##
action='count')
option_group.add_option(
'--origins',
help=
'For each imported symbol in each module, '
'print the module that defined it. '
'(This is useful for import refactoring.)',
action='count')
option_parser.add_option_group(option_group)
option_group = optparse.OptionGroup(
option_parser,
'Sort options',
'These options define the sort order for output lines. '
'At most one of these options is allowed. '
'Unsorted output will reflect the order in which imports happened.')
option_group.add_option(
'--by-importer',
help='Sort output lines by name of importing module.',
action='count')
option_group.add_option(
'--by-origin',
help='Sort output lines by name of imported module.',
action='count')
option_parser.add_option_group(option_group)
(options, args) = option_parser.parse_args()
if args:
option_parser.error(
'Unexpected arguments %s (try %s --help)' % (args, sys.argv[0]))
if options.problems > 1:
option_parser.error('--problems must not be given more than once.')
if options.origins > 1:
option_parser.error('--origins must not be given more than once.')
if options.by_importer > 1:
option_parser.error('--by-importer must not be given more than once.')
if options.by_origin > 1:
option_parser.error('--by-origin must not be given more than once.')
options.problems = options.problems == 1
options.origins = options.origins == 1
options.by_importer = options.by_importer == 1
options.by_origin = options.by_origin == 1
if not options.problems and not options.origins:
option_parser.error(
'At least one of --problems and --origins is required')
if options.problems and options.origins:
option_parser.error(
'At most one of --problems and --origins is allowed')
if options.by_importer and options.by_origin:
option_parser.error(
'At most one of --by-importer and --by-origin is allowed')
options.by_process = not options.by_importer and not options.by_origin
builtin_import = builtins.__import__
class Definition:
"""Information about a symbol's definition."""
def __init__(self, name, value, definer):
self.name = name
self.value = value
self.definer = definer
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == other.name and self.value == other.value
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return 'Definition(%s, ..., %s)' % (
repr(self.name), repr(self.definer))
# Maps each function/variable to name of module to define it
symbol_definers: dict[Definition, str] = {}
def in_module(a, b):
"""Is a the same module as or a submodule of b?"""
return a == b or a != None and b != None and a.startswith(b + '.')
def relevant(module):
"""Is module relevant for import checking?
Only imports between relevant modules will be checked."""
return in_module(module, 'sympy')
sorted_messages = []
def msg(msg, *args):
global options, sorted_messages
if options.by_process:
print(msg % args)
else:
sorted_messages.append(msg % args)
def tracking_import(module, globals=globals(), locals=[], fromlist=None, level=-1):
"""__import__ wrapper - does not change imports at all, but tracks them.
Default order is implemented by doing output directly.
All other orders are implemented by collecting output information into
a sorted list that will be emitted after all imports are processed.
Indirect imports can only occur after the requested symbol has been
imported directly (because the indirect import would not have a module
to pick the symbol up from).
So this code detects indirect imports by checking whether the symbol in
question was already imported.
Keeps the semantics of __import__ unchanged."""
global options, symbol_definers
caller_frame = inspect.getframeinfo(sys._getframe(1))
importer_filename = caller_frame.filename
importer_module = globals['__name__']
if importer_filename == caller_frame.filename:
importer_reference = '%s line %s' % (
importer_filename, str(caller_frame.lineno))
else:
importer_reference = importer_filename
result = builtin_import(module, globals, locals, fromlist, level)
importee_module = result.__name__
# We're only interested if importer and importee are in SymPy
if relevant(importer_module) and relevant(importee_module):
for symbol in result.__dict__.iterkeys():
definition = Definition(
symbol, result.__dict__[symbol], importer_module)
if definition not in symbol_definers:
symbol_definers[definition] = importee_module
if hasattr(result, '__path__'):
##PACKAGE##
# The existence of __path__ is documented in the tutorial on modules.
# Python 3.3 documents this in http://docs.python.org/3.3/reference/import.html
if options.by_origin:
msg('Error: %s (a package) is imported by %s',
module, importer_reference)
else:
msg('Error: %s contains package import %s',
importer_reference, module)
if fromlist != None:
symbol_list = fromlist
if '*' in symbol_list:
if (importer_filename.endswith(("__init__.py", "__init__.pyc", "__init__.pyo"))):
# We do not check starred imports inside __init__
# That's the normal "please copy over its imports to my namespace"
symbol_list = []
else:
symbol_list = result.__dict__.iterkeys()
for symbol in symbol_list:
if symbol not in result.__dict__:
if options.by_origin:
msg('Error: %s.%s is not defined (yet), but %s tries to import it',
importee_module, symbol, importer_reference)
else:
msg('Error: %s tries to import %s.%s, which did not define it (yet)',
importer_reference, importee_module, symbol)
else:
definition = Definition(
symbol, result.__dict__[symbol], importer_module)
symbol_definer = symbol_definers[definition]
if symbol_definer == importee_module:
##DUPLICATE##
if options.by_origin:
msg('Error: %s.%s is imported again into %s',
importee_module, symbol, importer_reference)
else:
msg('Error: %s imports %s.%s again',
importer_reference, importee_module, symbol)
else:
##ORIGIN##
if options.by_origin:
msg('Error: %s.%s is imported by %s, which should import %s.%s instead',
importee_module, symbol, importer_reference, symbol_definer, symbol)
else:
msg('Error: %s imports %s.%s but should import %s.%s instead',
importer_reference, importee_module, symbol, symbol_definer, symbol)
return result
builtins.__import__ = tracking_import
__import__('sympy')
sorted_messages.sort()
for message in sorted_messages:
print(message)

View File

@ -0,0 +1,510 @@
# 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)

View File

@ -0,0 +1,5 @@
from sympy.testing.pytest import warns_deprecated_sympy
def test_deprecated_testing_randtest():
with warns_deprecated_sympy():
import sympy.testing.randtest # noqa:F401

View File

@ -0,0 +1,42 @@
"""
Checks that SymPy does not contain indirect imports.
An indirect import is importing a symbol from a module that itself imported the
symbol from elsewhere. Such a constellation makes it harder to diagnose
inter-module dependencies and import order problems, and is therefore strongly
discouraged.
(Indirect imports from end-user code is fine and in fact a best practice.)
Implementation note: Forcing Python into actually unloading already-imported
submodules is a tricky and partly undocumented process. To avoid these issues,
the actual diagnostic code is in bin/diagnose_imports, which is run as a
separate, pristine Python process.
"""
import subprocess
import sys
from os.path import abspath, dirname, join, normpath
import inspect
from sympy.testing.pytest import XFAIL
@XFAIL
def test_module_imports_are_direct():
my_filename = abspath(inspect.getfile(inspect.currentframe()))
my_dirname = dirname(my_filename)
diagnose_imports_filename = join(my_dirname, 'diagnose_imports.py')
diagnose_imports_filename = normpath(diagnose_imports_filename)
process = subprocess.Popen(
[
sys.executable,
normpath(diagnose_imports_filename),
'--problems',
'--by-importer'
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=-1)
output, _ = process.communicate()
assert output == '', "There are import problems:\n" + output.decode()

View File

@ -0,0 +1,211 @@
import warnings
from sympy.testing.pytest import (raises, warns, ignore_warnings,
warns_deprecated_sympy, Failed)
from sympy.utilities.exceptions import sympy_deprecation_warning
# Test callables
def test_expected_exception_is_silent_callable():
def f():
raise ValueError()
raises(ValueError, f)
# Under pytest raises will raise Failed rather than AssertionError
def test_lack_of_exception_triggers_AssertionError_callable():
try:
raises(Exception, lambda: 1 + 1)
assert False
except Failed as e:
assert "DID NOT RAISE" in str(e)
def test_unexpected_exception_is_passed_through_callable():
def f():
raise ValueError("some error message")
try:
raises(TypeError, f)
assert False
except ValueError as e:
assert str(e) == "some error message"
# Test with statement
def test_expected_exception_is_silent_with():
with raises(ValueError):
raise ValueError()
def test_lack_of_exception_triggers_AssertionError_with():
try:
with raises(Exception):
1 + 1
assert False
except Failed as e:
assert "DID NOT RAISE" in str(e)
def test_unexpected_exception_is_passed_through_with():
try:
with raises(TypeError):
raise ValueError("some error message")
assert False
except ValueError as e:
assert str(e) == "some error message"
# Now we can use raises() instead of try/catch
# to test that a specific exception class is raised
def test_second_argument_should_be_callable_or_string():
raises(TypeError, lambda: raises("irrelevant", 42))
def test_warns_catches_warning():
with warnings.catch_warnings(record=True) as w:
with warns(UserWarning):
warnings.warn('this is the warning message')
assert len(w) == 0
def test_warns_raises_without_warning():
with raises(Failed):
with warns(UserWarning):
pass
def test_warns_hides_other_warnings():
with raises(RuntimeWarning):
with warns(UserWarning):
warnings.warn('this is the warning message', UserWarning)
warnings.warn('this is the other message', RuntimeWarning)
def test_warns_continues_after_warning():
with warnings.catch_warnings(record=True) as w:
finished = False
with warns(UserWarning):
warnings.warn('this is the warning message')
finished = True
assert finished
assert len(w) == 0
def test_warns_many_warnings():
with warns(UserWarning):
warnings.warn('this is the warning message', UserWarning)
warnings.warn('this is the other warning message', UserWarning)
def test_warns_match_matching():
with warnings.catch_warnings(record=True) as w:
with warns(UserWarning, match='this is the warning message'):
warnings.warn('this is the warning message', UserWarning)
assert len(w) == 0
def test_warns_match_non_matching():
with warnings.catch_warnings(record=True) as w:
with raises(Failed):
with warns(UserWarning, match='this is the warning message'):
warnings.warn('this is not the expected warning message', UserWarning)
assert len(w) == 0
def _warn_sympy_deprecation(stacklevel=3):
sympy_deprecation_warning(
"feature",
active_deprecations_target="active-deprecations",
deprecated_since_version="0.0.0",
stacklevel=stacklevel,
)
def test_warns_deprecated_sympy_catches_warning():
with warnings.catch_warnings(record=True) as w:
with warns_deprecated_sympy():
_warn_sympy_deprecation()
assert len(w) == 0
def test_warns_deprecated_sympy_raises_without_warning():
with raises(Failed):
with warns_deprecated_sympy():
pass
def test_warns_deprecated_sympy_wrong_stacklevel():
with raises(Failed):
with warns_deprecated_sympy():
_warn_sympy_deprecation(stacklevel=1)
def test_warns_deprecated_sympy_doesnt_hide_other_warnings():
# Unlike pytest's deprecated_call, we should not hide other warnings.
with raises(RuntimeWarning):
with warns_deprecated_sympy():
_warn_sympy_deprecation()
warnings.warn('this is the other message', RuntimeWarning)
def test_warns_deprecated_sympy_continues_after_warning():
with warnings.catch_warnings(record=True) as w:
finished = False
with warns_deprecated_sympy():
_warn_sympy_deprecation()
finished = True
assert finished
assert len(w) == 0
def test_ignore_ignores_warning():
with warnings.catch_warnings(record=True) as w:
with ignore_warnings(UserWarning):
warnings.warn('this is the warning message')
assert len(w) == 0
def test_ignore_does_not_raise_without_warning():
with warnings.catch_warnings(record=True) as w:
with ignore_warnings(UserWarning):
pass
assert len(w) == 0
def test_ignore_allows_other_warnings():
with warnings.catch_warnings(record=True) as w:
# This is needed when pytest is run as -Werror
# the setting is reverted at the end of the catch_Warnings block.
warnings.simplefilter("always")
with ignore_warnings(UserWarning):
warnings.warn('this is the warning message', UserWarning)
warnings.warn('this is the other message', RuntimeWarning)
assert len(w) == 1
assert isinstance(w[0].message, RuntimeWarning)
assert str(w[0].message) == 'this is the other message'
def test_ignore_continues_after_warning():
with warnings.catch_warnings(record=True) as w:
finished = False
with ignore_warnings(UserWarning):
warnings.warn('this is the warning message')
finished = True
assert finished
assert len(w) == 0
def test_ignore_many_warnings():
with warnings.catch_warnings(record=True) as w:
# This is needed when pytest is run as -Werror
# the setting is reverted at the end of the catch_Warnings block.
warnings.simplefilter("always")
with ignore_warnings(UserWarning):
warnings.warn('this is the warning message', UserWarning)
warnings.warn('this is the other message', RuntimeWarning)
warnings.warn('this is the warning message', UserWarning)
warnings.warn('this is the other message', RuntimeWarning)
warnings.warn('this is the other message', RuntimeWarning)
assert len(w) == 3
for wi in w:
assert isinstance(wi.message, RuntimeWarning)
assert str(wi.message) == 'this is the other message'

View File

@ -0,0 +1,178 @@
import pathlib
from typing import List
import pytest
from sympy.testing.runtests_pytest import (
make_absolute_path,
sympy_dir,
update_args_with_paths,
update_args_with_rootdir,
)
def test_update_args_with_rootdir():
"""`--rootdir` and directory three above this added as arguments."""
args = update_args_with_rootdir([])
assert args == ['--rootdir', str(pathlib.Path(__file__).parents[3])]
class TestMakeAbsolutePath:
@staticmethod
@pytest.mark.parametrize(
'partial_path', ['sympy', 'sympy/core', 'sympy/nonexistant_directory'],
)
def test_valid_partial_path(partial_path: str):
"""Paths that start with `sympy` are valid."""
_ = make_absolute_path(partial_path)
@staticmethod
@pytest.mark.parametrize(
'partial_path', ['not_sympy', 'also/not/sympy'],
)
def test_invalid_partial_path_raises_value_error(partial_path: str):
"""A `ValueError` is raises on paths that don't start with `sympy`."""
with pytest.raises(ValueError):
_ = make_absolute_path(partial_path)
class TestUpdateArgsWithPaths:
@staticmethod
def test_no_paths():
"""If no paths are passed, only `sympy` and `doc/src` are appended.
`sympy` and `doc/src` are the `testpaths` stated in `pytest.ini`. They
need to be manually added as if any path-related arguments are passed
to `pytest.main` then the settings in `pytest.ini` may be ignored.
"""
paths = []
args = update_args_with_paths(paths=paths, keywords=None, args=[])
expected = [
str(pathlib.Path(sympy_dir(), 'sympy')),
str(pathlib.Path(sympy_dir(), 'doc/src')),
]
assert args == expected
@staticmethod
@pytest.mark.parametrize(
'path',
['sympy/core/tests/test_basic.py', '_basic']
)
def test_one_file(path: str):
"""Single files/paths, full or partial, are matched correctly."""
args = update_args_with_paths(paths=[path], keywords=None, args=[])
expected = [
str(pathlib.Path(sympy_dir(), 'sympy/core/tests/test_basic.py')),
]
assert args == expected
@staticmethod
def test_partial_path_from_root():
"""Partial paths from the root directly are matched correctly."""
args = update_args_with_paths(paths=['sympy/functions'], keywords=None, args=[])
expected = [str(pathlib.Path(sympy_dir(), 'sympy/functions'))]
assert args == expected
@staticmethod
def test_multiple_paths_from_root():
"""Multiple paths, partial or full, are matched correctly."""
paths = ['sympy/core/tests/test_basic.py', 'sympy/functions']
args = update_args_with_paths(paths=paths, keywords=None, args=[])
expected = [
str(pathlib.Path(sympy_dir(), 'sympy/core/tests/test_basic.py')),
str(pathlib.Path(sympy_dir(), 'sympy/functions')),
]
assert args == expected
@staticmethod
@pytest.mark.parametrize(
'paths, expected_paths',
[
(
['/core', '/util'],
[
'doc/src/modules/utilities',
'doc/src/reference/public/utilities',
'sympy/core',
'sympy/logic/utilities',
'sympy/utilities',
]
),
]
)
def test_multiple_paths_from_non_root(paths: List[str], expected_paths: List[str]):
"""Multiple partial paths are matched correctly."""
args = update_args_with_paths(paths=paths, keywords=None, args=[])
assert len(args) == len(expected_paths)
for arg, expected in zip(sorted(args), expected_paths):
assert expected in arg
@staticmethod
@pytest.mark.parametrize(
'paths',
[
[],
['sympy/physics'],
['sympy/physics/mechanics'],
['sympy/physics/mechanics/tests'],
['sympy/physics/mechanics/tests/test_kane3.py'],
]
)
def test_string_as_keyword(paths: List[str]):
"""String keywords are matched correctly."""
keywords = ('bicycle', )
args = update_args_with_paths(paths=paths, keywords=keywords, args=[])
expected_args = ['sympy/physics/mechanics/tests/test_kane3.py::test_bicycle']
assert len(args) == len(expected_args)
for arg, expected in zip(sorted(args), expected_args):
assert expected in arg
@staticmethod
@pytest.mark.parametrize(
'paths',
[
[],
['sympy/core'],
['sympy/core/tests'],
['sympy/core/tests/test_sympify.py'],
]
)
def test_integer_as_keyword(paths: List[str]):
"""Integer keywords are matched correctly."""
keywords = ('3538', )
args = update_args_with_paths(paths=paths, keywords=keywords, args=[])
expected_args = ['sympy/core/tests/test_sympify.py::test_issue_3538']
assert len(args) == len(expected_args)
for arg, expected in zip(sorted(args), expected_args):
assert expected in arg
@staticmethod
def test_multiple_keywords():
"""Multiple keywords are matched correctly."""
keywords = ('bicycle', '3538')
args = update_args_with_paths(paths=[], keywords=keywords, args=[])
expected_args = [
'sympy/core/tests/test_sympify.py::test_issue_3538',
'sympy/physics/mechanics/tests/test_kane3.py::test_bicycle',
]
assert len(args) == len(expected_args)
for arg, expected in zip(sorted(args), expected_args):
assert expected in arg
@staticmethod
def test_keyword_match_in_multiple_files():
"""Keywords are matched across multiple files."""
keywords = ('1130', )
args = update_args_with_paths(paths=[], keywords=keywords, args=[])
expected_args = [
'sympy/integrals/tests/test_heurisch.py::test_heurisch_symbolic_coeffs_1130',
'sympy/utilities/tests/test_lambdify.py::test_python_div_zero_issue_11306',
]
assert len(args) == len(expected_args)
for arg, expected in zip(sorted(args), expected_args):
assert expected in arg