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,10 @@
"""This module contains code for running the tests in SymPy."""
from .runtests import doctest
from .runtests_pytest import test
__all__ = [
'test', 'doctest',
]

View File

@ -0,0 +1,8 @@
def allclose(A, B, rtol=1e-05, atol=1e-08):
if len(A) != len(B):
return False
for x, y in zip(A, B):
if abs(x-y) > atol + rtol * max(abs(x), abs(y)):
return False
return True

View File

@ -0,0 +1,401 @@
"""py.test hacks to support XFAIL/XPASS"""
import sys
import re
import functools
import os
import contextlib
import warnings
import inspect
import pathlib
from typing import Any, Callable
from sympy.utilities.exceptions import SymPyDeprecationWarning
# Imported here for backwards compatibility. Note: do not import this from
# here in library code (importing sympy.pytest in library code will break the
# pytest integration).
from sympy.utilities.exceptions import ignore_warnings # noqa:F401
ON_CI = os.getenv('CI', None) == "true"
try:
import pytest
USE_PYTEST = getattr(sys, '_running_pytest', False)
except ImportError:
USE_PYTEST = False
raises: Callable[[Any, Any], Any]
XFAIL: Callable[[Any], Any]
skip: Callable[[Any], Any]
SKIP: Callable[[Any], Any]
slow: Callable[[Any], Any]
tooslow: Callable[[Any], Any]
nocache_fail: Callable[[Any], Any]
if USE_PYTEST:
raises = pytest.raises
skip = pytest.skip
XFAIL = pytest.mark.xfail
SKIP = pytest.mark.skip
slow = pytest.mark.slow
tooslow = pytest.mark.tooslow
nocache_fail = pytest.mark.nocache_fail
from _pytest.outcomes import Failed
else:
# Not using pytest so define the things that would have been imported from
# there.
# _pytest._code.code.ExceptionInfo
class ExceptionInfo:
def __init__(self, value):
self.value = value
def __repr__(self):
return "<ExceptionInfo {!r}>".format(self.value)
def raises(expectedException, code=None):
"""
Tests that ``code`` raises the exception ``expectedException``.
``code`` may be a callable, such as a lambda expression or function
name.
If ``code`` is not given or None, ``raises`` will return a context
manager for use in ``with`` statements; the code to execute then
comes from the scope of the ``with``.
``raises()`` does nothing if the callable raises the expected exception,
otherwise it raises an AssertionError.
Examples
========
>>> from sympy.testing.pytest import raises
>>> raises(ZeroDivisionError, lambda: 1/0)
<ExceptionInfo ZeroDivisionError(...)>
>>> raises(ZeroDivisionError, lambda: 1/2)
Traceback (most recent call last):
...
Failed: DID NOT RAISE
>>> with raises(ZeroDivisionError):
... n = 1/0
>>> with raises(ZeroDivisionError):
... n = 1/2
Traceback (most recent call last):
...
Failed: DID NOT RAISE
Note that you cannot test multiple statements via
``with raises``:
>>> with raises(ZeroDivisionError):
... n = 1/0 # will execute and raise, aborting the ``with``
... n = 9999/0 # never executed
This is just what ``with`` is supposed to do: abort the
contained statement sequence at the first exception and let
the context manager deal with the exception.
To test multiple statements, you'll need a separate ``with``
for each:
>>> with raises(ZeroDivisionError):
... n = 1/0 # will execute and raise
>>> with raises(ZeroDivisionError):
... n = 9999/0 # will also execute and raise
"""
if code is None:
return RaisesContext(expectedException)
elif callable(code):
try:
code()
except expectedException as e:
return ExceptionInfo(e)
raise Failed("DID NOT RAISE")
elif isinstance(code, str):
raise TypeError(
'\'raises(xxx, "code")\' has been phased out; '
'change \'raises(xxx, "expression")\' '
'to \'raises(xxx, lambda: expression)\', '
'\'raises(xxx, "statement")\' '
'to \'with raises(xxx): statement\'')
else:
raise TypeError(
'raises() expects a callable for the 2nd argument.')
class RaisesContext:
def __init__(self, expectedException):
self.expectedException = expectedException
def __enter__(self):
return None
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
raise Failed("DID NOT RAISE")
return issubclass(exc_type, self.expectedException)
class XFail(Exception):
pass
class XPass(Exception):
pass
class Skipped(Exception):
pass
class Failed(Exception): # type: ignore
pass
def XFAIL(func):
def wrapper():
try:
func()
except Exception as e:
message = str(e)
if message != "Timeout":
raise XFail(func.__name__)
else:
raise Skipped("Timeout")
raise XPass(func.__name__)
wrapper = functools.update_wrapper(wrapper, func)
return wrapper
def skip(str):
raise Skipped(str)
def SKIP(reason):
"""Similar to ``skip()``, but this is a decorator. """
def wrapper(func):
def func_wrapper():
raise Skipped(reason)
func_wrapper = functools.update_wrapper(func_wrapper, func)
return func_wrapper
return wrapper
def slow(func):
func._slow = True
def func_wrapper():
func()
func_wrapper = functools.update_wrapper(func_wrapper, func)
func_wrapper.__wrapped__ = func
return func_wrapper
def tooslow(func):
func._slow = True
func._tooslow = True
def func_wrapper():
skip("Too slow")
func_wrapper = functools.update_wrapper(func_wrapper, func)
func_wrapper.__wrapped__ = func
return func_wrapper
def nocache_fail(func):
"Dummy decorator for marking tests that fail when cache is disabled"
return func
@contextlib.contextmanager
def warns(warningcls, *, match='', test_stacklevel=True):
'''
Like raises but tests that warnings are emitted.
>>> from sympy.testing.pytest import warns
>>> import warnings
>>> with warns(UserWarning):
... warnings.warn('deprecated', UserWarning, stacklevel=2)
>>> with warns(UserWarning):
... pass
Traceback (most recent call last):
...
Failed: DID NOT WARN. No warnings of type UserWarning\
was emitted. The list of emitted warnings is: [].
``test_stacklevel`` makes it check that the ``stacklevel`` parameter to
``warn()`` is set so that the warning shows the user line of code (the
code under the warns() context manager). Set this to False if this is
ambiguous or if the context manager does not test the direct user code
that emits the warning.
If the warning is a ``SymPyDeprecationWarning``, this additionally tests
that the ``active_deprecations_target`` is a real target in the
``active-deprecations.md`` file.
'''
# Absorbs all warnings in warnrec
with warnings.catch_warnings(record=True) as warnrec:
# Any warning other than the one we are looking for is an error
warnings.simplefilter("error")
warnings.filterwarnings("always", category=warningcls)
# Now run the test
yield warnrec
# Raise if expected warning not found
if not any(issubclass(w.category, warningcls) for w in warnrec):
msg = ('Failed: DID NOT WARN.'
' No warnings of type %s was emitted.'
' The list of emitted warnings is: %s.'
) % (warningcls, [w.message for w in warnrec])
raise Failed(msg)
# We don't include the match in the filter above because it would then
# fall to the error filter, so we instead manually check that it matches
# here
for w in warnrec:
# Should always be true due to the filters above
assert issubclass(w.category, warningcls)
if not re.compile(match, re.I).match(str(w.message)):
raise Failed(f"Failed: WRONG MESSAGE. A warning with of the correct category ({warningcls.__name__}) was issued, but it did not match the given match regex ({match!r})")
if test_stacklevel:
for f in inspect.stack():
thisfile = f.filename
file = os.path.split(thisfile)[1]
if file.startswith('test_'):
break
elif file == 'doctest.py':
# skip the stacklevel testing in the doctests of this
# function
return
else:
raise RuntimeError("Could not find the file for the given warning to test the stacklevel")
for w in warnrec:
if w.filename != thisfile:
msg = f'''\
Failed: Warning has the wrong stacklevel. The warning stacklevel needs to be
set so that the line of code shown in the warning message is user code that
calls the deprecated code (the current stacklevel is showing code from
{w.filename} (line {w.lineno}), expected {thisfile})'''.replace('\n', ' ')
raise Failed(msg)
if warningcls == SymPyDeprecationWarning:
this_file = pathlib.Path(__file__)
active_deprecations_file = (this_file.parent.parent.parent / 'doc' /
'src' / 'explanation' /
'active-deprecations.md')
if not active_deprecations_file.exists():
# We can only test that the active_deprecations_target works if we are
# in the git repo.
return
targets = []
for w in warnrec:
targets.append(w.message.active_deprecations_target)
with open(active_deprecations_file, encoding="utf-8") as f:
text = f.read()
for target in targets:
if f'({target})=' not in text:
raise Failed(f"The active deprecations target {target!r} does not appear to be a valid target in the active-deprecations.md file ({active_deprecations_file}).")
def _both_exp_pow(func):
"""
Decorator used to run the test twice: the first time `e^x` is represented
as ``Pow(E, x)``, the second time as ``exp(x)`` (exponential object is not
a power).
This is a temporary trick helping to manage the elimination of the class
``exp`` in favor of a replacement by ``Pow(E, ...)``.
"""
from sympy.core.parameters import _exp_is_pow
def func_wrap():
with _exp_is_pow(True):
func()
with _exp_is_pow(False):
func()
wrapper = functools.update_wrapper(func_wrap, func)
return wrapper
@contextlib.contextmanager
def warns_deprecated_sympy():
'''
Shorthand for ``warns(SymPyDeprecationWarning)``
This is the recommended way to test that ``SymPyDeprecationWarning`` is
emitted for deprecated features in SymPy. To test for other warnings use
``warns``. To suppress warnings without asserting that they are emitted
use ``ignore_warnings``.
.. note::
``warns_deprecated_sympy()`` is only intended for internal use in the
SymPy test suite to test that a deprecation warning triggers properly.
All other code in the SymPy codebase, including documentation examples,
should not use deprecated behavior.
If you are a user of SymPy and you want to disable
SymPyDeprecationWarnings, use ``warnings`` filters (see
:ref:`silencing-sympy-deprecation-warnings`).
>>> from sympy.testing.pytest import warns_deprecated_sympy
>>> from sympy.utilities.exceptions import sympy_deprecation_warning
>>> with warns_deprecated_sympy():
... sympy_deprecation_warning("Don't use",
... deprecated_since_version="1.0",
... active_deprecations_target="active-deprecations")
>>> with warns_deprecated_sympy():
... pass
Traceback (most recent call last):
...
Failed: DID NOT WARN. No warnings of type \
SymPyDeprecationWarning was emitted. The list of emitted warnings is: [].
.. note::
Sometimes the stacklevel test will fail because the same warning is
emitted multiple times. In this case, you can use
:func:`sympy.utilities.exceptions.ignore_warnings` in the code to
prevent the ``SymPyDeprecationWarning`` from being emitted again
recursively. In rare cases it is impossible to have a consistent
``stacklevel`` for deprecation warnings because different ways of
calling a function will produce different call stacks.. In those cases,
use ``warns(SymPyDeprecationWarning)`` instead.
See Also
========
sympy.utilities.exceptions.SymPyDeprecationWarning
sympy.utilities.exceptions.sympy_deprecation_warning
sympy.utilities.decorator.deprecated
'''
with warns(SymPyDeprecationWarning):
yield
def _running_under_pyodide():
"""Test if running under pyodide."""
try:
import pyodide_js # type: ignore # noqa
except ImportError:
return False
else:
return True
def skip_under_pyodide(message):
"""Decorator to skip a test if running under pyodide."""
def decorator(test_func):
@functools.wraps(test_func)
def test_wrapper():
if _running_under_pyodide():
skip(message)
return test_func()
return test_wrapper
return decorator

