diff --git a/CHANGELOG.md b/CHANGELOG.md index eeb32c244e1..d80971bd0c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.22.0] - 2027-MM-DD + +### Added + +### Changed + +### Deprecated + +* Deprecated the upcast of `arr` by a Python numeric scalar in `dpnp.append` with `axis=None` (e.g. `int8` array and `1` giving `int64`) [#3125](https://github.com/IntelPython/dpnp/pull/3125) + +### Removed + +### Fixed + +### Security + ## [0.21.0] - 2026-MM-DD This release achieves 100% compliance with Python Array API specification (revision [2025.12](https://data-apis.org/array-api/2025.12/)). diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 86b34c2f037..05a5f1bd14c 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -573,6 +573,16 @@ def append(arr, values, axis=None): `append` does not occur in-place: a new array is allocated and filled. If `axis` is ``None``, `out` is a flattened array. + Warnings + -------- + When `axis` is ``None``, a Python numeric scalar in `values` is converted + to an array before the result dtype is chosen, so it can upcast `arr` + (e.g. appending ``1`` to an ``int8`` array gives ``int64``). This is + deprecated: a future release will use the Python scalar promotion rules + and keep ``int8``. A ``FutureWarning`` is emitted when the result dtype + will change. Wrap the scalar in a list to keep the current result, or + convert it to an array of the desired dtype. + See Also -------- :obj:`dpnp.insert` : Insert elements into an array. @@ -602,6 +612,11 @@ def append(arr, values, axis=None): """ dpnp.check_supported_arrays_type(arr) + + # only exact Python int/float/complex are weak (not bool, numpy scalars) + is_pyscalar = axis is None and type(values) in (int, float, complex) + future_dtype = dpnp.result_type(arr, values) if is_pyscalar else None + if not dpnp.is_supported_array_type(values): values = dpnp.array( values, usm_type=arr.usm_type, sycl_queue=arr.sycl_queue @@ -613,7 +628,22 @@ def append(arr, values, axis=None): if values.ndim != 1: values = dpnp.ravel(values) axis = 0 - return dpnp.concatenate((arr, values), axis=axis) + res = dpnp.concatenate((arr, values), axis=axis) + + if is_pyscalar and res.dtype != future_dtype: + # deprecated in dpnp 0.22.0, following NumPy 2.6 + warnings.warn( + "dpnp.append converted Python numeric scalars to arrays before " + f"choosing the result dtype, producing dtype {res.dtype}. This " + "behavior will change in a future release, these inputs will use " + f"dtype {future_dtype}, which may round values differently or " + "raise an error. Wrap the scalar in a list to keep the current " + "result, or convert it to an array of dtype " + f"{str(future_dtype)!r} for the future behavior.", + FutureWarning, + stacklevel=2, + ) + return res def array_split(ary, indices_or_sections, axis=0): diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index f14c8644108..c6256310d8d 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -1,4 +1,5 @@ import itertools +import warnings import numpy import pytest @@ -23,6 +24,7 @@ get_integer_float_dtypes, get_unsigned_dtypes, has_support_aspect64, + numpy_version, ) from .tensor.helper import get_queue_or_skip from .third_party.cupy import testing @@ -174,6 +176,59 @@ def test_axis(self, dtype1, dtype2): result = dpnp.append(ia, ib, axis=1) assert_array_equal(result, expected) + @pytest.mark.parametrize( + "dtype, scalar", + [("i1", 1), ("u1", 300), ("u1", -1), ("i2", 1), ("u4", 1), ("u8", 1)] + + [ + # without fp64 an inexact arr keeps its dtype, so no warning there + pytest.param( + dt, + val, + marks=pytest.mark.skipif( + not has_support_aspect64(), reason="fp64 required" + ), + ) + for dt, val in [("f4", 1), ("f4", 1.5), ("c8", 1), ("c8", 1j)] + ], + ) + def test_scalar_promotion_deprecated(self, dtype, scalar): + a = numpy.ones(3, dtype=dtype) + ia = dpnp.array(a) + + msg = rf"append converted .* will use dtype {numpy.dtype(dtype)}," + with pytest.warns(FutureWarning, match=msg) as record: + result = dpnp.append(ia, scalar) + assert record[0].filename == __file__ + + if numpy_version() >= "2.6.0.dev0": + with pytest.warns(FutureWarning, match=msg) as record: + expected = numpy.append(a, scalar) + else: + expected = numpy.append(a, scalar) + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize( + "dtype, value", + [ + ("i8", 1), + ("i1", 1.5), + ("i4", 1j), + ("i1", True), + ("i1", numpy.int64(1)), + ("f4", numpy.float64(1.5)), + ("i1", [1]), + ], + ) + def test_scalar_promotion_not_deprecated(self, dtype, value): + a = numpy.ones(3, dtype=dtype) + ia = dpnp.array(a) + + with warnings.catch_warnings(): + warnings.simplefilter("error", FutureWarning) + result = dpnp.append(ia, value) + expected = numpy.append(a, value) + assert_dtype_allclose(result, expected) + class TestArraySplit: @pytest.mark.parametrize("xp", [numpy, dpnp]) diff --git a/dpnp/tests/third_party/cupy/manipulation_tests/test_add_remove.py b/dpnp/tests/third_party/cupy/manipulation_tests/test_add_remove.py index ba29b67ba6e..22e2d54faef 100644 --- a/dpnp/tests/third_party/cupy/manipulation_tests/test_add_remove.py +++ b/dpnp/tests/third_party/cupy/manipulation_tests/test_add_remove.py @@ -123,6 +123,9 @@ def test_scalar_lhs(self, xp, dtype1, dtype2): scalar = xp.dtype(dtype1).type(10).item() return xp.append(scalar, xp.arange(20, dtype=dtype2)) + @pytest.mark.filterwarnings( + "ignore:.*append converted Python numeric scalars:FutureWarning" + ) @testing.for_all_dtypes_combination( names=["dtype1", "dtype2"], no_bool=True )