AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
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',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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