chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
@@ -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',
|
||||
]
|
||||
@@ -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
|
||||
@@ -0,0 +1,392 @@
|
||||
"""py.test hacks to support XFAIL/XPASS"""
|
||||
|
||||
import platform
|
||||
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
|
||||
|
||||
IS_WASM: bool = sys.platform == 'emscripten' or platform.machine() in ["wasm32", "wasm64"]
|
||||
|
||||
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.IGNORECASE).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)
|
||||
text = pathlib.Path(active_deprecations_file).read_text(encoding="utf-8")
|
||||
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 skip_under_pyodide(message):
|
||||
"""Decorator to skip a test if running under Pyodide/WASM."""
|
||||
def decorator(test_func):
|
||||
@functools.wraps(test_func)
|
||||
def test_wrapper():
|
||||
if IS_WASM:
|
||||
skip(message)
|
||||
return test_func()
|
||||
return test_wrapper
|
||||
return decorator
|
||||
@@ -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)
|
||||
@@ -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
@@ -0,0 +1,461 @@
|
||||
"""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 maintenance 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_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 = []
|
||||
source = pathlib.Path(filepath).read_text(encoding='utf-8')
|
||||
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 successfully 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 = []
|
||||
|
||||
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
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/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):
|
||||
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."""
|
||||
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)
|
||||
@@ -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"}, "*")
|
||||
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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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'
|
||||
@@ -0,0 +1,171 @@
|
||||
import pathlib
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from sympy.testing.runtests_pytest import (
|
||||
make_absolute_path,
|
||||
sympy_dir,
|
||||
update_args_with_paths,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user