Aluode/PerceptionLabPortable
0
1# Authors: The scikit-learn developers
2# SPDX-License-Identifier: BSD-3-Clause
3
4import builtins
5import faulthandler
6import platform
7import sys
8from contextlib import suppress
9from functools import wraps
10from os import environ
11from unittest import SkipTest
12
13import joblib
14import numpy as np
15import pytest
16from _pytest.doctest import DoctestItem
17from threadpoolctl import threadpool_limits
18
19from sklearn import set_config
20from sklearn._min_dependencies import PYTEST_MIN_VERSION
21from sklearn.datasets import (
22 fetch_20newsgroups,
23 fetch_20newsgroups_vectorized,
24 fetch_california_housing,
25 fetch_covtype,
26 fetch_kddcup99,
27 fetch_lfw_pairs,
28 fetch_lfw_people,
29 fetch_olivetti_faces,
30 fetch_rcv1,
31 fetch_species_distributions,
32)
33from sklearn.utils._testing import get_pytest_filterwarning_lines
34from sklearn.utils.fixes import (
35 _IS_32BIT,
36 np_base_version,
37 parse_version,
38 sp_version,
39)
40
41try:
42 from scipy_doctest.conftest import dt_config
43except ModuleNotFoundError:
44 dt_config = None
45
46if parse_version(pytest.__version__) < parse_version(PYTEST_MIN_VERSION):
47 raise ImportError(
48 f"Your version of pytest is too old. Got version {pytest.__version__}, you"
49 f" should have pytest >= {PYTEST_MIN_VERSION} installed."
50 )
51
52scipy_datasets_require_network = sp_version >= parse_version("1.10")
53
54
55def raccoon_face_or_skip():
56 # SciPy >= 1.10 requires network to access to get data
57 if scipy_datasets_require_network:
58 run_network_tests = environ.get("SKLEARN_SKIP_NETWORK_TESTS", "1") == "0"
59 if not run_network_tests:
60 raise SkipTest("test is enabled when SKLEARN_SKIP_NETWORK_TESTS=0")
61
62 try:
63 import pooch # noqa: F401
64 except ImportError:
65 raise SkipTest("test requires pooch to be installed")
66
67 from scipy.datasets import face
68 else:
69 from scipy.misc import face
70
71 return face(gray=True)
72
73
74dataset_fetchers = {
75 "fetch_20newsgroups_fxt": fetch_20newsgroups,
76 "fetch_20newsgroups_vectorized_fxt": fetch_20newsgroups_vectorized,
77 "fetch_california_housing_fxt": fetch_california_housing,
78 "fetch_covtype_fxt": fetch_covtype,
79 "fetch_kddcup99_fxt": fetch_kddcup99,
80 "fetch_lfw_pairs_fxt": fetch_lfw_pairs,
81 "fetch_lfw_people_fxt": fetch_lfw_people,
82 "fetch_olivetti_faces_fxt": fetch_olivetti_faces,
83 "fetch_rcv1_fxt": fetch_rcv1,
84 "fetch_species_distributions_fxt": fetch_species_distributions,
85}
86
87if scipy_datasets_require_network:
88 dataset_fetchers["raccoon_face_fxt"] = raccoon_face_or_skip
89
90_SKIP32_MARK = pytest.mark.skipif(
91 environ.get("SKLEARN_RUN_FLOAT32_TESTS", "0") != "1",
92 reason="Set SKLEARN_RUN_FLOAT32_TESTS=1 to run float32 dtype tests",
93)
94
95
96# Global fixtures
97@pytest.fixture(params=[pytest.param(np.float32, marks=_SKIP32_MARK), np.float64])
98def global_dtype(request):
99 yield request.param
100
101
102def _fetch_fixture(f):
103 """Fetch dataset (download if missing and requested by environment)."""
104 download_if_missing = environ.get("SKLEARN_SKIP_NETWORK_TESTS", "1") == "0"
105
106 @wraps(f)
107 def wrapped(*args, **kwargs):
108 kwargs["download_if_missing"] = download_if_missing
109 try:
110 return f(*args, **kwargs)
111 except OSError as e:
112 if str(e) != "Data not found and `download_if_missing` is False":
113 raise
114 pytest.skip("test is enabled when SKLEARN_SKIP_NETWORK_TESTS=0")
115
116 return pytest.fixture(lambda: wrapped)
117
118
119# Adds fixtures for fetching data
120fetch_20newsgroups_fxt = _fetch_fixture(fetch_20newsgroups)
121fetch_20newsgroups_vectorized_fxt = _fetch_fixture(fetch_20newsgroups_vectorized)
122fetch_california_housing_fxt = _fetch_fixture(fetch_california_housing)
123fetch_covtype_fxt = _fetch_fixture(fetch_covtype)
124fetch_kddcup99_fxt = _fetch_fixture(fetch_kddcup99)
125fetch_lfw_pairs_fxt = _fetch_fixture(fetch_lfw_pairs)
126fetch_lfw_people_fxt = _fetch_fixture(fetch_lfw_people)
127fetch_olivetti_faces_fxt = _fetch_fixture(fetch_olivetti_faces)
128fetch_rcv1_fxt = _fetch_fixture(fetch_rcv1)
129fetch_species_distributions_fxt = _fetch_fixture(fetch_species_distributions)
130raccoon_face_fxt = pytest.fixture(raccoon_face_or_skip)
131
132
133def pytest_collection_modifyitems(config, items):
134 """Called after collect is completed.
135
136 Parameters
137 ----------
138 config : pytest config
139 items : list of collected items
140 """
141 run_network_tests = environ.get("SKLEARN_SKIP_NETWORK_TESTS", "1") == "0"
142 skip_network = pytest.mark.skip(
143 reason="test is enabled when SKLEARN_SKIP_NETWORK_TESTS=0"
144 )
145
146 # download datasets during collection to avoid thread unsafe behavior
147 # when running pytest in parallel with pytest-xdist
148 dataset_features_set = set(dataset_fetchers)
149 datasets_to_download = set()
150
151 for item in items:
152 if isinstance(item, DoctestItem) and "fetch_" in item.name:
153 fetcher_function_name = item.name.split(".")[-1]
154 dataset_fetchers_key = f"{fetcher_function_name}_fxt"
155 dataset_to_fetch = set([dataset_fetchers_key]) & dataset_features_set
156 elif not hasattr(item, "fixturenames"):
157 continue
158 else:
159 item_fixtures = set(item.fixturenames)
160 dataset_to_fetch = item_fixtures & dataset_features_set
161
162 if not dataset_to_fetch:
163 continue
164
165 if run_network_tests:
166 datasets_to_download |= dataset_to_fetch
167 else:
168 # network tests are skipped
169 item.add_marker(skip_network)
170
171 # Only download datasets on the first worker spawned by pytest-xdist
172 # to avoid thread unsafe behavior. If pytest-xdist is not used, we still
173 # download before tests run.
174 worker_id = environ.get("PYTEST_XDIST_WORKER", "gw0")
175 if worker_id == "gw0" and run_network_tests:
176 for name in datasets_to_download:
177 with suppress(SkipTest):
178 dataset_fetchers[name]()
179
180 for item in items:
181 # Known failure on with GradientBoostingClassifier on ARM64
182 if (
183 item.name.endswith("GradientBoostingClassifier")
184 and platform.machine() == "aarch64"
185 ):
186 marker = pytest.mark.xfail(
187 reason=(
188 "know failure. See "
189 "https://github.com/scikit-learn/scikit-learn/issues/17797"
190 )
191 )
192 item.add_marker(marker)
193
194 skip_doctests = False
195 try:
196 import matplotlib # noqa: F401
197 except ImportError:
198 skip_doctests = True
199 reason = "matplotlib is required to run the doctests"
200
201 if _IS_32BIT:
202 reason = "doctest are only run when the default numpy int is 64 bits."
203 skip_doctests = True
204 elif sys.platform.startswith("win32"):
205 reason = (
206 "doctests are not run for Windows because numpy arrays "
207 "repr is inconsistent across platforms."
208 )
209 skip_doctests = True
210
211 if np_base_version < parse_version("2"):
212 # TODO: configure numpy to output scalar arrays as regular Python scalars
213 # once possible to improve readability of the tests docstrings.
214 # https://numpy.org/neps/nep-0051-scalar-representation.html#implementation
215 reason = "Due to NEP 51 numpy scalar repr has changed in numpy 2"
216 skip_doctests = True
217
218 if sp_version < parse_version("1.14"):
219 reason = "Scipy sparse matrix repr has changed in scipy 1.14"
220 skip_doctests = True
221
222 # Normally doctest has the entire module's scope. Here we set globs to an empty dict
223 # to remove the module's scope:
224 # https://docs.python.org/3/library/doctest.html#what-s-the-execution-context
225 for item in items:
226 if isinstance(item, DoctestItem):
227 item.dtest.globs = {}
228
229 if skip_doctests:
230 skip_marker = pytest.mark.skip(reason=reason)
231
232 for item in items:
233 if isinstance(item, DoctestItem):
234 # work-around an internal error with pytest if adding a skip
235 # mark to a doctest in a contextmanager, see
236 # https://github.com/pytest-dev/pytest/issues/8796 for more
237 # details.
238 if item.name != "sklearn._config.config_context":
239 item.add_marker(skip_marker)
240 try:
241 import PIL # noqa: F401
242
243 pillow_installed = True
244 except ImportError:
245 pillow_installed = False
246
247 if not pillow_installed:
248 skip_marker = pytest.mark.skip(reason="pillow (or PIL) not installed!")
249 for item in items:
250 if item.name in [
251 "sklearn.feature_extraction.image.PatchExtractor",
252 "sklearn.feature_extraction.image.extract_patches_2d",
253 ]:
254 item.add_marker(skip_marker)
255
256
257@pytest.fixture(scope="function")
258def pyplot():
259 """Setup and teardown fixture for matplotlib.
260
261 This fixture checks if we can import matplotlib. If not, the tests will be
262 skipped. Otherwise, we close the figures before and after running the
263 functions.
264
265 Returns
266 -------
267 pyplot : module
268 The ``matplotlib.pyplot`` module.
269 """
270 pyplot = pytest.importorskip("matplotlib.pyplot")
271 pyplot.close("all")
272 yield pyplot
273 pyplot.close("all")
274
275
276def pytest_generate_tests(metafunc):
277 """Parametrization of global_random_seed fixture
278
279 based on the SKLEARN_TESTS_GLOBAL_RANDOM_SEED environment variable.
280
281 The goal of this fixture is to prevent tests that use it to be sensitive
282 to a specific seed value while still being deterministic by default.
283
284 See the documentation for the SKLEARN_TESTS_GLOBAL_RANDOM_SEED
285 variable for instructions on how to use this fixture.
286
287 https://scikit-learn.org/dev/computing/parallelism.html#sklearn-tests-global-random-seed
288
289 """
290 # When using pytest-xdist this function is called in the xdist workers.
291 # We rely on SKLEARN_TESTS_GLOBAL_RANDOM_SEED environment variable which is
292 # set in before running pytest and is available in xdist workers since they
293 # are subprocesses.
294 RANDOM_SEED_RANGE = list(range(100)) # All seeds in [0, 99] should be valid.
295 random_seed_var = environ.get("SKLEARN_TESTS_GLOBAL_RANDOM_SEED")
296
297 default_random_seeds = [42]
298
299 if random_seed_var is None:
300 random_seeds = default_random_seeds
301 elif random_seed_var == "all":
302 random_seeds = RANDOM_SEED_RANGE
303 else:
304 if "-" in random_seed_var:
305 start, stop = random_seed_var.split("-")
306 random_seeds = list(range(int(start), int(stop) + 1))
307 else:
308 random_seeds = [int(random_seed_var)]
309
310 if min(random_seeds) < 0 or max(random_seeds) > 99:
311 raise ValueError(
312 "The value(s) of the environment variable "
313 "SKLEARN_TESTS_GLOBAL_RANDOM_SEED must be in the range [0, 99] "
314 f"(or 'all'), got: {random_seed_var}"
315 )
316
317 if "global_random_seed" in metafunc.fixturenames:
318 metafunc.parametrize("global_random_seed", random_seeds)
319
320
321def pytest_configure(config):
322 # Use matplotlib agg backend during the tests including doctests
323 try:
324 import matplotlib
325
326 matplotlib.use("agg")
327 except ImportError:
328 pass
329
330 allowed_parallelism = joblib.cpu_count(only_physical_cores=True)
331 xdist_worker_count = environ.get("PYTEST_XDIST_WORKER_COUNT")
332 if xdist_worker_count is not None:
333 # Set the number of OpenMP and BLAS threads based on the number of workers
334 # xdist is using to prevent oversubscription.
335 allowed_parallelism = max(allowed_parallelism // int(xdist_worker_count), 1)
336 threadpool_limits(allowed_parallelism)
337
338 if environ.get("SKLEARN_WARNINGS_AS_ERRORS", "0") != "0":
339 # This seems like the only way to programmatically change the config
340 # filterwarnings. This was suggested in
341 # https://github.com/pytest-dev/pytest/issues/3311#issuecomment-373177592
342 for line in get_pytest_filterwarning_lines():
343 config.addinivalue_line("filterwarnings", line)
344
345 faulthandler_timeout = int(environ.get("SKLEARN_FAULTHANDLER_TIMEOUT", "0"))
346 if faulthandler_timeout > 0:
347 faulthandler.enable()
348 faulthandler.dump_traceback_later(faulthandler_timeout, exit=True)
349
350
351@pytest.fixture
352def hide_available_pandas(monkeypatch):
353 """Pretend pandas was not installed."""
354 import_orig = builtins.__import__
355
356 def mocked_import(name, *args, **kwargs):
357 if name == "pandas":
358 raise ImportError()
359 return import_orig(name, *args, **kwargs)
360
361 monkeypatch.setattr(builtins, "__import__", mocked_import)
362
363
364@pytest.fixture
365def print_changed_only_false():
366 """Set `print_changed_only` to False for the duration of the test."""
367 set_config(print_changed_only=False)
368 yield
369 set_config(print_changed_only=True) # reset to default
370
371
372if dt_config is not None:
373 # Strict mode to differentiate between 3.14 and np.float64(3.14)
374 dt_config.strict_check = True
375 # dt_config.rtol = 0.01
376 