123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- """py.test hacks to support XFAIL/XPASS"""
- import sys
- import re
- import functools
- import os
- import contextlib
- import warnings
- import inspect
- import pathlib
- from typing import Any, Callable
- from sympy.utilities.exceptions import SymPyDeprecationWarning
- # Imported here for backwards compatibility. Note: do not import this from
- # here in library code (importing sympy.pytest in library code will break the
- # pytest integration).
- from sympy.utilities.exceptions import ignore_warnings # noqa:F401
- ON_TRAVIS = os.getenv('TRAVIS_BUILD_NUMBER', None)
- try:
- import pytest
- USE_PYTEST = getattr(sys, '_running_pytest', False)
- except ImportError:
- USE_PYTEST = False
- raises: Callable[[Any, Any], Any]
- XFAIL: Callable[[Any], Any]
- skip: Callable[[Any], Any]
- SKIP: Callable[[Any], Any]
- slow: Callable[[Any], Any]
- 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
- 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 nocache_fail(func):
- "Dummy decorator for marking tests that fail when cache is disabled"
- return func
- @contextlib.contextmanager
- def warns(warningcls, *, match='', test_stacklevel=True):
- '''
- Like raises but tests that warnings are emitted.
- >>> from sympy.testing.pytest import warns
- >>> import warnings
- >>> with warns(UserWarning):
- ... warnings.warn('deprecated', UserWarning, stacklevel=2)
- >>> with warns(UserWarning):
- ... pass
- Traceback (most recent call last):
- ...
- Failed: DID NOT WARN. No warnings of type UserWarning\
- was emitted. The list of emitted warnings is: [].
- ``test_stacklevel`` makes it check that the ``stacklevel`` parameter to
- ``warn()`` is set so that the warning shows the user line of code (the
- code under the warns() context manager). Set this to False if this is
- ambiguous or if the context manager does not test the direct user code
- that emits the warning.
- If the warning is a ``SymPyDeprecationWarning``, this additionally tests
- that the ``active_deprecations_target`` is a real target in the
- ``active-deprecations.md`` file.
- '''
- # Absorbs all warnings in warnrec
- with warnings.catch_warnings(record=True) as warnrec:
- # Any warning other than the one we are looking for is an error
- warnings.simplefilter("error")
- warnings.filterwarnings("always", category=warningcls)
- # Now run the test
- yield warnrec
- # Raise if expected warning not found
- if not any(issubclass(w.category, warningcls) for w in warnrec):
- msg = ('Failed: DID NOT WARN.'
- ' No warnings of type %s was emitted.'
- ' The list of emitted warnings is: %s.'
- ) % (warningcls, [w.message for w in warnrec])
- raise Failed(msg)
- # We don't include the match in the filter above because it would then
- # fall to the error filter, so we instead manually check that it matches
- # here
- for w in warnrec:
- # Should always be true due to the filters above
- assert issubclass(w.category, warningcls)
- if not re.compile(match, re.I).match(str(w.message)):
- raise Failed(f"Failed: WRONG MESSAGE. A warning with of the correct category ({warningcls.__name__}) was issued, but it did not match the given match regex ({match!r})")
- if test_stacklevel:
- for f in inspect.stack():
- thisfile = f.filename
- file = os.path.split(thisfile)[1]
- if file.startswith('test_'):
- break
- elif file == 'doctest.py':
- # skip the stacklevel testing in the doctests of this
- # function
- return
- else:
- raise RuntimeError("Could not find the file for the given warning to test the stacklevel")
- for w in warnrec:
- if w.filename != thisfile:
- msg = f'''\
- Failed: Warning has the wrong stacklevel. The warning stacklevel needs to be
- set so that the line of code shown in the warning message is user code that
- calls the deprecated code (the current stacklevel is showing code from
- {w.filename} (line {w.lineno}), expected {thisfile})'''.replace('\n', ' ')
- raise Failed(msg)
- if warningcls == SymPyDeprecationWarning:
- this_file = pathlib.Path(__file__)
- active_deprecations_file = (this_file.parent.parent.parent / 'doc' /
- 'src' / 'explanation' /
- 'active-deprecations.md')
- if not active_deprecations_file.exists():
- # We can only test that the active_deprecations_target works if we are
- # in the git repo.
- return
- targets = []
- for w in warnrec:
- targets.append(w.message.active_deprecations_target)
- with open(active_deprecations_file) as f:
- text = f.read()
- for target in targets:
- if f'({target})=' not in text:
- raise Failed(f"The active deprecations target {target!r} does not appear to be a valid target in the active-deprecations.md file ({active_deprecations_file}).")
- def _both_exp_pow(func):
- """
- Decorator used to run the test twice: the first time `e^x` is represented
- as ``Pow(E, x)``, the second time as ``exp(x)`` (exponential object is not
- a power).
- This is a temporary trick helping to manage the elimination of the class
- ``exp`` in favor of a replacement by ``Pow(E, ...)``.
- """
- from sympy.core.parameters import _exp_is_pow
- def func_wrap():
- with _exp_is_pow(True):
- func()
- with _exp_is_pow(False):
- func()
- wrapper = functools.update_wrapper(func_wrap, func)
- return wrapper
- @contextlib.contextmanager
- def warns_deprecated_sympy():
- '''
- Shorthand for ``warns(SymPyDeprecationWarning)``
- This is the recommended way to test that ``SymPyDeprecationWarning`` is
- emitted for deprecated features in SymPy. To test for other warnings use
- ``warns``. To suppress warnings without asserting that they are emitted
- use ``ignore_warnings``.
- .. note::
- ``warns_deprecated_sympy()`` is only intended for internal use in the
- SymPy test suite to test that a deprecation warning triggers properly.
- All other code in the SymPy codebase, including documentation examples,
- should not use deprecated behavior.
- If you are a user of SymPy and you want to disable
- SymPyDeprecationWarnings, use ``warnings`` filters (see
- :ref:`silencing-sympy-deprecation-warnings`).
- >>> from sympy.testing.pytest import warns_deprecated_sympy
- >>> from sympy.utilities.exceptions import sympy_deprecation_warning
- >>> with warns_deprecated_sympy():
- ... sympy_deprecation_warning("Don't use",
- ... deprecated_since_version="1.0",
- ... active_deprecations_target="active-deprecations")
- >>> with warns_deprecated_sympy():
- ... pass
- Traceback (most recent call last):
- ...
- Failed: DID NOT WARN. No warnings of type \
- SymPyDeprecationWarning was emitted. The list of emitted warnings is: [].
- .. note::
- Sometimes the stacklevel test will fail because the same warning is
- emitted multiple times. In this case, you can use
- :func:`sympy.utilities.exceptions.ignore_warnings` in the code to
- prevent the ``SymPyDeprecationWarning`` from being emitted again
- recursively. In rare cases it is impossible to have a consistent
- ``stacklevel`` for deprecation warnings because different ways of
- calling a function will produce different call stacks.. In those cases,
- use ``warns(SymPyDeprecationWarning)`` instead.
- See Also
- ========
- sympy.utilities.exceptions.SymPyDeprecationWarning
- sympy.utilities.exceptions.sympy_deprecation_warning
- sympy.utilities.decorator.deprecated
- '''
- with warns(SymPyDeprecationWarning):
- yield
|