View File

@ -0,0 +1,102 @@
import re
import fnmatch
message_unicode_B = \
"File contains a unicode character : %s, line %s. " \
"But not in the whitelist. " \
"Add the file to the whitelist in " + __file__
message_unicode_D = \
"File does not contain a unicode character : %s." \
"but is in the whitelist. " \
"Remove the file from the whitelist in " + __file__
encoding_header_re = re.compile(
r'^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)')
# Whitelist pattern for files which can have unicode.
unicode_whitelist = [
# Author names can include non-ASCII characters
r'*/bin/authors_update.py',
r'*/bin/mailmap_check.py',
# These files have functions and test functions for unicode input and
# output.
r'*/sympy/testing/tests/test_code_quality.py',
r'*/sympy/physics/vector/tests/test_printing.py',
r'*/physics/quantum/tests/test_printing.py',
r'*/sympy/vector/tests/test_printing.py',
r'*/sympy/parsing/tests/test_sympy_parser.py',
r'*/sympy/printing/pretty/stringpict.py',
r'*/sympy/printing/pretty/tests/test_pretty.py',
r'*/sympy/printing/tests/test_conventions.py',
r'*/sympy/printing/tests/test_preview.py',
r'*/liealgebras/type_g.py',
r'*/liealgebras/weyl_group.py',
r'*/liealgebras/tests/test_type_G.py',
# wigner.py and polarization.py have unicode doctests. These probably
# don't need to be there but some of the examples that are there are
# pretty ugly without use_unicode (matrices need to be wrapped across
# multiple lines etc)
r'*/sympy/physics/wigner.py',
r'*/sympy/physics/optics/polarization.py',
# joint.py uses some unicode for variable names in the docstrings
r'*/sympy/physics/mechanics/joint.py',
# lll method has unicode in docstring references and author name
r'*/sympy/polys/matrices/domainmatrix.py',
r'*/sympy/matrices/repmatrix.py',
# Explanation of symbols uses greek letters
r'*/sympy/core/symbol.py',
]
unicode_strict_whitelist = [
r'*/sympy/parsing/latex/_antlr/__init__.py',
# test_mathematica.py uses some unicode for testing Greek characters are working #24055
r'*/sympy/parsing/tests/test_mathematica.py',
]
def _test_this_file_encoding(
fname, test_file,
unicode_whitelist=unicode_whitelist,
unicode_strict_whitelist=unicode_strict_whitelist):
"""Test helper function for unicode test
The test may have to operate on filewise manner, so it had moved
to a separate process.
"""
has_unicode = False
is_in_whitelist = False
is_in_strict_whitelist = False
for patt in unicode_whitelist:
if fnmatch.fnmatch(fname, patt):
is_in_whitelist = True
break
for patt in unicode_strict_whitelist:
if fnmatch.fnmatch(fname, patt):
is_in_strict_whitelist = True
is_in_whitelist = True
break
if is_in_whitelist:
for idx, line in enumerate(test_file):
try:
line.encode(encoding='ascii')
except (UnicodeEncodeError, UnicodeDecodeError):
has_unicode = True
if not has_unicode and not is_in_strict_whitelist:
assert False, message_unicode_D % fname
else:
for idx, line in enumerate(test_file):
try:
line.encode(encoding='ascii')
except (UnicodeEncodeError, UnicodeDecodeError):
assert False, message_unicode_B % (fname, idx + 1)

