From 6b045de66e446d32bad80fef57107713470e1393 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Sat, 21 Oct 2023 11:25:53 -0700 Subject: [PATCH 1/2] MAINT: testing: rename parameters x/y to actual/desired --- .../upcoming_changes/24978.deprecation.rst | 5 ++ numpy/_utils/__init__.py | 58 +++++++++++++++++++ numpy/testing/_private/utils.py | 44 ++++++++------ numpy/testing/tests/test_utils.py | 29 ++++++++++ 4 files changed, 117 insertions(+), 19 deletions(-) create mode 100644 doc/release/upcoming_changes/24978.deprecation.rst diff --git a/doc/release/upcoming_changes/24978.deprecation.rst b/doc/release/upcoming_changes/24978.deprecation.rst new file mode 100644 index 000000000000..76bb23ec96c6 --- /dev/null +++ b/doc/release/upcoming_changes/24978.deprecation.rst @@ -0,0 +1,5 @@ +Use of keyword arguments ``x`` and ``y`` with functions +`numpy.testing.assert_array_equal` and +`numpy.testing.assert_array_almost_equal` +has been deprecated. Pass the first two arguments as positional arguments, +instead. diff --git a/numpy/_utils/__init__.py b/numpy/_utils/__init__.py index 5dcfcea30aa9..9794c4e0c4a1 100644 --- a/numpy/_utils/__init__.py +++ b/numpy/_utils/__init__.py @@ -8,6 +8,8 @@ in ``numpy._core``. """ +import functools +import warnings from ._convertions import asunicode, asbytes @@ -27,3 +29,59 @@ def decorator(func): func.__module__ = module return func return decorator + + +def _rename_parameter(old_names, new_names, dep_version=None): + """ + Generate decorator for backward-compatible keyword renaming. + + Apply the decorator generated by `_rename_parameter` to functions with a + renamed parameter to maintain backward-compatibility. + + After decoration, the function behaves as follows: + If only the new parameter is passed into the function, behave as usual. + If only the old parameter is passed into the function (as a keyword), raise + a DeprecationWarning if `dep_version` is provided, and behave as usual + otherwise. + If both old and new parameters are passed into the function, raise a + DeprecationWarning if `dep_version` is provided, and raise the appropriate + TypeError (function got multiple values for argument). + + Parameters + ---------- + old_names : list of str + Old names of parameters + new_name : list of str + New names of parameters + dep_version : str, optional + Version of NumPy in which old parameter was deprecated in the format + 'X.Y.Z'. If supplied, the deprecation message will indicate that + support for the old parameter will be removed in version 'X.Y+2.Z' + + Notes + ----- + Untested with functions that accept *args. Probably won't work as written. + + """ + def decorator(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + for old_name, new_name in zip(old_names, new_names): + if old_name in kwargs: + if dep_version: + end_version = dep_version.split('.') + end_version[1] = str(int(end_version[1]) + 2) + end_version = '.'.join(end_version) + msg = (f"Use of keyword argument `{old_name}` is " + f"deprecated and replaced by `{new_name}`. " + f"Support for `{old_name}` will be removed " + f"in NumPy {end_version}.") + warnings.warn(msg, DeprecationWarning, stacklevel=2) + if new_name in kwargs: + msg = (f"{fun.__name__}() got multiple values for " + f"argument now known as `{new_name}`") + raise TypeError(msg) + kwargs[new_name] = kwargs.pop(old_name) + return fun(*args, **kwargs) + return wrapper + return decorator diff --git a/numpy/testing/_private/utils.py b/numpy/testing/_private/utils.py index 3560c2e3fc93..ab7fbfa685e8 100644 --- a/numpy/testing/_private/utils.py +++ b/numpy/testing/_private/utils.py @@ -23,6 +23,7 @@ intp, float32, empty, arange, array_repr, ndarray, isnat, array) from numpy import isfinite, isnan, isinf import numpy.linalg._umath_linalg +from numpy._utils import _rename_parameter from io import StringIO @@ -874,7 +875,9 @@ def func_assert_same_pos(x, y, func=isnan, hasval='nan'): raise ValueError(msg) -def assert_array_equal(x, y, err_msg='', verbose=True, *, strict=False): +@_rename_parameter(['x', 'y'], ['actual', 'desired'], dep_version='2.0.0') +def assert_array_equal(actual, desired, err_msg='', verbose=True, *, + strict=False): """ Raises an AssertionError if two array_like objects are not equal. @@ -888,21 +891,22 @@ def assert_array_equal(x, y, err_msg='', verbose=True, *, strict=False): The usual caution for verifying equality with floating point numbers is advised. - .. note:: When either `x` or `y` is already an instance of `numpy.ndarray` - and `y` is not a ``dict``, the behavior of ``assert_equal(x, y)`` is - identical to the behavior of this function. Otherwise, this function - performs `np.asanyarray` on the inputs before comparison, whereas - `assert_equal` defines special comparison rules for common Python - types. For example, only `assert_equal` can be used to compare nested - Python lists. In new code, consider using only `assert_equal`, - explicitly converting either `x` or `y` to arrays if the behavior of - `assert_array_equal` is desired. + .. note:: When either `actual` or `desired` is already an instance of + `numpy.ndarray` and `desired` is not a ``dict``, the behavior of + ``assert_equal(actual, desired)`` is identical to the behavior of this + function. Otherwise, this function performs `np.asanyarray` on the + inputs before comparison, whereas `assert_equal` defines special + comparison rules for common Python types. For example, only + `assert_equal` can be used to compare nested Python lists. In new code, + consider using only `assert_equal`, explicitly converting either + `actual` or `desired` to arrays if the behavior of `assert_array_equal` + is desired. Parameters ---------- - x : array_like + actual : array_like The actual object to check. - y : array_like + desired : array_like The desired, expected object. err_msg : str, optional The error message to be printed in case of failure. @@ -928,8 +932,8 @@ def assert_array_equal(x, y, err_msg='', verbose=True, *, strict=False): Notes ----- - When one of `x` and `y` is a scalar and the other is array_like, the - function checks that each element of the array_like object is equal to + When one of `actual` and `desired` is a scalar and the other is array_like, + the function checks that each element of the array_like object is equal to the scalar. This behaviour can be disabled with the `strict` parameter. Examples @@ -996,13 +1000,15 @@ def assert_array_equal(x, y, err_msg='', verbose=True, *, strict=False): DESIRED: array([2., 2., 2.], dtype=float32) """ __tracebackhide__ = True # Hide traceback for py.test - assert_array_compare(operator.__eq__, x, y, err_msg=err_msg, + assert_array_compare(operator.__eq__, actual, desired, err_msg=err_msg, verbose=verbose, header='Arrays are not equal', strict=strict) @np._no_nep50_warning() -def assert_array_almost_equal(x, y, decimal=6, err_msg='', verbose=True): +@_rename_parameter(['x', 'y'], ['actual', 'desired'], dep_version='2.0.0') +def assert_array_almost_equal(actual, desired, decimal=6, err_msg='', + verbose=True): """ Raises an AssertionError if two objects are not equal up to desired precision. @@ -1025,9 +1031,9 @@ def assert_array_almost_equal(x, y, decimal=6, err_msg='', verbose=True): Parameters ---------- - x : array_like + actual : array_like The actual object to check. - y : array_like + desired : array_like The desired, expected object. decimal : int, optional Desired precision, default is 6. @@ -1110,7 +1116,7 @@ def compare(x, y): return z < 1.5 * 10.0**(-decimal) - assert_array_compare(compare, x, y, err_msg=err_msg, verbose=verbose, + assert_array_compare(compare, actual, desired, err_msg=err_msg, verbose=verbose, header=('Arrays are not almost equal to %d decimals' % decimal), precision=decimal) diff --git a/numpy/testing/tests/test_utils.py b/numpy/testing/tests/test_utils.py index ef485c1894e2..36f9c1617f44 100644 --- a/numpy/testing/tests/test_utils.py +++ b/numpy/testing/tests/test_utils.py @@ -1878,3 +1878,32 @@ def __del__(self): finally: # make sure that we stop creating reference cycles ReferenceCycleInDel.make_cycle = False + + +@pytest.mark.parametrize('assert_func', [assert_array_equal, + assert_array_almost_equal]) +def test_xy_rename(assert_func): + # Test that keywords `x` and `y` have been renamed to `actual` and + # `desired`, respectively. These tests and use of `_rename_parameter` + # decorator can be removed before the release of NumPy 2.2.0. + assert_func(1, 1) + assert_func(actual=1, desired=1) + + assert_message = "Arrays are not..." + with pytest.raises(AssertionError, match=assert_message): + assert_func(1, 2) + with pytest.raises(AssertionError, match=assert_message): + assert_func(actual=1, desired=2) + + dep_message = 'Use of keyword argument...' + with pytest.warns(DeprecationWarning, match=dep_message): + assert_func(x=1, desired=1) + with pytest.warns(DeprecationWarning, match=dep_message): + assert_func(1, y=1) + + type_message = '...got multiple values for argument' + # explicit linebreak to support Python 3.9 + with pytest.warns(DeprecationWarning, match=dep_message), \ + pytest.raises(TypeError, match=type_message): + assert_func(1, x=1) + assert_func(1, 2, y=2) From 93139e978c30a544d697855192c7bec579c86f4e Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Thu, 16 Nov 2023 10:23:10 -0800 Subject: [PATCH 2/2] Update numpy/testing/_private/utils.py [skip cirrus] [skip circle] [skip azp] --- numpy/testing/_private/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/numpy/testing/_private/utils.py b/numpy/testing/_private/utils.py index ab7fbfa685e8..fb91a1516603 100644 --- a/numpy/testing/_private/utils.py +++ b/numpy/testing/_private/utils.py @@ -1116,7 +1116,8 @@ def compare(x, y): return z < 1.5 * 10.0**(-decimal) - assert_array_compare(compare, actual, desired, err_msg=err_msg, verbose=verbose, + assert_array_compare(compare, actual, desired, err_msg=err_msg, + verbose=verbose, header=('Arrays are not almost equal to %d decimals' % decimal), precision=decimal)