codekingpro/portable-devtools
114k
1"""
2Tests related to deprecation warnings. Also a convenient place
3to document how deprecations should eventually be turned into errors.
4
5"""
6import contextlib
7import warnings
8
9import pytest
10
11import numpy as np
12import numpy._core._struct_ufunc_tests as struct_ufunc
13from numpy._core._multiarray_tests import fromstring_null_term_c_api # noqa: F401
14from numpy.testing import assert_raises
15
16
17class _DeprecationTestCase:
18 # Just as warning: warnings uses re.match, so the start of this message
19 # must match.
20 message = ''
21 warning_cls = DeprecationWarning
22
23 @contextlib.contextmanager
24 def filter_warnings(self):
25 with warnings.catch_warnings(record=True) as w:
26 # Do *not* ignore other DeprecationWarnings. Ignoring warnings
27 # can give very confusing results because of
28 # https://bugs.python.org/issue4180 and it is probably simplest to
29 # try to keep the tests cleanly giving only the right warning type.
30 # (While checking them set to "error" those are ignored anyway)
31 # We still have them show up, because otherwise they would be raised
32 warnings.filterwarnings("always", category=self.warning_cls)
33 warnings.filterwarnings("always", message=self.message,
34 category=self.warning_cls)
35 yield w
36 return
37
38 def assert_deprecated(self, function, num=1, ignore_others=False,
39 function_fails=False,
40 exceptions=np._NoValue,
41 args=(), kwargs={}):
42 """Test if DeprecationWarnings are given and raised.
43
44 This first checks if the function when called gives `num`
45 DeprecationWarnings, after that it tries to raise these
46 DeprecationWarnings and compares them with `exceptions`.
47 The exceptions can be different for cases where this code path
48 is simply not anticipated and the exception is replaced.
49
50 Parameters
51 ----------
52 function : callable
53 The function to test
54 num : int
55 Number of DeprecationWarnings to expect. This should normally be 1.
56 ignore_others : bool
57 Whether warnings of the wrong type should be ignored (note that
58 the message is not checked)
59 function_fails : bool
60 If the function would normally fail, setting this will check for
61 warnings inside a try/except block.
62 exceptions : Exception or tuple of Exceptions
63 Exception to expect when turning the warnings into an error.
64 The default checks for DeprecationWarnings. If exceptions is
65 empty the function is expected to run successfully.
66 args : tuple
67 Arguments for `function`
68 kwargs : dict
69 Keyword arguments for `function`
70 """
71 __tracebackhide__ = True # Hide traceback for py.test
72
73 if exceptions is np._NoValue:
74 exceptions = (self.warning_cls,)
75
76 if function_fails:
77 context_manager = contextlib.suppress(Exception)
78 else:
79 context_manager = contextlib.nullcontext()
80 with context_manager:
81 with self.filter_warnings() as w_context:
82 function(*args, **kwargs)
83
84 # just in case, clear the registry
85 num_found = 0
86 for warning in w_context:
87 if warning.category is self.warning_cls:
88 num_found += 1
89 elif not ignore_others:
90 raise AssertionError(
91 "expected %s but got: %s" %
92 (self.warning_cls.__name__, warning.category))
93 if num is not None and num_found != num:
94 msg = f"{len(w_context)} warnings found but {num} expected."
95 lst = [str(w) for w in w_context]
96 raise AssertionError("\n".join([msg] + lst))
97
98 with warnings.catch_warnings():
99 warnings.filterwarnings("error", message=self.message,
100 category=self.warning_cls)
101 try:
102 function(*args, **kwargs)
103 if exceptions != ():
104 raise AssertionError(
105 "No error raised during function call")
106 except exceptions:
107 if exceptions == ():
108 raise AssertionError(
109 "Error raised during function call")
110
111 def assert_not_deprecated(self, function, args=(), kwargs={}):
112 """Test that warnings are not raised.
113
114 This is just a shorthand for:
115
116 self.assert_deprecated(function, num=0, ignore_others=True,
117 exceptions=tuple(), args=args, kwargs=kwargs)
118 """
119 self.assert_deprecated(function, num=0, ignore_others=True,
120 exceptions=(), args=args, kwargs=kwargs)
121
122
123class _VisibleDeprecationTestCase(_DeprecationTestCase):
124 warning_cls = np.exceptions.VisibleDeprecationWarning
125
126
127class TestTestDeprecated:
128 def test_assert_deprecated(self):
129 test_case_instance = _DeprecationTestCase()
130 assert_raises(AssertionError,
131 test_case_instance.assert_deprecated,
132 lambda: None)
133
134 def foo():
135 warnings.warn("foo", category=DeprecationWarning, stacklevel=2)
136
137 test_case_instance.assert_deprecated(foo)
138
139
140class TestBincount(_DeprecationTestCase):
141 # 2024-07-29, 2.1.0
142 @pytest.mark.parametrize('badlist', [[0.5, 1.2, 1.5],
143 ['0', '1', '1']])
144 def test_bincount_bad_list(self, badlist):
145 self.assert_deprecated(lambda: np.bincount(badlist))
146
147
148class BuiltInRoundComplexDType(_DeprecationTestCase):
149 # 2020-03-31 1.19.0
150 deprecated_types = [np.csingle, np.cdouble, np.clongdouble]
151 not_deprecated_types = [
152 np.int8, np.int16, np.int32, np.int64,
153 np.uint8, np.uint16, np.uint32, np.uint64,
154 np.float16, np.float32, np.float64,
155 ]
156
157 def test_deprecated(self):
158 for scalar_type in self.deprecated_types:
159 scalar = scalar_type(0)
160 self.assert_deprecated(round, args=(scalar,))
161 self.assert_deprecated(round, args=(scalar, 0))
162 self.assert_deprecated(round, args=(scalar,), kwargs={'ndigits': 0})
163
164 def test_not_deprecated(self):
165 for scalar_type in self.not_deprecated_types:
166 scalar = scalar_type(0)
167 self.assert_not_deprecated(round, args=(scalar,))
168 self.assert_not_deprecated(round, args=(scalar, 0))
169 self.assert_not_deprecated(round, args=(scalar,), kwargs={'ndigits': 0})
170
171
172class FlatteningConcatenateUnsafeCast(_DeprecationTestCase):
173 # NumPy 1.20, 2020-09-03
174 message = "concatenate with `axis=None` will use same-kind casting"
175
176 def test_deprecated(self):
177 self.assert_deprecated(np.concatenate,
178 args=(([0.], [1.]),),
179 kwargs={'axis': None, 'out': np.empty(2, dtype=np.int64)})
180
181 def test_not_deprecated(self):
182 self.assert_not_deprecated(np.concatenate,
183 args=(([0.], [1.]),),
184 kwargs={'axis': None, 'out': np.empty(2, dtype=np.int64),
185 'casting': "unsafe"})
186
187 with assert_raises(TypeError):
188 # Tests should notice if the deprecation warning is given first...
189 np.concatenate(([0.], [1.]), out=np.empty(2, dtype=np.int64),
190 casting="same_kind")
191
192
193class TestCtypesGetter(_DeprecationTestCase):
194 ctypes = np.array([1]).ctypes
195
196 @pytest.mark.parametrize("name", ["data", "shape", "strides", "_as_parameter_"])
197 def test_not_deprecated(self, name: str) -> None:
198 self.assert_not_deprecated(lambda: getattr(self.ctypes, name))
199
200
201class TestPyIntConversion(_DeprecationTestCase):
202 message = r".*stop allowing conversion of out-of-bound.*"
203
204 @pytest.mark.parametrize("dtype", np.typecodes["AllInteger"])
205 def test_deprecated_scalar(self, dtype):
206 dtype = np.dtype(dtype)
207 info = np.iinfo(dtype)
208
209 # Cover the most common creation paths (all end up in the
210 # same place):
211 def scalar(value, dtype):
212 dtype.type(value)
213
214 def assign(value, dtype):
215 arr = np.array([0, 0, 0], dtype=dtype)
216 arr[2] = value
217
218 def create(value, dtype):
219 np.array([value], dtype=dtype)
220
221 for creation_func in [scalar, assign, create]:
222 try:
223 self.assert_deprecated(
224 lambda: creation_func(info.min - 1, dtype))
225 except OverflowError:
226 pass # OverflowErrors always happened also before and are OK.
227
228 try:
229 self.assert_deprecated(
230 lambda: creation_func(info.max + 1, dtype))
231 except OverflowError:
232 pass # OverflowErrors always happened also before and are OK.
233
234
235@pytest.mark.parametrize("name", ["str", "bytes", "object"])
236def test_future_scalar_attributes(name):
237 # FutureWarning added 2022-11-17, NumPy 1.24,
238 assert name not in dir(np) # we may want to not add them
239 with pytest.warns(FutureWarning,
240 match=f"In the future .*{name}"):
241 assert not hasattr(np, name)
242
243 # Unfortunately, they are currently still valid via `np.dtype()`
244 np.dtype(name)
245 name in np._core.sctypeDict
246
247
248# Ignore the above future attribute warning for this test.
249@pytest.mark.filterwarnings("ignore:In the future:FutureWarning")
250class TestRemovedGlobals:
251 # Removed 2023-01-12, NumPy 1.24.0
252 # Not a deprecation, but the large error was added to aid those who missed
253 # the previous deprecation, and should be removed similarly to one
254 # (or faster).
255 @pytest.mark.parametrize("name",
256 ["object", "float", "complex", "str", "int"])
257 def test_attributeerror_includes_info(self, name):
258 msg = f".*\n`np.{name}` was a deprecated alias for the builtin"
259 with pytest.raises(AttributeError, match=msg):
260 getattr(np, name)
261
262
263class TestDeprecatedFinfo(_DeprecationTestCase):
264 # Deprecated in NumPy 1.25, 2023-01-16
265 def test_deprecated_none(self):
266 self.assert_deprecated(np.finfo, args=(None,))
267
268
269class TestMathAlias(_DeprecationTestCase):
270 def test_deprecated_np_lib_math(self):
271 self.assert_deprecated(lambda: np.lib.math)
272
273
274class TestLibImports(_DeprecationTestCase):
275 # Deprecated in Numpy 1.26.0, 2023-09
276 def test_lib_functions_deprecation_call(self):
277 from numpy import row_stack
278 from numpy._core.numerictypes import maximum_sctype
279 from numpy.lib._npyio_impl import recfromcsv, recfromtxt
280 from numpy.lib._shape_base_impl import get_array_wrap
281 from numpy.lib._utils_impl import safe_eval
282 from numpy.lib.tests.test_io import TextIO
283
284 self.assert_deprecated(lambda: safe_eval("None"))
285
286 data_gen = lambda: TextIO('A,B\n0,1\n2,3')
287 kwargs = {'delimiter': ",", 'missing_values': "N/A", 'names': True}
288 self.assert_deprecated(lambda: recfromcsv(data_gen()))
289 self.assert_deprecated(lambda: recfromtxt(data_gen(), **kwargs))
290
291 self.assert_deprecated(get_array_wrap)
292 self.assert_deprecated(lambda: maximum_sctype(int))
293
294 self.assert_deprecated(lambda: row_stack([[]]))
295 self.assert_deprecated(lambda: np.chararray)
296
297
298class TestDeprecatedDTypeAliases(_DeprecationTestCase):
299
300 def _check_for_warning(self, func):
301 with pytest.warns(DeprecationWarning,
302 match="alias 'a' was deprecated in NumPy 2.0") as w:
303 func()
304 assert len(w) == 1
305
306 def test_a_dtype_alias(self):
307 for dtype in ["a", "a10"]:
308 f = lambda: np.dtype(dtype)
309 self._check_for_warning(f)
310 self.assert_deprecated(f)
311 f = lambda: np.array(["hello", "world"]).astype("a10")
312 self._check_for_warning(f)
313 self.assert_deprecated(f)
314
315
316class TestDeprecatedArrayWrap(_DeprecationTestCase):
317 message = "__array_wrap__.*"
318
319 def test_deprecated(self):
320 class Test1:
321 def __array__(self, dtype=None, copy=None):
322 return np.arange(4)
323
324 def __array_wrap__(self, arr, context=None):
325 self.called = True
326 return 'pass context'
327
328 class Test2(Test1):
329 def __array_wrap__(self, arr):
330 self.called = True
331 return 'pass'
332
333 test1 = Test1()
334 test2 = Test2()
335 self.assert_deprecated(lambda: np.negative(test1))
336 assert test1.called
337 self.assert_deprecated(lambda: np.negative(test2))
338 assert test2.called
339
340class TestDeprecatedArrayAttributeSetting(_DeprecationTestCase):
341 message = "Setting the .*on a NumPy array has been deprecated.*"
342
343 def test_deprecated_strides_set(self):
344 x = np.eye(2)
345 self.assert_deprecated(setattr, args=(x, 'strides', x.strides))
346
347
348class TestDeprecatedDTypeParenthesizedRepeatCount(_DeprecationTestCase):
349 message = "Passing in a parenthesized single number"
350
351 @pytest.mark.parametrize("string", ["(2)i,", "(3)3S,", "f,(2)f"])
352 def test_parenthesized_repeat_count(self, string):
353 self.assert_deprecated(np.dtype, args=(string,))
354
355
356class TestAddNewdocUFunc(_DeprecationTestCase):
357 # Deprecated in Numpy 2.2, 2024-11
358 @pytest.mark.thread_unsafe(
359 reason="modifies and checks docstring which is global state"
360 )
361 def test_deprecated(self):
362 doc = struct_ufunc.add_triplet.__doc__
363 # gh-26718
364 # This test mutates the C-level docstring pointer for add_triplet,
365 # which is permanent once set. Skip when re-running tests.
366 if doc is not None and "new docs" in doc:
367 pytest.skip("Cannot retest deprecation, otherwise ValueError: "
368 "Cannot change docstring of ufunc with non-NULL docstring")
369 self.assert_deprecated(
370 lambda: np._core.umath._add_newdoc_ufunc(
371 struct_ufunc.add_triplet, "new docs"
372 )
373 )
374
375
376class TestDTypeAlignBool(_VisibleDeprecationTestCase):
377 # Deprecated in Numpy 2.4, 2025-07
378 # NOTE: As you can see, finalizing this deprecation breaks some (very) old
379 # pickle files. This may be fine, but needs to be done with some care since
380 # it breaks all of them and not just some.
381 # (Maybe it should be a 3.0 or only after warning more explicitly around pickles.)
382 message = r"dtype\(\): align should be passed as Python or NumPy boolean but got "
383
384 def test_deprecated(self):
385 # in particular integers should be rejected because one may think they mean
386 # alignment, or pass them accidentally as a subarray shape (meaning to pass
387 # a tuple).
388 self.assert_deprecated(lambda: np.dtype("f8", align=3))
389
390 @pytest.mark.parametrize("align", [True, False, np.True_, np.False_])
391 def test_not_deprecated(self, align):
392 # if the user passes a bool, it is accepted.
393 self.assert_not_deprecated(lambda: np.dtype("f8", align=align))
394
395
396class TestFlatiterIndexing0dBoolIndex(_DeprecationTestCase):
397 # Deprecated in Numpy 2.4, 2025-07
398 message = r"Indexing flat iterators with a 0-dimensional boolean index"
399
400 def test_0d_boolean_index_deprecated(self):
401 arr = np.arange(3)
402 # 0d boolean indices on flat iterators are deprecated
403 self.assert_deprecated(lambda: arr.flat[True])
404
405 def test_0d_boolean_assign_index_deprecated(self):
406 arr = np.arange(3)
407
408 def assign_to_index():
409 arr.flat[True] = 10
410
411 self.assert_deprecated(assign_to_index)
412
413
414class TestFlatiterIndexingFloatIndex(_DeprecationTestCase):
415 # Deprecated in NumPy 2.4, 2025-07
416 message = r"Invalid non-array indices for iterator objects"
417
418 def test_float_index_deprecated(self):
419 arr = np.arange(3)
420 # float indices on flat iterators are deprecated
421 self.assert_deprecated(lambda: arr.flat[[1.]])
422
423 def test_float_assign_index_deprecated(self):
424 arr = np.arange(3)
425
426 def assign_to_index():
427 arr.flat[[1.]] = 10
428
429 self.assert_deprecated(assign_to_index)
430
431
432@pytest.mark.thread_unsafe(
433 reason="warning control utilities are deprecated due to being thread-unsafe"
434)
435class TestWarningUtilityDeprecations(_DeprecationTestCase):
436 # Deprecation in NumPy 2.4, 2025-08
437 message = r"NumPy warning suppression and assertion utilities are deprecated."
438
439 def test_assert_warns_deprecated(self):
440 def use_assert_warns():
441 with np.testing.assert_warns(RuntimeWarning):
442 warnings.warn("foo", RuntimeWarning, stacklevel=1)
443
444 self.assert_deprecated(use_assert_warns)
445
446 def test_suppress_warnings_deprecated(self):
447 def use_suppress_warnings():
448 with np.testing.suppress_warnings() as sup:
449 sup.filter(RuntimeWarning, 'invalid value encountered in divide')
450
451 self.assert_deprecated(use_suppress_warnings)
452
453
454class TestTooManyArgsExtremum(_DeprecationTestCase):
455 # Deprecated in Numpy 2.4, 2025-08, gh-27639
456 message = "Passing more than 2 positional arguments to np.maximum and np.minimum "
457
458 @pytest.mark.parametrize("ufunc", [np.minimum, np.maximum])
459 def test_extremem_3_args(self, ufunc):
460 self.assert_deprecated(ufunc, args=(np.ones(1), np.zeros(1), np.empty(1)))
461 