259 lines
8.4 KiB
Python
259 lines
8.4 KiB
Python
![]() |
"""
|
||
|
Pytest configuration and fixtures for the Numpy test suite.
|
||
|
"""
|
||
|
import os
|
||
|
import string
|
||
|
import sys
|
||
|
import tempfile
|
||
|
import warnings
|
||
|
from contextlib import contextmanager
|
||
|
|
||
|
import hypothesis
|
||
|
import pytest
|
||
|
|
||
|
import numpy
|
||
|
import numpy as np
|
||
|
from numpy._core._multiarray_tests import get_fpu_mode
|
||
|
from numpy._core.tests._natype import get_stringdtype_dtype, pd_NA
|
||
|
from numpy.testing._private.utils import NOGIL_BUILD
|
||
|
|
||
|
try:
|
||
|
from scipy_doctest.conftest import dt_config
|
||
|
HAVE_SCPDT = True
|
||
|
except ModuleNotFoundError:
|
||
|
HAVE_SCPDT = False
|
||
|
|
||
|
|
||
|
_old_fpu_mode = None
|
||
|
_collect_results = {}
|
||
|
|
||
|
# Use a known and persistent tmpdir for hypothesis' caches, which
|
||
|
# can be automatically cleared by the OS or user.
|
||
|
hypothesis.configuration.set_hypothesis_home_dir(
|
||
|
os.path.join(tempfile.gettempdir(), ".hypothesis")
|
||
|
)
|
||
|
|
||
|
# We register two custom profiles for Numpy - for details see
|
||
|
# https://hypothesis.readthedocs.io/en/latest/settings.html
|
||
|
# The first is designed for our own CI runs; the latter also
|
||
|
# forces determinism and is designed for use via np.test()
|
||
|
hypothesis.settings.register_profile(
|
||
|
name="numpy-profile", deadline=None, print_blob=True,
|
||
|
)
|
||
|
hypothesis.settings.register_profile(
|
||
|
name="np.test() profile",
|
||
|
deadline=None, print_blob=True, database=None, derandomize=True,
|
||
|
suppress_health_check=list(hypothesis.HealthCheck),
|
||
|
)
|
||
|
# Note that the default profile is chosen based on the presence
|
||
|
# of pytest.ini, but can be overridden by passing the
|
||
|
# --hypothesis-profile=NAME argument to pytest.
|
||
|
_pytest_ini = os.path.join(os.path.dirname(__file__), "..", "pytest.ini")
|
||
|
hypothesis.settings.load_profile(
|
||
|
"numpy-profile" if os.path.isfile(_pytest_ini) else "np.test() profile"
|
||
|
)
|
||
|
|
||
|
# The experimentalAPI is used in _umath_tests
|
||
|
os.environ["NUMPY_EXPERIMENTAL_DTYPE_API"] = "1"
|
||
|
|
||
|
def pytest_configure(config):
|
||
|
config.addinivalue_line("markers",
|
||
|
"valgrind_error: Tests that are known to error under valgrind.")
|
||
|
config.addinivalue_line("markers",
|
||
|
"leaks_references: Tests that are known to leak references.")
|
||
|
config.addinivalue_line("markers",
|
||
|
"slow: Tests that are very slow.")
|
||
|
config.addinivalue_line("markers",
|
||
|
"slow_pypy: Tests that are very slow on pypy.")
|
||
|
|
||
|
|
||
|
def pytest_addoption(parser):
|
||
|
parser.addoption("--available-memory", action="store", default=None,
|
||
|
help=("Set amount of memory available for running the "
|
||
|
"test suite. This can result to tests requiring "
|
||
|
"especially large amounts of memory to be skipped. "
|
||
|
"Equivalent to setting environment variable "
|
||
|
"NPY_AVAILABLE_MEM. Default: determined"
|
||
|
"automatically."))
|
||
|
|
||
|
|
||
|
gil_enabled_at_start = True
|
||
|
if NOGIL_BUILD:
|
||
|
gil_enabled_at_start = sys._is_gil_enabled()
|
||
|
|
||
|
|
||
|
def pytest_sessionstart(session):
|
||
|
available_mem = session.config.getoption('available_memory')
|
||
|
if available_mem is not None:
|
||
|
os.environ['NPY_AVAILABLE_MEM'] = available_mem
|
||
|
|
||
|
|
||
|
def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
||
|
if NOGIL_BUILD and not gil_enabled_at_start and sys._is_gil_enabled():
|
||
|
tr = terminalreporter
|
||
|
tr.ensure_newline()
|
||
|
tr.section("GIL re-enabled", sep="=", red=True, bold=True)
|
||
|
tr.line("The GIL was re-enabled at runtime during the tests.")
|
||
|
tr.line("This can happen with no test failures if the RuntimeWarning")
|
||
|
tr.line("raised by Python when this happens is filtered by a test.")
|
||
|
tr.line("")
|
||
|
tr.line("Please ensure all new C modules declare support for running")
|
||
|
tr.line("without the GIL. Any new tests that intentionally imports ")
|
||
|
tr.line("code that re-enables the GIL should do so in a subprocess.")
|
||
|
pytest.exit("GIL re-enabled during tests", returncode=1)
|
||
|
|
||
|
# FIXME when yield tests are gone.
|
||
|
@pytest.hookimpl()
|
||
|
def pytest_itemcollected(item):
|
||
|
"""
|
||
|
Check FPU precision mode was not changed during test collection.
|
||
|
|
||
|
The clumsy way we do it here is mainly necessary because numpy
|
||
|
still uses yield tests, which can execute code at test collection
|
||
|
time.
|
||
|
"""
|
||
|
global _old_fpu_mode
|
||
|
|
||
|
mode = get_fpu_mode()
|
||
|
|
||
|
if _old_fpu_mode is None:
|
||
|
_old_fpu_mode = mode
|
||
|
elif mode != _old_fpu_mode:
|
||
|
_collect_results[item] = (_old_fpu_mode, mode)
|
||
|
_old_fpu_mode = mode
|
||
|
|
||
|
|
||
|
@pytest.fixture(scope="function", autouse=True)
|
||
|
def check_fpu_mode(request):
|
||
|
"""
|
||
|
Check FPU precision mode was not changed during the test.
|
||
|
"""
|
||
|
old_mode = get_fpu_mode()
|
||
|
yield
|
||
|
new_mode = get_fpu_mode()
|
||
|
|
||
|
if old_mode != new_mode:
|
||
|
raise AssertionError(f"FPU precision mode changed from {old_mode:#x} to "
|
||
|
f"{new_mode:#x} during the test")
|
||
|
|
||
|
collect_result = _collect_results.get(request.node)
|
||
|
if collect_result is not None:
|
||
|
old_mode, new_mode = collect_result
|
||
|
raise AssertionError(f"FPU precision mode changed from {old_mode:#x} to "
|
||
|
f"{new_mode:#x} when collecting the test")
|
||
|
|
||
|
|
||
|
@pytest.fixture(autouse=True)
|
||
|
def add_np(doctest_namespace):
|
||
|
doctest_namespace['np'] = numpy
|
||
|
|
||
|
@pytest.fixture(autouse=True)
|
||
|
def env_setup(monkeypatch):
|
||
|
monkeypatch.setenv('PYTHONHASHSEED', '0')
|
||
|
|
||
|
|
||
|
if HAVE_SCPDT:
|
||
|
|
||
|
@contextmanager
|
||
|
def warnings_errors_and_rng(test=None):
|
||
|
"""Filter out the wall of DeprecationWarnings.
|
||
|
"""
|
||
|
msgs = ["The numpy.linalg.linalg",
|
||
|
"The numpy.fft.helper",
|
||
|
"dep_util",
|
||
|
"pkg_resources",
|
||
|
"numpy.core.umath",
|
||
|
"msvccompiler",
|
||
|
"Deprecated call",
|
||
|
"numpy.core",
|
||
|
"Importing from numpy.matlib",
|
||
|
"This function is deprecated.", # random_integers
|
||
|
"Data type alias 'a'", # numpy.rec.fromfile
|
||
|
"Arrays of 2-dimensional vectors", # matlib.cross
|
||
|
"`in1d` is deprecated", ]
|
||
|
msg = "|".join(msgs)
|
||
|
|
||
|
msgs_r = [
|
||
|
"invalid value encountered",
|
||
|
"divide by zero encountered"
|
||
|
]
|
||
|
msg_r = "|".join(msgs_r)
|
||
|
|
||
|
with warnings.catch_warnings():
|
||
|
warnings.filterwarnings(
|
||
|
'ignore', category=DeprecationWarning, message=msg
|
||
|
)
|
||
|
warnings.filterwarnings(
|
||
|
'ignore', category=RuntimeWarning, message=msg_r
|
||
|
)
|
||
|
yield
|
||
|
|
||
|
# find and check doctests under this context manager
|
||
|
dt_config.user_context_mgr = warnings_errors_and_rng
|
||
|
|
||
|
# numpy specific tweaks from refguide-check
|
||
|
dt_config.rndm_markers.add('#uninitialized')
|
||
|
dt_config.rndm_markers.add('# uninitialized')
|
||
|
|
||
|
# make the checker pick on mismatched dtypes
|
||
|
dt_config.strict_check = True
|
||
|
|
||
|
import doctest
|
||
|
dt_config.optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
|
||
|
|
||
|
# recognize the StringDType repr
|
||
|
dt_config.check_namespace['StringDType'] = numpy.dtypes.StringDType
|
||
|
|
||
|
# temporary skips
|
||
|
dt_config.skiplist = {
|
||
|
'numpy.savez', # unclosed file
|
||
|
'numpy.matlib.savez',
|
||
|
'numpy.__array_namespace_info__',
|
||
|
'numpy.matlib.__array_namespace_info__',
|
||
|
}
|
||
|
|
||
|
# xfail problematic tutorials
|
||
|
dt_config.pytest_extra_xfail = {
|
||
|
'how-to-verify-bug.rst': '',
|
||
|
'c-info.ufunc-tutorial.rst': '',
|
||
|
'basics.interoperability.rst': 'needs pandas',
|
||
|
'basics.dispatch.rst': 'errors out in /testing/overrides.py',
|
||
|
'basics.subclassing.rst': '.. testcode:: admonitions not understood',
|
||
|
'misc.rst': 'manipulates warnings',
|
||
|
}
|
||
|
|
||
|
# ignores are for things fail doctest collection (optionals etc)
|
||
|
dt_config.pytest_extra_ignore = [
|
||
|
'numpy/distutils',
|
||
|
'numpy/_core/cversions.py',
|
||
|
'numpy/_pyinstaller',
|
||
|
'numpy/random/_examples',
|
||
|
'numpy/f2py/_backends/_distutils.py',
|
||
|
]
|
||
|
|
||
|
|
||
|
@pytest.fixture
|
||
|
def random_string_list():
|
||
|
chars = list(string.ascii_letters + string.digits)
|
||
|
chars = np.array(chars, dtype="U1")
|
||
|
ret = np.random.choice(chars, size=100 * 10, replace=True)
|
||
|
return ret.view("U100")
|
||
|
|
||
|
|
||
|
@pytest.fixture(params=[True, False])
|
||
|
def coerce(request):
|
||
|
return request.param
|
||
|
|
||
|
|
||
|
@pytest.fixture(
|
||
|
params=["unset", None, pd_NA, np.nan, float("nan"), "__nan__"],
|
||
|
ids=["unset", "None", "pandas.NA", "np.nan", "float('nan')", "string nan"],
|
||
|
)
|
||
|
def na_object(request):
|
||
|
return request.param
|
||
|
|
||
|
|
||
|
@pytest.fixture()
|
||
|
def dtype(na_object, coerce):
|
||
|
return get_stringdtype_dtype(na_object, coerce)
|