View File

@ -0,0 +1,19 @@
"""
.. deprecated:: 1.10
``sympy.testing.randtest`` functions have been moved to
:mod:`sympy.core.random`.
"""
from sympy.utilities.exceptions import sympy_deprecation_warning
sympy_deprecation_warning("The sympy.testing.randtest submodule is deprecated. Use sympy.core.random instead.",
deprecated_since_version="1.10",
active_deprecations_target="deprecated-sympy-testing-randtest")
from sympy.core.random import ( # noqa:F401
random_complex_number,
verify_numerically,
test_derivative_numerically,
_randrange,
_randint)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,475 @@
"""Backwards compatible functions for running tests from SymPy using pytest.
SymPy historically had its own testing framework that aimed to:
- be compatible with pytest;
- operate similarly (or identically) to pytest;
- not require any external dependencies;
- have all the functionality in one file only;
- have no magic, just import the test file and execute the test functions; and
- be portable.
To reduce the maintence burden of developing an independent testing framework
and to leverage the benefits of existing Python testing infrastructure, SymPy
now uses pytest (and various of its plugins) to run the test suite.
To maintain backwards compatibility with the legacy testing interface of SymPy,
which implemented functions that allowed users to run the tests on their
installed version of SymPy, the functions in this module are implemented to
match the existing API while thinly wrapping pytest.
These two key functions are `test` and `doctest`.
"""
import functools
import importlib.util
import os
import pathlib
import re
from fnmatch import fnmatch
from typing import List, Optional, Tuple
try:
import pytest
except ImportError:
class NoPytestError(Exception):
"""Raise when an internal test helper function is called with pytest."""
class pytest: # type: ignore
"""Shadow to support pytest features when pytest can't be imported."""
@staticmethod
def main(*args, **kwargs):
msg = 'pytest must be installed to run tests via this function'
raise NoPytestError(msg)
from sympy.testing.runtests import test as test_sympy
TESTPATHS_DEFAULT = (
pathlib.Path('sympy'),
pathlib.Path('doc', 'src'),
)
BLACKLIST_DEFAULT = (
'sympy/integrals/rubi/rubi_tests/tests',
)
class PytestPluginManager:
"""Module names for pytest plugins used by SymPy."""
PYTEST: str = 'pytest'
RANDOMLY: str = 'pytest_randomly'
SPLIT: str = 'pytest_split'
TIMEOUT: str = 'pytest_timeout'
XDIST: str = 'xdist'
@functools.cached_property
def has_pytest(self) -> bool:
return bool(importlib.util.find_spec(self.PYTEST))
@functools.cached_property
def has_randomly(self) -> bool:
return bool(importlib.util.find_spec(self.RANDOMLY))
@functools.cached_property
def has_split(self) -> bool:
return bool(importlib.util.find_spec(self.SPLIT))
@functools.cached_property
def has_timeout(self) -> bool:
return bool(importlib.util.find_spec(self.TIMEOUT))
@functools.cached_property
def has_xdist(self) -> bool:
return bool(importlib.util.find_spec(self.XDIST))
split_pattern = re.compile(r'([1-9][0-9]*)/([1-9][0-9]*)')
@functools.lru_cache
def sympy_dir() -> pathlib.Path:
"""Returns the root SymPy directory."""
return pathlib.Path(__file__).parents[2]
def update_args_with_rootdir(args: List[str]) -> List[str]:
"""Adds `--rootdir` and path to the args `list` passed to `pytest.main`.
This is required to ensure that pytest is able to find the SymPy tests in
instances where it gets confused determining the root directory, e.g. when
running with Pyodide (e.g. `bin/test_pyodide.mjs`).
"""
args.extend(['--rootdir', str(sympy_dir())])
return args
def update_args_with_paths(
paths: List[str],
keywords: Optional[Tuple[str]],
args: List[str],
) -> List[str]:
"""Appends valid paths and flags to the args `list` passed to `pytest.main`.
The are three different types of "path" that a user may pass to the `paths`
positional arguments, all of which need to be handled slightly differently:
1. Nothing is passed
The paths to the `testpaths` defined in `pytest.ini` need to be appended
to the arguments list.
2. Full, valid paths are passed
These paths need to be validated but can then be directly appended to
the arguments list.
3. Partial paths are passed.
The `testpaths` defined in `pytest.ini` need to be recursed and any
matches be appended to the arguments list.
"""
def find_paths_matching_partial(partial_paths):
partial_path_file_patterns = []
for partial_path in partial_paths:
if len(partial_path) >= 4:
has_test_prefix = partial_path[:4] == 'test'
has_py_suffix = partial_path[-3:] == '.py'
elif len(partial_path) >= 3:
has_test_prefix = False
has_py_suffix = partial_path[-3:] == '.py'
else:
has_test_prefix = False
has_py_suffix = False
if has_test_prefix and has_py_suffix:
partial_path_file_patterns.append(partial_path)
elif has_test_prefix:
partial_path_file_patterns.append(f'{partial_path}*.py')
elif has_py_suffix:
partial_path_file_patterns.append(f'test*{partial_path}')
else:
partial_path_file_patterns.append(f'test*{partial_path}*.py')
matches = []
for testpath in valid_testpaths_default:
for path, dirs, files in os.walk(testpath, topdown=True):
zipped = zip(partial_paths, partial_path_file_patterns)
for (partial_path, partial_path_file) in zipped:
if fnmatch(path, f'*{partial_path}*'):
matches.append(str(pathlib.Path(path)))
dirs[:] = []
else:
for file in files:
if fnmatch(file, partial_path_file):
matches.append(str(pathlib.Path(path, file)))
return matches
def is_tests_file(filepath: str) -> bool:
path = pathlib.Path(filepath)
if not path.is_file():
return False
if not path.parts[-1].startswith('test_'):
return False
if not path.suffix == '.py':
return False
return True
def find_tests_matching_keywords(keywords, filepath):
matches = []
with open(filepath, encoding='utf-8') as tests_file:
source = tests_file.read()
for line in source.splitlines():
if line.lstrip().startswith('def '):
for kw in keywords:
if line.lower().find(kw.lower()) != -1:
test_name = line.split(' ')[1].split('(')[0]
full_test_path = filepath + '::' + test_name
matches.append(full_test_path)
return matches
valid_testpaths_default = []
for testpath in TESTPATHS_DEFAULT:
absolute_testpath = pathlib.Path(sympy_dir(), testpath)
if absolute_testpath.exists():
valid_testpaths_default.append(str(absolute_testpath))
candidate_paths = []
if paths:
full_paths = []
partial_paths = []
for path in paths:
if pathlib.Path(path).exists():
full_paths.append(str(pathlib.Path(sympy_dir(), path)))
else:
partial_paths.append(path)
matched_paths = find_paths_matching_partial(partial_paths)
candidate_paths.extend(full_paths)
candidate_paths.extend(matched_paths)
else:
candidate_paths.extend(valid_testpaths_default)
if keywords is not None and keywords != ():
matches = []
for path in candidate_paths:
if is_tests_file(path):
test_matches = find_tests_matching_keywords(keywords, path)
matches.extend(test_matches)
else:
for root, dirnames, filenames in os.walk(path):
for filename in filenames:
absolute_filepath = str(pathlib.Path(root, filename))
if is_tests_file(absolute_filepath):
test_matches = find_tests_matching_keywords(
keywords,
absolute_filepath,
)
matches.extend(test_matches)
args.extend(matches)
else:
args.extend(candidate_paths)
return args
def make_absolute_path(partial_path: str) -> str:
"""Convert a partial path to an absolute path.
A path such a `sympy/core` might be needed. However, absolute paths should
be used in the arguments to pytest in all cases as it avoids errors that
arise from nonexistent paths.
This function assumes that partial_paths will be passed in such that they
begin with the explicit `sympy` directory, i.e. `sympy/...`.
"""
def is_valid_partial_path(partial_path: str) -> bool:
"""Assumption that partial paths are defined from the `sympy` root."""
return pathlib.Path(partial_path).parts[0] == 'sympy'
if not is_valid_partial_path(partial_path):
msg = (
f'Partial path {dir(partial_path)} is invalid, partial paths are '
f'expected to be defined with the `sympy` directory as the root.'
)
raise ValueError(msg)
absolute_path = str(pathlib.Path(sympy_dir(), partial_path))
return absolute_path
def test(*paths, subprocess=True, rerun=0, **kwargs):
"""Interface to run tests via pytest compatible with SymPy's test runner.
Explanation
===========
Note that a `pytest.ExitCode`, which is an `enum`, is returned. This is
different to the legacy SymPy test runner which would return a `bool`. If
all tests sucessfully pass the `pytest.ExitCode.OK` with value `0` is
returned, whereas the legacy SymPy test runner would return `True`. In any
other scenario, a non-zero `enum` value is returned, whereas the legacy
SymPy test runner would return `False`. Users need to, therefore, be careful
if treating the pytest exit codes as booleans because
`bool(pytest.ExitCode.OK)` evaluates to `False`, the opposite of legacy
behaviour.
Examples
========
>>> import sympy # doctest: +SKIP
Run one file:
>>> sympy.test('sympy/core/tests/test_basic.py') # doctest: +SKIP
>>> sympy.test('_basic') # doctest: +SKIP
Run all tests in sympy/functions/ and some particular file:
>>> sympy.test("sympy/core/tests/test_basic.py",
... "sympy/functions") # doctest: +SKIP
Run all tests in sympy/core and sympy/utilities:
>>> sympy.test("/core", "/util") # doctest: +SKIP
Run specific test from a file:
>>> sympy.test("sympy/core/tests/test_basic.py",
... kw="test_equality") # doctest: +SKIP
Run specific test from any file:
>>> sympy.test(kw="subs") # doctest: +SKIP
Run the tests using the legacy SymPy runner:
>>> sympy.test(use_sympy_runner=True) # doctest: +SKIP
Note that this option is slated for deprecation in the near future and is
only currently provided to ensure users have an alternative option while the
pytest-based runner receives real-world testing.
Parameters
==========
paths : first n positional arguments of strings
Paths, both partial and absolute, describing which subset(s) of the test
suite are to be run.
subprocess : bool, default is True
Legacy option, is currently ignored.
rerun : int, default is 0
Legacy option, is ignored.
use_sympy_runner : bool or None, default is None
Temporary option to invoke the legacy SymPy test runner instead of
`pytest.main`. Will be removed in the near future.
verbose : bool, default is False
Sets the verbosity of the pytest output. Using `True` will add the
`--verbose` option to the pytest call.
tb : str, 'auto', 'long', 'short', 'line', 'native', or 'no'
Sets the traceback print mode of pytest using the `--tb` option.
kw : str
Only run tests which match the given substring expression. An expression
is a Python evaluatable expression where all names are substring-matched
against test names and their parent classes. Example: -k 'test_method or
test_other' matches all test functions and classes whose name contains
'test_method' or 'test_other', while -k 'not test_method' matches those
that don't contain 'test_method' in their names. -k 'not test_method and
not test_other' will eliminate the matches. Additionally keywords are
matched to classes and functions containing extra names in their
'extra_keyword_matches' set, as well as functions which have names
assigned directly to them. The matching is case-insensitive.
pdb : bool, default is False
Start the interactive Python debugger on errors or `KeyboardInterrupt`.
colors : bool, default is True
Color terminal output.
force_colors : bool, default is False
Legacy option, is ignored.
sort : bool, default is True
Run the tests in sorted order. pytest uses a sorted test order by
default. Requires pytest-randomly.
seed : int
Seed to use for random number generation. Requires pytest-randomly.
timeout : int, default is 0
Timeout in seconds before dumping the stacks. 0 means no timeout.
Requires pytest-timeout.
fail_on_timeout : bool, default is False
Legacy option, is currently ignored.
slow : bool, default is False
Run the subset of tests marked as `slow`.
enhance_asserts : bool, default is False
Legacy option, is currently ignored.
split : string in form `<SPLIT>/<GROUPS>` or None, default is None
Used to split the tests up. As an example, if `split='2/3' is used then
only the middle third of tests are run. Requires pytest-split.
time_balance : bool, default is True
Legacy option, is currently ignored.
blacklist : iterable of test paths as strings, default is BLACKLIST_DEFAULT
Blacklisted test paths are ignored using the `--ignore` option. Paths
may be partial or absolute. If partial then they are matched against
all paths in the pytest tests path.
parallel : bool, default is False
Parallelize the test running using pytest-xdist. If `True` then pytest
will automatically detect the number of CPU cores available and use them
all. Requires pytest-xdist.
store_durations : bool, False
Store test durations into the file `.test_durations`. The is used by
`pytest-split` to help determine more even splits when more than one
test group is being used. Requires pytest-split.
"""
# NOTE: to be removed alongside SymPy test runner
if kwargs.get('use_sympy_runner', False):
kwargs.pop('parallel', False)
kwargs.pop('store_durations', False)
kwargs.pop('use_sympy_runner', True)
if kwargs.get('slow') is None:
kwargs['slow'] = False
return test_sympy(*paths, subprocess=True, rerun=0, **kwargs)
pytest_plugin_manager = PytestPluginManager()
if not pytest_plugin_manager.has_pytest:
pytest.main()
args = []
args = update_args_with_rootdir(args)
if kwargs.get('verbose', False):
args.append('--verbose')
if tb := kwargs.get('tb'):
args.extend(['--tb', tb])
if kwargs.get('pdb'):
args.append('--pdb')
if not kwargs.get('colors', True):
args.extend(['--color', 'no'])
if seed := kwargs.get('seed'):
if not pytest_plugin_manager.has_randomly:
msg = '`pytest-randomly` plugin required to control random seed.'
raise ModuleNotFoundError(msg)
args.extend(['--randomly-seed', str(seed)])
if kwargs.get('sort', True) and pytest_plugin_manager.has_randomly:
args.append('--randomly-dont-reorganize')
elif not kwargs.get('sort', True) and not pytest_plugin_manager.has_randomly:
msg = '`pytest-randomly` plugin required to randomize test order.'
raise ModuleNotFoundError(msg)
if timeout := kwargs.get('timeout', None):
if not pytest_plugin_manager.has_timeout:
msg = '`pytest-timeout` plugin required to apply timeout to tests.'
raise ModuleNotFoundError(msg)
args.extend(['--timeout', str(int(timeout))])
# Skip slow tests by default and always skip tooslow tests
if kwargs.get('slow', False):
args.extend(['-m', 'slow and not tooslow'])
else:
args.extend(['-m', 'not slow and not tooslow'])
if (split := kwargs.get('split')) is not None:
if not pytest_plugin_manager.has_split:
msg = '`pytest-split` plugin required to run tests as groups.'
raise ModuleNotFoundError(msg)
match = split_pattern.match(split)
if not match:
msg = ('split must be a string of the form a/b where a and b are '
'positive nonzero ints')
raise ValueError(msg)
group, splits = map(str, match.groups())
args.extend(['--group', group, '--splits', splits])
if group > splits:
msg = (f'cannot have a group number {group} with only {splits} '
'splits')
raise ValueError(msg)
if blacklist := kwargs.get('blacklist', BLACKLIST_DEFAULT):
for path in blacklist:
args.extend(['--ignore', make_absolute_path(path)])
if kwargs.get('parallel', False):
if not pytest_plugin_manager.has_xdist:
msg = '`pytest-xdist` plugin required to run tests in parallel.'
raise ModuleNotFoundError(msg)
args.extend(['-n', 'auto'])
if kwargs.get('store_durations', False):
if not pytest_plugin_manager.has_split:
msg = '`pytest-split` plugin required to store test durations.'
raise ModuleNotFoundError(msg)
args.append('--store-durations')
if (keywords := kwargs.get('kw')) is not None:
keywords = tuple(str(kw) for kw in keywords)
else:
keywords = ()
args = update_args_with_paths(paths, keywords, args)
exit_code = pytest.main(args)
return exit_code
def doctest():
"""Interface to run doctests via pytest compatible with SymPy's test runner.
"""
raise NotImplementedError

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

View File

@ -0,0 +1,46 @@
"""
This module adds context manager for temporary files generated by the tests.
"""
import shutil
import os
class TmpFileManager:
"""
A class to track record of every temporary files created by the tests.
"""
tmp_files = set('')
tmp_folders = set('')
@classmethod
def tmp_file(cls, name=''):
cls.tmp_files.add(name)
return name
@classmethod
def tmp_folder(cls, name=''):
cls.tmp_folders.add(name)
return name
@classmethod
def cleanup(cls):
while cls.tmp_files:
file = cls.tmp_files.pop()
if os.path.isfile(file):
os.remove(file)
while cls.tmp_folders:
folder = cls.tmp_folders.pop()
shutil.rmtree(folder)
def cleanup_tmp_files(test_func):
"""
A decorator to help test codes remove temporary files after the tests.
"""
def wrapper_function():
try:
test_func()
finally:
TmpFileManager.cleanup()
return wrapper_function