diff --git a/doc/release/upcoming_changes/24540.change.rst b/doc/release/upcoming_changes/24540.change.rst new file mode 100644 index 000000000000..b4ca0db59bcd --- /dev/null +++ b/doc/release/upcoming_changes/24540.change.rst @@ -0,0 +1,3 @@ +* ``np.lib.array_utils`` public module has been introduced and in its initial version + it hosts three functions: ``byte_bounds`` (moved from ``np.lib.utils``), + ``normalize_axis_tuple`` and ``normalize_axis_index`` diff --git a/doc/source/reference/routines.other.rst b/doc/source/reference/routines.other.rst index 52660b31f590..f7c21f5f9623 100644 --- a/doc/source/reference/routines.other.rst +++ b/doc/source/reference/routines.other.rst @@ -21,7 +21,7 @@ Memory ranges shares_memory may_share_memory - byte_bounds + lib.array_utils.byte_bounds Array mixins ------------ @@ -47,6 +47,8 @@ Utility show_config show_runtime broadcast_shapes + lib.array_utils.normalize_axis_index + lib.array_utils.normalize_axis_tuple .. automodule:: numpy.exceptions diff --git a/numpy/__init__.py b/numpy/__init__.py index 12b371281ce6..3e7b48bf68a0 100644 --- a/numpy/__init__.py +++ b/numpy/__init__.py @@ -218,7 +218,7 @@ from .lib._ufunclike_impl import fix, isneginf, isposinf from .lib._arraypad_impl import pad from .lib._utils_impl import ( - byte_bounds, show_runtime, get_include, info + show_runtime, get_include, info ) from .lib._stride_tricks_impl import ( broadcast_arrays, broadcast_shapes, broadcast_to diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index c1a15ea8ee44..edb0da52f044 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -577,7 +577,6 @@ from numpy.lib._ufunclike_impl import ( from numpy.lib._utils_impl import ( get_include as get_include, info as info, - byte_bounds as byte_bounds, show_runtime as show_runtime, ) diff --git a/numpy/_expired_attrs_2_0.py b/numpy/_expired_attrs_2_0.py index dbe690c56063..8cf0ae95a297 100644 --- a/numpy/_expired_attrs_2_0.py +++ b/numpy/_expired_attrs_2_0.py @@ -65,7 +65,8 @@ "`scalar_types` argument, use `numpy.result_type` and pass the " "Python values `0`, `0.0`, or `0j`.", "round_": "Use `np.round` instead.", - "nbytes": "Use `np.dtype().itemsize` instead.", "get_array_wrap": "", "DataSource": "It's still available as `np.lib.npyio.DataSource`.", + "nbytes": "Use `np.dtype().itemsize` instead.", + "byte_bounds": "Now it's available under `np.lib.array_utils.byte_bounds`", } diff --git a/numpy/core/multiarray.py b/numpy/core/multiarray.py index 6a1b9d3e5009..4f3fbf466a2c 100644 --- a/numpy/core/multiarray.py +++ b/numpy/core/multiarray.py @@ -69,6 +69,7 @@ zeros.__module__ = 'numpy' _get_promotion_state.__module__ = 'numpy' _set_promotion_state.__module__ = 'numpy' +normalize_axis_index.__module__ = 'numpy.lib.array_utils' # We can't verify dispatcher signatures because NumPy's C functions don't diff --git a/numpy/core/numeric.py b/numpy/core/numeric.py index f0c04ca184f9..c542a9d94e40 100644 --- a/numpy/core/numeric.py +++ b/numpy/core/numeric.py @@ -1328,6 +1328,7 @@ def rollaxis(a, axis, start=0): return a.transpose(axes) +@set_module("numpy.lib.array_utils") def normalize_axis_tuple(axis, ndim, argname=None, allow_duplicate=False): """ Normalizes an axis argument into a tuple of non-negative integer axes. diff --git a/numpy/lib/__init__.py b/numpy/lib/__init__.py index 92f1d6561b76..fb6104665e71 100644 --- a/numpy/lib/__init__.py +++ b/numpy/lib/__init__.py @@ -36,6 +36,7 @@ from . import npyio from . import arrayterator from . import _arraypad_impl +from . import array_utils from . import _version from .arrayterator import Arrayterator diff --git a/numpy/lib/_array_utils_impl.py b/numpy/lib/_array_utils_impl.py new file mode 100644 index 000000000000..90ba24e91965 --- /dev/null +++ b/numpy/lib/_array_utils_impl.py @@ -0,0 +1,58 @@ +from numpy.core import asarray +from numpy.core.numeric import normalize_axis_tuple, normalize_axis_index +from numpy._utils import set_module + +__all__ = ["byte_bounds", "normalize_axis_tuple", "normalize_axis_index"] + + +@set_module("numpy.lib.array_utils") +def byte_bounds(a): + """ + Returns pointers to the end-points of an array. + + Parameters + ---------- + a : ndarray + Input array. It must conform to the Python-side of the array + interface. + + Returns + ------- + (low, high) : tuple of 2 integers + The first integer is the first byte of the array, the second + integer is just past the last byte of the array. If `a` is not + contiguous it will not use every byte between the (`low`, `high`) + values. + + Examples + -------- + >>> I = np.eye(2, dtype='f'); I.dtype + dtype('float32') + >>> low, high = np.byte_bounds(I) + >>> high - low == I.size*I.itemsize + True + >>> I = np.eye(2); I.dtype + dtype('float64') + >>> low, high = np.byte_bounds(I) + >>> high - low == I.size*I.itemsize + True + + """ + ai = a.__array_interface__ + a_data = ai['data'][0] + astrides = ai['strides'] + ashape = ai['shape'] + bytes_a = asarray(a).dtype.itemsize + + a_low = a_high = a_data + if astrides is None: + # contiguous case + a_high += a.size * bytes_a + else: + for shape, stride in zip(ashape, astrides): + if stride < 0: + a_low += (shape-1)*stride + else: + a_high += (shape-1)*stride + a_high += bytes_a + return a_low, a_high diff --git a/numpy/lib/_array_utils_impl.pyi b/numpy/lib/_array_utils_impl.pyi new file mode 100644 index 000000000000..a38a62f2813c --- /dev/null +++ b/numpy/lib/_array_utils_impl.pyi @@ -0,0 +1,25 @@ +from typing import Any, Iterable, Tuple + +from numpy import generic +from numpy.typing import NDArray + +__all__: list[str] + +# NOTE: In practice `byte_bounds` can (potentially) take any object +# implementing the `__array_interface__` protocol. The caveat is +# that certain keys, marked as optional in the spec, must be present for +# `byte_bounds`. This concerns `"strides"` and `"data"`. +def byte_bounds(a: generic | NDArray[Any]) -> tuple[int, int]: ... + +def normalize_axis_tuple( + axis: int | Iterable[int], + ndim: int = ..., + argname: None | str = ..., + allow_duplicate: None | bool = ..., +) -> Tuple[int, int]: ... + +def normalize_axis_index( + axis: int = ..., + ndim: int = ..., + msg_prefix: None | str = ..., +) -> int: ... diff --git a/numpy/lib/_utils_impl.py b/numpy/lib/_utils_impl.py index 4fefe152734c..fa87cdb7a2ac 100644 --- a/numpy/lib/_utils_impl.py +++ b/numpy/lib/_utils_impl.py @@ -7,12 +7,12 @@ import functools import platform -from numpy.core import ndarray, asarray +from numpy.core import ndarray from numpy._utils import set_module import numpy as np __all__ = [ - 'get_include', 'info', 'byte_bounds', 'show_runtime' + 'get_include', 'info', 'show_runtime' ] @@ -287,64 +287,6 @@ def deprecate_with_doc(msg): return _Deprecate(message=msg) -#-------------------------------------------- -# Determine if two arrays can share memory -#-------------------------------------------- - - -@set_module('numpy') -def byte_bounds(a): - """ - Returns pointers to the end-points of an array. - - Parameters - ---------- - a : ndarray - Input array. It must conform to the Python-side of the array - interface. - - Returns - ------- - (low, high) : tuple of 2 integers - The first integer is the first byte of the array, the second - integer is just past the last byte of the array. If `a` is not - contiguous it will not use every byte between the (`low`, `high`) - values. - - Examples - -------- - >>> I = np.eye(2, dtype='f'); I.dtype - dtype('float32') - >>> low, high = np.byte_bounds(I) - >>> high - low == I.size*I.itemsize - True - >>> I = np.eye(2); I.dtype - dtype('float64') - >>> low, high = np.byte_bounds(I) - >>> high - low == I.size*I.itemsize - True - - """ - ai = a.__array_interface__ - a_data = ai['data'][0] - astrides = ai['strides'] - ashape = ai['shape'] - bytes_a = asarray(a).dtype.itemsize - - a_low = a_high = a_data - if astrides is None: - # contiguous case - a_high += a.size * bytes_a - else: - for shape, stride in zip(ashape, astrides): - if stride < 0: - a_low += (shape-1)*stride - else: - a_high += (shape-1)*stride - a_high += bytes_a - return a_low, a_high - - #----------------------------------------------------------------------------- diff --git a/numpy/lib/_utils_impl.pyi b/numpy/lib/_utils_impl.pyi index 37845090441a..ccde53b7a91a 100644 --- a/numpy/lib/_utils_impl.pyi +++ b/numpy/lib/_utils_impl.pyi @@ -7,8 +7,6 @@ from typing import ( Protocol, ) -from numpy import generic -from numpy.typing import NDArray from numpy.core.numerictypes import ( issubdtype as issubdtype, ) @@ -56,12 +54,6 @@ def deprecate( def deprecate_with_doc(msg: None | str) -> _Deprecate: ... -# NOTE: In practice `byte_bounds` can (potentially) take any object -# implementing the `__array_interface__` protocol. The caveat is -# that certain keys, marked as optional in the spec, must be present for -# `byte_bounds`. This concerns `"strides"` and `"data"`. -def byte_bounds(a: generic | NDArray[Any]) -> tuple[int, int]: ... - def info( object: object = ..., maxwidth: int = ..., diff --git a/numpy/lib/array_utils.py b/numpy/lib/array_utils.py new file mode 100644 index 000000000000..94ba46eae5cd --- /dev/null +++ b/numpy/lib/array_utils.py @@ -0,0 +1,6 @@ +from ._array_utils_impl import ( + __all__, + byte_bounds, + normalize_axis_index, + normalize_axis_tuple, +) diff --git a/numpy/lib/array_utils.pyi b/numpy/lib/array_utils.pyi new file mode 100644 index 000000000000..4b9ebe334a1f --- /dev/null +++ b/numpy/lib/array_utils.pyi @@ -0,0 +1,6 @@ +from ._array_utils_impl import ( + __all__ as __all__, + byte_bounds as byte_bounds, + normalize_axis_index as normalize_axis_index, + normalize_axis_tuple as normalize_axis_tuple, +) diff --git a/numpy/lib/tests/test_array_utils.py b/numpy/lib/tests/test_array_utils.py new file mode 100644 index 000000000000..3d8b2bd4616e --- /dev/null +++ b/numpy/lib/tests/test_array_utils.py @@ -0,0 +1,33 @@ +import numpy as np + +from numpy.lib import array_utils +from numpy.testing import assert_equal + + +class TestByteBounds: + def test_byte_bounds(self): + # pointer difference matches size * itemsize + # due to contiguity + a = np.arange(12).reshape(3, 4) + low, high = array_utils.byte_bounds(a) + assert_equal(high - low, a.size * a.itemsize) + + def test_unusual_order_positive_stride(self): + a = np.arange(12).reshape(3, 4) + b = a.T + low, high = array_utils.byte_bounds(b) + assert_equal(high - low, b.size * b.itemsize) + + def test_unusual_order_negative_stride(self): + a = np.arange(12).reshape(3, 4) + b = a.T[::-1] + low, high = array_utils.byte_bounds(b) + assert_equal(high - low, b.size * b.itemsize) + + def test_strided(self): + a = np.arange(12) + b = a[::2] + low, high = array_utils.byte_bounds(b) + # the largest pointer address is lost (even numbers only in the + # stride), and compensate addresses for striding by 2 + assert_equal(high - low, b.size * 2 * b.itemsize - b.itemsize) diff --git a/numpy/lib/tests/test_utils.py b/numpy/lib/tests/test_utils.py index bceff04d5185..e2f72ac90c92 100644 --- a/numpy/lib/tests/test_utils.py +++ b/numpy/lib/tests/test_utils.py @@ -1,42 +1,12 @@ import pytest import numpy as np -from numpy.testing import assert_equal, assert_raises_regex +from numpy.testing import assert_raises_regex import numpy.lib._utils_impl as _utils_impl from io import StringIO -class TestByteBounds: - - def test_byte_bounds(self): - # pointer difference matches size * itemsize - # due to contiguity - a = np.arange(12).reshape(3, 4) - low, high = np.byte_bounds(a) - assert_equal(high - low, a.size * a.itemsize) - - def test_unusual_order_positive_stride(self): - a = np.arange(12).reshape(3, 4) - b = a.T - low, high = np.byte_bounds(b) - assert_equal(high - low, b.size * b.itemsize) - - def test_unusual_order_negative_stride(self): - a = np.arange(12).reshape(3, 4) - b = a.T[::-1] - low, high = np.byte_bounds(b) - assert_equal(high - low, b.size * b.itemsize) - - def test_strided(self): - a = np.arange(12) - b = a[::2] - low, high = np.byte_bounds(b) - # the largest pointer address is lost (even numbers only in the - # stride), and compensate addresses for striding by 2 - assert_equal(high - low, b.size * 2 * b.itemsize - b.itemsize) - - def test_assert_raises_regex_context_manager(): with assert_raises_regex(ValueError, 'no deprecation warning'): raise ValueError('no deprecation warning') diff --git a/numpy/tests/test_public_api.py b/numpy/tests/test_public_api.py index 19c98ea38ad2..4aded7fdde6e 100644 --- a/numpy/tests/test_public_api.py +++ b/numpy/tests/test_public_api.py @@ -123,6 +123,7 @@ def test_NPY_NO_EXPORT(): "lib.stride_tricks", "lib.npyio", "lib.introspect", + "lib.array_utils", "linalg", "ma", "ma.extras", diff --git a/numpy/typing/tests/data/fail/lib_utils.pyi b/numpy/typing/tests/data/fail/lib_utils.pyi index eee2bbe26936..8b8482eeff6d 100644 --- a/numpy/typing/tests/data/fail/lib_utils.pyi +++ b/numpy/typing/tests/data/fail/lib_utils.pyi @@ -1,3 +1,3 @@ -import numpy as np +import numpy.lib.array_utils as array_utils -np.byte_bounds(1) # E: incompatible type +array_utils.byte_bounds(1) # E: incompatible type diff --git a/numpy/typing/tests/data/pass/lib_utils.py b/numpy/typing/tests/data/pass/lib_utils.py index d6e6a3dfb068..f9b3381e13d2 100644 --- a/numpy/typing/tests/data/pass/lib_utils.py +++ b/numpy/typing/tests/data/pass/lib_utils.py @@ -3,6 +3,7 @@ from io import StringIO import numpy as np +import numpy.lib.array_utils as array_utils FILE = StringIO() AR = np.arange(10, dtype=np.float64) @@ -12,8 +13,7 @@ def func(a: int) -> bool: return True -np.byte_bounds(AR) -np.byte_bounds(np.float64()) +array_utils.byte_bounds(AR) +array_utils.byte_bounds(np.float64()) np.info(1, output=FILE) - diff --git a/numpy/typing/tests/data/pass/modules.py b/numpy/typing/tests/data/pass/modules.py index f2d779e20e63..f6302f0239ed 100644 --- a/numpy/typing/tests/data/pass/modules.py +++ b/numpy/typing/tests/data/pass/modules.py @@ -19,6 +19,7 @@ np.lib.mixins np.lib.scimath np.lib.stride_tricks +np.lib.array_utils np.ma.extras np.polynomial.chebyshev np.polynomial.hermite diff --git a/numpy/typing/tests/data/reveal/lib_utils.pyi b/numpy/typing/tests/data/reveal/lib_utils.pyi index ae0c9b40b4cb..094b60140833 100644 --- a/numpy/typing/tests/data/reveal/lib_utils.pyi +++ b/numpy/typing/tests/data/reveal/lib_utils.pyi @@ -3,6 +3,7 @@ from io import StringIO import numpy as np import numpy.typing as npt +import numpy.lib.array_utils as array_utils if sys.version_info >= (3, 11): from typing import assert_type @@ -15,7 +16,7 @@ FILE: StringIO def func(a: int) -> bool: ... -assert_type(np.byte_bounds(AR), tuple[int, int]) -assert_type(np.byte_bounds(np.float64()), tuple[int, int]) +assert_type(array_utils.byte_bounds(AR), tuple[int, int]) +assert_type(array_utils.byte_bounds(np.float64()), tuple[int, int]) assert_type(np.info(1, output=FILE), None)