From 179b60dc1b2050682a77bc41941f5145b1b4ada3 Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 29 Jun 2026 20:33:10 +0200 Subject: [PATCH 01/60] TYP: specialized ``bitwise_count`` ufunc --- numpy/__init__.pyi | 2 +- numpy/_core/umath.pyi | 105 +++++++++++++++++++++- numpy/typing/tests/data/reveal/ufuncs.pyi | 33 ++++--- 3 files changed, 126 insertions(+), 14 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index d3dbac6f5656..695b6f247f2a 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -405,6 +405,7 @@ from numpy._core.umath import ( arctan as atan, arctanh, arctanh as atanh, + bitwise_count, cbrt, ceil, conjugate, @@ -7601,7 +7602,6 @@ class ufunc: add: _UFunc_Nin2_Nout1[L["add"], L[22], L[0]] arctan2: _UFunc_Nin2_Nout1[L["arctan2"], L[5], None] bitwise_and: _UFunc_Nin2_Nout1[L["bitwise_and"], L[12], L[-1]] -bitwise_count: _UFunc_Nin1_Nout1[L["bitwise_count"], L[11], None] bitwise_or: _UFunc_Nin2_Nout1[L["bitwise_or"], L[12], L[0]] bitwise_xor: _UFunc_Nin2_Nout1[L["bitwise_xor"], L[12], L[0]] copysign: _UFunc_Nin2_Nout1[L["copysign"], L[4], None] diff --git a/numpy/_core/umath.pyi b/numpy/_core/umath.pyi index 600955316949..076a11185635 100644 --- a/numpy/_core/umath.pyi +++ b/numpy/_core/umath.pyi @@ -26,7 +26,6 @@ from numpy import ( add, arctan2, bitwise_and, - bitwise_count, bitwise_or, bitwise_xor, conj, @@ -659,6 +658,108 @@ class _ufunc_11_bifgco_bo(_ufunc_11): # type: ignore[misc] @overload def at[IxT, OutT](self, a: _CanUfuncAt1[IxT, OutT], indices: IxT, /) -> OutT: ... +# bBhHiIlLqQO => BO, where bBhHiIlLqQ => B and O => O +@type_check_only +class _ufunc_11_io(_ufunc_11): # type: ignore[misc] + @override + @overload # Nd, known shape + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_to_integer]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.ndarray[ShapeT, np.dtype[np.uint8]]: ... + @overload # Nd object, known shape + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[np.object_]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.ndarray[ShapeT, np.dtype[np.object_]]: ... + @overload # scalar + def __call__( + self, + x: int | _to_integer, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.uint8: ... + @overload # scalar, out=... + def __call__( + self, + x: int | _to_integer, + /, + *, + out: EllipsisType, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> _Array0D[np.uint8]: ... + @overload # 1d + def __call__( + self, + x: Sequence[int | _to_integer], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> _Array1D[np.uint8]: ... + @overload # 2d + def __call__( + self, + x: Sequence[Sequence[int | _to_integer]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> _Array2D[np.uint8]: ... + @overload # 3d + def __call__( + self, + x: Sequence[Sequence[Sequence[int | _to_integer]]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> _Array3D[np.uint8]: ... + @overload # out= + def __call__[OutT: np.ndarray]( + self, + x: _ArrayLikeInt_co, + /, + out: OutT, + *, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> OutT: ... + @overload # x.__array_ufunc__(...) -> OutT + def __call__[OutT]( + self, + x: _CanUfuncCall1[OutT], + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs11], + ) -> OutT: ... + + # + @override + @overload + def at(self, a: np.ndarray[_Shape, np.dtype[_to_integer]], indices: _ArrayLikeInt, /) -> None: ... # pyrefly:ignore[bad-override] + @overload + def at[IxT, OutT](self, a: _CanUfuncAt1[IxT, OutT], indices: IxT, /) -> OutT: ... + # efdg => efdg @type_check_only class _ufunc_11_f(_ufunc_11): # type: ignore[misc] @@ -2793,6 +2894,8 @@ isnan: Final[_ufunc_11_bifgcm_b] = ... # TODO: StringDType[?] support logical_not: Final[_ufunc_11_bifgco_bo] = ... +bitwise_count: Final[_ufunc_11_io] = ... + spacing: Final[_ufunc_11_f] = ... cbrt: Final[_ufunc_11_fo] = ... diff --git a/numpy/typing/tests/data/reveal/ufuncs.pyi b/numpy/typing/tests/data/reveal/ufuncs.pyi index 8ec60c516efb..dcc8a25a9812 100644 --- a/numpy/typing/tests/data/reveal/ufuncs.pyi +++ b/numpy/typing/tests/data/reveal/ufuncs.pyi @@ -70,18 +70,6 @@ assert_type(np.vecdot.signature, Literal["(n),(n)->()"]) assert_type(np.vecdot.identity, None) assert_type(np.vecdot(AR_f8, AR_f8), Any) -assert_type(np.bitwise_count.__name__, Literal["bitwise_count"]) -assert_type(np.bitwise_count.__qualname__, Literal["bitwise_count"]) -assert_type(np.bitwise_count.ntypes, Literal[11]) -assert_type(np.bitwise_count.identity, None) -assert_type(np.bitwise_count.nin, Literal[1]) -assert_type(np.bitwise_count.nout, Literal[1]) -assert_type(np.bitwise_count.nargs, Literal[2]) -assert_type(np.bitwise_count.signature, None) -assert_type(np.bitwise_count.identity, None) -assert_type(np.bitwise_count(i8), Any) -assert_type(np.bitwise_count(AR_i8), npt.NDArray[Any]) - def test_absolute_outer_invalid() -> None: assert_type(np.absolute.outer(AR_f8, AR_f8), NoReturn) # type: ignore[arg-type] def test_frexp_outer_invalid() -> None: @@ -291,6 +279,27 @@ assert_type(np.isinf(_td_ns_nd), npt.NDArray[np.bool]) assert_type(np.isinf(_py_i_2d, out=_f32_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) +# _ufunc_11_io +# (bitwise_count) + +assert_type(np.bitwise_count(_py_b_0d), np.uint8) +assert_type(np.bitwise_count(_py_b_1d), np.ndarray[tuple[int], np.dtype[np.uint8]]) +assert_type(np.bitwise_count(_py_b_2d), np.ndarray[tuple[int, int], np.dtype[np.uint8]]) +assert_type(np.bitwise_count(_py_i_0d), np.uint8) +assert_type(np.bitwise_count(_py_i_1d), np.ndarray[tuple[int], np.dtype[np.uint8]]) +assert_type(np.bitwise_count(_py_i_2d), np.ndarray[tuple[int, int], np.dtype[np.uint8]]) + +assert_type(np.bitwise_count(_bool_0d), np.uint8) +assert_type(np.bitwise_count(_bool_1d), np.ndarray[tuple[int], np.dtype[np.uint8]]) +assert_type(np.bitwise_count(_bool_2d), np.ndarray[tuple[int, int], np.dtype[np.uint8]]) +assert_type(np.bitwise_count(_bool_nd), npt.NDArray[np.uint8]) +assert_type(np.bitwise_count(_i16_0d), np.uint8) +assert_type(np.bitwise_count(_i16_1d), np.ndarray[tuple[int], np.dtype[np.uint8]]) +assert_type(np.bitwise_count(_i16_2d), np.ndarray[tuple[int, int], np.dtype[np.uint8]]) +assert_type(np.bitwise_count(_i16_nd), npt.NDArray[np.uint8]) + +assert_type(np.bitwise_count(_py_i_2d, out=_f32_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) + # _ufunc_11_f # (spacing) From a0563eea89dfc99405b80b0fe80204500a1c4a9a Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 29 Jun 2026 20:36:26 +0200 Subject: [PATCH 02/60] TYP: remove now-unused ``_UFunc_Nin1_Nout1`` phantom type --- numpy/__init__.pyi | 1 - numpy/_core/_umath_tests.pyi | 6 ++-- numpy/_typing/__init__.py | 1 - numpy/_typing/_ufunc.py | 1 - numpy/_typing/_ufunc.pyi | 69 ------------------------------------ 5 files changed, 3 insertions(+), 75 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index 695b6f247f2a..ca9d3960a627 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -111,7 +111,6 @@ from numpy._typing import ( # type: ignore[deprecated] _InexactCodes, _CharacterCodes, # Ufuncs - _UFunc_Nin1_Nout1, _UFunc_Nin2_Nout1, _UFunc_Nin1_Nout2, _UFunc_Nin2_Nout2, diff --git a/numpy/_core/_umath_tests.pyi b/numpy/_core/_umath_tests.pyi index cce84afdd081..bedb51f59e83 100644 --- a/numpy/_core/_umath_tests.pyi +++ b/numpy/_core/_umath_tests.pyi @@ -4,7 +4,7 @@ from typing import Final, Literal as L, TypedDict, type_check_only import numpy as np -from numpy._typing import _GUFunc_Nin2_Nout1, _UFunc_Nin1_Nout1, _UFunc_Nin2_Nout1 +from numpy._typing import _GUFunc_Nin2_Nout1, _UFunc_Nin2_Nout1 @type_check_only class _TestDispatchResult(TypedDict): @@ -36,7 +36,7 @@ def restore_loop(ufunc: np.ufunc, capsule: object, /) -> None: ... # undocumented ufuncs and gufuncs always_error: Final[_UFunc_Nin2_Nout1[L["always_error"], L[1], None]] = ... -always_error_unary: Final[_UFunc_Nin1_Nout1[L["always_error_unary"], L[1], None]] = ... +always_error_unary: Final[np.ufunc] = ... always_error_gufunc: Final[_GUFunc_Nin2_Nout1[L["always_error_gufunc"], L[1], None, L["(i),()->()"]]] = ... inner1d: Final[_GUFunc_Nin2_Nout1[L["inner1d"], L[2], None, L["(i),(i)->()"]]] = ... innerwt: Final[np.ufunc] = ... # we have no specialized type for 3->1 gufuncs @@ -47,5 +47,5 @@ cumsum: Final[np.ufunc] = ... # we have no specialized type for 1->1 gufuncs inner1d_no_doc: Final[_GUFunc_Nin2_Nout1[L["inner1d_no_doc"], L[2], None, L["(i),(i)->()"]]] = ... cross1d: Final[_GUFunc_Nin2_Nout1[L["cross1d"], L[2], None, L["(3),(3)->(3)"]]] = ... _pickleable_module_global_ufunc: Final[np.ufunc] = ... # 0->0 ufunc; segfaults if called -indexed_negative: Final[_UFunc_Nin1_Nout1[L["indexed_negative"], L[0], L[0]]] = ... # ntypes=0; can't be called +indexed_negative: Final[np.ufunc] = ... # ntypes=0; can't be called conv1d_full: Final[_GUFunc_Nin2_Nout1[L["conv1d_full"], L[1], None, L["(m),(n)->(p)"]]] = ... diff --git a/numpy/_typing/__init__.py b/numpy/_typing/__init__.py index f69368a95e10..56e7b0475133 100644 --- a/numpy/_typing/__init__.py +++ b/numpy/_typing/__init__.py @@ -140,7 +140,6 @@ # from ._ufunc import ( _GUFunc_Nin2_Nout1 as _GUFunc_Nin2_Nout1, - _UFunc_Nin1_Nout1 as _UFunc_Nin1_Nout1, _UFunc_Nin1_Nout2 as _UFunc_Nin1_Nout2, _UFunc_Nin2_Nout1 as _UFunc_Nin2_Nout1, _UFunc_Nin2_Nout2 as _UFunc_Nin2_Nout2, diff --git a/numpy/_typing/_ufunc.py b/numpy/_typing/_ufunc.py index db52a1fdb318..c40b02d039cd 100644 --- a/numpy/_typing/_ufunc.py +++ b/numpy/_typing/_ufunc.py @@ -1,6 +1,5 @@ from numpy import ufunc -_UFunc_Nin1_Nout1 = ufunc _UFunc_Nin2_Nout1 = ufunc _UFunc_Nin1_Nout2 = ufunc _UFunc_Nin2_Nout2 = ufunc diff --git a/numpy/_typing/_ufunc.pyi b/numpy/_typing/_ufunc.pyi index 5ace7d90d00a..77fd8dbea3fe 100644 --- a/numpy/_typing/_ufunc.pyi +++ b/numpy/_typing/_ufunc.pyi @@ -74,75 +74,6 @@ class _ReduceKwargs(TypedDict, total=False): # pyright: reportIncompatibleMethodOverride=false -@type_check_only -class _UFunc_Nin1_Nout1[NameT: LiteralString, NTypesT: int, IdentT](ufunc): # type: ignore[misc] - @property - def __name__(self) -> NameT: ... - @property - def __qualname__(self) -> NameT: ... # pyright: ignore[reportIncompatibleVariableOverride] - @property - def ntypes(self) -> NTypesT: ... - @property - def identity(self) -> IdentT: ... - @property - def nin(self) -> Literal[1]: ... - @property - def nout(self) -> Literal[1]: ... - @property - def nargs(self) -> Literal[2]: ... - @property - def signature(self) -> None: ... - - @overload - def __call__( - self, - x1: _ScalarLike_co, - /, - out: None = None, - *, - dtype: DTypeLike | None = None, - where: _ArrayLikeBool_co | None = True, - casting: _CastingKind = ..., - order: _OrderKACF = ..., - subok: bool = ..., - signature: str | _2Tuple[str | None] = ..., - ) -> Incomplete: ... - @overload - def __call__( - self, - x1: ArrayLike, - /, - out: np.ndarray | tuple[np.ndarray] | EllipsisType | None = None, - *, - dtype: DTypeLike | None = None, - where: _ArrayLikeBool_co | None = True, - casting: _CastingKind = ..., - order: _OrderKACF = ..., - subok: bool = ..., - signature: str | _2Tuple[str | None] = ..., - ) -> NDArray[Incomplete]: ... - @overload - def __call__( - self, - x1: _SupportsArrayUFunc, - /, - out: np.ndarray | tuple[np.ndarray] | EllipsisType | None = None, - *, - dtype: DTypeLike | None = None, - where: _ArrayLikeBool_co | None = True, - casting: _CastingKind = ..., - order: _OrderKACF = ..., - subok: bool = ..., - signature: str | _2Tuple[str | None] = ..., - ) -> Incomplete: ... - - def accumulate(self, array: Never, /) -> NoReturn: ... # type: ignore[override] - def reduce(self, array: Never, /) -> NoReturn: ... # type: ignore[override] - def reduceat(self, array: Never, /, indices: Never) -> NoReturn: ... # type: ignore[override] - def outer(self, A: Never, B: Never, /) -> NoReturn: ... # type: ignore[override] - - def at(self, a: np.ndarray | _SupportsArrayUFunc, indices: _ArrayLikeInt_co, /) -> None: ... # type: ignore[override] - @type_check_only class _UFunc_Nin2_Nout1[NameT: LiteralString, NTypesT: int, IdentT](ufunc): # type: ignore[misc] @property From d9850687b958b38e29999aec25d12bfe0e42c4a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 07:52:12 -0600 Subject: [PATCH 03/60] MAINT: Bump the python-deps group across 1 directory with 6 updates (#31772) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Charles Harris --- requirements/doc_requirements.txt | 4 ++-- requirements/emscripten_test_requirements.txt | 4 ++-- requirements/linter_requirements.txt | 2 +- requirements/test_requirements.txt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements/doc_requirements.txt b/requirements/doc_requirements.txt index 471d9d410e5d..6a58cde6ced5 100644 --- a/requirements/doc_requirements.txt +++ b/requirements/doc_requirements.txt @@ -1,11 +1,11 @@ # doxygen required, use apt-get or dnf sphinx==8.2.3 numpydoc==1.10.0 -pydata-sphinx-theme>=0.16.1 +pydata-sphinx-theme>=0.19.0 sphinx-copybutton sphinx-design scipy -matplotlib>=3.10.8 +matplotlib>=3.10.9 pandas breathe>=4.36.0 ipython>=9.12.0 diff --git a/requirements/emscripten_test_requirements.txt b/requirements/emscripten_test_requirements.txt index f405bb00d884..0963f89097a7 100644 --- a/requirements/emscripten_test_requirements.txt +++ b/requirements/emscripten_test_requirements.txt @@ -1,4 +1,4 @@ -hypothesis==6.152.1 -pytest==9.1.0 +hypothesis==6.155.7 +pytest==9.1.1 tzdata pytest-xdist diff --git a/requirements/linter_requirements.txt b/requirements/linter_requirements.txt index 27284b68b9da..f8adfe232826 100644 --- a/requirements/linter_requirements.txt +++ b/requirements/linter_requirements.txt @@ -1,5 +1,5 @@ # keep in sync with `environment.yml` cython-lint -ruff==0.15.17 +ruff==0.15.18 GitPython>=3.1.50 spin diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt index 958b541066c8..219eda52fd07 100644 --- a/requirements/test_requirements.txt +++ b/requirements/test_requirements.txt @@ -1,6 +1,6 @@ Cython -hypothesis==6.152.1 -pytest==9.1.0 +hypothesis==6.155.7 +pytest==9.1.1 pytest-cov==7.1.0 meson ninja; sys_platform != "emscripten" From b64f2621b16a0be40439bc250b00a7d72a6b6020 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Tue, 30 Jun 2026 15:55:03 +0200 Subject: [PATCH 04/60] TYP: specialized ``frexp`` and ``modf`` ufuncs (#31806) --- numpy/__init__.pyi | 5 +- numpy/_core/umath.pyi | 291 +++++++++++++++++++++- numpy/_typing/__init__.py | 1 - numpy/_typing/_ufunc.py | 1 - numpy/_typing/_ufunc.pyi | 74 ------ numpy/typing/tests/data/fail/ufuncs.pyi | 3 - numpy/typing/tests/data/reveal/ufuncs.pyi | 121 ++++++++- 7 files changed, 400 insertions(+), 96 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index ca9d3960a627..a826304c6323 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -112,7 +112,6 @@ from numpy._typing import ( # type: ignore[deprecated] _CharacterCodes, # Ufuncs _UFunc_Nin2_Nout1, - _UFunc_Nin1_Nout2, _UFunc_Nin2_Nout2, _GUFunc_Nin2_Nout1, ) @@ -418,6 +417,7 @@ from numpy._core.umath import ( expm1, fabs, floor, + frexp, invert, isfinite, isinf, @@ -428,6 +428,7 @@ from numpy._core.umath import ( log1p, log2, logical_not, + modf, positive, negative, rad2deg, @@ -7612,7 +7613,6 @@ floor_divide: _UFunc_Nin2_Nout1[L["floor_divide"], L[21], None] fmax: _UFunc_Nin2_Nout1[L["fmax"], L[21], None] fmin: _UFunc_Nin2_Nout1[L["fmin"], L[21], None] fmod: _UFunc_Nin2_Nout1[L["fmod"], L[15], None] -frexp: _UFunc_Nin1_Nout2[L["frexp"], L[4], None] gcd: _UFunc_Nin2_Nout1[L["gcd"], L[11], L[0]] greater: _UFunc_Nin2_Nout1[L["greater"], L[23], None] greater_equal: _UFunc_Nin2_Nout1[L["greater_equal"], L[23], None] @@ -7632,7 +7632,6 @@ matmul: _GUFunc_Nin2_Nout1[L["matmul"], L[19], None, L["(n?,k),(k,m?)->(n?,m?)"] matvec: _GUFunc_Nin2_Nout1[L["matvec"], L[19], None, L["(m,n),(n)->(m)"]] maximum: _UFunc_Nin2_Nout1[L["maximum"], L[21], None] minimum: _UFunc_Nin2_Nout1[L["minimum"], L[21], None] -modf: _UFunc_Nin1_Nout2[L["modf"], L[4], None] multiply: _UFunc_Nin2_Nout1[L["multiply"], L[23], L[1]] nextafter: _UFunc_Nin2_Nout1[L["nextafter"], L[4], None] not_equal: _UFunc_Nin2_Nout1[L["not_equal"], L[23], None] diff --git a/numpy/_core/umath.pyi b/numpy/_core/umath.pyi index 076a11185635..1e68943c1ebc 100644 --- a/numpy/_core/umath.pyi +++ b/numpy/_core/umath.pyi @@ -12,6 +12,7 @@ from typing import ( Self, TypedDict, Unpack, + final, overload, override, type_check_only, @@ -40,7 +41,6 @@ from numpy import ( fmax, fmin, fmod, - frexp, frompyfunc, gcd, greater, @@ -62,7 +62,6 @@ from numpy import ( maximum, minimum, mod, - modf, multiply, nextafter, not_equal, @@ -203,6 +202,8 @@ type _Array1D[ScalarT: np.generic] = np.ndarray[tuple[int], np.dtype[ScalarT]] type _Array2D[ScalarT: np.generic] = np.ndarray[tuple[int, int], np.dtype[ScalarT]] type _Array3D[ScalarT: np.generic] = np.ndarray[tuple[int, int, int], np.dtype[ScalarT]] +type _tuple2[T] = tuple[T, T] + type _ErrKind = Literal["ignore", "warn", "raise", "call", "print", "log"] type _ErrCall = Callable[[str, int], Any] | SupportsWrite[str] @@ -226,6 +227,7 @@ class _ExtOjbDict(TypedDict, total=False): bufsize: int type _Signature1 = tuple[str | None, str | None] | str +type _Signature2 = tuple[str | None, str | None, str | None] | str @type_check_only class _CanUfuncCall1[OutT](Protocol): @@ -250,6 +252,14 @@ class _Kwargs11(TypedDict, total=False): subok: bool # = True, signature: _Signature1 +@type_check_only +class _Kwargs12(TypedDict, total=False): + where: _ArrayLikeBool_co # = True + casting: _CastingKind # = "same_kind" + order: _OrderKACF # = "K", + subok: bool # = True, + signature: _Signature2 + _IdT_co = TypeVar("_IdT_co", covariant=True, default=None) @type_check_only @@ -269,14 +279,47 @@ class _ufunc_11(np.ufunc, Generic[_IdT_co]): # type: ignore[misc] @property @override def signature(self) -> None: ... + @override + def accumulate(self, array: Never, /) -> Never: ... # pyrefly:ignore[bad-override] + @override + def reduce(self, array: Never, /) -> Never: ... # pyrefly:ignore[bad-override] + @override + def reduceat(self, array: Never, /, indices: Never) -> Never: ... # pyrefly:ignore[bad-override] + @override + def outer(self, A: Never, B: Never, /) -> Never: ... # pyrefly:ignore[bad-override] + +@type_check_only +class _ufunc_12(np.ufunc): # type: ignore[misc] + @property + @override + def identity(self) -> None: ... + @property + @override + def nin(self) -> Literal[1]: ... + @property + @override + def nout(self) -> Literal[2]: ... + @property + @override + def nargs(self) -> Literal[3]: ... + @property + @override + def signature(self) -> None: ... # + @final + @override + def at(self, a: Never, indices: Never, /) -> Never: ... # pyrefly:ignore[bad-override] + @final @override def accumulate(self, array: Never, /) -> Never: ... # pyrefly:ignore[bad-override] + @final @override def reduce(self, array: Never, /) -> Never: ... # pyrefly:ignore[bad-override] + @final @override def reduceat(self, array: Never, /, indices: Never) -> Never: ... # pyrefly:ignore[bad-override] + @final @override def outer(self, A: Never, B: Never, /) -> Never: ... # pyrefly:ignore[bad-override] @@ -2885,7 +2928,6 @@ class _ufunc_11_sut_i(_ufunc_11[Literal[0]]): # type: ignore[misc] def at[IxT, OutT](self, a: _CanUfuncAt1[IxT, OutT], indices: IxT, /) -> OutT: ... isnat: Final[_ufunc_11_m_b] = ... - signbit: Final[_ufunc_11_f_b] = ... isfinite: Final[_ufunc_11_bifgcm_b] = ... @@ -2957,6 +2999,249 @@ isupper: Final[_ufunc_11_sut_b] = ... str_len: Final[_ufunc_11_sut_i] = ... +# efdg => (efdg, i) +@type_check_only +class _ufunc_12_frexp(_ufunc_12): # type: ignore[misc] + @override + @overload # Nd, known dtype + def __call__[ShapeT: _Shape, DTypeT: np.dtype[np.floating]]( + self, + x: np.ndarray[ShapeT, DTypeT], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[np.ndarray[ShapeT, DTypeT], np.ndarray[ShapeT, np.dtype[np.int32]]]: ... + @overload # Nd, +f64 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_to_integer]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[np.ndarray[ShapeT, np.dtype[np.float64]], np.ndarray[ShapeT, np.dtype[np.int32]]]: ... + @overload # scalar, known dtype + def __call__[ScalarT: np.floating]( + self, + x: ScalarT, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[ScalarT, np.int32]: ... + @overload # scalar, float | +f64 + def __call__( + self, + x: float | _to_integer, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[np.float64, np.int32]: ... + @overload # 1d, +float + def __call__( + self, + x: Sequence[float], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[_Array1D[np.float64], _Array1D[np.int32]]: ... + @overload # 2d, +float + def __call__( + self, + x: Sequence[Sequence[float]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[_Array2D[np.float64], _Array2D[np.int32]]: ... + @overload # ?d, unknown dtype + def __call__( + self, + x: _ArrayLikeFloat_co, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[Any]: ... + @overload # out= + def __call__[OutT1: np.ndarray, OutT2: np.ndarray]( + self, + x: _ArrayLikeFloat_co, + /, + out: tuple[OutT1, OutT2], + *, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[OutT1, OutT2]: ... + @overload # out= + def __call__[OutT]( + self, + x: _CanUfuncCall1[OutT], + /, + out: object | None = None, + *, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs12], + ) -> OutT: ... + +# efdg => (efdg, efdg) +@type_check_only +class _ufunc_12_modf(_ufunc_12): # type: ignore[misc] + @override + @overload # known shape, known scalar/array + def __call__[T: np.floating | npt.NDArray[np.floating]]( + self, + x: T, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[T]: ... + @overload # Nd, +f64 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_to_integer]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[np.ndarray[ShapeT, np.dtype[np.float64]]]: ... + @overload # scalar, float | +f64 + def __call__( + self, + x: float | _to_integer, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[np.float64]: ... + @overload # 1d, +float + def __call__( + self, + x: Sequence[float], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[_Array1D[np.float64]]: ... + @overload # 2d, +float + def __call__( + self, + x: Sequence[Sequence[float]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[_Array2D[np.float64]]: ... + @overload # scalar, dtype= + def __call__[ScalarT: np.floating]( + self, + x: _FloatLike_co, + /, + *, + out: None = None, + dtype: _DTypeLike[ScalarT], + **kwargs: Unpack[_Kwargs11], + ) -> _tuple2[ScalarT]: ... + @overload # Nd, dtype= + def __call__[ShapeT: _Shape, ScalarT: np.floating]( + self, + x: np.ndarray[ShapeT, np.dtype[_to_floating]], + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLike[ScalarT], + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[np.ndarray[ShapeT, np.dtype[ScalarT]]]: ... + @overload # Nd, dtype= + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_to_floating]], + /, + *, + out: EllipsisType | None = None, + dtype: npt.DTypeLike, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[np.ndarray[ShapeT]]: ... + @overload # Nd, dtype= + def __call__[ScalarT: np.floating]( + self, + x: _NestedSequence[float], + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLike[ScalarT], + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[npt.NDArray[ScalarT]]: ... + @overload # Nd, dtype= + def __call__( + self, + x: _NestedSequence[float], + /, + *, + out: EllipsisType | None = None, + dtype: npt.DTypeLike, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[np.ndarray]: ... + @overload # ?d, dtype= + def __call__[ScalarT: np.floating]( + self, + x: _ArrayLikeFloat_co, + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLike[ScalarT], + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[npt.NDArray[ScalarT] | Any]: ... # `| Any` because of overlap + @overload # ?d, dtype= + def __call__( + self, + x: _ArrayLikeFloat_co, + /, + *, + out: EllipsisType | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[Any]: ... + @overload # out= + def __call__[OutT1: np.ndarray, OutT2: np.ndarray]( + self, + x: _ArrayLikeFloat_co, + /, + out: tuple[OutT1, OutT2], + *, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[OutT1, OutT2]: ... + @overload # out= + def __call__[OutT]( + self, + x: _CanUfuncCall1[OutT], + /, + out: object | None = None, + *, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs12], + ) -> OutT: ... + +frexp: Final[_ufunc_12_frexp] = ... +modf: Final[_ufunc_12_modf] = ... + ### # re-exports from `_core._multiarray_umath` that are used by `_core._ufunc_config` diff --git a/numpy/_typing/__init__.py b/numpy/_typing/__init__.py index 56e7b0475133..aebd9cc80615 100644 --- a/numpy/_typing/__init__.py +++ b/numpy/_typing/__init__.py @@ -140,7 +140,6 @@ # from ._ufunc import ( _GUFunc_Nin2_Nout1 as _GUFunc_Nin2_Nout1, - _UFunc_Nin1_Nout2 as _UFunc_Nin1_Nout2, _UFunc_Nin2_Nout1 as _UFunc_Nin2_Nout1, _UFunc_Nin2_Nout2 as _UFunc_Nin2_Nout2, ) diff --git a/numpy/_typing/_ufunc.py b/numpy/_typing/_ufunc.py index c40b02d039cd..7f90b906c541 100644 --- a/numpy/_typing/_ufunc.py +++ b/numpy/_typing/_ufunc.py @@ -1,6 +1,5 @@ from numpy import ufunc _UFunc_Nin2_Nout1 = ufunc -_UFunc_Nin1_Nout2 = ufunc _UFunc_Nin2_Nout2 = ufunc _GUFunc_Nin2_Nout1 = ufunc diff --git a/numpy/_typing/_ufunc.pyi b/numpy/_typing/_ufunc.pyi index 77fd8dbea3fe..1890bea74b50 100644 --- a/numpy/_typing/_ufunc.pyi +++ b/numpy/_typing/_ufunc.pyi @@ -273,80 +273,6 @@ class _UFunc_Nin2_Nout1[NameT: LiteralString, NTypesT: int, IdentT](ufunc): # t /, ) -> None: ... -@type_check_only -class _UFunc_Nin1_Nout2[NameT: LiteralString, NTypesT: int, IdentT](ufunc): # type: ignore[misc] - @property - def __name__(self) -> NameT: ... - @property - def __qualname__(self) -> NameT: ... # pyright: ignore[reportIncompatibleVariableOverride] - @property - def ntypes(self) -> NTypesT: ... - @property - def identity(self) -> IdentT: ... - @property - def nin(self) -> Literal[1]: ... - @property - def nout(self) -> Literal[2]: ... - @property - def nargs(self) -> Literal[3]: ... - @property - def signature(self) -> None: ... - - @overload - def __call__( - self, - x1: _ScalarLike_co, - out1: EllipsisType | None = ..., - out2: None = None, - /, - *, - out: EllipsisType | None = ..., - dtype: DTypeLike | None = None, - where: _ArrayLikeBool_co | None = True, - casting: _CastingKind = ..., - order: _OrderKACF = ..., - subok: bool = ..., - signature: str | _3Tuple[str | None] = ..., - ) -> _2Tuple[Incomplete]: ... - @overload - def __call__( - self, - x1: ArrayLike, - out1: np.ndarray | EllipsisType | None = ..., - out2: np.ndarray | None = ..., - /, - *, - out: _2Tuple[np.ndarray] | EllipsisType = ..., - dtype: DTypeLike | None = None, - where: _ArrayLikeBool_co | None = True, - casting: _CastingKind = ..., - order: _OrderKACF = ..., - subok: bool = ..., - signature: str | _3Tuple[str | None] = ..., - ) -> _2Tuple[NDArray[Incomplete]]: ... - @overload - def __call__( - self, - x1: _SupportsArrayUFunc, - out1: np.ndarray | EllipsisType | None = ..., - out2: np.ndarray | None = ..., - /, - *, - out: _2Tuple[np.ndarray] | EllipsisType = ..., - dtype: DTypeLike | None = None, - where: _ArrayLikeBool_co | None = True, - casting: _CastingKind = ..., - order: _OrderKACF = ..., - subok: bool = ..., - signature: str | _3Tuple[str | None] = ..., - ) -> _2Tuple[Incomplete]: ... - - def accumulate(self, array: Never, /) -> NoReturn: ... # type: ignore[override] - def reduce(self, array: Never, /) -> NoReturn: ... # type: ignore[override] - def reduceat(self, array: Never, /, indices: Never) -> NoReturn: ... # type: ignore[override] - def outer(self, A: Never, B: Never, /) -> NoReturn: ... # type: ignore[override] - def at(self, a: Never, indices: Never, /) -> NoReturn: ... # type: ignore[override] - @type_check_only class _UFunc_Nin2_Nout2[NameT: LiteralString, NTypesT: int, IdentT](ufunc): # type: ignore[misc] @property diff --git a/numpy/typing/tests/data/fail/ufuncs.pyi b/numpy/typing/tests/data/fail/ufuncs.pyi index 1b1628d7da44..1e6ec7ed15f6 100644 --- a/numpy/typing/tests/data/fail/ufuncs.pyi +++ b/numpy/typing/tests/data/fail/ufuncs.pyi @@ -12,6 +12,3 @@ np.add(1, 1, 1) # type: ignore[call-overload] np.add(1, 1, axis=0) # type: ignore[call-overload] np.matmul(AR_f8, AR_f8, where=True) # type: ignore[call-overload] - -np.frexp(AR_f8, out=None) # type: ignore[call-overload] -np.frexp(AR_f8, out=AR_f8) # type: ignore[call-overload] diff --git a/numpy/typing/tests/data/reveal/ufuncs.pyi b/numpy/typing/tests/data/reveal/ufuncs.pyi index dcc8a25a9812..c38314272cb9 100644 --- a/numpy/typing/tests/data/reveal/ufuncs.pyi +++ b/numpy/typing/tests/data/reveal/ufuncs.pyi @@ -25,17 +25,6 @@ assert_type(np.add.reduceat(AR_f8, AR_i8), npt.NDArray[Any]) assert_type(np.add.outer(f8, f8), Any) assert_type(np.add.outer(AR_f8, f8), npt.NDArray[Any]) -assert_type(np.frexp.__name__, Literal["frexp"]) -assert_type(np.frexp.__qualname__, Literal["frexp"]) -assert_type(np.frexp.ntypes, Literal[4]) -assert_type(np.frexp.identity, None) -assert_type(np.frexp.nin, Literal[1]) -assert_type(np.frexp.nout, Literal[2]) -assert_type(np.frexp.nargs, Literal[3]) -assert_type(np.frexp.signature, None) -assert_type(np.frexp(f8), tuple[Any, Any]) -assert_type(np.frexp(AR_f8), tuple[npt.NDArray[Any], npt.NDArray[Any]]) - assert_type(np.divmod.__name__, Literal["divmod"]) assert_type(np.divmod.__qualname__, Literal["divmod"]) assert_type(np.divmod.ntypes, Literal[15]) @@ -671,3 +660,113 @@ assert_type(np.abs(_u8_2d, dtype="f4"), np.ndarray[tuple[int, int]]) assert_type(np.abs(_py_b_1d, out=_i16_1d), np.ndarray[tuple[int], np.dtype[np.int16]]) assert_type(np.abs(_py_i_2d, out=_f32_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) + +# _ufunc_12_frexp + +assert_type(np.frexp(_py_i_0d), tuple[np.float64, np.int32]) +assert_type( + np.frexp(_py_i_1d), + tuple[ + np.ndarray[tuple[int], np.dtype[np.float64]], + np.ndarray[tuple[int], np.dtype[np.int32]], + ], +) +assert_type( + np.frexp(_py_i_2d), + tuple[ + np.ndarray[tuple[int, int], np.dtype[np.float64]], + np.ndarray[tuple[int, int], np.dtype[np.int32]], + ], +) +assert_type(np.frexp(_py_f_0d), tuple[np.float64, np.int32]) +assert_type( + np.frexp(_py_f_1d), + tuple[ + np.ndarray[tuple[int], np.dtype[np.float64]], + np.ndarray[tuple[int], np.dtype[np.int32]], + ], +) +assert_type( + np.frexp(_py_f_2d), + tuple[ + np.ndarray[tuple[int, int], np.dtype[np.float64]], + np.ndarray[tuple[int, int], np.dtype[np.int32]], + ], +) + +assert_type(np.frexp(_i16_0d), tuple[np.float64, np.int32]) +assert_type( + np.frexp(_i16_1d), + tuple[ + np.ndarray[tuple[int], np.dtype[np.float64]], + np.ndarray[tuple[int], np.dtype[np.int32]], + ], +) +assert_type( + np.frexp(_i16_2d), + tuple[ + np.ndarray[tuple[int, int], np.dtype[np.float64]], + np.ndarray[tuple[int, int], np.dtype[np.int32]], + ], +) +assert_type(np.frexp(_i16_nd), tuple[npt.NDArray[np.float64], npt.NDArray[np.int32]]) +assert_type(np.frexp(_f32_0d), tuple[np.float32, np.int32]) +assert_type( + np.frexp(_f32_1d), + tuple[ + np.ndarray[tuple[int], np.dtype[np.float32]], + np.ndarray[tuple[int], np.dtype[np.int32]], + ], +) +assert_type( + np.frexp(_f32_2d), + tuple[ + np.ndarray[tuple[int, int], np.dtype[np.float32]], + np.ndarray[tuple[int, int], np.dtype[np.int32]], + ], +) +assert_type(np.frexp(_f32_nd), tuple[npt.NDArray[np.float32], npt.NDArray[np.int32]]) + +assert_type( + np.frexp(_py_i_2d, out=(_f32_2d, _i16_2d)), + tuple[ + np.ndarray[tuple[int, int], np.dtype[np.float32]], + np.ndarray[tuple[int, int], np.dtype[np.int16]], + ] +) + +# _ufunc_12_modf + +type _tuple2[T] = tuple[T, T] + +assert_type(np.modf(_py_i_0d), _tuple2[np.float64]) +assert_type(np.modf(_py_i_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float64]]]) +assert_type(np.modf(_py_i_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float64]]]) +assert_type(np.modf(_py_f_0d), _tuple2[np.float64]) +assert_type(np.modf(_py_f_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float64]]]) +assert_type(np.modf(_py_f_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float64]]]) + +assert_type(np.modf(_i16_0d), _tuple2[np.float64]) +assert_type(np.modf(_i16_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float64]]]) +assert_type(np.modf(_i16_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float64]]]) +assert_type(np.modf(_i16_nd), _tuple2[npt.NDArray[np.float64]]) +assert_type(np.modf(_f32_0d), _tuple2[np.float32]) +assert_type(np.modf(_f32_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float32]]]) +assert_type(np.modf(_f32_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float32]]]) +assert_type(np.modf(_f32_nd), _tuple2[npt.NDArray[np.float32]]) + +assert_type(np.modf(_py_i_0d, dtype=np.float32), _tuple2[np.float32]) +assert_type(np.modf(_py_i_1d, dtype=np.float32), _tuple2[npt.NDArray[np.float32]]) +assert_type(np.modf(_i16_2d, dtype=np.float32), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float32]]]) + +assert_type(np.modf(_py_i_0d, dtype="f4"), _tuple2[Any]) +assert_type(np.modf(_py_i_1d, dtype="f4"), _tuple2[np.ndarray]) +assert_type(np.modf(_i16_2d, dtype="f4"), _tuple2[np.ndarray[tuple[int, int]]]) + +assert_type( + np.modf(_py_i_2d, out=(_f32_2d, _i16_2d)), + tuple[ + np.ndarray[tuple[int, int], np.dtype[np.float32]], + np.ndarray[tuple[int, int], np.dtype[np.int16]], + ] +) From bc98cbc85219b748f86afd7035ef09ad7eed87da Mon Sep 17 00:00:00 2001 From: Shoumik Chakravarty Date: Tue, 30 Jun 2026 09:57:54 -0500 Subject: [PATCH 05/60] DOC: fix duplicated-word and grammar typos in C-API reference (#31802) --- doc/source/reference/c-api/array.rst | 2 +- doc/source/reference/c-api/datetimes.rst | 2 +- numpy/_core/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/reference/c-api/array.rst b/doc/source/reference/c-api/array.rst index 07dd7068f3a9..daeef17d1ad7 100644 --- a/doc/source/reference/c-api/array.rst +++ b/doc/source/reference/c-api/array.rst @@ -4088,7 +4088,7 @@ Other conversions Convert any Python sequence (or single Python number) passed in as *seq* to (up to) *maxvals* pointer-sized integers and place them - in the *vals* array. The sequence can be smaller then *maxvals* as + in the *vals* array. The sequence can be smaller than *maxvals* as the number of converted objects is returned. .. _including-the-c-api: diff --git a/doc/source/reference/c-api/datetimes.rst b/doc/source/reference/c-api/datetimes.rst index 34fc81ed1351..63d2365f6358 100644 --- a/doc/source/reference/c-api/datetimes.rst +++ b/doc/source/reference/c-api/datetimes.rst @@ -3,7 +3,7 @@ Datetime API NumPy represents dates internally using an int64 counter and a unit metadata struct. Time differences are represented similarly using an int64 and a unit -metadata struct. The functions described below are available to to facilitate +metadata struct. The functions described below are available to facilitate converting between ISO 8601 date strings, NumPy datetimes, and Python datetime objects in C. diff --git a/numpy/_core/__init__.py b/numpy/_core/__init__.py index ede50aaeefc3..e32751d220ca 100644 --- a/numpy/_core/__init__.py +++ b/numpy/_core/__init__.py @@ -49,7 +49,7 @@ tag = sys.implementation.cache_tag or sys.implementation.name bad_c_module_info = ( f"The following compiled module files exist, but seem incompatible\n" - f"with with either python '{tag}' or the " + f"with either python '{tag}' or the " f"platform '{sys.platform}':\n\n * {candidate_str}\n" ) else: From 984467d8ff93d5f95685d15556d4ecdfa1469db0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 30 Jun 2026 13:39:29 -0600 Subject: [PATCH 06/60] BUG: Fix interaction between warnings and the promotion cache (#31617) --- numpy/_core/src/multiarray/npy_static_data.c | 6 ++ numpy/_core/src/multiarray/npy_static_data.h | 7 ++ numpy/_core/src/umath/dispatching.cpp | 13 ++- numpy/_core/src/umath/ufunc_type_resolution.c | 81 ++++++++++++------- numpy/_core/src/umath/ufunc_type_resolution.h | 11 +++ numpy/_core/tests/test_deprecations.py | 25 +++++- 6 files changed, 112 insertions(+), 31 deletions(-) diff --git a/numpy/_core/src/multiarray/npy_static_data.c b/numpy/_core/src/multiarray/npy_static_data.c index 09c1fc713f30..78c6f6875a9c 100644 --- a/numpy/_core/src/multiarray/npy_static_data.c +++ b/numpy/_core/src/multiarray/npy_static_data.c @@ -180,6 +180,12 @@ initialize_static_globals(void) return -1; } + npy_static_pydata.legacy_resolver_promoting = + PyContextVar_New("numpy._legacy_resolver_promoting", Py_False); + if (npy_static_pydata.legacy_resolver_promoting == NULL) { + return -1; + } + npy_static_pydata.kwnames_is_copy = PyTuple_Pack(1, npy_interned_str.copy); if (npy_static_pydata.kwnames_is_copy == NULL) { return -1; diff --git a/numpy/_core/src/multiarray/npy_static_data.h b/numpy/_core/src/multiarray/npy_static_data.h index 3536dac8f1ea..c8c730d3dc28 100644 --- a/numpy/_core/src/multiarray/npy_static_data.h +++ b/numpy/_core/src/multiarray/npy_static_data.h @@ -132,6 +132,13 @@ typedef struct npy_static_pydata_struct { PyObject *os_fspath; PyObject *format_options; + /* + * Context variable set to True while the legacy ufunc type resolvers + * run for promotion, to suppress their deprecation warnings (the + * resolution step warns on every call). + */ + PyObject *legacy_resolver_promoting; + /* * Used in the __array__ internals to avoid building a tuple inline */ diff --git a/numpy/_core/src/umath/dispatching.cpp b/numpy/_core/src/umath/dispatching.cpp index 340c0bd7f8e2..afe0d911e494 100644 --- a/numpy/_core/src/umath/dispatching.cpp +++ b/numpy/_core/src/umath/dispatching.cpp @@ -735,9 +735,18 @@ legacy_promote_using_legacy_type_resolver(PyUFuncObject *ufunc, * difference. Whether the actual operands can be casts must be checked * during the type resolution step (which may _also_ calls this!). */ - if (ufunc->type_resolver(ufunc, + PyObject *promotion_token = npy_begin_legacy_resolver_promotion(); + if (promotion_token == NULL) { + Py_XDECREF(type_tuple); + return -1; + } + int resolver_result = ufunc->type_resolver(ufunc, NPY_UNSAFE_CASTING, (PyArrayObject **)ops, type_tuple, - out_descrs) < 0) { + out_descrs); + if (npy_end_legacy_resolver_promotion(promotion_token) < 0) { + resolver_result = -1; + } + if (resolver_result < 0) { Py_XDECREF(type_tuple); /* Not all legacy resolvers clean up on failures: */ for (int i = 0; i < nargs; i++) { diff --git a/numpy/_core/src/umath/ufunc_type_resolution.c b/numpy/_core/src/umath/ufunc_type_resolution.c index fb90dac1d0ca..7fb02558810b 100644 --- a/numpy/_core/src/umath/ufunc_type_resolution.c +++ b/numpy/_core/src/umath/ufunc_type_resolution.c @@ -38,6 +38,7 @@ #include "numpy/ndarraytypes.h" #include "numpy/ufuncobject.h" #include "npy_import.h" +#include "npy_static_data.h" #include "ufunc_type_resolution.h" #include "ufunc_object.h" #include "common.h" @@ -72,6 +73,51 @@ npy_casting_to_py_object(NPY_CASTING casting) } +/* + * The dispatching code calls the legacy type resolvers a second time to + * promote (`legacy_promote_using_legacy_type_resolver`); that result is + * cached while the type-resolution call happens on every ufunc invocation. + * The promotion call is bracketed with this context variable so each + * invocation warns exactly once, no matter the promotion cache state. + * The warnings machinery cannot deduplicate the two emissions for us: + * under `simplefilter("always")` it delivers everything by design. + */ +NPY_NO_EXPORT PyObject * +npy_begin_legacy_resolver_promotion(void) +{ + return PyContextVar_Set( + npy_static_pydata.legacy_resolver_promoting, Py_True); +} + +NPY_NO_EXPORT int +npy_end_legacy_resolver_promotion(PyObject *token) +{ + int result = PyContextVar_Reset( + npy_static_pydata.legacy_resolver_promoting, token); + Py_DECREF(token); + return result; +} + +static int +deprecate_integer_datetime_operation(void) +{ + PyObject *promoting; + if (PyContextVar_Get(npy_static_pydata.legacy_resolver_promoting, + Py_False, &promoting) < 0) { + return -1; + } + int skip = (promoting == Py_True); + Py_DECREF(promoting); + if (skip) { + return 0; + } + return DEPRECATE( + "The 'generic' unit for NumPy timedelta is deprecated, " + "and will raise an error in the future. " + "Please convert the integer with an explicit unit."); +} + + /** * Always returns -1 to indicate the exception was raised, for convenience */ @@ -818,10 +864,7 @@ PyUFunc_AdditionTypeResolver(PyUFuncObject *ufunc, /* m8[] + int => m8[] + m8[] */ else if (PyTypeNum_ISINTEGER(type_num2) || PyTypeNum_ISBOOL(type_num2)) { - if (DEPRECATE( - "The 'generic' unit for NumPy timedelta is deprecated, " - "and will raise an error in the future. " - "Please convert the integer with an explicit unit.") < 0) { + if (deprecate_integer_datetime_operation() < 0) { return -1; } out_dtypes[0] = NPY_DT_CALL_ensure_canonical( @@ -861,10 +904,7 @@ PyUFunc_AdditionTypeResolver(PyUFuncObject *ufunc, /* M8[] + int => M8[] + m8[] */ else if (PyTypeNum_ISINTEGER(type_num2) || PyTypeNum_ISBOOL(type_num2)) { - if (DEPRECATE( - "The 'generic' unit for NumPy timedelta is deprecated, " - "and will raise an error in the future. " - "Please convert the integer with an explicit unit.") < 0) { + if (deprecate_integer_datetime_operation() < 0) { return -1; } out_dtypes[0] = NPY_DT_CALL_ensure_canonical( @@ -892,10 +932,7 @@ PyUFunc_AdditionTypeResolver(PyUFuncObject *ufunc, else if (PyTypeNum_ISINTEGER(type_num1) || PyTypeNum_ISBOOL(type_num1)) { /* int + m8[] => m8[] + m8[] */ if (type_num2 == NPY_TIMEDELTA) { - if (DEPRECATE( - "The 'generic' unit for NumPy timedelta is deprecated, " - "and will raise an error in the future. " - "Please convert the integer with an explicit unit.") < 0) { + if (deprecate_integer_datetime_operation() < 0) { return -1; } out_dtypes[0] = NPY_DT_CALL_ensure_canonical( @@ -911,10 +948,7 @@ PyUFunc_AdditionTypeResolver(PyUFuncObject *ufunc, type_num1 = NPY_TIMEDELTA; } else if (type_num2 == NPY_DATETIME) { - if (DEPRECATE( - "The 'generic' unit for NumPy timedelta is deprecated, " - "and will raise an error in the future. " - "Please convert the integer with an explicit unit.") < 0) { + if (deprecate_integer_datetime_operation() < 0) { return -1; } /* Make a new NPY_TIMEDELTA, and copy type2's metadata */ @@ -1015,10 +1049,7 @@ PyUFunc_SubtractionTypeResolver(PyUFuncObject *ufunc, /* m8[] - int => m8[] - m8[] */ else if (PyTypeNum_ISINTEGER(type_num2) || PyTypeNum_ISBOOL(type_num2)) { - if (DEPRECATE( - "The 'generic' unit for NumPy timedelta is deprecated, " - "and will raise an error in the future. " - "Please convert the integer with an explicit unit.") < 0) { + if (deprecate_integer_datetime_operation() < 0) { return -1; } out_dtypes[0] = NPY_DT_CALL_ensure_canonical( @@ -1058,10 +1089,7 @@ PyUFunc_SubtractionTypeResolver(PyUFuncObject *ufunc, /* M8[] - int => M8[] - m8[] */ else if (PyTypeNum_ISINTEGER(type_num2) || PyTypeNum_ISBOOL(type_num2)) { - if (DEPRECATE( - "The 'generic' unit for NumPy timedelta is deprecated, " - "and will raise an error in the future. " - "Please convert the integer with an explicit unit.") < 0) { + if (deprecate_integer_datetime_operation() < 0) { return -1; } out_dtypes[0] = NPY_DT_CALL_ensure_canonical( @@ -1105,10 +1133,7 @@ PyUFunc_SubtractionTypeResolver(PyUFuncObject *ufunc, else if (PyTypeNum_ISINTEGER(type_num1) || PyTypeNum_ISBOOL(type_num1)) { /* int - m8[] => m8[] - m8[] */ if (type_num2 == NPY_TIMEDELTA) { - if (DEPRECATE( - "The 'generic' unit for NumPy timedelta is deprecated, " - "and will raise an error in the future. " - "Please convert the integer with an explicit unit.") < 0) { + if (deprecate_integer_datetime_operation() < 0) { return -1; } out_dtypes[0] = NPY_DT_CALL_ensure_canonical( diff --git a/numpy/_core/src/umath/ufunc_type_resolution.h b/numpy/_core/src/umath/ufunc_type_resolution.h index 531e9267afa2..77cd5165dd39 100644 --- a/numpy/_core/src/umath/ufunc_type_resolution.h +++ b/numpy/_core/src/umath/ufunc_type_resolution.h @@ -5,6 +5,17 @@ extern "C" { #endif +/* + * Bracket legacy type resolver calls made (only) for promotion, so they + * do not emit deprecation warnings. `begin` returns a context variable + * token (or NULL with an error set); `end` consumes the token. + */ +NPY_NO_EXPORT PyObject * +npy_begin_legacy_resolver_promotion(void); + +NPY_NO_EXPORT int +npy_end_legacy_resolver_promotion(PyObject *token); + NPY_NO_EXPORT int PyUFunc_SimpleBinaryComparisonTypeResolver(PyUFuncObject *ufunc, NPY_CASTING casting, diff --git a/numpy/_core/tests/test_deprecations.py b/numpy/_core/tests/test_deprecations.py index 70eedde7003a..c9d1561d46c9 100644 --- a/numpy/_core/tests/test_deprecations.py +++ b/numpy/_core/tests/test_deprecations.py @@ -5,6 +5,8 @@ """ import contextlib import re +import sys +import textwrap import warnings from collections.abc import Callable @@ -12,7 +14,8 @@ import numpy as np from numpy._core._multiarray_tests import fromstring_null_term_c_api # noqa: F401 -from numpy.testing import assert_raises +from numpy.testing import IS_WASM, assert_raises +from numpy.testing._private.utils import run_subprocess class _DeprecationTestCase: @@ -519,6 +522,26 @@ def test_non_associative_case_warns(self): self.assert_deprecated(np.add, num=None, args=(a, b)) self.assert_deprecated(np.add, num=None, args=(b, c)) + @pytest.mark.skipif(IS_WASM, reason="wasm doesn't have support for subprocess") + def test_first_call_warns_once(self): + # The legacy type resolver used to also warn when invoked to populate + # the ufunc promotion cache, so the first call in a process emitted + # two warnings and later calls one. Use a fresh process to test with + # a cold cache. + code = textwrap.dedent(""" + import warnings + import numpy as np + + for args in [(np.timedelta64(3, "s"), 5), + (np.array([3], dtype="m8[s]"), np.array([5]))]: + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + np.subtract(*args) + assert len(w) == 1, [str(x.message) for x in w] + """) + p = run_subprocess([sys.executable, "-c", code]) + assert p.returncode == 0, p.stderr + class TestTriDeprecationWithNonInteger(_DeprecationTestCase): # Deprecation in NumPy 2.5, 2026-03 From dd1680dce5ff62afed52f48caf41809a447b6a88 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Wed, 1 Jul 2026 00:26:44 +0200 Subject: [PATCH 07/60] TYP: specialized binary comparison ufuncs (#31813) --- numpy/__init__.pyi | 16 +- numpy/_core/umath.pyi | 432 ++++++++++++++++++++- numpy/typing/tests/data/reveal/strings.pyi | 36 +- numpy/typing/tests/data/reveal/ufuncs.pyi | 67 ++++ 4 files changed, 518 insertions(+), 33 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index a826304c6323..57a4998ed249 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -412,23 +412,31 @@ from numpy._core.umath import ( cosh, deg2rad, degrees, + equal, exp, exp2, expm1, fabs, floor, frexp, + greater, + greater_equal, invert, + invert as bitwise_invert, + invert as bitwise_not, isfinite, isinf, isnan, isnat, + less, + less_equal, log, log10, log1p, log2, logical_not, modf, + not_equal, positive, negative, rad2deg, @@ -7607,22 +7615,17 @@ bitwise_xor: _UFunc_Nin2_Nout1[L["bitwise_xor"], L[12], L[0]] copysign: _UFunc_Nin2_Nout1[L["copysign"], L[4], None] divide: _UFunc_Nin2_Nout1[L["divide"], L[11], None] divmod: _UFunc_Nin2_Nout2[L["divmod"], L[15], None] -equal: _UFunc_Nin2_Nout1[L["equal"], L[23], None] float_power: _UFunc_Nin2_Nout1[L["float_power"], L[4], None] floor_divide: _UFunc_Nin2_Nout1[L["floor_divide"], L[21], None] fmax: _UFunc_Nin2_Nout1[L["fmax"], L[21], None] fmin: _UFunc_Nin2_Nout1[L["fmin"], L[21], None] fmod: _UFunc_Nin2_Nout1[L["fmod"], L[15], None] gcd: _UFunc_Nin2_Nout1[L["gcd"], L[11], L[0]] -greater: _UFunc_Nin2_Nout1[L["greater"], L[23], None] -greater_equal: _UFunc_Nin2_Nout1[L["greater_equal"], L[23], None] heaviside: _UFunc_Nin2_Nout1[L["heaviside"], L[4], None] hypot: _UFunc_Nin2_Nout1[L["hypot"], L[5], L[0]] lcm: _UFunc_Nin2_Nout1[L["lcm"], L[11], None] ldexp: _UFunc_Nin2_Nout1[L["ldexp"], L[8], None] left_shift: _UFunc_Nin2_Nout1[L["left_shift"], L[11], None] -less: _UFunc_Nin2_Nout1[L["less"], L[23], None] -less_equal: _UFunc_Nin2_Nout1[L["less_equal"], L[23], None] logaddexp2: _UFunc_Nin2_Nout1[L["logaddexp2"], L[4], float] logaddexp: _UFunc_Nin2_Nout1[L["logaddexp"], L[4], float] logical_and: _UFunc_Nin2_Nout1[L["logical_and"], L[20], L[True]] @@ -7634,7 +7637,6 @@ maximum: _UFunc_Nin2_Nout1[L["maximum"], L[21], None] minimum: _UFunc_Nin2_Nout1[L["minimum"], L[21], None] multiply: _UFunc_Nin2_Nout1[L["multiply"], L[23], L[1]] nextafter: _UFunc_Nin2_Nout1[L["nextafter"], L[4], None] -not_equal: _UFunc_Nin2_Nout1[L["not_equal"], L[23], None] power: _UFunc_Nin2_Nout1[L["power"], L[18], None] remainder: _UFunc_Nin2_Nout1[L["remainder"], L[16], None] right_shift: _UFunc_Nin2_Nout1[L["right_shift"], L[11], None] @@ -7645,8 +7647,6 @@ vecmat: _GUFunc_Nin2_Nout1[L["vecmat"], L[19], None, L["(n),(n,m)->(m)"]] atan2 = arctan2 concat = concatenate bitwise_left_shift = left_shift -bitwise_not = invert -bitwise_invert = invert bitwise_right_shift = right_shift mod = remainder permute_dims = transpose diff --git a/numpy/_core/umath.pyi b/numpy/_core/umath.pyi index 1e68943c1ebc..4abdc01517a3 100644 --- a/numpy/_core/umath.pyi +++ b/numpy/_core/umath.pyi @@ -34,7 +34,6 @@ from numpy import ( divide, divmod, e, - equal, euler_gamma, float_power, floor_divide, @@ -43,15 +42,11 @@ from numpy import ( fmod, frompyfunc, gcd, - greater, - greater_equal, heaviside, hypot, lcm, ldexp, left_shift, - less, - less_equal, logaddexp, logaddexp2, logical_and, @@ -64,7 +59,6 @@ from numpy import ( mod, multiply, nextafter, - not_equal, pi, power, remainder, @@ -91,6 +85,7 @@ from numpy._typing import ( _IntLike_co, _NestedSequence, _NumberLike_co, + _ScalarLike_co, _Shape, ) from numpy._typing._array_like import _DualArrayLike @@ -229,14 +224,53 @@ class _ExtOjbDict(TypedDict, total=False): type _Signature1 = tuple[str | None, str | None] | str type _Signature2 = tuple[str | None, str | None, str | None] | str +# TODO(@jorenham): make these `__array_ufunc__` protocols generic on `ufunc` so that +# implementing types can structurally determine the signature of the calling `ufunc`. + @type_check_only class _CanUfuncCall1[OutT](Protocol): - def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["__call__"], /, *args: Any, **kwargs: Any) -> OutT: ... + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["__call__"], x: Self, /) -> OutT: ... @type_check_only class _CanUfuncAt1[IxT, OutT](Protocol): def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["at"], a: Self, indices: IxT, /) -> OutT: ... +@type_check_only +class _CanUfuncCall2L[OtherT, OutT](Protocol): + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["__call__"], lhs: Self, rhs: OtherT, /) -> OutT: ... + +@type_check_only +class _CanUfuncCall2R[OtherT, OutT](Protocol): + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["__call__"], lhs: OtherT, rhs: Self, /) -> OutT: ... + +@type_check_only +class _CanUfuncOuterL[OtherT, OutT](Protocol): + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["outer"], lhs: Self, rhs: OtherT, /) -> OutT: ... + +@type_check_only +class _CanUfuncOuterR[OtherT, OutT](Protocol): + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["outer"], lhs: OtherT, rhs: Self, /) -> OutT: ... + +@type_check_only +class _CanUfuncAt2L[OtherT, IxT, OutT](Protocol): + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["at"], a: Self, indices: IxT, b: OtherT, /) -> OutT: ... + +@type_check_only +class _CanUfuncAt2R[OtherT, IxT, OutT](Protocol): + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["at"], a: OtherT, indices: IxT, b: Self, /) -> OutT: ... + +@type_check_only +class _CanUfuncAccumulate[OutT](Protocol): + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["accumulate"], a: Self, /) -> OutT: ... + +@type_check_only +class _CanUfuncReduce[OutT](Protocol): + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["reduce"], a: Self, /) -> OutT: ... + +@type_check_only +class _CanUfuncReduceAt[IxT, OutT](Protocol): + def __array_ufunc__(self, ufunc: np.ufunc, method: Literal["reduceat"], a: Self, indices: IxT, /) -> OutT: ... + ### # ufunc specializations @@ -260,6 +294,14 @@ class _Kwargs12(TypedDict, total=False): subok: bool # = True, signature: _Signature2 +@type_check_only +class _Kwargs21(TypedDict, total=False): + where: _ArrayLikeBool_co # = True + casting: _CastingKind # = "same_kind" + order: _OrderKACF # = "K", + subok: bool # = True, + signature: _Signature2 + _IdT_co = TypeVar("_IdT_co", covariant=True, default=None) @type_check_only @@ -279,6 +321,8 @@ class _ufunc_11(np.ufunc, Generic[_IdT_co]): # type: ignore[misc] @property @override def signature(self) -> None: ... + + # @override def accumulate(self, array: Never, /) -> Never: ... # pyrefly:ignore[bad-override] @override @@ -323,6 +367,24 @@ class _ufunc_12(np.ufunc): # type: ignore[misc] @override def outer(self, A: Never, B: Never, /) -> Never: ... # pyrefly:ignore[bad-override] +@type_check_only +class _ufunc_21(np.ufunc, Generic[_IdT_co]): # type: ignore[misc] + @property + @override + def identity(self) -> _IdT_co: ... + @property + @override + def nin(self) -> Literal[2]: ... + @property + @override + def nout(self) -> Literal[1]: ... + @property + @override + def nargs(self) -> Literal[3]: ... + @property + @override + def signature(self) -> None: ... + # Mm => ? @type_check_only class _ufunc_11_m_b(_ufunc_11): # type: ignore[misc] @@ -3242,6 +3304,362 @@ class _ufunc_12_modf(_ufunc_12): # type: ignore[misc] frexp: Final[_ufunc_12_frexp] = ... modf: Final[_ufunc_12_modf] = ... +# ?bBhHiIlLqQefdgFDGMmOSUT => ?O +# NOTE: These theoretically also supports O => O but that's an unlikely usecase because +# that'd require something incompatible with `bool` or `np.bool` to be returned from the +# respective "rich" comparison dunder methods of the underlying python objects. +# So for the sake of simplicity we assume that all `np.generic` are accepted and that +# the output dtype is always `np.bool`. +@type_check_only +class _ufunc_21_cmp(_ufunc_21[None]): # type: ignore[misc] + # TODO(@jorenham): shape-typing + + @override + @overload # 0d, 0d + def __call__( + self, + x1: _ScalarLike_co, + x2: _ScalarLike_co, + /, + *, + out: None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> np.bool: ... + @overload # >0d, >=0d + def __call__( + self, + x1: np.ndarray | _NestedSequence[_ScalarLike_co], + x2: npt.ArrayLike, + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # >=0d, >0d + def __call__( + self, + x1: npt.ArrayLike, + x2: np.ndarray | _NestedSequence[_ScalarLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # >=0d, >=0d, out=... + def __call__( + self, + x1: npt.ArrayLike, + x2: npt.ArrayLike, + /, + *, + out: EllipsisType, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # out= + def __call__[OutT: np.ndarray]( + self, + x1: npt.ArrayLike, + x2: npt.ArrayLike, + /, + out: OutT, + *, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x1.__array_ufunc__(self, "__call__", x1, x2, ...) + def __call__[OtherT, OutT]( + self, + x1: _CanUfuncCall2L[OtherT, OutT], + x2: OtherT, + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x2.__array_ufunc__(self, "__call__", x1, x2, ...) + def __call__[OtherT, OutT]( + self, + x1: OtherT, + x2: _CanUfuncCall2R[OtherT, OutT], + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + + # + @override + @overload # 0d, 0d + def outer( + self, + x1: _ScalarLike_co, + x2: _ScalarLike_co, + /, + *, + out: None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> np.bool: ... + @overload # >0d, >=0d + def outer( + self, + x1: np.ndarray | _NestedSequence[_ScalarLike_co], + x2: npt.ArrayLike, + /, + *, + out: None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # >=0d, >0d + def outer( + self, + x1: npt.ArrayLike, + x2: np.ndarray | _NestedSequence[_ScalarLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # >=0d, >=0d, out= + def outer[OutT: np.ndarray]( + self, + x1: npt.ArrayLike, + x2: npt.ArrayLike, + /, + *, + out: OutT, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # >=0d, >=0d (fallback) + def outer( + self, + x1: npt.ArrayLike, + x2: npt.ArrayLike, + /, + *, + out: None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool] | Any: ... + @overload # x1.__array_ufunc__(self, "outer", x1, x2, ...) + def outer[OtherT, OutT]( + self, + x1: _CanUfuncOuterL[OtherT, OutT], + x2: OtherT, + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x2.__array_ufunc__(self, "outer", x1, x2, ...) + def outer[OtherT, OutT]( + self, + x1: OtherT, + x2: _CanUfuncOuterR[OtherT, OutT], + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + + # + @override + @overload + def at(self, a: np.ndarray, indices: _ArrayLikeInt, b: npt.ArrayLike, /) -> None: ... # pyrefly:ignore[bad-override] + @overload + def at[OtherT, IxT, OutT](self, a: _CanUfuncAt2L[OtherT, IxT, OutT], indices: IxT, b: OtherT, /) -> OutT: ... + @overload + def at[OtherT, IxT, OutT](self, a: OtherT, indices: IxT, b: _CanUfuncAt2R[OtherT, IxT, OutT], /) -> OutT: ... + + # + @override + @overload # unknown shape + def reduce( # pyrefly:ignore[bad-override] + self, + array: _ArrayLikeBool_co, + /, + *, + axis: int | tuple[int, ...] = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.bool] | np.bool: ... + @overload # unknown shape, axis=None + def reduce( + self, + array: _ArrayLikeBool_co, + /, + *, + axis: None, + dtype: _DTypeLikeBool | None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> np.bool: ... + @overload # unknown shape, keepdims=True + def reduce( + self, + array: _ArrayLikeBool_co, + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + keepdims: Literal[True], + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.bool]: ... + @overload # unknown shape, out=... + def reduce( + self, + array: _ArrayLikeBool_co, + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: _DTypeLikeBool | None = None, + out: EllipsisType, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.bool]: ... + @overload # out= + def reduce[OutT: np.ndarray]( + self, + array: _ArrayLikeBool_co, + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: _DTypeLikeBool | None = None, + out: OutT, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> OutT: ... + @overload # array.__array_ufunc__(self, "reduceat", array, ...) + def reduce[OutT]( + self, + array: _CanUfuncReduce[OutT], + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: npt.DTypeLike | None = None, + out: np.ndarray | EllipsisType | None = None, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> OutT: ... + + # + @override + @overload # known shape + def reduceat[ShapeT: _Shape]( # pyrefly:ignore[bad-override] + self, + array: np.ndarray[ShapeT, np.dtype[np.bool]], + /, + indices: tuple[int, ...], + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + ) -> np.ndarray[ShapeT, np.dtype[np.bool]]: ... + @overload # unknown shape + def reduceat( + self, + array: _ArrayLikeBool_co, + /, + indices: tuple[int, ...], + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + ) -> npt.NDArray[np.bool]: ... + @overload # out= + def reduceat[OutT: np.ndarray]( + self, + array: _ArrayLikeBool_co, + /, + indices: tuple[int, ...], + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: OutT, + ) -> OutT: ... + @overload # array.__array_ufunc__(self, "reduceat", array, ...) + def reduceat[IxT, OutT]( + self, + array: _CanUfuncReduceAt[IxT, OutT], + /, + indices: IxT, + *, + axis: int = 0, + dtype: npt.DTypeLike | None = None, + out: np.ndarray | None = None, + ) -> OutT: ... + + # + @override + @overload # known shape + def accumulate[ShapeT: _Shape]( # pyrefly:ignore[bad-override] + self, + array: np.ndarray[ShapeT, np.dtype[np.bool]], + /, + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + ) -> np.ndarray[ShapeT, np.dtype[np.bool]]: ... + @overload # unknown shape + def accumulate( + self, + array: _ArrayLikeBool_co, + /, + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + ) -> npt.NDArray[np.bool]: ... + @overload # out= + def accumulate[OutT: np.ndarray]( + self, + array: _ArrayLikeBool_co, + /, + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: OutT, + ) -> OutT: ... + @overload # array.__array_ufunc__(self, "accumulate", array, ...) + def accumulate[OutT]( + self, + array: _CanUfuncAccumulate[OutT], + /, + *, + axis: int = 0, + dtype: npt.DTypeLike | None = None, + out: np.ndarray | None = None, + ) -> OutT: ... + +equal: Final[_ufunc_21_cmp] = ... +greater: Final[_ufunc_21_cmp] = ... +greater_equal: Final[_ufunc_21_cmp] = ... +less: Final[_ufunc_21_cmp] = ... +less_equal: Final[_ufunc_21_cmp] = ... +not_equal: Final[_ufunc_21_cmp] = ... + ### # re-exports from `_core._multiarray_umath` that are used by `_core._ufunc_config` diff --git a/numpy/typing/tests/data/reveal/strings.pyi b/numpy/typing/tests/data/reveal/strings.pyi index 357568c232b5..08694cf07b9d 100644 --- a/numpy/typing/tests/data/reveal/strings.pyi +++ b/numpy/typing/tests/data/reveal/strings.pyi @@ -11,29 +11,29 @@ AR_U: npt.NDArray[np.str_] AR_S: npt.NDArray[np.bytes_] AR_T: AR_T_alias -assert_type(np.strings.equal(AR_U, AR_U), np.ndarray) -assert_type(np.strings.equal(AR_S, AR_S), np.ndarray) -assert_type(np.strings.equal(AR_T, AR_T), np.ndarray) +assert_type(np.strings.equal(AR_U, AR_U), npt.NDArray[np.bool]) +assert_type(np.strings.equal(AR_S, AR_S), npt.NDArray[np.bool]) +assert_type(np.strings.equal(AR_T, AR_T), npt.NDArray[np.bool]) -assert_type(np.strings.not_equal(AR_U, AR_U), np.ndarray) -assert_type(np.strings.not_equal(AR_S, AR_S), np.ndarray) -assert_type(np.strings.not_equal(AR_T, AR_T), np.ndarray) +assert_type(np.strings.not_equal(AR_U, AR_U), npt.NDArray[np.bool]) +assert_type(np.strings.not_equal(AR_S, AR_S), npt.NDArray[np.bool]) +assert_type(np.strings.not_equal(AR_T, AR_T), npt.NDArray[np.bool]) -assert_type(np.strings.greater_equal(AR_U, AR_U), np.ndarray) -assert_type(np.strings.greater_equal(AR_S, AR_S), np.ndarray) -assert_type(np.strings.greater_equal(AR_T, AR_T), np.ndarray) +assert_type(np.strings.greater_equal(AR_U, AR_U), npt.NDArray[np.bool]) +assert_type(np.strings.greater_equal(AR_S, AR_S), npt.NDArray[np.bool]) +assert_type(np.strings.greater_equal(AR_T, AR_T), npt.NDArray[np.bool]) -assert_type(np.strings.less_equal(AR_U, AR_U), np.ndarray) -assert_type(np.strings.less_equal(AR_S, AR_S), np.ndarray) -assert_type(np.strings.less_equal(AR_T, AR_T), np.ndarray) +assert_type(np.strings.less_equal(AR_U, AR_U), npt.NDArray[np.bool]) +assert_type(np.strings.less_equal(AR_S, AR_S), npt.NDArray[np.bool]) +assert_type(np.strings.less_equal(AR_T, AR_T), npt.NDArray[np.bool]) -assert_type(np.strings.greater(AR_U, AR_U), np.ndarray) -assert_type(np.strings.greater(AR_S, AR_S), np.ndarray) -assert_type(np.strings.greater(AR_T, AR_T), np.ndarray) +assert_type(np.strings.greater(AR_U, AR_U), npt.NDArray[np.bool]) +assert_type(np.strings.greater(AR_S, AR_S), npt.NDArray[np.bool]) +assert_type(np.strings.greater(AR_T, AR_T), npt.NDArray[np.bool]) -assert_type(np.strings.less(AR_U, AR_U), np.ndarray) -assert_type(np.strings.less(AR_S, AR_S), np.ndarray) -assert_type(np.strings.less(AR_T, AR_T), np.ndarray) +assert_type(np.strings.less(AR_U, AR_U), npt.NDArray[np.bool]) +assert_type(np.strings.less(AR_S, AR_S), npt.NDArray[np.bool]) +assert_type(np.strings.less(AR_T, AR_T), npt.NDArray[np.bool]) assert_type(np.strings.add(AR_U, AR_U), np.ndarray) assert_type(np.strings.add(AR_S, AR_S), np.ndarray) diff --git a/numpy/typing/tests/data/reveal/ufuncs.pyi b/numpy/typing/tests/data/reveal/ufuncs.pyi index c38314272cb9..7cbc18c8a374 100644 --- a/numpy/typing/tests/data/reveal/ufuncs.pyi +++ b/numpy/typing/tests/data/reveal/ufuncs.pyi @@ -770,3 +770,70 @@ assert_type( np.ndarray[tuple[int, int], np.dtype[np.int16]], ] ) + +# _ufunc_21_cmp +# (equal, greater, greater_equal, less, less_equal, not_equal) + +assert_type(np.less(_py_b_0d, _py_b_0d), np.bool) +assert_type(np.less(_py_b_1d, _py_b_1d), npt.NDArray[np.bool]) +assert_type(np.less(_py_b_2d, _py_b_2d), npt.NDArray[np.bool]) +assert_type(np.less(_py_i_0d, _py_i_0d), np.bool) +assert_type(np.less(_py_i_1d, _py_i_1d), npt.NDArray[np.bool]) +assert_type(np.less(_py_i_2d, _py_i_2d), npt.NDArray[np.bool]) +assert_type(np.less(_py_f_0d, _py_f_0d), np.bool) +assert_type(np.less(_py_f_1d, _py_f_1d), npt.NDArray[np.bool]) +assert_type(np.less(_py_f_2d, _py_f_2d), npt.NDArray[np.bool]) +assert_type(np.less(_py_c_0d, _py_c_0d), np.bool) +assert_type(np.less(_py_c_1d, _py_c_1d), npt.NDArray[np.bool]) +assert_type(np.less(_py_c_2d, _py_c_2d), npt.NDArray[np.bool]) + +assert_type(np.less(_bool_0d, _bool_0d), np.bool) +assert_type(np.less(_bool_1d, _bool_1d), npt.NDArray[np.bool]) +assert_type(np.less(_bool_2d, _bool_2d), npt.NDArray[np.bool]) +assert_type(np.less(_bool_nd, _bool_nd), npt.NDArray[np.bool]) +assert_type(np.less(_i16_0d, _i16_0d), np.bool) +assert_type(np.less(_i16_1d, _i16_1d), npt.NDArray[np.bool]) +assert_type(np.less(_i16_2d, _i16_2d), npt.NDArray[np.bool]) +assert_type(np.less(_i16_nd, _i16_nd), npt.NDArray[np.bool]) +assert_type(np.less(_f32_0d, _f32_0d), np.bool) +assert_type(np.less(_f32_1d, _f32_1d), npt.NDArray[np.bool]) +assert_type(np.less(_f32_2d, _f32_2d), npt.NDArray[np.bool]) +assert_type(np.less(_f32_nd, _f32_nd), npt.NDArray[np.bool]) +assert_type(np.less(_c64_0d, _c64_0d), np.bool) +assert_type(np.less(_c64_1d, _c64_1d), npt.NDArray[np.bool]) +assert_type(np.less(_c64_2d, _c64_2d), npt.NDArray[np.bool]) +assert_type(np.less(_c64_nd, _c64_nd), npt.NDArray[np.bool]) +assert_type(np.less(_dt_ns_0d, _dt_ns_0d), np.bool) +assert_type(np.less(_dt_ns_1d, _dt_ns_1d), npt.NDArray[np.bool]) +assert_type(np.less(_dt_ns_2d, _dt_ns_2d), npt.NDArray[np.bool]) +assert_type(np.less(_dt_ns_nd, _dt_ns_nd), npt.NDArray[np.bool]) +assert_type(np.less(_td_ns_0d, _td_ns_0d), np.bool) +assert_type(np.less(_td_ns_1d, _td_ns_1d), npt.NDArray[np.bool]) +assert_type(np.less(_td_ns_2d, _td_ns_2d), npt.NDArray[np.bool]) +assert_type(np.less(_td_ns_nd, _td_ns_nd), npt.NDArray[np.bool]) + +assert_type(np.less(_py_i_2d, _py_i_2d, out=_f32_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) + +assert_type(np.less.outer(_py_c_0d, _py_c_0d), np.bool) +assert_type(np.less.outer(_py_c_1d, _py_c_0d), npt.NDArray[np.bool]) +assert_type(np.less.outer(_py_c_0d, _py_c_1d), npt.NDArray[np.bool]) +assert_type(np.less.outer(_py_c_1d, _py_c_1d), npt.NDArray[np.bool]) + +assert_type(np.less.at(_c64_1d, 1, _py_c_1d), None) + +assert_type(np.less.accumulate(_bool_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) +assert_type(np.less.accumulate(_bool_2d), np.ndarray[tuple[int, int], np.dtype[np.bool]]) +assert_type(np.less.accumulate(_py_b_1d), npt.NDArray[np.bool]) +assert_type(np.less.accumulate(_py_b_1d, out=_bool_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) + +assert_type(np.less.reduce(_bool_1d), npt.NDArray[np.bool] | np.bool) +assert_type(np.less.reduce(_bool_2d), npt.NDArray[np.bool] | np.bool) +assert_type(np.less.reduce(_bool_1d, axis=None), np.bool) +assert_type(np.less.reduce(_bool_1d, keepdims=True), npt.NDArray[np.bool]) +assert_type(np.less.reduce(_bool_1d, out=...), npt.NDArray[np.bool]) +assert_type(np.less.reduce(_bool_1d, out=_bool_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) + +assert_type(np.less.reduceat(_bool_1d, (0,)), np.ndarray[tuple[int], np.dtype[np.bool]]) +assert_type(np.less.reduceat(_bool_2d, (0,)), np.ndarray[tuple[int, int], np.dtype[np.bool]]) +assert_type(np.less.reduceat(_py_b_1d, (0,)), npt.NDArray[np.bool]) +assert_type(np.less.reduceat(_py_b_1d, (0,), out=_bool_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) From 84671a854ddebb707b724da0ce43ba7cfe4b40ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:57:07 -0600 Subject: [PATCH 08/60] MAINT: Bump actions/cache from 5.0.5 to 6.1.0 (#31808) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/linux_qemu.yml | 4 ++-- .github/workflows/macos.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linux_qemu.yml b/.github/workflows/linux_qemu.yml index 3031d04fa110..e22e70c09b9b 100644 --- a/.github/workflows/linux_qemu.yml +++ b/.github/workflows/linux_qemu.yml @@ -82,7 +82,7 @@ jobs: sudo apt install -y ninja-build gcc-${TOOLCHAIN_NAME} g++-${TOOLCHAIN_NAME} gfortran-${TOOLCHAIN_NAME} - name: Cache docker container - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 id: container-cache with: path: ~/docker_${{ matrix.BUILD_PROP[1] }} @@ -199,7 +199,7 @@ jobs: sudo apt install -y ninja-build gcc-14-${TOOLCHAIN_NAME} g++-14-${TOOLCHAIN_NAME} gfortran-14-${TOOLCHAIN_NAME} - name: Cache docker container - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 id: container-cache with: path: ~/docker_${{ matrix.BUILD_PROP[1] }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 426c04dae726..d8c85c9b87c1 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -50,7 +50,7 @@ jobs: echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_OUTPUT - name: Setup compiler cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 id: cache-ccache with: path: ${{ steps.prep-ccache.outputs.dir }} @@ -74,7 +74,7 @@ jobs: # ensure we re-solve once a day (since we don't lock versions). Could be # replaced by a conda-lock based approach in the future. - name: Cache conda environment - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 env: # Increase this value to reset cache if environment.yml has not changed CACHE_NUMBER: 1 From 5d4b1f00536fe993d2e3e3fda5cfbe5bece20df2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:07:12 -0600 Subject: [PATCH 09/60] MAINT: Bump actions/setup-python from 6.2.0 to 6.3.0 (#31809) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/linux.yml | 18 +++++++++--------- .github/workflows/linux_blas.yml | 12 ++++++------ .github/workflows/linux_simd.yml | 12 ++++++------ .github/workflows/macos.yml | 2 +- .github/workflows/mypy_primer.yml | 2 +- .github/workflows/windows.yml | 6 +++--- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index f266e23f3650..3192a2fcb3c8 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -43,7 +43,7 @@ jobs: submodules: recursive fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' - name: Install linter requirements @@ -74,7 +74,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.version }} - uses: ./.github/meson_actions @@ -124,7 +124,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.version }} - uses: ./.github/meson_actions @@ -139,7 +139,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' - name: Install build and test dependencies from PyPI @@ -226,7 +226,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' - name: Install build and benchmarking dependencies @@ -265,7 +265,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' - name: Install gfortran and setup OpenBLAS (sdist build) @@ -315,7 +315,7 @@ jobs: path: 'array-api-tests' persist-credentials: false - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' - name: Install build and test dependencies from PyPI @@ -357,7 +357,7 @@ jobs: path: 'ml_dtypes' persist-credentials: false - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' - name: Install build and test dependencies from PyPI @@ -397,7 +397,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/linux_blas.yml b/.github/workflows/linux_blas.yml index 9b84082fe181..d86937f9c058 100644 --- a/.github/workflows/linux_blas.yml +++ b/.github/workflows/linux_blas.yml @@ -75,7 +75,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' @@ -198,7 +198,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' @@ -226,7 +226,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' @@ -289,7 +289,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' @@ -354,7 +354,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' @@ -391,7 +391,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' diff --git a/.github/workflows/linux_simd.yml b/.github/workflows/linux_simd.yml index d79adca0cc29..ba986889db05 100644 --- a/.github/workflows/linux_simd.yml +++ b/.github/workflows/linux_simd.yml @@ -67,7 +67,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' - uses: ./.github/meson_actions @@ -85,7 +85,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' @@ -131,7 +131,7 @@ jobs: with: submodules: recursive fetch-tags: true - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' - name: Install dependencies @@ -179,7 +179,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "${{ matrix.BUILD_PROP[2] }}" - uses: ./.github/meson_actions @@ -194,7 +194,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' @@ -248,7 +248,7 @@ jobs: submodules: recursive fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d8c85c9b87c1..f1f0d1531035 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -128,7 +128,7 @@ jobs: fetch-tags: true persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.version }} diff --git a/.github/workflows/mypy_primer.yml b/.github/workflows/mypy_primer.yml index 2fb5aa54994f..688e4a0f9c7a 100644 --- a/.github/workflows/mypy_primer.yml +++ b/.github/workflows/mypy_primer.yml @@ -32,7 +32,7 @@ jobs: with: path: numpy_to_test fetch-depth: 0 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 81fc307ffd48..2bc4d6326988 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -33,7 +33,7 @@ jobs: persist-credentials: false - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.15t-dev" @@ -93,7 +93,7 @@ jobs: persist-credentials: false - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.python_version }} architecture: ${{ matrix.architecture }} @@ -149,7 +149,7 @@ jobs: persist-credentials: false - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.pyver }} From 300c2b6c01ba4e85f407831d7ca9f6a4f35d6268 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:08:01 -0600 Subject: [PATCH 10/60] MAINT: Bump actions/cache/restore from 5.0.5 to 6.1.0 (#31811) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/linux_riscv64.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux_riscv64.yml b/.github/workflows/linux_riscv64.yml index d73466254b40..4de4368b8325 100644 --- a/.github/workflows/linux_riscv64.yml +++ b/.github/workflows/linux_riscv64.yml @@ -49,7 +49,7 @@ jobs: persist-credentials: false - name: Restore compilation cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 id: ccache-restore with: path: ${{ env.CCACHE_DIR }} From 595613663fe3719b1f5ad2379197ad65a7837c65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:35:34 -0600 Subject: [PATCH 11/60] MAINT: Bump actions/cache/save from 5.0.5 to 6.1.0 (#31812) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/linux_riscv64.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux_riscv64.yml b/.github/workflows/linux_riscv64.yml index 4de4368b8325..79e741b15b7e 100644 --- a/.github/workflows/linux_riscv64.yml +++ b/.github/workflows/linux_riscv64.yml @@ -75,7 +75,7 @@ jobs: - name: Save compilation cache if: always() && github.ref == 'refs/heads/main' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.CCACHE_DIR }} key: ccache-wheels-manylinux_riscv64-${{ matrix.python }}-${{ github.run_id }} From 13f9ffd23081c959ec92b86deb6343acc57f5211 Mon Sep 17 00:00:00 2001 From: shipitdev Date: Sat, 27 Jun 2026 18:06:17 +0530 Subject: [PATCH 12/60] DOC: fix copy-paste error in nanprod docstring --- numpy/lib/_nanfunctions_impl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpy/lib/_nanfunctions_impl.py b/numpy/lib/_nanfunctions_impl.py index 44110dd751e4..252f9cc83f8c 100644 --- a/numpy/lib/_nanfunctions_impl.py +++ b/numpy/lib/_nanfunctions_impl.py @@ -751,7 +751,7 @@ def nanprod(a, axis=None, dtype=None, out=None, keepdims=np._NoValue, the product of the flattened array. dtype : data-type, optional The type of the returned array and of the accumulator in which the - elements are summed. By default, the dtype of `a` is used. An + elements are multiplied. By default, the dtype of `a` is used. An exception is when `a` has an integer type with less precision than the platform (u)intp. In that case, the default will be either (u)int32 or (u)int64 depending on whether the platform is 32 or 64 From a2c09e6522932bc319c42531db2734f9e492ddea Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 1 Jul 2026 16:25:24 +0530 Subject: [PATCH 13/60] BUG: fix numpy datetime cython APIs to be compatible with older versions --- numpy/_core/code_generators/cversions.txt | 2 +- numpy/_core/code_generators/numpy_api.py | 8 ++++---- numpy/_core/include/numpy/ndarraytypes.h | 8 ++++++++ numpy/_core/src/multiarray/arrayobject.c | 4 ++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/numpy/_core/code_generators/cversions.txt b/numpy/_core/code_generators/cversions.txt index 57dde2ef8131..959d9a45b913 100644 --- a/numpy/_core/code_generators/cversions.txt +++ b/numpy/_core/code_generators/cversions.txt @@ -87,4 +87,4 @@ # Version 22 (NumPy 2.5.0) Opaque PyObject ABI support and # NPY_DT_legacy_descriptor_proto support. # Version 22 (NumPy 2.6.0) No change -0x00000016 = cdfba10e4e01454262d2293c394cbed5 +0x00000016 = 83149cba850edb61bf48e1dd180f17a6 diff --git a/numpy/_core/code_generators/numpy_api.py b/numpy/_core/code_generators/numpy_api.py index 7f78d467d10b..e06d6d15dccf 100644 --- a/numpy/_core/code_generators/numpy_api.py +++ b/numpy/_core/code_generators/numpy_api.py @@ -415,10 +415,10 @@ def get_annotations(): '_PyDataType_GET_ITEM_DATA': (372, MinVersion("2.5")), '_PyArrayMultiIter_GET_ITEM_DATA': (373, MinVersion("2.5")), '_PyArrayNeighborhoodIter_GET_ITEM_DATA': (374, MinVersion("2.5")), - '_PyDatetimeScalarObject_GetMetadata': (375,), - '_PyTimedeltaScalarObject_GetMetadata': (376,), - '_PyDatetimeScalarObject_GetValue': (377,), - '_PyTimedeltaScalarObject_GetValue': (378,), + '_PyDatetimeScalarObject_GetMetadata': (375, MinVersion("2.5")), + '_PyTimedeltaScalarObject_GetMetadata': (376, MinVersion("2.5")), + '_PyDatetimeScalarObject_GetValue': (377, MinVersion("2.5")), + '_PyTimedeltaScalarObject_GetValue': (378, MinVersion("2.5")), } ufunc_types_api = { diff --git a/numpy/_core/include/numpy/ndarraytypes.h b/numpy/_core/include/numpy/ndarraytypes.h index 92062bc52a3c..5434df33bd97 100644 --- a/numpy/_core/include/numpy/ndarraytypes.h +++ b/numpy/_core/include/numpy/ndarraytypes.h @@ -1547,6 +1547,14 @@ typedef struct { #define _PyDataType_GET_ITEM_DATA(descr) ((PyArray_Descr_fields *)(descr)) #undef _PyArray_LegacyDescr_GET_ITEM_DATA #define _PyArray_LegacyDescr_GET_ITEM_DATA(descr) ((_PyArray_LegacyDescr_fields *)(descr)) +#undef _PyDatetimeScalarObject_GetMetadata +#define _PyDatetimeScalarObject_GetMetadata(self) ((PyDatetimeScalarObject *)self)->obmeta +#undef _PyTimedeltaScalarObject_GetMetadata +#define _PyTimedeltaScalarObject_GetMetadata(self) ((PyTimedeltaScalarObject *)self)->obmeta +#undef _PyDatetimeScalarObject_GetValue +#define _PyDatetimeScalarObject_GetValue(self) ((PyDatetimeScalarObject *)self)->obval +#undef _PyTimedeltaScalarObject_GetValue +#define _PyTimedeltaScalarObject_GetValue(self) (((PyTimedeltaScalarObject *)self)->obval) #endif /* diff --git a/numpy/_core/src/multiarray/arrayobject.c b/numpy/_core/src/multiarray/arrayobject.c index 23bba6a43d51..ba8efe2f9209 100644 --- a/numpy/_core/src/multiarray/arrayobject.c +++ b/numpy/_core/src/multiarray/arrayobject.c @@ -1325,24 +1325,28 @@ _PyArrayNeighborhoodIter_GET_ITEM_DATA(const PyArrayNeighborhoodIterObject *iter { return (PyArrayNeighborhoodIterObject_fields *)(((char *)iter) + sizeof(PyObject)); } +#undef _PyDatetimeScalarObject_GetMetadata /*NUMPY_API*/ NPY_NO_EXPORT PyArray_DatetimeMetaData _PyDatetimeScalarObject_GetMetadata(PyObject *self) { return ((PyDatetimeScalarObject *)self)->obmeta; } +#undef _PyTimedeltaScalarObject_GetMetadata /*NUMPY_API*/ NPY_NO_EXPORT PyArray_DatetimeMetaData _PyTimedeltaScalarObject_GetMetadata(PyObject *self) { return ((PyTimedeltaScalarObject *)self)->obmeta; } +#undef _PyDatetimeScalarObject_GetValue /*NUMPY_API*/ NPY_NO_EXPORT npy_datetime _PyDatetimeScalarObject_GetValue(PyObject *self) { return ((PyDatetimeScalarObject *)self)->obval; } +#undef _PyTimedeltaScalarObject_GetValue /*NUMPY_API*/ NPY_NO_EXPORT npy_timedelta _PyTimedeltaScalarObject_GetValue(PyObject *self) From fc8373305e4b522304e482f93d3c5789923406ba Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 1 Jul 2026 04:57:01 -0600 Subject: [PATCH 14/60] BUG: avoid deadlocks using NpyString API (#31682) For NpyString_acquire_allocators and NpyString_release_allocators, the fix is to sort the mutexes by address before locking. I added tests for ufuncs that need 2, 3, and 4 allocators as well as one that uses a duplicate allocator that all deadlock on main and don't with the fixes here. Also releases the string allocator mutex before grabbing the GIL, as this in theory not necessary safe if arbitrary Python could be running (there are more paths that would need this fix, though). --- doc/source/reference/c-api/strings.rst | 7 +- numpy/_core/src/common/raii_utils.hpp | 14 +- .../src/multiarray/stringdtype/casts.cpp | 30 ++- .../_core/src/multiarray/stringdtype/dtype.c | 53 ++-- .../multiarray/stringdtype/static_string.c | 94 +++++-- numpy/_core/src/multiarray/unique.cpp | 4 +- numpy/_core/tests/test_multithreading.py | 229 +++++++++++++++++- numpy/_core/tests/test_stringdtype.py | 19 ++ 8 files changed, 384 insertions(+), 66 deletions(-) diff --git a/doc/source/reference/c-api/strings.rst b/doc/source/reference/c-api/strings.rst index 2e7dc34a337f..e338685742d6 100644 --- a/doc/source/reference/c-api/strings.rst +++ b/doc/source/reference/c-api/strings.rst @@ -188,8 +188,8 @@ Functions Acquire the mutex locking the allocator attached to ``descr``. ``NpyString_release_allocator`` must be called on the allocator returned by this function exactly once. Note that functions requiring the - GIL should not be called while the allocator mutex is held, as doing so may - cause deadlocks. + GIL should not be called while the allocator mutex is held unless + re-entrancy is impossible, as doing so may cause deadlocks. .. c:function:: void NpyString_acquire_allocators( \ size_t n_descriptors, PyArray_Descr *const descrs[], \ @@ -212,7 +212,8 @@ Functions ``NpyString_release_allocators``. Note that functions requiring the GIL should not be called while the - allocator mutex is held, as doing so may cause deadlocks. + allocator mutex is held and reentrancy is possible, as doing so may cause + deadlocks. .. c:function:: void NpyString_release_allocator( \ npy_string_allocator *allocator) diff --git a/numpy/_core/src/common/raii_utils.hpp b/numpy/_core/src/common/raii_utils.hpp index e92d0eae9269..7e95eb62357c 100644 --- a/numpy/_core/src/common/raii_utils.hpp +++ b/numpy/_core/src/common/raii_utils.hpp @@ -148,7 +148,7 @@ class NpyStringAcquireAllocator } ~NpyStringAcquireAllocator() { - NpyString_release_allocator(_allocator); + release(); } NpyStringAcquireAllocator(const NpyStringAcquireAllocator&) = delete; @@ -159,6 +159,18 @@ class NpyStringAcquireAllocator npy_string_allocator *allocator() { return _allocator; } + + // Release the allocator lock early, before this object goes out of scope. + // Use this when a scope must drop the lock partway through, e.g. before + // running Python C API code that must not hold the allocator lock (which + // could otherwise deadlock on re-entrancy). Idempotent: the destructor + // also calls it, so an explicit call never double-releases. + void release() { + if (_allocator != NULL) { + NpyString_release_allocator(_allocator); + _allocator = NULL; + } + } }; }} // namespace np { namespace raii { diff --git a/numpy/_core/src/multiarray/stringdtype/casts.cpp b/numpy/_core/src/multiarray/stringdtype/casts.cpp index aa7004f83a98..cb29ef9bdc00 100644 --- a/numpy/_core/src/multiarray/stringdtype/casts.cpp +++ b/numpy/_core/src/multiarray/stringdtype/casts.cpp @@ -1918,8 +1918,6 @@ string_to_bytes(PyArrayMethod_Context *context, char *const data[], NpyAuxData *NPY_UNUSED(auxdata)) { PyArray_StringDTypeObject *descr = (PyArray_StringDTypeObject *)context->descriptors[0]; - np::raii::NpyStringAcquireAllocator alloc(descr); - int has_null = descr->na_object != NULL; int has_string_na = descr->has_string_na; const npy_static_string *default_string = &descr->default_string; @@ -1931,6 +1929,8 @@ string_to_bytes(PyArrayMethod_Context *context, char *const data[], npy_intp out_stride = strides[1]; size_t max_out_size = context->descriptors[1]->elsize; + np::raii::NpyStringAcquireAllocator alloc(descr); + while (N--) { const npy_packed_static_string *ps = (npy_packed_static_string *)in; npy_static_string s = {0, NULL}; @@ -1940,11 +1940,29 @@ string_to_bytes(PyArrayMethod_Context *context, char *const data[], return -1; } - for (size_t i=0; i 127) { + // Building the UnicodeEncodeError needs the GIL and must not + // run while the allocator is held (re-entrant Python could + // deadlock on it), so copy the offending bytes out, release the + // allocator, then raise. + char *bad = (char *)PyMem_RawMalloc(s.size); + if (bad == NULL) { + alloc.release(); + npy_gil_error(PyExc_MemoryError, + "Failed to allocate memory for unicode " + "encoding error"); + return -1; + } + memcpy(bad, s.buf, s.size); + size_t bad_size = s.size; + size_t bad_pos = i; + alloc.release(); + np::raii::EnsureGIL ensure_gil{}; - PyObject *str = PyUnicode_FromStringAndSize(s.buf, s.size); + PyObject *str = PyUnicode_FromStringAndSize(bad, bad_size); + PyMem_RawFree(bad); if (str == NULL) { PyErr_SetString( @@ -1958,8 +1976,8 @@ string_to_bytes(PyArrayMethod_Context *context, char *const data[], "sOnns", "ascii", str, - (Py_ssize_t)i, - (Py_ssize_t)(i+1), + (Py_ssize_t)bad_pos, + (Py_ssize_t)(bad_pos + 1), "ordinal not in range(128)" ); diff --git a/numpy/_core/src/multiarray/stringdtype/dtype.c b/numpy/_core/src/multiarray/stringdtype/dtype.c index ef63c1684c30..c3d4c4f20f8b 100644 --- a/numpy/_core/src/multiarray/stringdtype/dtype.c +++ b/numpy/_core/src/multiarray/stringdtype/dtype.c @@ -315,62 +315,55 @@ stringdtype_setitem(PyArray_StringDTypeObject *descr, PyObject *obj, char **data { npy_packed_static_string *sdata = (npy_packed_static_string *)dataptr; - // borrow reference + // borrowed reference PyObject *na_object = descr->na_object; - // We need the result of the comparison after acquiring the allocator, but - // cannot use functions requiring the GIL when the allocator is acquired, - // so we do the comparison before acquiring the allocator. - + // We need the result of the comparison before packing below, but cannot + // use functions requiring the GIL when the allocator is acquired. int na_cmp = na_eq_cmp(obj, na_object); if (na_cmp == -1) { return -1; } - npy_string_allocator *allocator = NpyString_acquire_allocator(descr); - - if (na_object != NULL) { - if (na_cmp) { - if (NpyString_pack_null(allocator, sdata) < 0) { - PyErr_SetString(PyExc_MemoryError, - "Failed to pack null string during StringDType " - "setitem"); - goto fail; - } - goto success; + if (na_object != NULL && na_cmp) { + npy_string_allocator *allocator = NpyString_acquire_allocator(descr); + int pack_status = NpyString_pack_null(allocator, sdata); + NpyString_release_allocator(allocator); + if (pack_status < 0) { + PyErr_SetString(PyExc_MemoryError, + "Failed to pack null string during StringDType " + "setitem"); + return -1; } + return 0; } + PyObject *val_obj = as_pystring(obj, descr->coerce); if (val_obj == NULL) { - goto fail; + return -1; } Py_ssize_t length = 0; const char *val = PyUnicode_AsUTF8AndSize(val_obj, &length); if (val == NULL) { Py_DECREF(val_obj); - goto fail; + return -1; } - if (NpyString_pack(allocator, sdata, val, length) < 0) { + npy_string_allocator *allocator = NpyString_acquire_allocator(descr); + int pack_status = NpyString_pack(allocator, sdata, val, length); + NpyString_release_allocator(allocator); + Py_DECREF(val_obj); + + if (pack_status < 0) { PyErr_SetString(PyExc_MemoryError, "Failed to pack string during StringDType " "setitem"); - Py_DECREF(val_obj); - goto fail; + return -1; } - Py_DECREF(val_obj); - -success: - NpyString_release_allocator(allocator); return 0; - -fail: - NpyString_release_allocator(allocator); - - return -1; } static PyObject * diff --git a/numpy/_core/src/multiarray/stringdtype/static_string.c b/numpy/_core/src/multiarray/stringdtype/static_string.c index 1f2ed81f2f0e..a8396d96075c 100644 --- a/numpy/_core/src/multiarray/stringdtype/static_string.c +++ b/numpy/_core/src/multiarray/stringdtype/static_string.c @@ -229,6 +229,37 @@ arena_free(npy_string_arena *arena, _npy_static_string_u *str) static npy_string_arena NEW_ARENA = {0, 0, NULL}; +static void +acquire_allocator_lock(npy_string_allocator *allocator) +{ +#if PY_VERSION_HEX < 0x30d00b3 + if (!PyThread_acquire_lock(allocator->allocator_lock, NOWAIT_LOCK)) { + if (PyGILState_Check()) { + Py_BEGIN_ALLOW_THREADS + PyThread_acquire_lock(allocator->allocator_lock, WAIT_LOCK); + Py_END_ALLOW_THREADS + } + else { + PyThread_acquire_lock(allocator->allocator_lock, WAIT_LOCK); + } + } +#else + PyMutex_Lock(&allocator->allocator_lock); +#endif +} + +static int +allocator_seen(npy_string_allocator *allocator, + npy_string_allocator *allocators[], size_t end) +{ + for (size_t i = 0; i < end; i++) { + if (allocators[i] == allocator) { + return 1; + } + } + return 0; +} + NPY_NO_EXPORT npy_string_allocator * NpyString_new_allocator(npy_string_malloc_func m, npy_string_free_func f, npy_string_realloc_func r) @@ -281,18 +312,13 @@ NpyString_free_allocator(npy_string_allocator *allocator) * by this function exactly once. * * Note that functions requiring the GIL should not be called while the - * allocator mutex is held, as doing so may cause deadlocks. + * allocator mutex is held and reentrant calls are possible, as doing so may + * cause deadlocks. */ NPY_NO_EXPORT npy_string_allocator * NpyString_acquire_allocator(const PyArray_StringDTypeObject *descr) { -#if PY_VERSION_HEX < 0x30d00b3 - if (!PyThread_acquire_lock(descr->allocator->allocator_lock, NOWAIT_LOCK)) { - PyThread_acquire_lock(descr->allocator->allocator_lock, WAIT_LOCK); - } -#else - PyMutex_Lock(&descr->allocator->allocator_lock); -#endif + acquire_allocator_lock(descr->allocator); return descr->allocator; } @@ -309,42 +335,64 @@ NpyString_acquire_allocator(const PyArray_StringDTypeObject *descr) * ignored. A buffer overflow will happen if the *descrs* array does not * contain n_descriptors elements. * - * If pointers to the same descriptor are passed multiple times, only acquires + * If pointers to the same allocator are passed multiple times, only acquires * the allocator mutex once but sets identical allocator pointers appropriately. + * Distinct allocator mutexes are acquired in ascending allocator address order + * to avoid lock-ordering deadlocks when callers pass different descriptor + * orders. * * The allocator mutexes must be released after this function returns, see * NpyString_release_allocators. * * Note that functions requiring the GIL should not be called while the - * allocator mutex is held, as doing so may cause deadlocks. + * allocator mutex is held and reentrant calls are possible, as doing so may + * cause deadlocks. */ NPY_NO_EXPORT void NpyString_acquire_allocators(size_t n_descriptors, PyArray_Descr *const descrs[], npy_string_allocator *allocators[]) { - for (size_t i=0; iallocator; + } + + uintptr_t last_locked_addr = 0; + int have_last_locked_addr = 0; + + for (;;) { + npy_string_allocator *next_allocator = NULL; + uintptr_t next_addr = UINTPTR_MAX; + + for (size_t i = 0; i < n_descriptors; i++) { + npy_string_allocator *allocator = allocators[i]; + + if (allocator == NULL || allocator_seen(allocator, allocators, i)) { continue; } - if (((PyArray_StringDTypeObject *)descrs[i])->allocator == - ((PyArray_StringDTypeObject *)descrs[j])->allocator) - { - allocators[i] = allocators[j]; - allocators_match = 1; - break; + + uintptr_t addr = (uintptr_t)allocator; + + if (have_last_locked_addr && addr <= last_locked_addr) { + continue; + } + if (next_allocator == NULL || addr < next_addr) { + next_allocator = allocator; + next_addr = addr; } } - if (!allocators_match) { - allocators[i] = NpyString_acquire_allocator( - (PyArray_StringDTypeObject *)descrs[i]); + + if (next_allocator == NULL) { + break; } + + acquire_allocator_lock(next_allocator); + last_locked_addr = next_addr; + have_last_locked_addr = 1; } } diff --git a/numpy/_core/src/multiarray/unique.cpp b/numpy/_core/src/multiarray/unique.cpp index c3028d183555..213ecd441039 100644 --- a/numpy/_core/src/multiarray/unique.cpp +++ b/numpy/_core/src/multiarray/unique.cpp @@ -357,8 +357,8 @@ unique_vstring(PyArrayObject *self, npy_bool equal_nan) { PyArray_StringDTypeObject *descr = reinterpret_cast(PyArray_DESCR(self)); - np::raii::NpyStringAcquireAllocator alloc(descr); np::raii::SaveThreadState save_thread_state{}; + np::raii::NpyStringAcquireAllocator alloc(descr); char *idata = PyArray_BYTES(self); npy_intp istride = PyArray_STRIDES(self)[0]; @@ -387,8 +387,8 @@ unique_vstring(PyArrayObject *self, npy_bool equal_nan) { PyArray_StringDTypeObject *res_descr = reinterpret_cast(PyArray_DESCR(res_obj)); - np::raii::NpyStringAcquireAllocator alloc(res_descr); np::raii::SaveThreadState save_thread_state{}; + np::raii::NpyStringAcquireAllocator alloc(res_descr); char *odata = PyArray_BYTES(res_obj); npy_intp ostride = PyArray_STRIDES(res_obj)[0]; diff --git a/numpy/_core/tests/test_multithreading.py b/numpy/_core/tests/test_multithreading.py index 6c2c7d214cf2..1866d4efdc10 100644 --- a/numpy/_core/tests/test_multithreading.py +++ b/numpy/_core/tests/test_multithreading.py @@ -1,4 +1,8 @@ import concurrent.futures +import inspect +import subprocess +import sys +import textwrap import threading import pytest @@ -7,7 +11,7 @@ from numpy._core import _rational_tests from numpy._core.tests.test_stringdtype import random_unicode_string_list from numpy.testing import IS_64BIT, IS_WASM -from numpy.testing._private.utils import run_threaded +from numpy.testing._private.utils import run_subprocess, run_threaded if IS_WASM: pytest.skip(allow_module_level=True, reason="no threading support in wasm") @@ -474,3 +478,226 @@ def func(b): x.__buffer__(flags[i % 2]) run_threaded(func, max_workers=8, pass_barrier=True) + + +def assert_no_deadlock(workload, *, args=(), helpers=(), timeout=30, + reason="deadlock"): + """Run ``workload`` in a fresh subprocess; fail if it does not finish in time. + + ``workload`` is a self-contained function: its source is extracted with + ``inspect.getsource`` and executed in a clean interpreter. It must do its + own imports and may only reference arguments and helpers explicitly passed + to this function. + + For regression tests whose failure mode is a hang. Delegates to + ``run_subprocess`` (which folds the child's output into the failure on a + nonzero exit); this only adds the timeout watchdog, reporting a likely + ``reason`` if the child is killed. + + """ + source = "\n".join( + textwrap.dedent(inspect.getsource(helper)) for helper in helpers + ) + source += "\n" + textwrap.dedent(inspect.getsource(workload)) + script = ( + "import faulthandler\n" + f"faulthandler.dump_traceback_later({timeout}, exit=True)\n" + f"{source}\n" + f"{workload.__name__}(*{args!r})\n" + "faulthandler.cancel_dump_traceback_later()\n" + ) + try: + run_subprocess([sys.executable, "-c", script], timeout=timeout + 15) + except subprocess.TimeoutExpired: + raise AssertionError( + f"subprocess did not finish within {timeout}s -- likely {reason}" + ) from None + + +def threaded_deadlock_reproducer(operation, nworkers, niters, stall): + import faulthandler + import os + import sys + import threading + import time + + progress = [0] * nworkers + errors = [] + barrier = threading.Barrier(nworkers) + + def worker(idx): + try: + barrier.wait() + for _ in range(niters): + operation(idx) + progress[idx] += 1 + except BaseException as exc: + errors.append(exc) + raise + + workers = [threading.Thread(target=worker, args=(i,), daemon=True) + for i in range(nworkers)] + for t in workers: + t.start() + + last_total, last_change = 0, time.monotonic() + while any(t.is_alive() for t in workers): + time.sleep(0.05) + if errors: + raise AssertionError(f"worker failed: {errors[0]!r}") from errors[0] + total = sum(progress) + now = time.monotonic() + if total != last_total: + last_total, last_change = total, now + elif now - last_change > stall: + # dump the wedged threads' tracebacks, then os._exit to avoid a + # shutdown hang on the daemon threads. + sys.stderr.write("Probably deadlocked!\n") + faulthandler.dump_traceback() + sys.stderr.flush() + os._exit(1) + if errors: + raise AssertionError(f"worker failed: {errors[0]!r}") from errors[0] + + +def allocator_lock_order_workload(case_): + import numpy as np + + N = 1_000 + NWORKERS = 8 + NITERS = 1000 + STALL = 2.0 + + if case_ == "two-allocator": + a = np.array(["alpha"] * N, dtype="T") + b = np.array(["bravo"] * N, dtype="T") + + def operation(idx): + if idx % 2 == 0: + np.less(a, b) # locks alloc(a) then alloc(b) + else: + np.less(b, a) # locks alloc(b) then alloc(a) + + elif case_ == "three-allocator": + a = np.array(["alpha"] * N, dtype="T") + b = np.array(["bravo"] * N, dtype="T") + c = np.array(["charlie"] * N, dtype="T") + + def operation(idx): + order = idx % 3 + if order == 0: + np.maximum(a, b, out=c) # locks alloc(a), alloc(b), alloc(c) + elif order == 1: + np.maximum(b, c, out=a) # locks alloc(b), alloc(c), alloc(a) + else: + np.maximum(c, a, out=b) # locks alloc(c), alloc(a), alloc(b) + + elif case_ == "four-allocator": + from numpy._core.umath import _replace + + a = np.array(["a"] * N, dtype="T") + b = np.array(["b"] * N, dtype="T") + c = np.array(["c"] * N, dtype="T") + d = np.array(["d"] * N, dtype="T") + counts = np.ones(N, dtype=np.int64) + + def operation(idx): + order = idx % 4 + if order == 0: + _replace(a, b, c, counts, out=d) + elif order == 1: + _replace(b, c, d, counts, out=a) + elif order == 2: + _replace(c, d, a, counts, out=b) + else: + _replace(d, a, b, counts, out=c) + + elif case_ == "duplicate-allocator": + from numpy._core.umath import _replace + + a = np.array(["same"] * N, dtype="T") + b = np.array(["other"] * N, dtype="T") + c = np.array(["out"] * N, dtype="T") + counts = np.ones(N, dtype=np.int64) + + def operation(idx): + if idx % 2 == 0: + np.maximum(a, a, out=b) + else: + _replace(a, a, b, counts, out=c) + + else: + raise ValueError(f"unknown allocator deadlock case: {case_}") + + threaded_deadlock_reproducer( + operation, NWORKERS, NITERS, stall=STALL, + ) + + +def test_setitem_reentrant_no_deadlock(): + def workload(): + import numpy as np + + arr = np.empty(2, dtype="T") + + class Reenter: + def __str__(self): + # re-enters stringdtype_setitem and re-acquires the allocator + arr[1] = "inner" + return "outer" + + arr[0] = Reenter() + assert arr[0] == "outer" + assert arr[1] == "inner" + + assert_no_deadlock( + workload, + reason="re-entrant allocator acquire in stringdtype_setitem", + ) + + +@pytest.mark.parametrize( + "case_", + ["two-allocator", "three-allocator", "four-allocator", + "duplicate-allocator"], +) +def test_concurrent_allocator_acquire_no_deadlock(case_): + # NpyString_acquire_allocators must lock distinct allocators in a + # canonical order, otherwise deadlocks are possible from ABBA cycles + assert_no_deadlock( + allocator_lock_order_workload, + args=(case_,), + helpers=(threaded_deadlock_reproducer,), + reason=f"allocator lock-ordering in {case_}", + ) + + +def unique_deadlock_workload(): + import numpy as np + from numpy._core._multiarray_umath import _unique_hash + + NWORKERS = 8 + NITERS = 200 + STALL = 2.0 + + # _unique_hash (the engine under np.unique) rather than np.unique itself: + # it spends almost all its time in the GIL-released, allocator-locked load + # loop, so workers reliably overlap there. A single shared array with many + # duplicates concentrates contention on one allocator. + data = np.array([f"v{i % 128}" for i in range(50_000)], dtype="T") + + def operation(idx): + _unique_hash(data, equal_nan=False) + + threaded_deadlock_reproducer(operation, NWORKERS, NITERS, stall=STALL) + + +def test_concurrent_unique_no_deadlock(): + # Concurrent unique on a shared array deadlocks the allocator lock against + # the GIL unless unique_vstring releases the GIL before locking. Only + # reproduces on Python <= 3.12; PyMutex detaches on 3.13+. + assert_no_deadlock( + unique_deadlock_workload, + helpers=(threaded_deadlock_reproducer,), + reason="unique allocator lock vs GIL", + ) diff --git a/numpy/_core/tests/test_stringdtype.py b/numpy/_core/tests/test_stringdtype.py index 81fb18d03ebe..72684de49563 100644 --- a/numpy/_core/tests/test_stringdtype.py +++ b/numpy/_core/tests/test_stringdtype.py @@ -892,6 +892,25 @@ def test_float_nan_cast_na_object(): assert arr[0] == '1.2' +def test_string_to_bytes_invalid_ascii_error(): + # The cast builds this UnicodeEncodeError only after releasing the allocator + # lock and copying the offending bytes out of the arena; check the reported + # character and position survive that. + arr = np.array(["abc", "café", "xy"], dtype="T") + with pytest.raises(UnicodeEncodeError) as excinfo: + arr.astype("S10") + exc = excinfo.value + assert exc.encoding == "ascii" + assert exc.object == "café" + assert exc.start == 3 + assert exc.end == 4 + assert exc.reason == "ordinal not in range(128)" + assert_array_equal( + np.array(["abc", "xy"], dtype="T").astype("S3"), + np.array([b"abc", b"xy"], dtype="S3"), + ) + + @pytest.mark.parametrize( "typename", [ From c261a4869e89358274c366b455012b3f2bbc584b Mon Sep 17 00:00:00 2001 From: Charles Harris Date: Wed, 1 Jul 2026 08:21:56 -0600 Subject: [PATCH 15/60] MAINT: Tell dependabot to ignore pkgconf (#31815) --- .github/dependabot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4d1e5f9161df..7b28b685d3d0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -32,3 +32,4 @@ updates: - dependency-name: "scipy-openblas64" - dependency-name: "jupyterlite-pyodide-kernel" - dependency-name: "sphinx" + - dependency-name: "pkgconf" From 0be092ec488cd5f2afd390c004c5618b50f75930 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:23:39 +0000 Subject: [PATCH 16/60] MAINT: Bump ruff in /requirements in the python-deps group Bumps the python-deps group in /requirements with 1 update: [ruff](https://github.com/astral-sh/ruff). Updates `ruff` from 0.15.18 to 0.15.19 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.15.18...0.15.19) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.19 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-deps ... Signed-off-by: dependabot[bot] --- requirements/linter_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/linter_requirements.txt b/requirements/linter_requirements.txt index f8adfe232826..65afc1649755 100644 --- a/requirements/linter_requirements.txt +++ b/requirements/linter_requirements.txt @@ -1,5 +1,5 @@ # keep in sync with `environment.yml` cython-lint -ruff==0.15.18 +ruff==0.15.19 GitPython>=3.1.50 spin From 6eaa0cd79a1cb7b44c97123f18c65cb6c8524295 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:57:59 +0000 Subject: [PATCH 17/60] MAINT: Update cython requirement Updates the requirements on [cython](https://github.com/cython/cython) to permit the latest version. Updates `cython` to 3.2.6 - [Release notes](https://github.com/cython/cython/releases) - [Changelog](https://github.com/cython/cython/blob/master/CHANGES.rst) - [Commits](https://github.com/cython/cython/compare/3.2.5...3.2.6) --- updated-dependencies: - dependency-name: cython dependency-version: 3.2.6 dependency-type: direct:production dependency-group: python-deps ... Signed-off-by: dependabot[bot] --- requirements/build_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/build_requirements.txt b/requirements/build_requirements.txt index a571c57c68cb..2dc663424035 100644 --- a/requirements/build_requirements.txt +++ b/requirements/build_requirements.txt @@ -1,5 +1,5 @@ meson-python>=0.20.0 -Cython>=3.2.5 +Cython>=3.2.6 ninja spin build From 697d6c2c863763f9029a80ba6e98c50b79fa4117 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:06:50 +0000 Subject: [PATCH 18/60] MAINT: Bump ruff in /requirements in the python-deps group Bumps the python-deps group in /requirements with 1 update: [ruff](https://github.com/astral-sh/ruff). Updates `ruff` from 0.15.19 to 0.15.20 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.15.19...0.15.20) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.20 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-deps ... Signed-off-by: dependabot[bot] --- requirements/linter_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/linter_requirements.txt b/requirements/linter_requirements.txt index 65afc1649755..82a1c1d72355 100644 --- a/requirements/linter_requirements.txt +++ b/requirements/linter_requirements.txt @@ -1,5 +1,5 @@ # keep in sync with `environment.yml` cython-lint -ruff==0.15.19 +ruff==0.15.20 GitPython>=3.1.50 spin From 722b0a79c099b8c31925f3b60071d0c4916cde80 Mon Sep 17 00:00:00 2001 From: Aniket <148300120+Aniketsy@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:22:14 +0530 Subject: [PATCH 19/60] Update checkout action version in scorecards.yml --- .github/workflows/scorecards.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 8d38d70110d0..3080ed567eee 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v3.1.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false From 16d735bc5642c0f48999298d9341650cdca1f8da Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 3 Jul 2026 03:56:33 -0600 Subject: [PATCH 20/60] BLD: set minimum required gcc version to 10.3 (#31843) --- .github/workflows/linux_simd.yml | 12 ++---------- doc/release/upcoming_changes/31843.change.rst | 1 + meson.build | 4 ++-- 3 files changed, 5 insertions(+), 12 deletions(-) create mode 100644 doc/release/upcoming_changes/31843.change.rst diff --git a/.github/workflows/linux_simd.yml b/.github/workflows/linux_simd.yml index ba986889db05..3d221174d7c2 100644 --- a/.github/workflows/linux_simd.yml +++ b/.github/workflows/linux_simd.yml @@ -89,19 +89,11 @@ jobs: with: python-version: '3.12' - - name: Install GCC9/10 + - name: Install GCC10 run: | echo "deb http://archive.ubuntu.com/ubuntu focal main universe" | sudo tee /etc/apt/sources.list.d/focal.list sudo apt update - sudo apt install -y g++-9 g++-10 - - - name: Enable gcc-9 - run: | - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 1 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 1 - - - uses: ./.github/meson_actions - name: Build/Test against gcc-9 + sudo apt install -y g++-10 - name: Enable gcc-10 run: | diff --git a/doc/release/upcoming_changes/31843.change.rst b/doc/release/upcoming_changes/31843.change.rst new file mode 100644 index 000000000000..78071a9c960c --- /dev/null +++ b/doc/release/upcoming_changes/31843.change.rst @@ -0,0 +1 @@ +* The minimum supported GCC version has been updated from 9.3.0 to 10.3.0 diff --git a/meson.build b/meson.build index b5c4347fddb0..533d6c18223f 100644 --- a/meson.build +++ b/meson.build @@ -21,8 +21,8 @@ cy = meson.get_compiler('cython') # Check compiler is recent enough (see the SciPy Toolchain Roadmap for details) if cc.get_id() == 'gcc' - if not cc.version().version_compare('>=9.3') - error('NumPy requires GCC >= 9.3') + if not cc.version().version_compare('>=10.3') + error('NumPy requires GCC >= 10.3') endif elif cc.get_id() == 'msvc' if not cc.version().version_compare('>=19.35') From 7562c0877724a069dc06e374dcf7e821106ae762 Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 1 Jul 2026 18:11:39 +0200 Subject: [PATCH 21/60] TYP: specialized ``logical_{and,or,xor}`` ufuncs --- numpy/__init__.pyi | 6 +- numpy/_core/umath.pyi | 495 +++++++++++++++++++++- numpy/typing/tests/data/reveal/ufuncs.pyi | 87 ++++ 3 files changed, 582 insertions(+), 6 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index 57a4998ed249..2e4557cc091c 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -434,7 +434,10 @@ from numpy._core.umath import ( log10, log1p, log2, + logical_and, logical_not, + logical_or, + logical_xor, modf, not_equal, positive, @@ -7628,9 +7631,6 @@ ldexp: _UFunc_Nin2_Nout1[L["ldexp"], L[8], None] left_shift: _UFunc_Nin2_Nout1[L["left_shift"], L[11], None] logaddexp2: _UFunc_Nin2_Nout1[L["logaddexp2"], L[4], float] logaddexp: _UFunc_Nin2_Nout1[L["logaddexp"], L[4], float] -logical_and: _UFunc_Nin2_Nout1[L["logical_and"], L[20], L[True]] -logical_or: _UFunc_Nin2_Nout1[L["logical_or"], L[20], L[False]] -logical_xor: _UFunc_Nin2_Nout1[L["logical_xor"], L[19], L[False]] matmul: _GUFunc_Nin2_Nout1[L["matmul"], L[19], None, L["(n?,k),(k,m?)->(n?,m?)"]] matvec: _GUFunc_Nin2_Nout1[L["matvec"], L[19], None, L["(m,n),(n)->(m)"]] maximum: _UFunc_Nin2_Nout1[L["maximum"], L[21], None] diff --git a/numpy/_core/umath.pyi b/numpy/_core/umath.pyi index 4abdc01517a3..fb3953fab5e2 100644 --- a/numpy/_core/umath.pyi +++ b/numpy/_core/umath.pyi @@ -49,9 +49,6 @@ from numpy import ( left_shift, logaddexp, logaddexp2, - logical_and, - logical_or, - logical_xor, matmul, matvec, maximum, @@ -81,6 +78,7 @@ from numpy._typing import ( _CharLike_co, _DTypeLike, _DTypeLikeBool, + _DTypeLikeObject, _FloatLike_co, _IntLike_co, _NestedSequence, @@ -208,10 +206,14 @@ type _to_number = np.number | np.bool type _to_numeric = _to_number | np.timedelta64 type _numeric = np.number | np.timedelta64 type _time = np.datetime64 | np.timedelta64 +type _non_object = _to_number | _time | np.flexible # np.generic - np.object_ type _ArrayLikeNumericObj = _DualArrayLike[np.dtype[_numeric | np.object_], complex] type _ArrayLikeNumericObj_co = _DualArrayLike[np.dtype[_to_numeric | np.object_], complex] +type _ArrayUnlikeObject = _DualArrayLike[np.dtype[_non_object] | np.dtypes.StringDType, complex | bytes | str] +type _ScalarUnlikeObject = complex | bytes | str | np.generic # bare `np.object_` don't exist + @type_check_only class _ExtOjbDict(TypedDict, total=False): divide: _ErrKind @@ -3304,6 +3306,489 @@ class _ufunc_12_modf(_ufunc_12): # type: ignore[misc] frexp: Final[_ufunc_12_frexp] = ... modf: Final[_ufunc_12_modf] = ... +# ?bBhHiIlLqQefdgFDGOSUVT, ?bBhHiIlLqQefdgFDGOSUVT => ?O; (also supports `mM` input) +@type_check_only +class _ufunc_21_logical[IdT: bool](_ufunc_21[IdT]): # type: ignore[misc] + # TODO(@jorenham): shape-typing + + @override + @overload # 0d, 0d + def __call__( + self, + x1: _ScalarUnlikeObject, + x2: _ScalarUnlikeObject, + /, + *, + out: None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> np.bool: ... + @overload # >0d, >=0d + def __call__( + self, + x1: np.ndarray[Any, np.dtype[_non_object] | np.dtypes.StringDType] | _NestedSequence[_ScalarUnlikeObject], + x2: _ArrayUnlikeObject, + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # >=0d, >0d + def __call__( + self, + x1: _ArrayUnlikeObject, + x2: np.ndarray[Any, np.dtype[_non_object] | np.dtypes.StringDType] | _NestedSequence[_ScalarUnlikeObject], + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # >0d object_, >=0d + def __call__( + self, + x1: npt.NDArray[np.object_], + x2: npt.ArrayLike, + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeObject | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.object_]: ... + @overload # >=0d, >0d object_ + def __call__( + self, + x1: npt.ArrayLike, + x2: npt.NDArray[np.object_], + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeObject | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.object_]: ... + @overload # >=0d, >=0d, out=... + def __call__( + self, + x1: _ArrayUnlikeObject, + x2: _ArrayUnlikeObject, + /, + *, + out: EllipsisType, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # out= + def __call__[OutT: np.ndarray]( + self, + x1: npt.ArrayLike, + x2: npt.ArrayLike, + /, + out: OutT, + *, + dtype: _DTypeLikeBool | _DTypeLikeObject | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x1.__array_ufunc__(self, "__call__", x1, x2, ...) + def __call__[OtherT, OutT]( + self, + x1: _CanUfuncCall2L[OtherT, OutT], + x2: OtherT, + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x2.__array_ufunc__(self, "__call__", x1, x2, ...) + def __call__[OtherT, OutT]( + self, + x1: OtherT, + x2: _CanUfuncCall2R[OtherT, OutT], + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # +object, +object (overlaps with, well, everything...) + def __call__( + self, + x1: object, + x2: object, + /, + *, + out: None = None, + dtype: _DTypeLikeObject | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> Any: ... + + # + @override + @overload # 0d, 0d + def outer( + self, + x1: _ScalarUnlikeObject, + x2: _ScalarUnlikeObject, + /, + *, + out: None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> np.bool: ... + @overload # >0d, >=0d + def outer( + self, + x1: np.ndarray[Any, np.dtype[_non_object] | np.dtypes.StringDType] | _NestedSequence[_ScalarUnlikeObject], + x2: _ArrayUnlikeObject, + /, + *, + out: None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # >=0d, >0d + def outer( + self, + x1: _ArrayUnlikeObject, + x2: np.ndarray[Any, np.dtype[_non_object] | np.dtypes.StringDType] | _NestedSequence[_ScalarUnlikeObject], + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool]: ... + @overload # >0d object, >=0d + def outer( + self, + x1: npt.NDArray[np.object_], + x2: npt.ArrayLike, + /, + *, + out: None = None, + dtype: _DTypeLikeObject | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.object_]: ... + @overload # >=0d, >0d object + def outer( + self, + x1: npt.ArrayLike, + x2: npt.NDArray[np.object_], + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeObject | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.object_]: ... + @overload # >=0d, >=0d, out= + def outer[OutT: np.ndarray]( + self, + x1: npt.ArrayLike, + x2: npt.ArrayLike, + /, + *, + out: OutT, + dtype: _DTypeLikeBool | _DTypeLikeObject | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # >=0d, >=0d (fallback) + def outer( + self, + x1: _ArrayUnlikeObject, + x2: _ArrayUnlikeObject, + /, + *, + out: EllipsisType | None = None, + dtype: _DTypeLikeBool | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.bool] | Any: ... + @overload # x1.__array_ufunc__(self, "outer", x1, x2, ...) + def outer[OtherT, OutT]( + self, + x1: _CanUfuncOuterL[OtherT, OutT], + x2: OtherT, + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x2.__array_ufunc__(self, "outer", x1, x2, ...) + def outer[OtherT, OutT]( + self, + x1: OtherT, + x2: _CanUfuncOuterR[OtherT, OutT], + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # +object, +object (overlaps with everything...) + def outer( + self, + x1: object, + x2: object, + /, + *, + out: None = None, + dtype: _DTypeLikeObject | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> Any: ... + + # + @override + @overload + def at(self, a: np.ndarray, indices: _ArrayLikeInt, b: npt.ArrayLike, /) -> None: ... # pyrefly:ignore[bad-override] + @overload + def at[OtherT, IxT, OutT](self, a: _CanUfuncAt2L[OtherT, IxT, OutT], indices: IxT, b: OtherT, /) -> OutT: ... + @overload + def at[OtherT, IxT, OutT](self, a: OtherT, indices: IxT, b: _CanUfuncAt2R[OtherT, IxT, OutT], /) -> OutT: ... + + # + @override + @overload # unknown shape, not object_ + def reduce( # pyrefly:ignore[bad-override] + self, + array: _ArrayUnlikeObject, + /, + *, + axis: int | tuple[int, ...] = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.bool] | Any: ... + @overload # unknown shape, not object_, axis=None + def reduce( + self, + array: _ArrayUnlikeObject, + /, + *, + axis: None, + dtype: _DTypeLikeBool | None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> np.bool: ... + @overload # unknown shape, not object_ keepdims=True + def reduce( + self, + array: _ArrayUnlikeObject, + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + keepdims: Literal[True], + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.bool]: ... + @overload # unknown shape, not object_, out=... + def reduce( + self, + array: _ArrayUnlikeObject, + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: _DTypeLikeBool | None = None, + out: EllipsisType, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.bool]: ... + @overload # unknown shape, object_ + def reduce( + self, + array: npt.NDArray[np.object_], + /, + *, + axis: int | tuple[int, ...] = 0, + dtype: _DTypeLikeObject | None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.object_] | Any: ... + @overload # unknown shape, object_, axis=None + def reduce( + self, + array: npt.NDArray[np.object_], + /, + *, + axis: None, + dtype: _DTypeLikeObject | None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> Any: ... + @overload # unknown shape, not object_ keepdims=True + def reduce( + self, + array: npt.NDArray[np.object_], + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: _DTypeLikeObject | None = None, + out: None = None, + keepdims: Literal[True], + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.object_]: ... + @overload # unknown shape, object_, out=... + def reduce( + self, + array: npt.NDArray[np.object_], + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: _DTypeLikeBool | None = None, + out: EllipsisType, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.object_]: ... + @overload # out= + def reduce[OutT: np.ndarray]( + self, + array: npt.ArrayLike, + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: _DTypeLikeBool | _DTypeLikeObject | None = None, + out: OutT, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> OutT: ... + @overload # array.__array_ufunc__(self, "reduceat", array, ...) + def reduce[OutT]( + self, + array: _CanUfuncReduce[OutT], + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: npt.DTypeLike | None = None, + out: np.ndarray | EllipsisType | None = None, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> OutT: ... + + # + @override + @overload # known shape + def reduceat[ShapeT: _Shape]( # pyrefly:ignore[bad-override] + self, + array: np.ndarray[ShapeT, np.dtype[_non_object] | np.dtypes.StringDType], + /, + indices: tuple[int, ...], + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + ) -> np.ndarray[ShapeT, np.dtype[np.bool]]: ... + @overload # known shape, object_ + def reduceat[ShapeT: _Shape]( # pyrefly:ignore[bad-override] + self, + array: np.ndarray[ShapeT, np.dtype[np.object_]], + /, + indices: tuple[int, ...], + *, + axis: int = 0, + dtype: _DTypeLikeObject | None = None, + out: None = None, + ) -> np.ndarray[ShapeT, np.dtype[np.object_]]: ... + @overload # unknown shape + def reduceat( + self, + array: _ArrayUnlikeObject, + /, + indices: tuple[int, ...], + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + ) -> npt.NDArray[np.bool]: ... + @overload # out= + def reduceat[OutT: np.ndarray]( + self, + array: npt.ArrayLike, + /, + indices: tuple[int, ...], + *, + axis: int = 0, + dtype: _DTypeLikeBool | _DTypeLikeObject | None = None, + out: OutT, + ) -> OutT: ... + @overload # array.__array_ufunc__(self, "reduceat", array, ...) + def reduceat[IxT, OutT]( + self, + array: _CanUfuncReduceAt[IxT, OutT], + /, + indices: IxT, + *, + axis: int = 0, + dtype: npt.DTypeLike | None = None, + out: np.ndarray | None = None, + ) -> OutT: ... + + # TODO + @override + @overload # known shape + def accumulate[ShapeT: _Shape]( # pyrefly:ignore[bad-override] + self, + array: np.ndarray[ShapeT, np.dtype[_non_object] | np.dtypes.StringDType], + /, + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + ) -> np.ndarray[ShapeT, np.dtype[np.bool]]: ... + @overload # known shape, OBJECT_ + def accumulate[ShapeT: _Shape]( + self, + array: np.ndarray[ShapeT, np.dtype[np.object_]], + /, + *, + axis: int = 0, + dtype: _DTypeLikeObject | None = None, + out: None = None, + ) -> np.ndarray[ShapeT, np.dtype[np.object_]]: ... + @overload # unknown shape + def accumulate( + self, + array: _ArrayUnlikeObject, + /, + *, + axis: int = 0, + dtype: _DTypeLikeBool | None = None, + out: None = None, + ) -> npt.NDArray[np.bool]: ... + @overload # out= + def accumulate[OutT: np.ndarray]( + self, + array: npt.ArrayLike, + /, + *, + axis: int = 0, + dtype: _DTypeLikeBool | _DTypeLikeObject | None = None, + out: OutT, + ) -> OutT: ... + @overload # array.__array_ufunc__(self, "accumulate", array, ...) + def accumulate[OutT]( + self, + array: _CanUfuncAccumulate[OutT], + /, + *, + axis: int = 0, + dtype: npt.DTypeLike | None = None, + out: np.ndarray | None = None, + ) -> OutT: ... + # ?bBhHiIlLqQefdgFDGMmOSUT => ?O # NOTE: These theoretically also supports O => O but that's an unlikely usecase because # that'd require something incompatible with `bool` or `np.bool` to be returned from the @@ -3653,6 +4138,10 @@ class _ufunc_21_cmp(_ufunc_21[None]): # type: ignore[misc] out: np.ndarray | None = None, ) -> OutT: ... +logical_and: Final[_ufunc_21_logical[Literal[True]]] = ... +logical_or: Final[_ufunc_21_logical[Literal[False]]] = ... +logical_xor: Final[_ufunc_21_logical[Literal[False]]] = ... + equal: Final[_ufunc_21_cmp] = ... greater: Final[_ufunc_21_cmp] = ... greater_equal: Final[_ufunc_21_cmp] = ... diff --git a/numpy/typing/tests/data/reveal/ufuncs.pyi b/numpy/typing/tests/data/reveal/ufuncs.pyi index 7cbc18c8a374..321795c93fdc 100644 --- a/numpy/typing/tests/data/reveal/ufuncs.pyi +++ b/numpy/typing/tests/data/reveal/ufuncs.pyi @@ -771,6 +771,93 @@ assert_type( ] ) +# _ufunc_21_logical +# (logical_and, logical_or, logical_xor) + +assert_type(np.logical_or(_py_b_0d, _py_b_0d), np.bool) +assert_type(np.logical_or(_py_b_1d, _py_b_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_py_b_2d, _py_b_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_py_i_0d, _py_i_0d), np.bool) +assert_type(np.logical_or(_py_i_1d, _py_i_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_py_i_2d, _py_i_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_py_f_0d, _py_f_0d), np.bool) +assert_type(np.logical_or(_py_f_1d, _py_f_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_py_f_2d, _py_f_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_py_c_0d, _py_c_0d), np.bool) +assert_type(np.logical_or(_py_c_1d, _py_c_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_py_c_2d, _py_c_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(object(), object()), Any) + +assert_type(np.logical_or(_bool_0d, _bool_0d), np.bool) +assert_type(np.logical_or(_bool_1d, _bool_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_bool_2d, _bool_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_bool_nd, _bool_nd), npt.NDArray[np.bool]) +assert_type(np.logical_or(_i16_0d, _i16_0d), np.bool) +assert_type(np.logical_or(_i16_1d, _i16_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_i16_2d, _i16_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_i16_nd, _i16_nd), npt.NDArray[np.bool]) +assert_type(np.logical_or(_f32_0d, _f32_0d), np.bool) +assert_type(np.logical_or(_f32_1d, _f32_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_f32_2d, _f32_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_f32_nd, _f32_nd), npt.NDArray[np.bool]) +assert_type(np.logical_or(_c64_0d, _c64_0d), np.bool) +assert_type(np.logical_or(_c64_1d, _c64_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_c64_2d, _c64_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_c64_nd, _c64_nd), npt.NDArray[np.bool]) +assert_type(np.logical_or(_dt_ns_0d, _dt_ns_0d), np.bool) +assert_type(np.logical_or(_dt_ns_1d, _dt_ns_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_dt_ns_2d, _dt_ns_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_dt_ns_nd, _dt_ns_nd), npt.NDArray[np.bool]) +assert_type(np.logical_or(_td_ns_0d, _td_ns_0d), np.bool) +assert_type(np.logical_or(_td_ns_1d, _td_ns_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_td_ns_2d, _td_ns_2d), npt.NDArray[np.bool]) +assert_type(np.logical_or(_td_ns_nd, _td_ns_nd), npt.NDArray[np.bool]) +assert_type(np.logical_or(_obj_nd, _bool_nd), npt.NDArray[np.object_]) +assert_type(np.logical_or(_bool_nd, _obj_nd), npt.NDArray[np.object_]) +assert_type(np.logical_or(_obj_nd, _obj_nd), npt.NDArray[np.object_]) + +assert_type(np.logical_or(_py_i_2d, _py_i_2d, out=_f32_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) + +assert_type(np.logical_or.outer(_f32_0d, _c64_0d), np.bool) +assert_type(np.logical_or.outer(_f32_1d, _c64_0d), npt.NDArray[np.bool]) +assert_type(np.logical_or.outer(_f32_0d, _c64_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or.outer(_f32_1d, _c64_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or.outer(_obj_1d, _obj_1d), npt.NDArray[np.object_]) +assert_type(np.logical_or.outer(_obj_2d, _obj_1d), npt.NDArray[np.object_]) +assert_type(np.logical_or.outer(_obj_1d, _obj_2d), npt.NDArray[np.object_]) +assert_type(np.logical_or.outer(_obj_2d, _obj_2d), npt.NDArray[np.object_]) +assert_type(np.logical_or.outer(object(), object()), Any) + +assert_type(np.logical_or.at(_c64_2d, 1, _py_c_1d), None) +assert_type(np.logical_or.at(_obj_2d, 1, _obj_1d), None) + +assert_type(np.logical_or.accumulate(_dt_ns_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) +assert_type(np.logical_or.accumulate(_dt_ns_2d), np.ndarray[tuple[int, int], np.dtype[np.bool]]) +assert_type(np.logical_or.accumulate(_obj_1d), np.ndarray[tuple[int], np.dtype[np.object_]]) +assert_type(np.logical_or.accumulate(_obj_2d), np.ndarray[tuple[int, int], np.dtype[np.object_]]) +assert_type(np.logical_or.accumulate(_py_f_1d), npt.NDArray[np.bool]) +assert_type(np.logical_or.accumulate(_py_f_1d, out=_bool_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) + +assert_type(np.logical_or.reduce(_i16_1d), npt.NDArray[np.bool] | Any) +assert_type(np.logical_or.reduce(_i16_2d), npt.NDArray[np.bool] | Any) +assert_type(np.logical_or.reduce(_i16_1d, axis=None), np.bool) +assert_type(np.logical_or.reduce(_i16_1d, keepdims=True), npt.NDArray[np.bool]) +assert_type(np.logical_or.reduce(_i16_1d, out=...), npt.NDArray[np.bool]) +assert_type(np.logical_or.reduce(_i16_1d, out=_bool_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) +assert_type(np.logical_or.reduce(_obj_1d), npt.NDArray[np.object_] | Any) +assert_type(np.logical_or.reduce(_obj_2d), npt.NDArray[np.object_] | Any) +assert_type(np.logical_or.reduce(_obj_1d, axis=None), Any) +assert_type(np.logical_or.reduce(_obj_1d, keepdims=True), npt.NDArray[np.object_]) +assert_type(np.logical_or.reduce(_obj_1d, out=...), npt.NDArray[np.object_]) +assert_type(np.logical_or.reduce(_obj_1d, out=_bool_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) + +assert_type(np.logical_or.reduceat(_td_ns_1d, (0,)), np.ndarray[tuple[int], np.dtype[np.bool]]) +assert_type(np.logical_or.reduceat(_td_ns_2d, (0,)), np.ndarray[tuple[int, int], np.dtype[np.bool]]) +assert_type(np.logical_or.reduceat(_obj_1d, (0,)), np.ndarray[tuple[int], np.dtype[np.object_]]) +assert_type(np.logical_or.reduceat(_obj_2d, (0,)), np.ndarray[tuple[int, int], np.dtype[np.object_]]) +assert_type(np.logical_or.reduceat(_py_c_1d, (0,)), npt.NDArray[np.bool]) +assert_type(np.logical_or.reduceat(_py_c_1d, (0,), out=_bool_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) + # _ufunc_21_cmp # (equal, greater, greater_equal, less, less_equal, not_equal) From 1ef4f89ffa00efbc22554543e427922126a8115c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 3 Jul 2026 11:40:45 -0600 Subject: [PATCH 22/60] BUG: fix several bugs in StringDType operations (#31846) --- .../src/multiarray/stringdtype/casts.cpp | 13 +++- .../_core/src/multiarray/stringdtype/dtype.c | 17 ++++- numpy/_core/tests/test_stringdtype.py | 72 +++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) diff --git a/numpy/_core/src/multiarray/stringdtype/casts.cpp b/numpy/_core/src/multiarray/stringdtype/casts.cpp index cb29ef9bdc00..f8cc1486876b 100644 --- a/numpy/_core/src/multiarray/stringdtype/casts.cpp +++ b/numpy/_core/src/multiarray/stringdtype/casts.cpp @@ -147,6 +147,9 @@ string_to_string_resolve_descriptors(PyObject *NPY_UNUSED(self), { if (given_descrs[1] == NULL) { loop_descrs[1] = stringdtype_finalize_descr(given_descrs[0]); + if (loop_descrs[1] == NULL) { + return (NPY_CASTING)-1; + } } else { Py_INCREF(given_descrs[1]); @@ -852,9 +855,13 @@ make_type2s_name(NPY_TYPES typenum) { size_t nlen = strlen(type_name); const char suffix[] = "_to_StringDType"; - size_t slen = sizeof(prefix)/sizeof(char) - 1; + size_t slen = sizeof(suffix)/sizeof(char) - 1; char *buf = (char *)PyMem_RawCalloc(sizeof(char), plen + nlen + slen + 1); + if (buf == NULL) { + npy_gil_error(PyExc_MemoryError, "Failed allocate memory for cast"); + return NULL; + } // memcpy instead of strcpy/strncat to avoid stringop-truncation warning, // since we are not including the trailing null character @@ -1873,7 +1880,7 @@ void_to_string(PyArrayMethod_Context *context, char *const data[], npy_intp out_stride = strides[1]; while(N--) { - size_t out_num_bytes = utf8_buffer_size(in, max_in_size); + Py_ssize_t out_num_bytes = utf8_buffer_size(in, max_in_size); if (out_num_bytes < 0) { npy_gil_error(PyExc_TypeError, "Invalid UTF-8 bytes found, cannot convert to UTF-8"); @@ -1881,7 +1888,7 @@ void_to_string(PyArrayMethod_Context *context, char *const data[], } npy_static_string out_ss = {0, NULL}; if (load_new_string((npy_packed_static_string *)out, - &out_ss, out_num_bytes, allocator, + &out_ss, (size_t)out_num_bytes, allocator, "void to string cast") == -1) { goto fail; } diff --git a/numpy/_core/src/multiarray/stringdtype/dtype.c b/numpy/_core/src/multiarray/stringdtype/dtype.c index c3d4c4f20f8b..c5d47b2b8278 100644 --- a/numpy/_core/src/multiarray/stringdtype/dtype.c +++ b/numpy/_core/src/multiarray/stringdtype/dtype.c @@ -230,7 +230,7 @@ common_instance(PyArray_StringDTypeObject *dtype1, PyArray_StringDTypeObject *dt } return (PyArray_StringDTypeObject *)new_stringdtype_instance( - out_na_object, dtype1->coerce && dtype1->coerce); + out_na_object, dtype1->coerce && dtype2->coerce); } /* @@ -266,6 +266,13 @@ as_pystring(PyObject *scalar, int coerce) Py_INCREF(scalar); return scalar; } + // str subclasses (e.g. np.str_) are string data even when coercion is + // disabled; convert to an exact str, preserving embedded and trailing + // nulls that a round-trip through a fixed-width unicode descriptor + // would strip + if (PyUnicode_Check(scalar)) { + return PyUnicode_FromObject(scalar); + } if (coerce == 0) { PyErr_SetString(PyExc_ValueError, "StringDType only allows string data when " @@ -635,6 +642,11 @@ stringdtype_is_known_scalar_type(PyArray_DTypeMeta *cls, { return 1; } + // otherwise np.str_ discovers its fixed-width 'U' descriptor, whose + // cast into StringDType strips trailing NULs setitem would preserve + else if (pytype == &PyUnicodeArrType_Type) { + return 1; + } return 0; } @@ -654,6 +666,9 @@ stringdtype_finalize_descr(PyArray_Descr *dtype) NpyString_release_allocator(allocator); PyArray_StringDTypeObject *ret = (PyArray_StringDTypeObject *)new_stringdtype_instance( sdtype->na_object, sdtype->coerce); + if (ret == NULL) { + return NULL; + } ret->array_owned = 1; return (PyArray_Descr *)ret; } diff --git a/numpy/_core/tests/test_stringdtype.py b/numpy/_core/tests/test_stringdtype.py index 72684de49563..5bb1085a9ddc 100644 --- a/numpy/_core/tests/test_stringdtype.py +++ b/numpy/_core/tests/test_stringdtype.py @@ -144,6 +144,35 @@ def test_create_with_na(dtype): assert arr[1] is dtype.na_object +def test_create_with_failing_na_comparison(): + # An error raised while re-creating an array-owned descriptor used to + # crash with a NULL dereference in stringdtype_finalize_descr instead + # of propagating. + class Flaky: + def __init__(self): + self.calls = 0 + + def __eq__(self, other): + return self is other + + def __ne__(self, other): + self.calls += 1 + if self.calls > 1: + raise RuntimeError("boom") + return False + + def __hash__(self): + return 0 + + dt = StringDType(na_object=Flaky()) + # the first array takes ownership of the descriptor + np.array(["x"], dtype=dt) + # the second array creation re-creates the descriptor, re-running the + # na object classification, which fails + with pytest.raises(RuntimeError, match="boom"): + np.array(["y"], dtype=dt) + + @pytest.mark.parametrize("i", list(range(5))) def test_set_replace_na(i): # Test strings of various lengths can be set to NaN and then replaced. @@ -168,6 +197,18 @@ def test_null_roundtripping(): assert data[1] == arr[1] +@pytest.mark.parametrize("coerce", [True, False]) +def test_np_str_trailing_nul_preserved(coerce): + dtype = StringDType(coerce=coerce) + value = np.str_("q\x00") + arr = np.empty(1, dtype=dtype) + arr[0] = value + assert arr[0] == "q\x00" + arr[0:1] = [value] + assert arr[0] == "q\x00" + assert np.array([value], dtype=dtype)[0] == "q\x00" + + @pytest.mark.parametrize( "op, pyop", [ @@ -311,6 +352,17 @@ def test_self_casts(dtype, dtype2, strings): assert_array_equal(arr[:-1], newarr[:-1]) +def test_cast_method_names(): + get_castingimpl = np._core._multiarray_umath._get_castingimpl + string_DT = type(StringDType()) + for other_DT, name in [(np.dtypes.BoolDType, "bool"), + (np.dtypes.Float64DType, "double")]: + to_string = get_castingimpl(other_DT, string_DT) + assert f"cast_{name}_to_StringDType" in repr(to_string) + from_string = get_castingimpl(string_DT, other_DT) + assert f"cast_StringDType_to_{name}" in repr(from_string) + + @pytest.mark.parametrize( ("strings"), [ @@ -911,6 +963,15 @@ def test_string_to_bytes_invalid_ascii_error(): ) +def test_void_to_string_invalid_utf8_error(): + # invalid UTF-8 in a void buffer used to surface as a confusing + # MemoryError because the error return of utf8_buffer_size was + # stored in an unsigned variable + varr = np.array([b"\xff\xff\xff\xff"], dtype="V4") + with pytest.raises(TypeError, match="Invalid UTF-8"): + varr.astype(StringDType()) + + @pytest.mark.parametrize( "typename", [ @@ -1786,6 +1847,17 @@ def test_unset_na_coercion(): arr == op +def test_coerce_promotion_commutative(): + # promoting a coerce=False instance with a coerce=True instance + # used to depend on the argument order + dt = StringDType() + dt_no_coerce = StringDType(coerce=False) + assert np.promote_types(dt, dt_no_coerce) == dt_no_coerce + assert np.promote_types(dt_no_coerce, dt) == dt_no_coerce + assert np.promote_types(dt, dt) == dt + assert np.promote_types(dt_no_coerce, dt_no_coerce) == dt_no_coerce + + def test_repeat(string_array): res = string_array.repeat(1000) # Create an empty array with expanded dimension, and fill it. Then, From 03f4b1dfec8b950473af64a8b8edce9b77aeba7e Mon Sep 17 00:00:00 2001 From: Aniket <148300120+Aniketsy@users.noreply.github.com> Date: Fri, 3 Jul 2026 23:31:54 +0530 Subject: [PATCH 23/60] MAINT: fix github/codeql-action comment version in `scorecards.yml` (#31850) --- .github/workflows/scorecards.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 3080ed567eee..a33d2f2b1d24 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -50,6 +50,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v2.1.27 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: results.sarif From aa91363c3c4d3512e5d6c2b3f6526c4f19c3caa1 Mon Sep 17 00:00:00 2001 From: stratakis Date: Fri, 3 Jul 2026 20:20:10 +0200 Subject: [PATCH 24/60] TST: Add MaskedRecords.view ndarray subclass tests (#31818) Co-authored-by: Saba Siddique --- numpy/ma/tests/test_mrecords.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/numpy/ma/tests/test_mrecords.py b/numpy/ma/tests/test_mrecords.py index b4070df0f9a3..0cd4aa62ec32 100644 --- a/numpy/ma/tests/test_mrecords.py +++ b/numpy/ma/tests/test_mrecords.py @@ -384,6 +384,24 @@ def test_view_flexible_type(self): assert_equal(test.dtype, np.dtype(alttype)) assert_(test._fill_value is None) + def test_view_ndarray_subclass_preserves_dtype(self): + mrec = self._create_data()[0] + + class MySub(np.ndarray): + pass + + test = mrec.view(MySub) + assert_(isinstance(test, MySub)) + assert_equal(test.dtype, mrec.dtype) + + def test_view_maskedarray_preserves_fill_value(self): + mrec = self._create_data()[0] + original_fv = mrec.fill_value + + test = mrec.view(ma.MaskedArray) + assert_(isinstance(test, ma.MaskedArray)) + assert_equal(test.fill_value, original_fv) + ############################################################################## class TestMRecordsImport: From 20451cf018403cf0813833ad40dc725136322275 Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 1 Jul 2026 20:13:38 +0200 Subject: [PATCH 25/60] TYP: specialized ``ldexp`` ufunc --- numpy/__init__.pyi | 2 +- numpy/_core/umath.pyi | 350 +++++++++++++++++++++- numpy/typing/tests/data/reveal/ufuncs.pyi | 48 +++ 3 files changed, 398 insertions(+), 2 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index 2e4557cc091c..dd223979e59a 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -428,6 +428,7 @@ from numpy._core.umath import ( isinf, isnan, isnat, + ldexp, less, less_equal, log, @@ -7627,7 +7628,6 @@ gcd: _UFunc_Nin2_Nout1[L["gcd"], L[11], L[0]] heaviside: _UFunc_Nin2_Nout1[L["heaviside"], L[4], None] hypot: _UFunc_Nin2_Nout1[L["hypot"], L[5], L[0]] lcm: _UFunc_Nin2_Nout1[L["lcm"], L[11], None] -ldexp: _UFunc_Nin2_Nout1[L["ldexp"], L[8], None] left_shift: _UFunc_Nin2_Nout1[L["left_shift"], L[11], None] logaddexp2: _UFunc_Nin2_Nout1[L["logaddexp2"], L[4], float] logaddexp: _UFunc_Nin2_Nout1[L["logaddexp"], L[4], float] diff --git a/numpy/_core/umath.pyi b/numpy/_core/umath.pyi index fb3953fab5e2..f73be75436e2 100644 --- a/numpy/_core/umath.pyi +++ b/numpy/_core/umath.pyi @@ -45,7 +45,6 @@ from numpy import ( heaviside, hypot, lcm, - ldexp, left_shift, logaddexp, logaddexp2, @@ -4138,6 +4137,353 @@ class _ufunc_21_cmp(_ufunc_21[None]): # type: ignore[misc] out: np.ndarray | None = None, ) -> OutT: ... +# efdg, il => efdg +@type_check_only +class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] + # TODO(@jorenham): shape-typing + + @override + @overload # 0d T@floating, 0d + def __call__[ScalarT: np.floating]( + self, + x1: ScalarT, + x2: _IntLike_co, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> ScalarT: ... + @overload # 0d np.float64: ... + @overload # >0d T@floating, >=0d + def __call__[ScalarT: np.floating]( + self, + x1: npt.NDArray[ScalarT] | _NestedSequence[ScalarT], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[ScalarT]: ... + @overload # >0d =0d + def __call__( + self, + x1: npt.NDArray[_to_integer] | _NestedSequence[float | _to_integer], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float64]: ... + @overload # >=0d T@floating, >0d + def __call__[ScalarT: np.floating]( + self, + x1: _ArrayLike[ScalarT], + x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[ScalarT]: ... + @overload # >=0d 0d + def __call__( + self, + x1: _DualArrayLike[np.dtype[_to_integer], float], + x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float64]: ... + @overload # >=0d T@floating, >=0d, out=... + def __call__[ScalarT: np.floating]( + self, + x1: _ArrayLike[ScalarT], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[ScalarT]: ... + @overload # >=0d =0d, out=... + def __call__( + self, + x1: _DualArrayLike[np.dtype[_to_integer], float], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float64]: ... + @overload # out= + def __call__[OutT: np.ndarray]( + self, + x1: _ArrayLikeFloat_co, + x2: _ArrayLikeInt_co, + /, + out: OutT, + *, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x1.__array_ufunc__(self, "__call__", x1, x2, ...) + def __call__[OtherT, OutT]( + self, + x1: _CanUfuncCall2L[OtherT, OutT], + x2: OtherT, + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x2.__array_ufunc__(self, "__call__", x1, x2, ...) + def __call__[OtherT, OutT]( + self, + x1: OtherT, + x2: _CanUfuncCall2R[OtherT, OutT], + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + + # + @override + @overload # 0d T@floating, 0d + def outer[ScalarT: np.floating]( # pyrefly:ignore[bad-override] + self, + x1: ScalarT, + x2: _IntLike_co, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> ScalarT: ... + @overload # 0d np.float64: ... + @overload # >0d T@floating, >=0d + def outer[ScalarT: np.floating]( + self, + x1: npt.NDArray[ScalarT] | _NestedSequence[ScalarT], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[ScalarT]: ... + @overload # >0d =0d + def outer( + self, + x1: npt.NDArray[_to_integer] | _NestedSequence[float | _to_integer], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float64]: ... + @overload # >=0d T@floating, >0d + def outer[ScalarT: np.floating]( + self, + x1: _ArrayLike[ScalarT], + x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[ScalarT]: ... + @overload # >=0d 0d + def outer( + self, + x1: _DualArrayLike[np.dtype[_to_integer], float], + x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float64]: ... + @overload # >=0d T@floating, >=0d, out=... + def outer[ScalarT: np.floating]( + self, + x1: _ArrayLike[ScalarT], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[ScalarT]: ... + @overload # >=0d =0d, out=... + def outer( + self, + x1: _DualArrayLike[np.dtype[_to_integer], float], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float64]: ... + @overload # out= + def outer[OutT: np.ndarray]( + self, + x1: _ArrayLikeFloat_co, + x2: _ArrayLikeInt_co, + /, + out: OutT, + *, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x1.__array_ufunc__(self, "outer", x1, x2, ...) + def outer[OtherT, OutT]( + self, + x1: _CanUfuncCall2L[OtherT, OutT], + x2: OtherT, + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + @overload # x2.__array_ufunc__(self, "outer", x1, x2, ...) + def outer[OtherT, OutT]( + self, + x1: OtherT, + x2: _CanUfuncCall2R[OtherT, OutT], + /, + *, + out: object | None = None, + dtype: npt.DTypeLike | None = None, + **kwargs: Unpack[_Kwargs21], + ) -> OutT: ... + + # + @override + @overload + def at(self, a: npt.NDArray[_to_floating], indices: _ArrayLikeInt, b: _ArrayLikeInt_co, /) -> None: ... # pyrefly:ignore[bad-override] + @overload + def at[OtherT, IxT, OutT](self, a: _CanUfuncAt2L[OtherT, IxT, OutT], indices: IxT, b: OtherT, /) -> OutT: ... + @overload + def at[OtherT, IxT, OutT](self, a: OtherT, indices: IxT, b: _CanUfuncAt2R[OtherT, IxT, OutT], /) -> OutT: ... + + # + @override + @overload # unknown shape + def reduce( # pyrefly:ignore[bad-override] + self, + array: _ArrayLikeInt, + /, + *, + axis: int | tuple[int, ...] = 0, + dtype: None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.float64] | np.float64: ... + @overload # unknown shape, axis=None + def reduce( + self, + array: _ArrayLikeInt, + /, + *, + axis: None, + dtype: None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> np.float64: ... + @overload # unknown shape, keepdims=True + def reduce( + self, + array: _ArrayLikeInt, + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: None = None, + out: None = None, + keepdims: Literal[True], + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.float64]: ... + @overload # unknown shape, out=... + def reduce( + self, + array: _ArrayLikeInt, + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: None = None, + out: EllipsisType, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.float64]: ... + @overload # out= + def reduce[OutT: np.ndarray]( + self, + array: _ArrayLikeInt, + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: None = None, + out: OutT, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> OutT: ... + @overload # array.__array_ufunc__(self, "reduce", array, ...) + def reduce[OutT]( + self, + array: _CanUfuncReduce[OutT], + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: npt.DTypeLike | None = None, + out: np.ndarray | EllipsisType | None = None, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> OutT: ... + + @override + def reduceat(self, array: Never, /, indices: Never) -> Never: ... # pyrefly:ignore[bad-override] + @override + def accumulate(self, array: Never, /) -> Never: ... # pyrefly:ignore[bad-override] + logical_and: Final[_ufunc_21_logical[Literal[True]]] = ... logical_or: Final[_ufunc_21_logical[Literal[False]]] = ... logical_xor: Final[_ufunc_21_logical[Literal[False]]] = ... @@ -4149,6 +4495,8 @@ less: Final[_ufunc_21_cmp] = ... less_equal: Final[_ufunc_21_cmp] = ... not_equal: Final[_ufunc_21_cmp] = ... +ldexp: Final[_ufunc_21_ldexp] = ... + ### # re-exports from `_core._multiarray_umath` that are used by `_core._ufunc_config` diff --git a/numpy/typing/tests/data/reveal/ufuncs.pyi b/numpy/typing/tests/data/reveal/ufuncs.pyi index 321795c93fdc..bfc52fed6317 100644 --- a/numpy/typing/tests/data/reveal/ufuncs.pyi +++ b/numpy/typing/tests/data/reveal/ufuncs.pyi @@ -924,3 +924,51 @@ assert_type(np.less.reduceat(_bool_1d, (0,)), np.ndarray[tuple[int], np.dtype[np assert_type(np.less.reduceat(_bool_2d, (0,)), np.ndarray[tuple[int, int], np.dtype[np.bool]]) assert_type(np.less.reduceat(_py_b_1d, (0,)), npt.NDArray[np.bool]) assert_type(np.less.reduceat(_py_b_1d, (0,), out=_bool_1d), np.ndarray[tuple[int], np.dtype[np.bool]]) + +# _ufunc_21_ldexp +# (ldexp) + +assert_type(np.ldexp(_py_b_0d, _py_b_0d), np.float64) +assert_type(np.ldexp(_py_b_1d, _py_b_1d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_py_b_2d, _py_b_2d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_py_i_0d, _py_i_0d), np.float64) +assert_type(np.ldexp(_py_i_1d, _py_i_1d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_py_i_2d, _py_i_2d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_py_f_0d, _py_i_0d), np.float64) +assert_type(np.ldexp(_py_f_1d, _py_i_1d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_py_f_2d, _py_i_2d), npt.NDArray[np.float64]) + +assert_type(np.ldexp(_bool_0d, _bool_0d), np.float64) +assert_type(np.ldexp(_bool_1d, _bool_1d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_bool_2d, _bool_2d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_bool_nd, _bool_nd), npt.NDArray[np.float64]) +assert_type(np.ldexp(_i16_0d, _i16_0d), np.float64) +assert_type(np.ldexp(_i16_1d, _i16_1d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_i16_2d, _i16_2d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_i16_nd, _i16_nd), npt.NDArray[np.float64]) +assert_type(np.ldexp(_f32_0d, _i16_0d), np.float32) +assert_type(np.ldexp(_f32_1d, _i16_1d), npt.NDArray[np.float32]) +assert_type(np.ldexp(_f32_2d, _i16_2d), npt.NDArray[np.float32]) +assert_type(np.ldexp(_f32_nd, _i16_nd), npt.NDArray[np.float32]) + +assert_type(np.ldexp(_py_i_2d, _py_i_2d, out=_f32_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) + +assert_type(np.ldexp.outer(_py_f_0d, _py_i_0d), np.float64) +assert_type(np.ldexp.outer(_py_f_1d, _py_i_0d), npt.NDArray[np.float64]) +assert_type(np.ldexp.outer(_py_f_0d, _py_i_1d), npt.NDArray[np.float64]) +assert_type(np.ldexp.outer(_py_f_1d, _py_i_1d), npt.NDArray[np.float64]) +assert_type(np.ldexp.outer(_f32_0d, _py_i_0d), np.float32) +assert_type(np.ldexp.outer(_f32_1d, _py_i_0d), npt.NDArray[np.float32]) +assert_type(np.ldexp.outer(_f32_0d, _py_i_1d), npt.NDArray[np.float32]) +assert_type(np.ldexp.outer(_f32_1d, _py_i_1d), npt.NDArray[np.float32]) + +assert_type(np.ldexp.at(_f32_2d, 1, _py_i_1d), None) + +assert_type(np.ldexp.reduce(_i16_1d), npt.NDArray[np.float64] | np.float64) +assert_type(np.ldexp.reduce(_i16_2d), npt.NDArray[np.float64] | np.float64) +assert_type(np.ldexp.reduce(_i16_1d, axis=None), np.float64) +assert_type(np.ldexp.reduce(_i16_1d, keepdims=True), npt.NDArray[np.float64]) +assert_type(np.ldexp.reduce(_i16_1d, out=...), npt.NDArray[np.float64]) +assert_type(np.ldexp.reduce(_i16_1d, out=_f32_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) + +# ldexp doesn't support reduceat / accumulate From 1cfe55c4c307dca9b4e9fc92ecf11ec3d1ef3bbf Mon Sep 17 00:00:00 2001 From: Adhyan <123641130+Adhyansinghgupta@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:16:25 +0000 Subject: [PATCH 26/60] BUG: Fix segfault in MT19937 by preventing recursive seed lists --- numpy/random/bit_generator.pyx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/numpy/random/bit_generator.pyx b/numpy/random/bit_generator.pyx index 676f95e5ad70..aa3389c9ccf4 100644 --- a/numpy/random/bit_generator.pyx +++ b/numpy/random/bit_generator.pyx @@ -249,6 +249,18 @@ cdef class SeedlessSeedSequence: # We cannot directly subclass a `cdef class` type from an `ABC` in Cython, so # we must register it after the fact. ISpawnableSeedSequence.register(SeedlessSeedSequence) +def _has_cycle(obj, visited=None): + if visited is None: + visited = set() + if id(obj) in visited: + return True + visited.add(id(obj)) + if isinstance(obj, list): + for item in obj: + if _has_cycle(item, visited): + return True + visited.remove(id(obj)) + return False cdef class SeedSequence: @@ -300,6 +312,12 @@ cdef class SeedSequence: def __init__(self, entropy=None, *, spawn_key=(), pool_size=DEFAULT_POOL_SIZE, n_children_spawned=0): + + # --- OUR FIX --- + if isinstance(entropy, list) and _has_cycle(entropy): + raise ValueError("Seed (entropy) cannot be a recursive or self-referencing list.") + # --------------- + if pool_size < DEFAULT_POOL_SIZE: raise ValueError("The size of the entropy pool should be at least " f"{DEFAULT_POOL_SIZE}") From 6a5bd7df70099bd95587abb3f57cbbfe8bfbce9f Mon Sep 17 00:00:00 2001 From: Adhyan <123641130+Adhyansinghgupta@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:30:05 +0000 Subject: [PATCH 27/60] MAINT: Move SeedSequence recursion guard to C-layer and add tests --- numpy/random/tests/test_seed_sequence.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/numpy/random/tests/test_seed_sequence.py b/numpy/random/tests/test_seed_sequence.py index 87ae4ff72139..8f03eb3da07f 100644 --- a/numpy/random/tests/test_seed_sequence.py +++ b/numpy/random/tests/test_seed_sequence.py @@ -77,3 +77,20 @@ def test_zero_padding(): np.not_equal, SeedSequence(42, spawn_key=(0,)).generate_state(4), expected42) +def test_seedsequence_rejects_nested_sequence(): + import pytest # <-- We just added this line! + from numpy.random import SeedSequence + + # Prevents infinite recursion (Issue #28822) and rejects invalid types (Issue #27380) + match_str = "SeedSequence does not accept nested sequences." + + # Test standard nested lists + with pytest.raises(TypeError, match=match_str): + SeedSequence([[1, 2], [3, 4]]) + + # Test self-referencing/cyclic lists + with pytest.raises(TypeError, match=match_str): + cyclic_seed = [] + cyclic_seed.append(cyclic_seed) + SeedSequence(cyclic_seed) + From 6f48eadc80d8c5d74bd35ad619d35ab69e7dd0e2 Mon Sep 17 00:00:00 2001 From: Adhyan <123641130+Adhyansinghgupta@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:43:03 +0000 Subject: [PATCH 28/60] MAINT: Remove deprecated Python recursion fix --- numpy/random/bit_generator.pyx | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/numpy/random/bit_generator.pyx b/numpy/random/bit_generator.pyx index aa3389c9ccf4..eb59a98ac824 100644 --- a/numpy/random/bit_generator.pyx +++ b/numpy/random/bit_generator.pyx @@ -142,9 +142,16 @@ def _coerce_to_uint32_array(x): else: if len(x) == 0: return np.array([], dtype=np.uint32) + # Should be a sequence of interpretable-as-ints. Convert each one to # a uint32 array and concatenate. - subseqs = [_coerce_to_uint32_array(v) for v in x] + subseqs = [] + for v in x: + if hasattr(v, '__len__') and not isinstance(v, str): + raise TypeError("SeedSequence does not accept nested sequences.") + + subseqs.append(_coerce_to_uint32_array(v)) + return np.concatenate(subseqs) @@ -249,18 +256,6 @@ cdef class SeedlessSeedSequence: # We cannot directly subclass a `cdef class` type from an `ABC` in Cython, so # we must register it after the fact. ISpawnableSeedSequence.register(SeedlessSeedSequence) -def _has_cycle(obj, visited=None): - if visited is None: - visited = set() - if id(obj) in visited: - return True - visited.add(id(obj)) - if isinstance(obj, list): - for item in obj: - if _has_cycle(item, visited): - return True - visited.remove(id(obj)) - return False cdef class SeedSequence: @@ -313,10 +308,6 @@ cdef class SeedSequence: def __init__(self, entropy=None, *, spawn_key=(), pool_size=DEFAULT_POOL_SIZE, n_children_spawned=0): - # --- OUR FIX --- - if isinstance(entropy, list) and _has_cycle(entropy): - raise ValueError("Seed (entropy) cannot be a recursive or self-referencing list.") - # --------------- if pool_size < DEFAULT_POOL_SIZE: raise ValueError("The size of the entropy pool should be at least " From 70b7a1f2961274ad55a1a977fa4e46fb0a8da116 Mon Sep 17 00:00:00 2001 From: Adhyan <123641130+Adhyansinghgupta@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:21:11 +0000 Subject: [PATCH 29/60] TEST: Refactor tests to use np.testing.assert_raises_regex per review --- numpy/random/bit_generator.pyx | 2 -- numpy/random/tests/test_seed_sequence.py | 12 +++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/numpy/random/bit_generator.pyx b/numpy/random/bit_generator.pyx index eb59a98ac824..614b33726a34 100644 --- a/numpy/random/bit_generator.pyx +++ b/numpy/random/bit_generator.pyx @@ -307,8 +307,6 @@ cdef class SeedSequence: def __init__(self, entropy=None, *, spawn_key=(), pool_size=DEFAULT_POOL_SIZE, n_children_spawned=0): - - if pool_size < DEFAULT_POOL_SIZE: raise ValueError("The size of the entropy pool should be at least " f"{DEFAULT_POOL_SIZE}") diff --git a/numpy/random/tests/test_seed_sequence.py b/numpy/random/tests/test_seed_sequence.py index 8f03eb3da07f..6cb1ef8db9ba 100644 --- a/numpy/random/tests/test_seed_sequence.py +++ b/numpy/random/tests/test_seed_sequence.py @@ -1,6 +1,6 @@ import numpy as np from numpy.random import SeedSequence -from numpy.testing import assert_array_compare, assert_array_equal +from numpy.testing import assert_array_compare, assert_array_equal, assert_raises, assert_raises_regex def test_reference_data(): @@ -77,19 +77,21 @@ def test_zero_padding(): np.not_equal, SeedSequence(42, spawn_key=(0,)).generate_state(4), expected42) + + def test_seedsequence_rejects_nested_sequence(): - import pytest # <-- We just added this line! - from numpy.random import SeedSequence + with assert_raises(TypeError): + SeedSequence(SeedSequence(42)) # Prevents infinite recursion (Issue #28822) and rejects invalid types (Issue #27380) match_str = "SeedSequence does not accept nested sequences." # Test standard nested lists - with pytest.raises(TypeError, match=match_str): + with assert_raises_regex(TypeError, match_str): SeedSequence([[1, 2], [3, 4]]) # Test self-referencing/cyclic lists - with pytest.raises(TypeError, match=match_str): + with assert_raises_regex(TypeError, match_str): cyclic_seed = [] cyclic_seed.append(cyclic_seed) SeedSequence(cyclic_seed) From c51a4ed809a10e116e5d0f60f025b0f860e0d8e2 Mon Sep 17 00:00:00 2001 From: Adhyan <123641130+Adhyansinghgupta@users.noreply.github.com> Date: Sat, 27 Jun 2026 20:34:46 +0000 Subject: [PATCH 30/60] TST: Clean up imports, formatting, and assertions --- numpy/random/tests/test_seed_sequence.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/numpy/random/tests/test_seed_sequence.py b/numpy/random/tests/test_seed_sequence.py index 6cb1ef8db9ba..7e91244c0fed 100644 --- a/numpy/random/tests/test_seed_sequence.py +++ b/numpy/random/tests/test_seed_sequence.py @@ -1,6 +1,11 @@ import numpy as np from numpy.random import SeedSequence -from numpy.testing import assert_array_compare, assert_array_equal, assert_raises, assert_raises_regex +from numpy.testing import ( + assert_array_compare, + assert_array_equal, + assert_raises, + assert_raises_regex, +) def test_reference_data(): @@ -82,17 +87,17 @@ def test_zero_padding(): def test_seedsequence_rejects_nested_sequence(): with assert_raises(TypeError): SeedSequence(SeedSequence(42)) - - # Prevents infinite recursion (Issue #28822) and rejects invalid types (Issue #27380) + + # Prevents infinite recursion (Issue #28822) and rejects + # invalid types (Issue #27380) match_str = "SeedSequence does not accept nested sequences." - + # Test standard nested lists with assert_raises_regex(TypeError, match_str): SeedSequence([[1, 2], [3, 4]]) - + # Test self-referencing/cyclic lists with assert_raises_regex(TypeError, match_str): cyclic_seed = [] cyclic_seed.append(cyclic_seed) - SeedSequence(cyclic_seed) - + SeedSequence(cyclic_seed) From 4deb9ccea1063f833560e2da8249177d124960b7 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 3 Jul 2026 13:31:57 -0600 Subject: [PATCH 31/60] BLD: lower cython and meson-python minimum version and adjust dependabot config (#31841) --- .github/dependabot.yml | 3 ++- requirements/build_requirements.txt | 4 ++-- requirements/doc_requirements.txt | 12 ++++++------ requirements/linter_requirements.txt | 2 +- requirements/release_requirements.txt | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7b28b685d3d0..0efbf18aa0ae 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,7 @@ updates: - package-ecosystem: github-actions directory: / schedule: - interval: daily + interval: weekly cooldown: default-days: 7 # optional commit-message: @@ -16,6 +16,7 @@ updates: directory: /requirements schedule: interval: daily + versioning-strategy: increase-if-necessary cooldown: default-days: 7 # optional groups: diff --git a/requirements/build_requirements.txt b/requirements/build_requirements.txt index 2dc663424035..8075e645b10e 100644 --- a/requirements/build_requirements.txt +++ b/requirements/build_requirements.txt @@ -1,5 +1,5 @@ -meson-python>=0.20.0 -Cython>=3.2.6 +meson-python>=0.18.0 +Cython>=3.0.6 ninja spin build diff --git a/requirements/doc_requirements.txt b/requirements/doc_requirements.txt index 6a58cde6ced5..f55a119056e2 100644 --- a/requirements/doc_requirements.txt +++ b/requirements/doc_requirements.txt @@ -1,14 +1,14 @@ # doxygen required, use apt-get or dnf sphinx==8.2.3 numpydoc==1.10.0 -pydata-sphinx-theme>=0.19.0 +pydata-sphinx-theme sphinx-copybutton sphinx-design scipy -matplotlib>=3.10.9 +matplotlib pandas -breathe>=4.36.0 -ipython>=9.12.0 +breathe +ipython # Needed for ipython>=8.17 # https://github.com/ipython/ipython/issues/14237 pickleshare @@ -17,10 +17,10 @@ pickleshare towncrier toml -scipy-doctest>=2.2.0 +scipy-doctest # interactive documentation utilities # see https://github.com/jupyterlite/pyodide-kernel#compatibility -jupyterlite-sphinx>=0.22.1 +jupyterlite-sphinx # Works with Pyodide 0.27.1 jupyterlite-pyodide-kernel==0.5.2 diff --git a/requirements/linter_requirements.txt b/requirements/linter_requirements.txt index 82a1c1d72355..5cb0cff1b3c6 100644 --- a/requirements/linter_requirements.txt +++ b/requirements/linter_requirements.txt @@ -1,5 +1,5 @@ # keep in sync with `environment.yml` cython-lint ruff==0.15.20 -GitPython>=3.1.50 +GitPython==3.1.50 spin diff --git a/requirements/release_requirements.txt b/requirements/release_requirements.txt index 9bbefcd01dbc..1e2acd619d3c 100644 --- a/requirements/release_requirements.txt +++ b/requirements/release_requirements.txt @@ -3,7 +3,7 @@ # changelog.py pygithub -gitpython>=3.1.50 +gitpython==3.1.50 # uploading release documentation packaging From 064b75cfbee7229ba639cbadacc74d9e3a1fb2a5 Mon Sep 17 00:00:00 2001 From: AnkitAhlawat Date: Sat, 4 Jul 2026 01:51:23 +0530 Subject: [PATCH 32/60] fix:Signed integer overflow in datetime.c (#31688) --- numpy/_core/src/multiarray/datetime.c | 15 +++++++++++++-- numpy/_core/tests/test_datetime.py | 10 ++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/numpy/_core/src/multiarray/datetime.c b/numpy/_core/src/multiarray/datetime.c index a15ad5703658..be6f8ca03669 100644 --- a/numpy/_core/src/multiarray/datetime.c +++ b/numpy/_core/src/multiarray/datetime.c @@ -167,12 +167,23 @@ static npy_int64 days_to_yearsdays(npy_int64 *days_) { const npy_int64 days_per_400years = (400*365 + 100 - 4 + 1); + const npy_int64 days_offset = (365*30 + 7); /* Adjust so it's relative to the year 2000 (divisible by 400) */ - npy_int64 days = (*days_) - (365*30 + 7); + npy_int64 days; npy_int64 year; + if (*days_ < NPY_MIN_INT64 + days_offset) { + /* Extract 400-year cycles first to reduce magnitude */ + days = *days_ + (days_per_400years - days_offset); + year = 400 * extract_unit_64(&days, days_per_400years); + year -= 400; + } + else { + days = (*days_) - days_offset; + year = 400 * extract_unit_64(&days, days_per_400years); + } + /* Break down the 400 year cycle to get the year and day within the year */ - year = 400 * extract_unit_64(&days, days_per_400years); /* Work out the year/day within the 400 year cycle */ if (days >= 366) { diff --git a/numpy/_core/tests/test_datetime.py b/numpy/_core/tests/test_datetime.py index 16e0265e6afa..ab94706a1cf0 100644 --- a/numpy/_core/tests/test_datetime.py +++ b/numpy/_core/tests/test_datetime.py @@ -1265,6 +1265,16 @@ def test_arithmetic_valid_boundary(self): assert small * np.int64(7) == np.timedelta64(21, "s") assert np.int64(7) * small == np.timedelta64(21, "s") + def test_datetime64_item_int64_min_edge_case(self): + info = np.iinfo(np.int64) + + for offset in [1, 2, 100, 1000, 10000]: + dt64 = np.datetime64(info.min + offset, "D") + result = dt64.item() + # expected integer value + assert result == info.min + offset + assert isinstance(result, int) + def test_pyobject_roundtrip(self): # All datetime types should be able to roundtrip through object a = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, From 1a61c82350982775b61a4f0e426de70872e031d5 Mon Sep 17 00:00:00 2001 From: Aniket <148300120+Aniketsy@users.noreply.github.com> Date: Sat, 4 Jul 2026 05:02:19 +0530 Subject: [PATCH 33/60] BUG: fix `np.insert` ignoring out of bounds negative indices in multi element obj (#31782) Co-authored-by: Nathan Goldbaum --- doc/release/upcoming_changes/31782.compatibility.rst | 6 ++++++ numpy/lib/_function_base_impl.py | 9 +++++++++ numpy/lib/tests/test_function_base.py | 10 +++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 doc/release/upcoming_changes/31782.compatibility.rst diff --git a/doc/release/upcoming_changes/31782.compatibility.rst b/doc/release/upcoming_changes/31782.compatibility.rst new file mode 100644 index 000000000000..eaed3cabfecf --- /dev/null +++ b/doc/release/upcoming_changes/31782.compatibility.rst @@ -0,0 +1,6 @@ +`numpy.insert` out-of-bounds index detection +--------------------------------------------- +``numpy.insert`` now consistently raises ``IndexError`` for any +out-of-bounds index, including when out-of-bounds and in-bounds +indices are mixed in the same call. Previously such cases could +silently succeed or produce incorrect results. diff --git a/numpy/lib/_function_base_impl.py b/numpy/lib/_function_base_impl.py index 18b3ce783c8f..aaac4abea546 100644 --- a/numpy/lib/_function_base_impl.py +++ b/numpy/lib/_function_base_impl.py @@ -5562,6 +5562,15 @@ def insert(arr, obj, values, axis=None): # Can safely cast the empty list to intp indices = indices.astype(intp) + if indices.size > 0: + min_idx = indices.min() + max_idx = indices.max() + if min_idx < -N or max_idx > N: + oob = min_idx if min_idx < -N else max_idx + raise IndexError( + f"index {oob} is out of bounds " + f"for axis {axis} with size {N}" + ) indices[indices < 0] += N numnew = len(indices) diff --git a/numpy/lib/tests/test_function_base.py b/numpy/lib/tests/test_function_base.py index cd3d34893cbb..9440ec3f1c5f 100644 --- a/numpy/lib/tests/test_function_base.py +++ b/numpy/lib/tests/test_function_base.py @@ -698,10 +698,14 @@ def test_index_floats(self): with pytest.raises(IndexError): np.insert([0, 1, 2], np.array([], dtype=float), []) - @pytest.mark.parametrize('idx', [4, -4]) - def test_index_out_of_bounds(self, idx): + @pytest.mark.parametrize('idx,values', [ + ([4], [3, 4]), + ([-4], [3, 4]), + ([-6, 0], [9, 8]), + ]) + def test_index_out_of_bounds(self, idx, values): with pytest.raises(IndexError, match='out of bounds'): - np.insert([0, 1, 2], [idx], [3, 4]) + np.insert([0, 1, 2], idx, values) class TestAmax: From 9e9170ab889428f22c77ea454880b2a9755f483b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 3 Jul 2026 18:42:20 -0600 Subject: [PATCH 34/60] CI: fix hangs on MacOS ASan CI (#31853) --- .github/workflows/compiler_sanitizers.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/compiler_sanitizers.yml b/.github/workflows/compiler_sanitizers.yml index ccc9a1ad0d81..56d88194d013 100644 --- a/.github/workflows/compiler_sanitizers.yml +++ b/.github/workflows/compiler_sanitizers.yml @@ -33,7 +33,7 @@ jobs: clang_ASAN_UBSAN: # To enable this workflow on a fork, comment out: if: github.repository == 'numpy/numpy' - runs-on: macos-latest + runs-on: macos-26 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -42,7 +42,7 @@ jobs: persist-credentials: false - name: Set up pyenv run: | - git clone --branch v2.6.25 --depth 1 https://github.com/pyenv/pyenv.git "$HOME/.pyenv" + git clone --branch v2.7.3 --depth 1 https://github.com/pyenv/pyenv.git "$HOME/.pyenv" PYENV_ROOT="$HOME/.pyenv" PYENV_BIN="$PYENV_ROOT/bin" PYENV_SHIMS="$PYENV_ROOT/shims" @@ -54,15 +54,15 @@ jobs: pyenv --version - name: Set up LLVM run: | - brew install llvm@20 - LLVM_PREFIX=$(brew --prefix llvm@20) + brew install llvm + LLVM_PREFIX=$(brew --prefix llvm) echo CC="$LLVM_PREFIX/bin/clang" >> $GITHUB_ENV echo CXX="$LLVM_PREFIX/bin/clang++" >> $GITHUB_ENV echo LDFLAGS="-L$LLVM_PREFIX/lib" >> $GITHUB_ENV echo CPPFLAGS="-I$LLVM_PREFIX/include" >> $GITHUB_ENV - name: Build Python with address sanitizer run: | - CONFIGURE_OPTS="--with-address-sanitizer" pyenv install 3.14 + CONFIGURE_OPTS="--with-address-sanitizer" pyenv install -v 3.14 pyenv global 3.14 - name: Install dependencies run: | From 5e9a29bc3c1cbbce5d807bec506388255c6dfd01 Mon Sep 17 00:00:00 2001 From: Charles Harris Date: Sat, 4 Jul 2026 12:08:36 -0600 Subject: [PATCH 35/60] MAINT: Update main after 2.5.1 release. - Forward port 2.5.1-notes.rst - Forward port 2.5.1-changelog.rst - Update .mailmap - Update release.rst [skip azp] [skip actions] --- .mailmap | 1 + doc/changelog/2.5.1-changelog.rst | 44 ++++++++++++++++++++ doc/source/release.rst | 1 + doc/source/release/2.5.1-notes.rst | 66 ++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 doc/changelog/2.5.1-changelog.rst create mode 100644 doc/source/release/2.5.1-notes.rst diff --git a/.mailmap b/.mailmap index f609723222c5..b8a23ae370e8 100644 --- a/.mailmap +++ b/.mailmap @@ -83,6 +83,7 @@ Abhishek Kumar Abhishek Kumar <142383124+abhishek-iitmadras@users.noreply.github.com> Abhishek Tiwari <27881020+Abhi210@users.noreply.github.com> Abraham Medina +Adhyan Gupta <123641130+Adhyansinghgupta@users.noreply.github.com> Adrin Jalali Akhil Kannan Akhil Kannan <143798318+Alverok@users.noreply.github.com> diff --git a/doc/changelog/2.5.1-changelog.rst b/doc/changelog/2.5.1-changelog.rst new file mode 100644 index 000000000000..ec56838f13e9 --- /dev/null +++ b/doc/changelog/2.5.1-changelog.rst @@ -0,0 +1,44 @@ + +Contributors +============ + +A total of 10 people contributed to this release. People with a "+" by their +names contributed a patch for the first time. + +* Adhyan Gupta + +* Ankit Ahlawat +* Charles Harris +* Iason Krommydas +* Joren Hammudoglu +* Kumar Aditya +* Nathan Goldbaum +* Sebastian Berg +* Ties Jan Hefting + +* Vineet Kumar + +Pull requests merged +==================== + +A total of 20 pull requests were merged for this release. + +* `#31707 `__: MAINT: Prepare 2.5.x for further development +* `#31721 `__: CI: fix new ``cython-lint`` errors (#31711) +* `#31723 `__: MAINT: Update meson to match main +* `#31729 `__: TST: use setup-sde instead of curl to get SDE binaries (#31727) +* `#31829 `__: BUG: Relax finfo to be easier accessible for all user dtypes... +* `#31831 `__: TYP: Fix ``flatiter.__next__`` return type for ``object_`` and... +* `#31832 `__: BUG: avoid deadlocks using NpyString API (#31682) +* `#31833 `__: BUG: fix out array leak in reduceat and accumulate when dtype... +* `#31835 `__: BUG: fix numpy datetime cython APIs to be compatible with older... +* `#31836 `__: TYP: Fix incorrect dtype inference of ``asarray([])`` (#31732) +* `#31837 `__: TYP: Fix ``np.ma.masked_array`` 2.5.0 regression +* `#31838 `__: FIX: Refactor error handling in array_setstate to prevent typecode... +* `#31839 `__: TST: xfail multithreaded BLAS test more generously +* `#31840 `__: MAINT: Rename subroutine for crackfortran tests +* `#31842 `__: BUG: fix leak in reductions when a ufunc override errors or is... +* `#31849 `__: BLD: set minimum required gcc version to 10.3 (#31843) +* `#31855 `__: CI: fix hangs on MacOS ASan CI (#31853) +* `#31856 `__: BUG: fix several bugs in StringDType operations (#31846) +* `#31857 `__: BUG: Fix segfault in MT19937 by preventing recursive seed lists... +* `#31858 `__: BUG: Fix signed integer overflow in datetime.c (#31688) + diff --git a/doc/source/release.rst b/doc/source/release.rst index 6019f9ad9249..d33a53e4d606 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -6,6 +6,7 @@ Release notes :maxdepth: 2 2.6.0 + 2.5.1 2.5.0 2.4.6 2.4.5 diff --git a/doc/source/release/2.5.1-notes.rst b/doc/source/release/2.5.1-notes.rst new file mode 100644 index 000000000000..cc3e91e59d9e --- /dev/null +++ b/doc/source/release/2.5.1-notes.rst @@ -0,0 +1,66 @@ +.. currentmodule:: numpy + +========================= +NumPy 2.5.1 Release Notes +========================= + +The NumPy 2.5.1 is a patch release that fixes bugs discovered after the 2.5.0 +release. The most noticeable is the fix is to the numpy datetime cython API +which should allow downstream to support NumPy versions older than 2.5. +Preparation for Python 3.15 continues along with typing improvements. + +This release supports Python versions 3.12-3.14 + + +Changes +======= + +* The minimum supported GCC version has been updated from 9.3.0 to 10.3.0 + + (`gh-31843 `__) + + +Contributors +============ + +A total of 10 people contributed to this release. People with a "+" by their +names contributed a patch for the first time. + +* Adhyan Gupta + +* Ankit Ahlawat +* Charles Harris +* Iason Krommydas +* Joren Hammudoglu +* Kumar Aditya +* Nathan Goldbaum +* Sebastian Berg +* Ties Jan Hefting + +* Vineet Kumar + + +Pull requests merged +==================== + +A total of 20 pull requests were merged for this release. + +* `#31707 `__: MAINT: Prepare 2.5.x for further development +* `#31721 `__: CI: fix new ``cython-lint`` errors (#31711) +* `#31723 `__: MAINT: Update meson to match main +* `#31729 `__: TST: use setup-sde instead of curl to get SDE binaries (#31727) +* `#31829 `__: BUG: Relax finfo to be easier accessible for all user dtypes... +* `#31831 `__: TYP: Fix ``flatiter.__next__`` return type for ``object_`` and... +* `#31832 `__: BUG: avoid deadlocks using NpyString API (#31682) +* `#31833 `__: BUG: fix out array leak in reduceat and accumulate when dtype... +* `#31835 `__: BUG: fix numpy datetime cython APIs to be compatible with older... +* `#31836 `__: TYP: Fix incorrect dtype inference of ``asarray([])`` (#31732) +* `#31837 `__: TYP: Fix ``np.ma.masked_array`` 2.5.0 regression +* `#31838 `__: FIX: Refactor error handling in array_setstate to prevent typecode... +* `#31839 `__: TST: xfail multithreaded BLAS test more generously +* `#31840 `__: MAINT: Rename subroutine for crackfortran tests +* `#31842 `__: BUG: fix leak in reductions when a ufunc override errors or is... +* `#31849 `__: BLD: set minimum required gcc version to 10.3 (#31843) +* `#31855 `__: CI: fix hangs on MacOS ASan CI (#31853) +* `#31856 `__: BUG: fix several bugs in StringDType operations (#31846) +* `#31857 `__: BUG: Fix segfault in MT19937 by preventing recursive seed lists... +* `#31858 `__: BUG: Fix signed integer overflow in datetime.c (#31688) + From 2b4a64c154a2852ebace274b5a4aba6d3fa662bf Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Sun, 5 Jul 2026 19:50:33 +0300 Subject: [PATCH 36/60] MAINT: update openblas to 0.3.33.201 (#31870) --- requirements/ci32_requirements.txt | 2 +- requirements/ci_requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/ci32_requirements.txt b/requirements/ci32_requirements.txt index df42864374a4..85593f420cc5 100644 --- a/requirements/ci32_requirements.txt +++ b/requirements/ci32_requirements.txt @@ -1,3 +1,3 @@ spin # Keep this in sync with ci_requirements.txt -scipy-openblas32==0.3.33.112.0 +scipy-openblas32==0.3.33.201.0 diff --git a/requirements/ci_requirements.txt b/requirements/ci_requirements.txt index c33fc3274070..1aa588d42b7e 100644 --- a/requirements/ci_requirements.txt +++ b/requirements/ci_requirements.txt @@ -1,4 +1,4 @@ spin # Keep this in sync with ci32_requirements.txt -scipy-openblas32==0.3.33.112.0 -scipy-openblas64==0.3.33.112.0 +scipy-openblas32==0.3.33.201.0 +scipy-openblas64==0.3.33.201.0 From 49900d4f1eb89c8f25da1e5f3e6eb2a1f31ac8a3 Mon Sep 17 00:00:00 2001 From: Ijtihed Kilani Date: Sat, 13 Jun 2026 18:47:18 +0300 Subject: [PATCH 37/60] BUG: fix linspace returning NaN for equal infinite endpoints (gh-26699) --- .../upcoming_changes/31620.improvement.rst | 4 ++ numpy/_core/function_base.py | 15 +++++- numpy/_core/tests/test_function_base.py | 54 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 doc/release/upcoming_changes/31620.improvement.rst diff --git a/doc/release/upcoming_changes/31620.improvement.rst b/doc/release/upcoming_changes/31620.improvement.rst new file mode 100644 index 000000000000..3fa3465b1f13 --- /dev/null +++ b/doc/release/upcoming_changes/31620.improvement.rst @@ -0,0 +1,4 @@ +``np.linspace`` no longer returns NaN for equal infinite endpoints +------------------------------------------------------------------ +``np.linspace(np.inf, np.inf, N)`` (and ``-np.inf``) now correctly returns +an array filled with ``inf`` instead of ``nan``. diff --git a/numpy/_core/function_base.py b/numpy/_core/function_base.py index 30ce0197ba42..c792621f8cfb 100644 --- a/numpy/_core/function_base.py +++ b/numpy/_core/function_base.py @@ -137,8 +137,19 @@ def linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None, else: integer_dtype = _nx.issubdtype(dtype, _nx.integer) - # Use `dtype=type(dt)` to enforce a floating point evaluation: - delta = np.subtract(stop, start, dtype=type(dt)) + # Use `dtype=type(dt)` to enforce a floating point evaluation. + # Equal endpoints (including equal infinities) must produce a zero step, + # so skip the subtraction there: `inf - inf` would otherwise yield a + # spurious nan and an "invalid value" warning. Using `where=` avoids the + # bad element entirely rather than suppressing warnings globally, so + # genuine invalid operations (e.g. mixed infinities) still warn. + equal = start == stop + if equal.ndim == 0: + delta = dt.type(0) if equal else np.subtract( + stop, start, dtype=type(dt)) + else: + delta = np.subtract(stop, start, dtype=type(dt), where=~equal, + out=np.zeros(equal.shape, dtype=type(dt))) y = _nx.arange( 0, num, dtype=dt, device=device ).reshape((-1,) + (1,) * ndim(delta)) diff --git a/numpy/_core/tests/test_function_base.py b/numpy/_core/tests/test_function_base.py index b78c79a6f032..7ea1b7a4876c 100644 --- a/numpy/_core/tests/test_function_base.py +++ b/numpy/_core/tests/test_function_base.py @@ -1,5 +1,6 @@ import platform import sys +import warnings import pytest @@ -21,6 +22,7 @@ from numpy._core import sctypes from numpy._core.function_base import add_newdoc from numpy.testing import ( + IS_WASM, assert_, assert_allclose, assert_array_equal, @@ -482,6 +484,58 @@ def test_any_step_zero_and_not_mult_inplace(self): y = linspace(start, stop, 3) assert_array_equal(y, array([[0.0, 1.0], [1.0, 1.0], [2.0, 1.0]])) + def test_inf_equal_endpoints(self): + # Regression test for gh-26699, equal infinite endpoint were returning NaN. + # Equal endpoints skip the `inf - inf` subtraction entirely, so no + # spurious "invalid value" RuntimeWarning must be emitted (the fix does + # not rely on suppressing warnings via errstate). + with warnings.catch_warnings(): + warnings.simplefilter("error", RuntimeWarning) + for inf in [np.inf, -np.inf]: + assert_array_equal(linspace(inf, inf, 0), array([])) + assert_array_equal(linspace(inf, inf, 1), array([inf])) + for num in [2, 5, 50]: + assert_array_equal(linspace(inf, inf, num), np.full(num, inf)) + + assert_array_equal(linspace(np.inf, np.inf, 5, endpoint=False), + np.full(5, np.inf)) + + y, step = linspace(np.inf, np.inf, 2, retstep=True) + assert_array_equal(y, array([np.inf, np.inf])) + assert_equal(step, 0.0) + + start = array([np.inf, 1.0]) + stop = array([np.inf, 3.0]) + result = linspace(start, stop, 3) + assert_array_equal(result, + array([[np.inf, 1.0], [np.inf, 2.0], + [np.inf, 3.0]])) + + # Complex infinities + for cinf in [complex(np.inf, 0), complex(0, np.inf), + complex(np.inf, np.inf), complex(-np.inf, -np.inf)]: + assert_array_equal(linspace(cinf, cinf, 3), np.full(3, cinf)) + + @pytest.mark.skipif(IS_WASM, reason="fp errors don't work in wasm") + def test_inf_mixed_endpoints_warns(self): + # Mixed infinite endpoints produce NaN interior points and should warn. + cases = [(np.inf, -np.inf, -np.inf), (-np.inf, np.inf, np.inf)] + for num in [3, 5]: + for start, stop, expected_stop in cases: + with pytest.warns(RuntimeWarning, match="invalid value"): + mixed = linspace(start, stop, num) + assert_(isnan(mixed[:-1]).all()) + assert_equal(mixed[-1], expected_stop) + + def test_nan_endpoints(self): + # NaN inputs: behavior unchanged by gh-26699 fix (nan != nan so no + # replacement occurs; delta stays nan and propagates normally). + assert_(isnan(linspace(np.nan, np.nan, 5)).all()) + assert_(isnan(linspace(np.nan, np.nan, 1)).all()) + result = linspace(np.nan, 1.0, 5) + assert_(isnan(result[:-1]).all()) + assert_equal(result[-1], 1.0) + class TestAdd_newdoc: From 30315bdec39cb7b4ae9e55189aeaa6a073c45af1 Mon Sep 17 00:00:00 2001 From: agp19d Date: Sun, 12 Apr 2026 14:41:36 -0500 Subject: [PATCH 38/60] DOC: fix project name capitalization (Numpy -> NumPy) Fix inconsistent capitalization of the project name in developer docs, user docs, and benchmarks. The correct spelling is "NumPy" (capital N and P). --- benchmarks/benchmarks/bench_app.py | 2 +- doc/source/dev/development_advanced_debugging.rst | 4 ++-- doc/source/dev/development_workflow.rst | 2 +- doc/source/user/theory.broadcasting.rst | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/benchmarks/benchmarks/bench_app.py b/benchmarks/benchmarks/bench_app.py index 06a9401b02f5..cbad8263a599 100644 --- a/benchmarks/benchmarks/bench_app.py +++ b/benchmarks/benchmarks/bench_app.py @@ -70,7 +70,7 @@ def maxes_of_dots(self, arrays): Arrays must agree only on the first dimension. - Numpy uses this as a simultaneous benchmark of 1) dot products + NumPy uses this as a simultaneous benchmark of 1) dot products and 2) max(, axis=). """ feature_scores = ([0] * len(arrays)) diff --git a/doc/source/dev/development_advanced_debugging.rst b/doc/source/dev/development_advanced_debugging.rst index eb81b335f56a..da8bf2cbf832 100644 --- a/doc/source/dev/development_advanced_debugging.rst +++ b/doc/source/dev/development_advanced_debugging.rst @@ -72,7 +72,7 @@ e.g. ``pythond -m sysconfig`` to get the build configuration for the Python executable. A debug build will be built with debug compiler options in ``CFLAGS`` (e.g. ``-g -Og``). -Running the Numpy tests or an interactive terminal is usually as easy as:: +Running the NumPy tests or an interactive terminal is usually as easy as:: python3.8d runtests.py # or @@ -231,7 +231,7 @@ crash. The call stack often provides valuable context to understand the nature of a crash. C debuggers are also very useful during development, allowing interactive debugging in the C implementation of NumPy. -The NumPy developers often use both ``gdb`` and ``lldb`` to debug Numpy. As a +The NumPy developers often use both ``gdb`` and ``lldb`` to debug NumPy. As a rule of thumb, ``gdb`` is often easier to use on Linux while ``lldb`` is easier to use on a Mac environment. They have disjoint user interfaces, so you will need to learn how to use whichever one you land on. The ``gdb`` to ``lldb`` `command map diff --git a/doc/source/dev/development_workflow.rst b/doc/source/dev/development_workflow.rst index 16e383e125dc..7215d55af371 100644 --- a/doc/source/dev/development_workflow.rst +++ b/doc/source/dev/development_workflow.rst @@ -223,7 +223,7 @@ these fragments in each commit message of a PR: Test building wheels ~~~~~~~~~~~~~~~~~~~~ -Numpy currently uses `cibuildwheel `_ +NumPy currently uses `cibuildwheel `_ in order to build wheels through continuous integration services. To save resources, the cibuildwheel wheel builders are not run by default on every single PR or commit to main. diff --git a/doc/source/user/theory.broadcasting.rst b/doc/source/user/theory.broadcasting.rst index f277d4afde15..82400f2e172f 100644 --- a/doc/source/user/theory.broadcasting.rst +++ b/doc/source/user/theory.broadcasting.rst @@ -1,7 +1,7 @@ :orphan: =========================== -Array broadcasting in Numpy +Array broadcasting in NumPy =========================== .. From 914cfc1b3e71504876130a167984106fbb7f41ae Mon Sep 17 00:00:00 2001 From: agp19d Date: Sun, 12 Apr 2026 17:09:07 -0500 Subject: [PATCH 39/60] DOC: clarify that read_array_header requires prior read_magic call Update docstrings for read_array_header_1_0 and read_array_header_2_0 to note that fp must be positioned after the magic string and version bytes, e.g. by calling read_magic first. Closes #29159 --- numpy/lib/_format_impl.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/numpy/lib/_format_impl.py b/numpy/lib/_format_impl.py index 51b16ce0de48..5c79947cb3a1 100644 --- a/numpy/lib/_format_impl.py +++ b/numpy/lib/_format_impl.py @@ -519,6 +519,9 @@ def read_array_header_1_0(fp, max_header_size=_MAX_HEADER_SIZE): ---------- fp : filelike object A file object or something with a `.read()` method like a file. + Must be positioned at the start of the header, just after the + magic string and version bytes. This can be done by calling + `read_magic` on ``fp`` first. Returns ------- @@ -557,6 +560,9 @@ def read_array_header_2_0(fp, max_header_size=_MAX_HEADER_SIZE): ---------- fp : filelike object A file object or something with a `.read()` method like a file. + Must be positioned at the start of the header, just after the + magic string and version bytes. This can be done by calling + `read_magic` on ``fp`` first. max_header_size : int, optional Maximum allowed size of the header. Large headers may not be safe to load securely and thus require explicitly passing a larger value. From ca0ed27f64a3801e82de60ba48d0b12cd04c3b97 Mon Sep 17 00:00:00 2001 From: vishalrk Date: Fri, 13 Mar 2026 02:26:42 +0100 Subject: [PATCH 40/60] DOC: add missing one-character dtype codes to arrays.dtypes table (gh-15859) --- doc/source/reference/arrays.dtypes.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/source/reference/arrays.dtypes.rst b/doc/source/reference/arrays.dtypes.rst index b1887fc09d23..a4296d1f58dc 100644 --- a/doc/source/reference/arrays.dtypes.rst +++ b/doc/source/reference/arrays.dtypes.rst @@ -270,16 +270,23 @@ Array-protocol type strings (see :ref:`arrays.interface`) ``'L'`` unsigned long integer ``'q'`` (signed) long long integer ``'Q'`` unsigned long long integer + ``'n'`` signed :class:`intp` + ``'N'`` unsigned :class:`uintp` + ``'p'`` signed integer large enough for pointers + ``'P'`` unsigned integer large enough for pointers + ``'e'`` half precision ``'f'`` single precision ``'F'`` complex single precision ``'d'`` double precision ``'D'`` complex double precision ``'g'`` long precision ``'G'`` complex long double precision - ``'O'`` (Python) objects ``'S'`` zero-terminated bytes (not recommended) ``'U'`` Unicode string ``'V'`` raw data (:class:`void`) + ``'O'`` (Python) objects + ``'M'`` :class:`datetime64` + ``'m'`` :class:`timedelta64` ================== ======================== .. admonition:: Example From b88b69d585e0b63c96b665fe6fc22fe75eca5c97 Mon Sep 17 00:00:00 2001 From: Abhijeetsingh Meena Date: Mon, 6 Jul 2026 18:28:16 +0530 Subject: [PATCH 41/60] BUG: fix `StringDType` coerce flag in binary ufunc promotion (#31861) (#31862) --- numpy/_core/src/umath/stringdtype_ufuncs.cpp | 2 +- numpy/_core/tests/test_stringdtype.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/numpy/_core/src/umath/stringdtype_ufuncs.cpp b/numpy/_core/src/umath/stringdtype_ufuncs.cpp index 79e662cb9f38..64b99644c971 100644 --- a/numpy/_core/src/umath/stringdtype_ufuncs.cpp +++ b/numpy/_core/src/umath/stringdtype_ufuncs.cpp @@ -248,7 +248,7 @@ binary_resolve_descriptors(struct PyArrayMethodObject_tag *NPY_UNUSED(method), { PyArray_StringDTypeObject *descr1 = (PyArray_StringDTypeObject *)given_descrs[0]; PyArray_StringDTypeObject *descr2 = (PyArray_StringDTypeObject *)given_descrs[1]; - int out_coerce = descr1->coerce && descr1->coerce; + int out_coerce = descr1->coerce && descr2->coerce; PyObject *out_na_object = NULL; if (stringdtype_compatible_na( diff --git a/numpy/_core/tests/test_stringdtype.py b/numpy/_core/tests/test_stringdtype.py index 5bb1085a9ddc..ea90bb81071c 100644 --- a/numpy/_core/tests/test_stringdtype.py +++ b/numpy/_core/tests/test_stringdtype.py @@ -1820,10 +1820,13 @@ def test_unset_na_coercion(): StringDType(na_object=None)]: if op_dtype is None: op = "2" + expected = StringDType(na_object=None) # coerce defaults to True else: op = np.array("2", dtype=op_dtype) + expected = StringDType(na_object=None, coerce=op_dtype.coerce) res = arr + op assert_array_equal(res, ["hello2", "world2"]) + assert res.dtype == expected # dtype instances with distinct explicitly set NA objects are incompatible for op_dtype in [StringDType(na_object=pd_NA), StringDType(na_object="")]: From 8bbc6a8dc80f10e189685a4163724187a692e1ee Mon Sep 17 00:00:00 2001 From: Shoumik Chakravarty Date: Mon, 6 Jul 2026 08:34:00 -0500 Subject: [PATCH 42/60] DOC: cross-reference np.ma.correlate from np.ma.convolve for propagate_mask (#31852) --- numpy/ma/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 7ec99eb30fb0..d2cdd5c8e42a 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -8451,6 +8451,8 @@ def convolve(a, v, mode='full', propagate_mask=True): See Also -------- numpy.convolve : Equivalent function in the top-level NumPy module. + numpy.ma.correlate : Cross-correlation of two 1-D sequences; see its + examples for ``propagate_mask`` behaviour. """ return _convolve_or_correlate(np.convolve, a, v, mode, propagate_mask) From c2c0494fe8efa10f033e7f60575707a81357ac45 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Mon, 6 Jul 2026 19:40:47 +0530 Subject: [PATCH 43/60] TST: add tests for stable ABI numpy extensions (#31822) Co-authored-by: da-woods Co-authored-by: Nathan Goldbaum --- .../{limited_api_opaque.c => limited_api.c} | 108 +++++++--- .../tests/examples/limited_api/limited_api1.c | 15 -- .../examples/limited_api/limited_api2.pyx | 11 -- .../limited_api/limited_api_cython.pyx | 155 +++++++++++++++ .../examples/limited_api/limited_api_latest.c | 19 -- .../tests/examples/limited_api/meson.build | 81 ++++---- numpy/_core/tests/test_cython.py | 2 +- numpy/_core/tests/test_limited_api.py | 185 +++++++++++++----- numpy/random/tests/test_extending.py | 2 +- pyproject.toml | 2 +- requirements/build_requirements.txt | 2 +- 11 files changed, 420 insertions(+), 162 deletions(-) rename numpy/_core/tests/examples/limited_api/{limited_api_opaque.c => limited_api.c} (77%) delete mode 100644 numpy/_core/tests/examples/limited_api/limited_api1.c delete mode 100644 numpy/_core/tests/examples/limited_api/limited_api2.pyx create mode 100644 numpy/_core/tests/examples/limited_api/limited_api_cython.pyx delete mode 100644 numpy/_core/tests/examples/limited_api/limited_api_latest.c diff --git a/numpy/_core/tests/examples/limited_api/limited_api_opaque.c b/numpy/_core/tests/examples/limited_api/limited_api.c similarity index 77% rename from numpy/_core/tests/examples/limited_api/limited_api_opaque.c rename to numpy/_core/tests/examples/limited_api/limited_api.c index 86a934fd1622..72a8c7476387 100644 --- a/numpy/_core/tests/examples/limited_api/limited_api_opaque.c +++ b/numpy/_core/tests/examples/limited_api/limited_api.c @@ -1,13 +1,28 @@ -#ifndef Py_TARGET_ABI3T -#error "This file must be compiled with -DPy_TARGET_ABI3T" +#ifndef MODULE_NAME +#error "MODULE_NAME must be defined before including this file" +#endif + +#ifndef Py_LIMITED_API +#error "Py_LIMITED_API must be defined before including this file" #endif #include #include #include #include +#include +#include + + +#define MODULE_NAME_STR NPY_TOSTRING(MODULE_NAME) + +#ifdef Py_TARGET_ABI3T +#define ENTRY_FUNC_NAME NPY_CAT(PyModExport_, MODULE_NAME) +#else +#define ENTRY_FUNC_NAME NPY_CAT(PyInit_, MODULE_NAME) +#endif -static PyObject *limited_api_opaque_nonzero(PyObject *mod, PyArrayObject *self) +static PyObject *limited_api_nonzero(PyObject *mod, PyArrayObject *self) { PyArray_NonzeroFunc* nonzero = PyDataType_GetArrFuncs(PyArray_DESCR(self))->nonzero; @@ -91,7 +106,7 @@ static PyObject *limited_api_opaque_nonzero(PyObject *mod, PyArrayObject *self) * legacy iterator macros. */ static PyObject * -limited_api_opaque_iter_next(PyObject *mod, PyArrayObject *self) +limited_api_iter_next(PyObject *mod, PyArrayObject *self) { PyObject *iter_obj = PyArray_IterNew((PyObject *)self); if (iter_obj == NULL) { @@ -110,7 +125,7 @@ limited_api_opaque_iter_next(PyObject *mod, PyArrayObject *self) * Test PyArray_ITER_GOTO1D by accessing a specific flat index. */ static PyObject * -limited_api_opaque_iter_goto1d(PyObject *mod, PyObject *args) +limited_api_iter_goto1d(PyObject *mod, PyObject *args) { PyArrayObject *arr; npy_intp index; @@ -132,7 +147,7 @@ limited_api_opaque_iter_goto1d(PyObject *mod, PyObject *args) * Returns the sum from the second pass (should equal the first). */ static PyObject * -limited_api_opaque_iter_reset(PyObject *mod, PyArrayObject *self) +limited_api_iter_reset(PyObject *mod, PyArrayObject *self) { PyObject *iter_obj = PyArray_IterNew((PyObject *)self); if (iter_obj == NULL) { @@ -160,7 +175,7 @@ limited_api_opaque_iter_reset(PyObject *mod, PyArrayObject *self) * Returns the total sum of (a + b) for all broadcast elements. */ static PyObject * -limited_api_opaque_multi_iter_next(PyObject *mod, PyObject *args) +limited_api_multi_iter_next(PyObject *mod, PyObject *args) { PyArrayObject *a, *b; if (!PyArg_ParseTuple(args, "O!O!", &PyArray_Type, &a, @@ -200,7 +215,7 @@ limited_api_opaque_multi_iter_next(PyObject *mod, PyObject *args) * Test PyArray_ITER_GOTO by jumping to a coordinate and reading the value. */ static PyObject * -limited_api_opaque_iter_goto(PyObject *mod, PyObject *args) +limited_api_iter_goto(PyObject *mod, PyObject *args) { PyArrayObject *arr; PyObject *coord_tuple; @@ -235,7 +250,7 @@ limited_api_opaque_iter_goto(PyObject *mod, PyObject *args) * and returning (a_val, b_val) at that position. */ static PyObject * -limited_api_opaque_multi_iter_goto(PyObject *mod, PyObject *args) +limited_api_multi_iter_goto(PyObject *mod, PyObject *args) { PyArrayObject *a, *b; PyObject *coord_tuple; @@ -274,7 +289,7 @@ limited_api_opaque_multi_iter_goto(PyObject *mod, PyObject *args) * and returning (a_val, b_val) at that position. */ static PyObject * -limited_api_opaque_multi_iter_goto1d(PyObject *mod, PyObject *args) +limited_api_multi_iter_goto1d(PyObject *mod, PyObject *args) { PyArrayObject *a, *b; npy_intp index; @@ -298,7 +313,7 @@ limited_api_opaque_multi_iter_goto1d(PyObject *mod, PyObject *args) * and returning its data pointer value after N steps. */ static PyObject * -limited_api_opaque_multi_iter_nexti(PyObject *mod, PyObject *args) +limited_api_multi_iter_nexti(PyObject *mod, PyObject *args) { PyArrayObject *a, *b; int steps; @@ -318,46 +333,87 @@ limited_api_opaque_multi_iter_nexti(PyObject *mod, PyObject *args) return PyFloat_FromDouble(val); } -static PyMethodDef limited_api_opaque_methods[] = { - {"nonzero", (PyCFunction)limited_api_opaque_nonzero, METH_O, +static PyObject * +limited_api_datetime_metadata(PyObject *mod, PyArrayObject *arr) +{ + PyArray_Descr *descr = PyArray_DESCR(arr); + npy_uint64 flags = PyDataType_FLAGS(descr); + PyArray_DatetimeDTypeMetaData *dt_meta = + (PyArray_DatetimeDTypeMetaData *)PyDataType_C_METADATA(descr); + if (dt_meta == NULL) { + PyErr_SetString( + PyExc_RuntimeError, + "PyDataType_C_METADATA() returned NULL for a datetime descriptor"); + return NULL; + } + return Py_BuildValue("Kii", (unsigned long long)flags, + (int)dt_meta->meta.base, dt_meta->meta.num); +} + +static PyMethodDef limited_api_methods[] = { + {"nonzero", (PyCFunction)limited_api_nonzero, METH_O, "Count the number of non-zero elements in the array."}, - {"iter_next", (PyCFunction)limited_api_opaque_iter_next, METH_O, + {"iter_next", (PyCFunction)limited_api_iter_next, METH_O, "Sum array elements using PyArray_ITER_NEXT."}, - {"iter_goto1d", (PyCFunction)limited_api_opaque_iter_goto1d, METH_VARARGS, + {"iter_goto1d", (PyCFunction)limited_api_iter_goto1d, METH_VARARGS, "Get element at flat index using PyArray_ITER_GOTO1D."}, - {"iter_reset", (PyCFunction)limited_api_opaque_iter_reset, METH_O, + {"iter_reset", (PyCFunction)limited_api_iter_reset, METH_O, "Sum array elements after reset using PyArray_ITER_RESET."}, - {"multi_iter_next", (PyCFunction)limited_api_opaque_multi_iter_next, + {"multi_iter_next", (PyCFunction)limited_api_multi_iter_next, METH_VARARGS, "Sum broadcast (a+b) using PyArray_MultiIter_NEXT."}, - {"iter_goto", (PyCFunction)limited_api_opaque_iter_goto, METH_VARARGS, + {"iter_goto", (PyCFunction)limited_api_iter_goto, METH_VARARGS, "Get element at coordinate using PyArray_ITER_GOTO."}, - {"multi_iter_goto", (PyCFunction)limited_api_opaque_multi_iter_goto, + {"multi_iter_goto", (PyCFunction)limited_api_multi_iter_goto, METH_VARARGS, "Get (a, b) at coordinate using PyArray_MultiIter_GOTO."}, - {"multi_iter_goto1d", (PyCFunction)limited_api_opaque_multi_iter_goto1d, + {"multi_iter_goto1d", (PyCFunction)limited_api_multi_iter_goto1d, METH_VARARGS, "Get (a, b) at flat index using PyArray_MultiIter_GOTO1D."}, - {"multi_iter_nexti", (PyCFunction)limited_api_opaque_multi_iter_nexti, + {"multi_iter_nexti", (PyCFunction)limited_api_multi_iter_nexti, METH_VARARGS, "Advance only iter 0 N steps using PyArray_MultiIter_NEXTi."}, + {"datetime_metadata", (PyCFunction)limited_api_datetime_metadata, + METH_O, + "Get (flags, unit base, unit num) from a datetime64/timedelta64 array."}, {NULL, NULL, 0, NULL} /* Sentinel */ }; +#ifdef Py_TARGET_ABI3T + PyABIInfo_VAR(abi_info); -static PySlot limited_api_opaque_slots[] = { +static PySlot limited_api_slots[] = { PySlot_STATIC_DATA(Py_mod_abi, &abi_info), - PySlot_STATIC_DATA(Py_mod_name, "limited_api_opaque"), - PySlot_STATIC_DATA(Py_mod_methods, limited_api_opaque_methods), + PySlot_STATIC_DATA(Py_mod_name, MODULE_NAME_STR), + PySlot_STATIC_DATA(Py_mod_methods, limited_api_methods), PySlot_STATIC_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), PySlot_END, }; PyMODEXPORT_FUNC -PyModExport_limited_api_opaque(void) +ENTRY_FUNC_NAME(void) { import_array(); import_umath(); - return limited_api_opaque_slots; + return limited_api_slots; } + +#else + +static PyModuleDef moduledef = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = MODULE_NAME_STR, + .m_size = -1, + .m_methods = limited_api_methods, +}; + +PyMODINIT_FUNC +ENTRY_FUNC_NAME(void) +{ + import_array(); + import_umath(); + return PyModule_Create(&moduledef); +} + +#endif diff --git a/numpy/_core/tests/examples/limited_api/limited_api1.c b/numpy/_core/tests/examples/limited_api/limited_api1.c deleted file mode 100644 index 115a3f3a6835..000000000000 --- a/numpy/_core/tests/examples/limited_api/limited_api1.c +++ /dev/null @@ -1,15 +0,0 @@ -#include -#include -#include - -static PyModuleDef moduledef = { - .m_base = PyModuleDef_HEAD_INIT, - .m_name = "limited_api1" -}; - -PyMODINIT_FUNC PyInit_limited_api1(void) -{ - import_array(); - import_umath(); - return PyModule_Create(&moduledef); -} diff --git a/numpy/_core/tests/examples/limited_api/limited_api2.pyx b/numpy/_core/tests/examples/limited_api/limited_api2.pyx deleted file mode 100644 index 327d5b038c6c..000000000000 --- a/numpy/_core/tests/examples/limited_api/limited_api2.pyx +++ /dev/null @@ -1,11 +0,0 @@ -#cython: language_level=3 - -""" -Make sure cython can compile in limited API mode (see meson.build) -""" - -cdef extern from "numpy/arrayobject.h": - pass -cdef extern from "numpy/arrayscalars.h": - pass - diff --git a/numpy/_core/tests/examples/limited_api/limited_api_cython.pyx b/numpy/_core/tests/examples/limited_api/limited_api_cython.pyx new file mode 100644 index 000000000000..740dbef03214 --- /dev/null +++ b/numpy/_core/tests/examples/limited_api/limited_api_cython.pyx @@ -0,0 +1,155 @@ +#cython: language_level=3 + +""" +Make sure cython can compile using the NumPy C-API in limited API mode +(see meson.build). + +""" + +cimport numpy as cnp + +cnp.import_array() + + +def nonzero(cnp.ndarray arr): + """Count non-zero elements of a float64 array using the NpyIter API.""" + cdef cnp.NpyIter *it + cdef cnp.NpyIter_IterNextFunc iternext + cdef char **dataptr + cdef cnp.npy_intp *strideptr + cdef cnp.npy_intp *innersizeptr + cdef cnp.npy_intp nonzero_count = 0 + cdef cnp.npy_intp stride, count + cdef char *data + + if cnp.PyArray_SIZE(arr) == 0: + return 0 + + it = cnp.NpyIter_New( + arr, + cnp.NPY_ITER_READONLY | cnp.NPY_ITER_EXTERNAL_LOOP | cnp.NPY_ITER_REFS_OK, + cnp.NPY_KEEPORDER, cnp.NPY_NO_CASTING, NULL) + if it == NULL: + raise RuntimeError("NpyIter_New failed") + iternext = cnp.NpyIter_GetIterNext(it, NULL) + if iternext == NULL: + cnp.NpyIter_Deallocate(it) + raise RuntimeError("NpyIter_GetIterNext failed") + dataptr = cnp.NpyIter_GetDataPtrArray(it) + strideptr = cnp.NpyIter_GetInnerStrideArray(it) + innersizeptr = cnp.NpyIter_GetInnerLoopSizePtr(it) + + while True: + data = dataptr[0] + stride = strideptr[0] + count = innersizeptr[0] + while count > 0: + if (data)[0] != 0.0: + nonzero_count += 1 + data += stride + count -= 1 + if not iternext(it): + break + + cnp.NpyIter_Deallocate(it) + return nonzero_count + + +def iter_next(cnp.ndarray arr): + """Sum float64 array elements using PyArray_ITER_NEXT / _DATA / _NOTDONE.""" + cdef cnp.flatiter it = cnp.PyArray_IterNew(arr) + cdef double total = 0.0 + while cnp.PyArray_ITER_NOTDONE(it): + total += (cnp.PyArray_ITER_DATA(it))[0] + cnp.PyArray_ITER_NEXT(it) + return total + + +def iter_goto1d(cnp.ndarray arr, cnp.npy_intp index): + """Get element at a flat index using PyArray_ITER_GOTO1D.""" + cdef cnp.flatiter it = cnp.PyArray_IterNew(arr) + cnp.PyArray_ITER_GOTO1D(it, index) + return (cnp.PyArray_ITER_DATA(it))[0] + + +def iter_reset(cnp.ndarray arr): + """Sum float64 array elements after PyArray_ITER_RESET.""" + cdef cnp.flatiter it = cnp.PyArray_IterNew(arr) + cdef double total = 0.0 + while cnp.PyArray_ITER_NOTDONE(it): + cnp.PyArray_ITER_NEXT(it) + cnp.PyArray_ITER_RESET(it) + while cnp.PyArray_ITER_NOTDONE(it): + total += (cnp.PyArray_ITER_DATA(it))[0] + cnp.PyArray_ITER_NEXT(it) + return total + + +def iter_goto(cnp.ndarray arr, tuple coord): + """Get element at a coordinate using PyArray_ITER_GOTO.""" + cdef int nd = cnp.PyArray_NDIM(arr) + cdef cnp.npy_intp destination[32] + cdef cnp.flatiter it + cdef int i + if len(coord) != nd: + raise ValueError("coordinate length mismatch") + for i in range(nd): + destination[i] = coord[i] + it = cnp.PyArray_IterNew(arr) + cnp.PyArray_ITER_GOTO(it, destination) + return (cnp.PyArray_ITER_DATA(it))[0] + + +def multi_iter_next(cnp.ndarray a, cnp.ndarray b): + """Sum broadcast (a + b) using PyArray_MultiIter_NEXT / _DATA.""" + cdef cnp.broadcast multi = cnp.PyArray_MultiIterNew2(a, b) + cdef double total = 0.0 + while cnp.PyArray_MultiIter_NOTDONE(multi): + total += (cnp.PyArray_MultiIter_DATA(multi, 0))[0] + total += (cnp.PyArray_MultiIter_DATA(multi, 1))[0] + cnp.PyArray_MultiIter_NEXT(multi) + return total + + +def multi_iter_goto1d(cnp.ndarray a, cnp.ndarray b, cnp.npy_intp index): + """Get (a, b) at a flat index using PyArray_MultiIter_GOTO1D.""" + cdef cnp.broadcast multi = cnp.PyArray_MultiIterNew2(a, b) + cdef double va, vb + cnp.PyArray_MultiIter_GOTO1D(multi, index) + va = (cnp.PyArray_MultiIter_DATA(multi, 0))[0] + vb = (cnp.PyArray_MultiIter_DATA(multi, 1))[0] + return va, vb + + +def multi_iter_nexti(cnp.ndarray a, cnp.ndarray b, int steps): + """Advance only iterator 0 by N steps using PyArray_MultiIter_NEXTi.""" + cdef cnp.broadcast multi = cnp.PyArray_MultiIterNew2(a, b) + cdef int _i + for _i in range(steps): + cnp.PyArray_MultiIter_NEXTi(multi, 0) + return (cnp.PyArray_MultiIter_DATA(multi, 0))[0] + + +def get_datetime_value(obj): + """Underlying int64 of a datetime64 scalar via get_datetime64_value.""" + return cnp.get_datetime64_value(obj) + + +def get_timedelta_value(obj): + """Underlying int64 of a timedelta64 scalar via get_timedelta64_value.""" + return cnp.get_timedelta64_value(obj) + + +def get_datetime_unit(obj): + """Unit (NPY_DATETIMEUNIT base) of a datetime64 scalar.""" + return cnp.get_datetime64_unit(obj) + + +def is_datetime64(obj): + """Cython is_datetime64_object (isinstance-like check).""" + return cnp.is_datetime64_object(obj) + + +def is_timedelta64(obj): + """Cython is_timedelta64_object (isinstance-like check).""" + return cnp.is_timedelta64_object(obj) diff --git a/numpy/_core/tests/examples/limited_api/limited_api_latest.c b/numpy/_core/tests/examples/limited_api/limited_api_latest.c deleted file mode 100644 index 92d83ea977a1..000000000000 --- a/numpy/_core/tests/examples/limited_api/limited_api_latest.c +++ /dev/null @@ -1,19 +0,0 @@ -#include -#include -#include - -#if Py_LIMITED_API != PY_VERSION_HEX & 0xffff0000 - # error "Py_LIMITED_API not defined to Python major+minor version" -#endif - -static PyModuleDef moduledef = { - .m_base = PyModuleDef_HEAD_INIT, - .m_name = "limited_api_latest" -}; - -PyMODINIT_FUNC PyInit_limited_api_latest(void) -{ - import_array(); - import_umath(); - return PyModule_Create(&moduledef); -} diff --git a/numpy/_core/tests/examples/limited_api/meson.build b/numpy/_core/tests/examples/limited_api/meson.build index d657af274e08..2973fd3ab629 100644 --- a/numpy/_core/tests/examples/limited_api/meson.build +++ b/numpy/_core/tests/examples/limited_api/meson.build @@ -28,40 +28,52 @@ npy_path = run_command(py, [ # where cython may not find the right __init__.pyd file. add_project_arguments('-I', npy_path, language : 'cython') -if py.get_variable('Py_GIL_DISABLED', 0) == 0 - py.extension_module( - 'limited_api1', - 'limited_api1.c', - c_args: [ - '-DNPY_NO_DEPRECATED_API=NPY_1_21_API_VERSION', - ], - include_directories: [npy_include_path], - limited_api: '3.9', - ) - - py.extension_module( - 'limited_api_latest', - 'limited_api_latest.c', - c_args: [ - '-DNPY_NO_DEPRECATED_API=NPY_1_21_API_VERSION', - ], - include_directories: [npy_include_path], - limited_api: py.language_version(), - ) +# Build limited API extension modules for all supported Python and NumPy versions +# The _PY_ABI3_VERSIONS and _NPY_TARGET_VERSIONS lists should be kept in sync +# with the lists defined in test_limited_api.py, and the test should be updated if new versions are added here. +# The special 'default' entry builds without defining NPY_TARGET_VERSION at all, +# which exercises the path where numpyconfig.h picks the current API version. +_PY_ABI3_VERSIONS = ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.15'] +_NPY_TARGET_VERSIONS = ['2.0', '2.1', '2.2', '2.3', '2.4', '2.5', 'default'] - py.extension_module( - 'limited_api2', - 'limited_api2.pyx', - install: false, - c_args: [ - '-DNPY_NO_DEPRECATED_API=0', - # Require 1.25+ to test datetime additions - '-DNPY_TARGET_VERSION=NPY_2_0_API_VERSION', - '-DCYTHON_LIMITED_API=1', - ], - include_directories: [npy_include_path], - limited_api: '3.9', -) +if py.get_variable('Py_GIL_DISABLED', 0) == 0 + foreach py_ver : _PY_ABI3_VERSIONS + # skip abi3 version higher than the current interpreter version + if py.language_version().version_compare('>=' + py_ver) + foreach npy_ver : _NPY_TARGET_VERSIONS + # 'default' leaves NPY_TARGET_VERSION undefined + if npy_ver == 'default' + npy_target_args = [] + else + npy_macro = 'NPY_' + npy_ver.replace('.', '_') + '_API_VERSION' + npy_target_args = ['-DNPY_TARGET_VERSION=' + npy_macro] + endif + modname = 'limited_api_' + py_ver.replace('.', '_') + '_npy' + npy_ver.replace('.', '_') + py.extension_module( + modname, + 'limited_api.c', + c_args: [ + '-DNPY_NO_DEPRECATED_API=NPY_1_21_API_VERSION', + '-DMODULE_NAME=' + modname, + ] + npy_target_args, + include_directories: [npy_include_path], + limited_api: py_ver, + ) + modname = 'limited_api_cython_' + py_ver.replace('.', '_') + '_npy' + npy_ver.replace('.', '_') + py.extension_module( + modname, + 'limited_api_cython.pyx', + install: false, + cython_args: ['--module-name', modname], + c_args: [ + '-DNPY_NO_DEPRECATED_API=NPY_1_21_API_VERSION', + ] + npy_target_args, + include_directories: [npy_include_path], + limited_api: py_ver, + ) + endforeach + endif + endforeach endif if ( @@ -70,8 +82,9 @@ if ( ) py.extension_module( 'limited_api_opaque', - 'limited_api_opaque.c', + 'limited_api.c', c_args: [ + '-DMODULE_NAME=limited_api_opaque', '-DNPY_TARGET_VERSION=NPY_2_5_API_VERSION', '-DPy_TARGET_ABI3T=0x030F0000', ], diff --git a/numpy/_core/tests/test_cython.py b/numpy/_core/tests/test_cython.py index c5752253409a..e4269efb2a89 100644 --- a/numpy/_core/tests/test_cython.py +++ b/numpy/_core/tests/test_cython.py @@ -21,7 +21,7 @@ from numpy._utils import _pep440 # Note: keep in sync with the one in pyproject.toml - required_version = "3.0.6" + required_version = "3.1.0" if _pep440.parse(cython_version) < _pep440.Version(required_version): # too old or wrong cython, skip the test cython = None diff --git a/numpy/_core/tests/test_limited_api.py b/numpy/_core/tests/test_limited_api.py index c052f34c1ba4..07d2bab7341d 100644 --- a/numpy/_core/tests/test_limited_api.py +++ b/numpy/_core/tests/test_limited_api.py @@ -1,3 +1,4 @@ +import importlib import os import shutil import subprocess @@ -6,7 +7,8 @@ import pytest -from numpy.testing import IS_EDITABLE, IS_WASM, NOGIL_BUILD +import numpy as np +from numpy.testing import IS_64BIT, IS_EDITABLE, IS_WASM, NOGIL_BUILD from numpy.testing._private.utils import run_subprocess # This import is copied from random.tests.test_extending @@ -19,7 +21,7 @@ from numpy._utils import _pep440 # Note: keep in sync with the one in pyproject.toml - required_version = "3.0.6" + required_version = "3.1.0" if _pep440.parse(cython_version) < _pep440.Version(required_version): # too old or wrong cython, skip the test cython = None @@ -82,6 +84,125 @@ def install_temp(tmpdir_factory): sys.path.append(str(build_dir)) +def _check_api_module(mod, cython=False): + arr = np.ones((200, 200)) + assert mod.nonzero(arr) == 200 * 200 + + # Legacy single-array iterator: PyArray_ITER_NEXT / _DATA / _NOTDONE. + arr = np.arange(12.0).reshape(3, 4) + assert mod.iter_next(arr) == 66.0 + assert mod.iter_goto1d(arr, 5) == 5.0 + assert mod.iter_goto1d(arr, -1) == 11.0 + assert mod.iter_reset(arr) == 66.0 + assert mod.iter_goto(arr, (1, 2)) == 6.0 + assert mod.iter_goto(arr, (2, 3)) == 11.0 + + # Broadcasting multi-iterator. + a = np.arange(3.0).reshape(3, 1) + b = np.arange(4.0).reshape(1, 4) + assert mod.multi_iter_next(a, b) == float(np.sum(a + b)) + if not cython: + va, vb = mod.multi_iter_goto(a, b, (1, 2)) + assert va == 1.0 and vb == 2.0 + va, vb = mod.multi_iter_goto1d(a, b, 6) + assert va == 1.0 and vb == 2.0 + + a = np.arange(6.0).reshape(2, 3) + b = np.zeros((2, 3)) + assert mod.multi_iter_nexti(a, b, 3) == 3.0 + + if cython: + # Datetime / timedelta scalar accessors (.pxd helpers). + dt = np.datetime64("2021-01-01", "D") + assert mod.get_datetime_value(dt) == dt.astype("int64") + assert mod.get_datetime_unit(dt) == 4 + assert mod.is_datetime64(dt) + assert not mod.is_timedelta64(dt) + + # A plain seconds timedelta: base NPY_FR_s, unit multiplier 1. + td = np.timedelta64(5, "s") + assert mod.get_timedelta_value(td) == 5 + assert mod.is_timedelta64(td) + assert not mod.is_datetime64(td) + + # A non-unit multiplier exercises the metadata `num` field. + td = np.timedelta64(1000, "ms").astype("timedelta64[10ms]") + assert mod.get_timedelta_value(td) == td.astype("int64") + else: + # PyDataType_FLAGS / PyDataType_C_METADATA on datetime descriptors. + dt = np.array(["2021-01-01"], dtype="datetime64[D]") + flags, base, num = mod.datetime_metadata(dt) + assert flags == dt.dtype.flags + assert base == 4 # NPY_FR_D + assert num == 1 + + # A plain seconds timedelta: base NPY_FR_s, unit multiplier 1. + td = np.array([5], dtype="timedelta64[s]") + flags, base, num = mod.datetime_metadata(td) + assert flags == td.dtype.flags + assert base == 7 # NPY_FR_s + assert num == 1 + + # A non-unit multiplier exercises the metadata `num` field. + td = np.array([1000], dtype="timedelta64[ms]").astype( + "timedelta64[10ms]") + flags, base, num = mod.datetime_metadata(td) + assert flags == td.dtype.flags + assert base == 8 # NPY_FR_ms + assert num == 10 + + # Non-datetime descriptors have no c_metadata and are rejected. + with pytest.raises(RuntimeError): + mod.datetime_metadata(np.arange(3)) + + +# Test limited API extension modules for all supported Python and NumPy versions +# The _PY_ABI3_VERSIONS and _NPY_TARGET_VERSIONS lists should be kept in sync +# with the lists defined in meson.build, and the test should be updated +# if new versions are added here. +# The special "default" entry builds without defining NPY_TARGET_VERSION at all, +# which exercises the path where numpyconfig.h picks the current API version. +_PY_ABI3_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] +_NPY_TARGET_VERSIONS = ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "default"] + +def _module_names(prefix, abi3_versions): + names = [] + for py_ver in abi3_versions: + if sys.version_info < tuple(map(int, py_ver.split('.'))): + continue + py = py_ver.replace('.', '_') + for npy_ver in _NPY_TARGET_VERSIONS: + npy = npy_ver.replace('.', '_') + names.append(f"{prefix}_{py}_npy{npy}") + return names + + +def limited_api_module_names(): + return _module_names("limited_api", _PY_ABI3_VERSIONS) + + +def limited_api_cython_module_names(): + return _module_names("limited_api_cython", _PY_ABI3_VERSIONS) + + +@pytest.mark.skipif(IS_WASM, reason="Can't start subprocess") +@pytest.mark.xfail( + sysconfig.get_config_var("Py_DEBUG"), + reason=( + "Py_LIMITED_API is incompatible with Py_DEBUG, Py_TRACE_REFS, " + "and Py_REF_DEBUG" + ), +) +@pytest.mark.xfail( + NOGIL_BUILD, + reason="Py_GIL_DISABLED builds do not support abi3", +) +@pytest.mark.parametrize("module_name", limited_api_module_names()) +def test_limited_api_abi3(install_temp, module_name): + mod = importlib.import_module(module_name) + _check_api_module(mod) + + @pytest.mark.skipif(IS_WASM, reason="Can't start subprocess") @pytest.mark.xfail( sysconfig.get_config_var("Py_DEBUG"), @@ -92,20 +213,20 @@ def install_temp(tmpdir_factory): ) @pytest.mark.xfail( NOGIL_BUILD, - reason="Py_GIL_DISABLED builds do not currently support abi3", + reason="Py_GIL_DISABLED builds do not support abi3", ) -def test_limited_api_abi3(install_temp): - """Test building a third-party C extension with the limited API - and building a cython extension with the limited API - """ +@pytest.mark.parametrize("module_name", limited_api_cython_module_names()) +def test_limited_api_cython(install_temp, module_name): + mod = importlib.import_module(module_name) + _check_api_module(mod, cython=True) - import limited_api1 # Earliest (3.6) # noqa: F401 - import limited_api2 # cython # noqa: F401 - import limited_api_latest # Latest version (current Python) # noqa: F401 @pytest.mark.skipif( sys.version_info < (3, 15), reason="opaque PyObject requires Python 3.15+" ) +@pytest.mark.skipif( + not IS_64BIT, reason="opaque abi3t extensions are broken on 32-bit" +) @pytest.mark.skipif( sys.platform == "win32" and not sysconfig.get_config_var('Py_GIL_DISABLED'), reason=("Meson does not yet support building abi3t extensions on the " @@ -114,46 +235,4 @@ def test_limited_api_abi3(install_temp): def test_limited_opaque(install_temp): import limited_api_opaque - import numpy as np - arr = np.ones((200, 200)) - assert limited_api_opaque.nonzero(arr) == 200 * 200 - - # Test PyArray_ITER_NEXT / PyArray_ITER_DATA / PyArray_ITER_NOTDONE - arr = np.arange(12.0).reshape(3, 4) - assert limited_api_opaque.iter_next(arr) == 66.0 - - # Test PyArray_ITER_GOTO1D - assert limited_api_opaque.iter_goto1d(arr, 5) == 5.0 - assert limited_api_opaque.iter_goto1d(arr, -1) == 11.0 - - # Test PyArray_ITER_RESET - assert limited_api_opaque.iter_reset(arr) == 66.0 - - # Test PyArray_MultiIter_NEXT / RESET / DATA with broadcasting - a = np.arange(3.0).reshape(3, 1) # shape (3, 1) - b = np.arange(4.0).reshape(1, 4) # shape (1, 4) - # Each broadcast element is a[i] + b[j], total sum: - expected = float(np.sum(a + b)) - assert limited_api_opaque.multi_iter_next(a, b) == expected - - # Test PyArray_ITER_GOTO - arr = np.arange(12.0).reshape(3, 4) - assert limited_api_opaque.iter_goto(arr, (1, 2)) == 6.0 - assert limited_api_opaque.iter_goto(arr, (2, 3)) == 11.0 - - # Test PyArray_MultiIter_GOTO - a = np.arange(3.0).reshape(3, 1) - b = np.arange(4.0).reshape(1, 4) - va, vb = limited_api_opaque.multi_iter_goto(a, b, (1, 2)) - assert va == 1.0 and vb == 2.0 - - # Test PyArray_MultiIter_GOTO1D - # flat index 6 in (3,4) broadcast → row 1, col 2 - va, vb = limited_api_opaque.multi_iter_goto1d(a, b, 6) - assert va == 1.0 and vb == 2.0 - - # Test PyArray_MultiIter_NEXTi - a = np.arange(6.0).reshape(2, 3) - b = np.zeros((2, 3)) - # Advance iter 0 by 3 steps → flat index 3 → value 3.0 - assert limited_api_opaque.multi_iter_nexti(a, b, 3) == 3.0 + _check_api_module(limited_api_opaque) diff --git a/numpy/random/tests/test_extending.py b/numpy/random/tests/test_extending.py index 23a82f8a3b32..64f3288ae9b7 100644 --- a/numpy/random/tests/test_extending.py +++ b/numpy/random/tests/test_extending.py @@ -38,7 +38,7 @@ else: from numpy._utils import _pep440 # Note: keep in sync with the one in pyproject.toml - required_version = '3.0.6' + required_version = '3.1.0' if _pep440.parse(cython_version) < _pep440.Version(required_version): # too old or wrong cython, skip the test cython = None diff --git a/pyproject.toml b/pyproject.toml index 418bdb1ee757..ef24b3fe0709 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ build-backend = "mesonpy" requires = [ "meson-python>=0.18.0", - "Cython>=3.0.6", # keep in sync with version check in meson.build + "Cython>=3.1.0", # keep in sync with version check in meson.build ] [project] diff --git a/requirements/build_requirements.txt b/requirements/build_requirements.txt index 8075e645b10e..fb0fc6f9863e 100644 --- a/requirements/build_requirements.txt +++ b/requirements/build_requirements.txt @@ -1,5 +1,5 @@ meson-python>=0.18.0 -Cython>=3.0.6 +Cython>=3.1.0 ninja spin build From 8188d77fd9502454b34e53ecc03d61c522a688a6 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 17:21:24 +0200 Subject: [PATCH 44/60] TYP: fix unit-dependent ``timedelta64``/``datetime64`` overloads (#31874) --- numpy/__init__.pyi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index dd223979e59a..69c42dfc2314 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -1016,7 +1016,7 @@ type _TimeUnit = L[_NativeTimeUnit, _IntTimeUnit] type _NativeTD64Unit = L[_DayUnit, _NativeTimeUnit] type _IntTD64Unit = L[_MonthUnit, _IntTimeUnit] type _TD64Unit = L[_DateUnit, _TimeUnit] -type _TimeUnitSpec[UnitT: _TD64Unit] = _TD64Unit | tuple[_TD64Unit, SupportsIndex] +type _TimeUnitSpec[UnitT: _TD64Unit] = UnitT | tuple[UnitT, SupportsIndex] ### TypedDict's (for internal use only) @@ -7334,11 +7334,11 @@ class datetime64(_RealMixin, generic[_DT64ItemT_co], Generic[_DT64ItemT_co]): @overload def __new__(cls, value: int | bytes | str | dt.date, format: _TimeUnitSpec[_IntTimeUnit], /) -> datetime64[int]: ... @overload - def __new__( # type: ignore[overload-cannot-match] + def __new__( cls, value: int | bytes | str | dt.date, format: _TimeUnitSpec[_NativeTimeUnit], / ) -> datetime64[dt.datetime]: ... @overload - def __new__(cls, value: int | bytes | str | dt.date, format: _TimeUnitSpec[_DateUnit], /) -> datetime64[dt.date]: ... # type: ignore[overload-cannot-match] + def __new__(cls, value: int | bytes | str | dt.date, format: _TimeUnitSpec[_DateUnit], /) -> datetime64[dt.date]: ... @overload def __new__(cls, value: bytes | str | dt.date | None, format: _TimeUnitSpec[_TD64Unit] = ..., /) -> Self: ... From 228830432c8bc2277566b273386f6fa2380ea184 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 17:24:30 +0200 Subject: [PATCH 45/60] TYP: fix specialized ufunc bool/int => float promotions (#31873) --- numpy/_core/umath.pyi | 556 ++++++++++++++++++++-- numpy/typing/tests/data/reveal/ufuncs.pyi | 144 ++++-- 2 files changed, 631 insertions(+), 69 deletions(-) diff --git a/numpy/_core/umath.pyi b/numpy/_core/umath.pyi index f73be75436e2..061cea1c4bb4 100644 --- a/numpy/_core/umath.pyi +++ b/numpy/_core/umath.pyi @@ -207,6 +207,10 @@ type _numeric = np.number | np.timedelta64 type _time = np.datetime64 | np.timedelta64 type _non_object = _to_number | _time | np.flexible # np.generic - np.object_ +type _as_f64 = np.int64 | np.uint64 | np.int32 | np.uint32 +type _as_f32 = np.int16 | np.uint16 +type _as_f16 = np.int8 | np.uint8 | np.bool + type _ArrayLikeNumericObj = _DualArrayLike[np.dtype[_numeric | np.object_], complex] type _ArrayLikeNumericObj_co = _DualArrayLike[np.dtype[_to_numeric | np.object_], complex] @@ -883,23 +887,63 @@ class _ufunc_11_f(_ufunc_11): # type: ignore[misc] @overload # Nd, +f64 def __call__[ShapeT: _Shape]( self, - x: np.ndarray[ShapeT, np.dtype[_to_integer]], + x: np.ndarray[ShapeT, np.dtype[_as_f64]], /, *, out: EllipsisType | None = None, dtype: None = None, **kwargs: Unpack[_Kwargs11], ) -> np.ndarray[ShapeT, np.dtype[np.float64]]: ... + @overload # Nd, +f32 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f32]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.ndarray[ShapeT, np.dtype[np.float32]]: ... + @overload # Nd, +f16 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f16]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.ndarray[ShapeT, np.dtype[np.float16]]: ... @overload # scalar, float | +f64 def __call__( self, - x: float | _to_integer, + x: float | _as_f64, /, *, out: None = None, dtype: None = None, **kwargs: Unpack[_Kwargs11], ) -> np.float64: ... + @overload # scalar, +f32 + def __call__( + self, + x: _as_f32, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.float32: ... + @overload # scalar, +f16 + def __call__( + self, + x: _as_f16, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.float16: ... @overload # 1d, +float def __call__( self, @@ -1035,23 +1079,63 @@ class _ufunc_11_fo(_ufunc_11): # type: ignore[misc] @overload # Nd, +f64 def __call__[ShapeT: _Shape]( self, - x: np.ndarray[ShapeT, np.dtype[np.integer | np.bool]], + x: np.ndarray[ShapeT, np.dtype[_as_f64]], /, *, out: EllipsisType | None = None, dtype: None = None, **kwargs: Unpack[_Kwargs11], ) -> np.ndarray[ShapeT, np.dtype[np.float64]]: ... + @overload # Nd, +f32 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f32]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.ndarray[ShapeT, np.dtype[np.float32]]: ... + @overload # Nd, +f16 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f16]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.ndarray[ShapeT, np.dtype[np.float16]]: ... @overload # scalar, float | +f64 def __call__( self, - x: float | np.integer | np.bool, + x: float | _as_f64, /, *, out: None = None, dtype: None = None, **kwargs: Unpack[_Kwargs11], ) -> np.float64: ... + @overload # scalar, +f32 + def __call__( + self, + x: _as_f32, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.float32: ... + @overload # scalar, +f16 + def __call__( + self, + x: _as_f16, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.float16: ... @overload # 1d, +float def __call__( self, @@ -1187,23 +1271,63 @@ class _ufunc_11_fco(_ufunc_11): # type: ignore[misc] @overload # Nd, +f64 def __call__[ShapeT: _Shape]( self, - x: np.ndarray[ShapeT, np.dtype[np.integer | np.bool]], + x: np.ndarray[ShapeT, np.dtype[_as_f64]], /, *, out: EllipsisType | None = None, dtype: None = None, **kwargs: Unpack[_Kwargs11], ) -> np.ndarray[ShapeT, np.dtype[np.float64]]: ... + @overload # Nd, +f32 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f32]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.ndarray[ShapeT, np.dtype[np.float32]]: ... + @overload # Nd, +f16 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f16]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.ndarray[ShapeT, np.dtype[np.float16]]: ... @overload # scalar, float | +f64 def __call__( self, - x: float | np.integer | np.bool, + x: float | _as_f64, /, *, out: None = None, dtype: None = None, **kwargs: Unpack[_Kwargs11], ) -> np.float64: ... + @overload # scalar, +f32 + def __call__( + self, + x: _as_f32, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.float32: ... + @overload # scalar, +f16 + def __call__( + self, + x: _as_f16, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs11], + ) -> np.float16: ... @overload # 1d, +float def __call__( self, @@ -3079,13 +3203,33 @@ class _ufunc_12_frexp(_ufunc_12): # type: ignore[misc] @overload # Nd, +f64 def __call__[ShapeT: _Shape]( self, - x: np.ndarray[ShapeT, np.dtype[_to_integer]], + x: np.ndarray[ShapeT, np.dtype[_as_f64]], /, *, out: EllipsisType | None = None, dtype: None = None, **kwargs: Unpack[_Kwargs12], ) -> tuple[np.ndarray[ShapeT, np.dtype[np.float64]], np.ndarray[ShapeT, np.dtype[np.int32]]]: ... + @overload # Nd, +f32 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f32]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[np.ndarray[ShapeT, np.dtype[np.float32]], np.ndarray[ShapeT, np.dtype[np.int32]]]: ... + @overload # Nd, +f16 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f16]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[np.ndarray[ShapeT, np.dtype[np.float16]], np.ndarray[ShapeT, np.dtype[np.int32]]]: ... @overload # scalar, known dtype def __call__[ScalarT: np.floating]( self, @@ -3099,13 +3243,33 @@ class _ufunc_12_frexp(_ufunc_12): # type: ignore[misc] @overload # scalar, float | +f64 def __call__( self, - x: float | _to_integer, + x: float | _as_f64, /, *, out: None = None, dtype: None = None, **kwargs: Unpack[_Kwargs12], ) -> tuple[np.float64, np.int32]: ... + @overload # scalar, +f32 + def __call__( + self, + x: _as_f32, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[np.float32, np.int32]: ... + @overload # scalar, +f16 + def __call__( + self, + x: _as_f16, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> tuple[np.float16, np.int32]: ... @overload # 1d, +float def __call__( self, @@ -3174,23 +3338,63 @@ class _ufunc_12_modf(_ufunc_12): # type: ignore[misc] @overload # Nd, +f64 def __call__[ShapeT: _Shape]( self, - x: np.ndarray[ShapeT, np.dtype[_to_integer]], + x: np.ndarray[ShapeT, np.dtype[_as_f64]], /, *, out: EllipsisType | None = None, dtype: None = None, **kwargs: Unpack[_Kwargs12], ) -> _tuple2[np.ndarray[ShapeT, np.dtype[np.float64]]]: ... + @overload # Nd, +f32 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f32]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[np.ndarray[ShapeT, np.dtype[np.float32]]]: ... + @overload # Nd, +f16 + def __call__[ShapeT: _Shape]( + self, + x: np.ndarray[ShapeT, np.dtype[_as_f16]], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[np.ndarray[ShapeT, np.dtype[np.float16]]]: ... @overload # scalar, float | +f64 def __call__( self, - x: float | _to_integer, + x: float | _as_f64, /, *, out: None = None, dtype: None = None, **kwargs: Unpack[_Kwargs12], ) -> _tuple2[np.float64]: ... + @overload # scalar, +f32 + def __call__( + self, + x: _as_f32, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[np.float32]: ... + @overload # scalar, +f16 + def __call__( + self, + x: _as_f16, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs12], + ) -> _tuple2[np.float16]: ... @overload # 1d, +float def __call__( self, @@ -4154,10 +4358,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> ScalarT: ... - @overload # 0d np.float64: ... + @overload # 0d +f32, 0d + def __call__( + self, + x1: _as_f32, + x2: _IntLike_co, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> np.float32: ... + @overload # 0d +f16, 0d + def __call__( + self, + x1: _as_f16, + x2: _IntLike_co, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> np.float16: ... @overload # >0d T@floating, >=0d def __call__[ScalarT: np.floating]( self, @@ -4176,10 +4402,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[ScalarT]: ... - @overload # >0d =0d + @overload # >0d +f64, >=0d def __call__( self, - x1: npt.NDArray[_to_integer] | _NestedSequence[float | _to_integer], + x1: npt.NDArray[_as_f64] | _NestedSequence[float | _as_f64], x2: _ArrayLikeInt_co, /, *, @@ -4187,6 +4413,28 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[np.float64]: ... + @overload # >0d +f32, >=0d + def __call__( + self, + x1: npt.NDArray[_as_f32] | _NestedSequence[_as_f32], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float32]: ... + @overload # >0d +f16, >=0d + def __call__( + self, + x1: npt.NDArray[_as_f16] | _NestedSequence[_as_f16], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float16]: ... @overload # >=0d T@floating, >0d def __call__[ScalarT: np.floating]( self, @@ -4198,10 +4446,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[ScalarT]: ... - @overload # >=0d 0d + @overload # >=0d +f64, >0d def __call__( self, - x1: _DualArrayLike[np.dtype[_to_integer], float], + x1: _DualArrayLike[np.dtype[_as_f64], float], x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], /, *, @@ -4209,6 +4457,28 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[np.float64]: ... + @overload # >=0d +f32, >0d + def __call__( + self, + x1: _ArrayLike[_as_f32], + x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float32]: ... + @overload # >=0d +f32, >0d + def __call__( + self, + x1: _ArrayLike[_as_f16], + x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float16]: ... @overload # >=0d T@floating, >=0d, out=... def __call__[ScalarT: np.floating]( self, @@ -4220,10 +4490,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[ScalarT]: ... - @overload # >=0d =0d, out=... + @overload # >=0d +f64, >=0d, out=... def __call__( self, - x1: _DualArrayLike[np.dtype[_to_integer], float], + x1: _DualArrayLike[np.dtype[_as_f64], float], x2: _ArrayLikeInt_co, /, *, @@ -4231,6 +4501,28 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[np.float64]: ... + @overload # >=0d +f32, >=0d, out=... + def __call__( + self, + x1: _ArrayLike[_as_f32], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float32]: ... + @overload # >=0d +f16, >=0d, out=... + def __call__( + self, + x1: _ArrayLike[_as_f16], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float16]: ... @overload # out= def __call__[OutT: np.ndarray]( self, @@ -4278,10 +4570,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> ScalarT: ... - @overload # 0d np.float64: ... + @overload # 0d +f32, 0d + def outer( + self, + x1: _as_f32, + x2: _IntLike_co, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> np.float32: ... + @overload # 0d +f16, 0d + def outer( + self, + x1: _as_f16, + x2: _IntLike_co, + /, + *, + out: None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> np.float16: ... @overload # >0d T@floating, >=0d def outer[ScalarT: np.floating]( self, @@ -4300,10 +4614,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[ScalarT]: ... - @overload # >0d =0d + @overload # >0d +f64, >=0d def outer( self, - x1: npt.NDArray[_to_integer] | _NestedSequence[float | _to_integer], + x1: npt.NDArray[_as_f64] | _NestedSequence[float | _as_f64], x2: _ArrayLikeInt_co, /, *, @@ -4311,6 +4625,28 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[np.float64]: ... + @overload # >0d +f32, >=0d + def outer( + self, + x1: npt.NDArray[_as_f32] | _NestedSequence[_as_f32], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float32]: ... + @overload # >0d +f16, >=0d + def outer( + self, + x1: npt.NDArray[_as_f16] | _NestedSequence[_as_f16], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float16]: ... @overload # >=0d T@floating, >0d def outer[ScalarT: np.floating]( self, @@ -4322,10 +4658,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[ScalarT]: ... - @overload # >=0d 0d + @overload # >=0d +f64, >0d def outer( self, - x1: _DualArrayLike[np.dtype[_to_integer], float], + x1: _DualArrayLike[np.dtype[_as_f64], float], x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], /, *, @@ -4333,6 +4669,28 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[np.float64]: ... + @overload # >=0d +f32, >0d + def outer( + self, + x1: _ArrayLike[_as_f32], + x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float32]: ... + @overload # >=0d +f16, >0d + def outer( + self, + x1: _ArrayLike[_as_f16], + x2: npt.NDArray[_to_integer] | _NestedSequence[_IntLike_co], + /, + *, + out: EllipsisType | None = None, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float16]: ... @overload # >=0d T@floating, >=0d, out=... def outer[ScalarT: np.floating]( self, @@ -4344,10 +4702,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[ScalarT]: ... - @overload # >=0d =0d, out=... + @overload # >=0d +f64, >=0d, out=... def outer( self, - x1: _DualArrayLike[np.dtype[_to_integer], float], + x1: _DualArrayLike[np.dtype[_as_f64], float], x2: _ArrayLikeInt_co, /, *, @@ -4355,6 +4713,28 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] dtype: None = None, **kwargs: Unpack[_Kwargs21], ) -> npt.NDArray[np.float64]: ... + @overload # >=0d +f32, >=0d, out=... + def outer( + self, + x1: _ArrayLike[_as_f32], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float32]: ... + @overload # >=0d +f16, >=0d, out=... + def outer( + self, + x1: _ArrayLike[_as_f16], + x2: _ArrayLikeInt_co, + /, + *, + out: EllipsisType, + dtype: None = None, + **kwargs: Unpack[_Kwargs21], + ) -> npt.NDArray[np.float16]: ... @overload # out= def outer[OutT: np.ndarray]( self, @@ -4400,10 +4780,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] # @override - @overload # unknown shape + @overload # unknown shape, +f64 def reduce( # pyrefly:ignore[bad-override] self, - array: _ArrayLikeInt, + array: _DualArrayLike[np.dtype[_as_f64], float], /, *, axis: int | tuple[int, ...] = 0, @@ -4413,10 +4793,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] initial: bool | np.bool = ..., where: _ArrayLikeBool_co = True, ) -> npt.NDArray[np.float64] | np.float64: ... - @overload # unknown shape, axis=None + @overload # unknown shape, +f64, axis=None def reduce( self, - array: _ArrayLikeInt, + array: _DualArrayLike[np.dtype[_as_f64], float], /, *, axis: None, @@ -4426,10 +4806,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] initial: bool | np.bool = ..., where: _ArrayLikeBool_co = True, ) -> np.float64: ... - @overload # unknown shape, keepdims=True + @overload # unknown shape, +f64, keepdims=True def reduce( self, - array: _ArrayLikeInt, + array: _DualArrayLike[np.dtype[_as_f64], float], /, *, axis: int | tuple[int, ...] | None = 0, @@ -4439,10 +4819,10 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] initial: bool | np.bool = ..., where: _ArrayLikeBool_co = True, ) -> npt.NDArray[np.float64]: ... - @overload # unknown shape, out=... + @overload # unknown shape, +f64, out=... def reduce( self, - array: _ArrayLikeInt, + array: _DualArrayLike[np.dtype[_as_f64], float], /, *, axis: int | tuple[int, ...] | None = 0, @@ -4452,6 +4832,110 @@ class _ufunc_21_ldexp(_ufunc_21[None]): # type: ignore[misc] initial: bool | np.bool = ..., where: _ArrayLikeBool_co = True, ) -> npt.NDArray[np.float64]: ... + @overload # unknown shape, +f32 + def reduce( + self, + array: _ArrayLike[_as_f32], + /, + *, + axis: int | tuple[int, ...] = 0, + dtype: None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.float32] | np.float32: ... + @overload # unknown shape, +f32, axis=None + def reduce( + self, + array: _ArrayLike[_as_f32], + /, + *, + axis: None, + dtype: None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> np.float32: ... + @overload # unknown shape, +f32, keepdims=True + def reduce( + self, + array: _ArrayLike[_as_f32], + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: None = None, + out: None = None, + keepdims: Literal[True], + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.float32]: ... + @overload # unknown shape, +f32, out=... + def reduce( + self, + array: _ArrayLike[_as_f32], + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: None = None, + out: EllipsisType, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.float32]: ... + @overload # unknown shape, +f16 + def reduce( + self, + array: _ArrayLike[_as_f16], + /, + *, + axis: int | tuple[int, ...] = 0, + dtype: None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.float16] | np.float16: ... + @overload # unknown shape, +f16, axis=None + def reduce( + self, + array: _ArrayLike[_as_f16], + /, + *, + axis: None, + dtype: None = None, + out: None = None, + keepdims: Literal[False] = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> np.float16: ... + @overload # unknown shape, +f16, keepdims=True + def reduce( + self, + array: _ArrayLike[_as_f16], + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: None = None, + out: None = None, + keepdims: Literal[True], + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.float16]: ... + @overload # unknown shape, +f16, out=... + def reduce( + self, + array: _ArrayLike[_as_f16], + /, + *, + axis: int | tuple[int, ...] | None = 0, + dtype: None = None, + out: EllipsisType, + keepdims: bool = False, + initial: bool | np.bool = ..., + where: _ArrayLikeBool_co = True, + ) -> npt.NDArray[np.float16]: ... @overload # out= def reduce[OutT: np.ndarray]( self, diff --git a/numpy/typing/tests/data/reveal/ufuncs.pyi b/numpy/typing/tests/data/reveal/ufuncs.pyi index bfc52fed6317..1878ddd8825b 100644 --- a/numpy/typing/tests/data/reveal/ufuncs.pyi +++ b/numpy/typing/tests/data/reveal/ufuncs.pyi @@ -129,6 +129,10 @@ _i16_0d: np.int16 _i16_1d: np.ndarray[tuple[int], np.dtype[np.int16]] _i16_2d: np.ndarray[tuple[int, int], np.dtype[np.int16]] _i16_nd: npt.NDArray[np.int16] +_i32_0d: np.int32 +_i32_1d: np.ndarray[tuple[int], np.dtype[np.int32]] +_i32_2d: np.ndarray[tuple[int, int], np.dtype[np.int32]] +_i32_nd: npt.NDArray[np.int32] _f32_0d: np.float32 _f32_1d: np.ndarray[tuple[int], np.dtype[np.float32]] _f32_2d: np.ndarray[tuple[int, int], np.dtype[np.float32]] @@ -299,10 +303,18 @@ assert_type(np.spacing(_py_f_0d), np.float64) assert_type(np.spacing(_py_f_1d), np.ndarray[tuple[int], np.dtype[np.float64]]) assert_type(np.spacing(_py_f_2d), np.ndarray[tuple[int, int], np.dtype[np.float64]]) -assert_type(np.spacing(_i16_0d), np.float64) -assert_type(np.spacing(_i16_1d), np.ndarray[tuple[int], np.dtype[np.float64]]) -assert_type(np.spacing(_i16_2d), np.ndarray[tuple[int, int], np.dtype[np.float64]]) -assert_type(np.spacing(_i16_nd), npt.NDArray[np.float64]) +assert_type(np.spacing(_bool_0d), np.float16) +assert_type(np.spacing(_bool_1d), np.ndarray[tuple[int], np.dtype[np.float16]]) +assert_type(np.spacing(_bool_2d), np.ndarray[tuple[int, int], np.dtype[np.float16]]) +assert_type(np.spacing(_bool_nd), npt.NDArray[np.float16]) +assert_type(np.spacing(_i16_0d), np.float32) +assert_type(np.spacing(_i16_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) +assert_type(np.spacing(_i16_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) +assert_type(np.spacing(_i16_nd), npt.NDArray[np.float32]) +assert_type(np.spacing(_i32_0d), np.float64) +assert_type(np.spacing(_i32_1d), np.ndarray[tuple[int], np.dtype[np.float64]]) +assert_type(np.spacing(_i32_2d), np.ndarray[tuple[int, int], np.dtype[np.float64]]) +assert_type(np.spacing(_i32_nd), npt.NDArray[np.float64]) assert_type(np.spacing(_f32_0d), np.float32) assert_type(np.spacing(_f32_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.spacing(_f32_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) @@ -328,10 +340,18 @@ assert_type(np.cbrt(_py_f_0d), np.float64) assert_type(np.cbrt(_py_f_1d), np.ndarray[tuple[int], np.dtype[np.float64]]) assert_type(np.cbrt(_py_f_2d), np.ndarray[tuple[int, int], np.dtype[np.float64]]) -assert_type(np.cbrt(_i16_0d), np.float64) -assert_type(np.cbrt(_i16_1d), np.ndarray[tuple[int], np.dtype[np.float64]]) -assert_type(np.cbrt(_i16_2d), np.ndarray[tuple[int, int], np.dtype[np.float64]]) -assert_type(np.cbrt(_i16_nd), npt.NDArray[np.float64]) +assert_type(np.cbrt(_bool_0d), np.float16) +assert_type(np.cbrt(_bool_1d), np.ndarray[tuple[int], np.dtype[np.float16]]) +assert_type(np.cbrt(_bool_2d), np.ndarray[tuple[int, int], np.dtype[np.float16]]) +assert_type(np.cbrt(_bool_nd), npt.NDArray[np.float16]) +assert_type(np.cbrt(_i16_0d), np.float32) +assert_type(np.cbrt(_i16_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) +assert_type(np.cbrt(_i16_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) +assert_type(np.cbrt(_i16_nd), npt.NDArray[np.float32]) +assert_type(np.cbrt(_i32_0d), np.float64) +assert_type(np.cbrt(_i32_1d), np.ndarray[tuple[int], np.dtype[np.float64]]) +assert_type(np.cbrt(_i32_2d), np.ndarray[tuple[int, int], np.dtype[np.float64]]) +assert_type(np.cbrt(_i32_nd), npt.NDArray[np.float64]) assert_type(np.cbrt(_f32_0d), np.float32) assert_type(np.cbrt(_f32_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.cbrt(_f32_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) @@ -362,10 +382,14 @@ assert_type(np.sin(_py_c_0d), np.complex128 | Any) # `complex` overlaps with `f assert_type(np.sin(_py_c_1d), np.ndarray[tuple[int], np.dtype[np.complex128]]) assert_type(np.sin(_py_c_2d), np.ndarray[tuple[int, int], np.dtype[np.complex128]]) -assert_type(np.sin(_i16_0d), np.float64) -assert_type(np.sin(_i16_1d), np.ndarray[tuple[int], np.dtype[np.float64]]) -assert_type(np.sin(_i16_2d), np.ndarray[tuple[int, int], np.dtype[np.float64]]) -assert_type(np.sin(_i16_nd), npt.NDArray[np.float64]) +assert_type(np.sin(_i16_0d), np.float32) +assert_type(np.sin(_i16_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) +assert_type(np.sin(_i16_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) +assert_type(np.sin(_i16_nd), npt.NDArray[np.float32]) +assert_type(np.sin(_i32_0d), np.float64) +assert_type(np.sin(_i32_1d), np.ndarray[tuple[int], np.dtype[np.float64]]) +assert_type(np.sin(_i32_2d), np.ndarray[tuple[int, int], np.dtype[np.float64]]) +assert_type(np.sin(_i32_nd), npt.NDArray[np.float64]) assert_type(np.sin(_f32_0d), np.float32) assert_type(np.sin(_f32_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.sin(_f32_2d), np.ndarray[tuple[int, int], np.dtype[np.float32]]) @@ -694,22 +718,54 @@ assert_type( ], ) -assert_type(np.frexp(_i16_0d), tuple[np.float64, np.int32]) +assert_type(np.frexp(_bool_0d), tuple[np.float16, np.int32]) +assert_type( + np.frexp(_bool_1d), + tuple[ + np.ndarray[tuple[int], np.dtype[np.float16]], + np.ndarray[tuple[int], np.dtype[np.int32]], + ], +) +assert_type( + np.frexp(_bool_2d), + tuple[ + np.ndarray[tuple[int, int], np.dtype[np.float16]], + np.ndarray[tuple[int, int], np.dtype[np.int32]], + ], +) +assert_type(np.frexp(_bool_nd), tuple[npt.NDArray[np.float16], npt.NDArray[np.int32]]) +assert_type(np.frexp(_i16_0d), tuple[np.float32, np.int32]) assert_type( np.frexp(_i16_1d), tuple[ - np.ndarray[tuple[int], np.dtype[np.float64]], + np.ndarray[tuple[int], np.dtype[np.float32]], np.ndarray[tuple[int], np.dtype[np.int32]], ], ) assert_type( np.frexp(_i16_2d), + tuple[ + np.ndarray[tuple[int, int], np.dtype[np.float32]], + np.ndarray[tuple[int, int], np.dtype[np.int32]], + ], +) +assert_type(np.frexp(_i16_nd), tuple[npt.NDArray[np.float32], npt.NDArray[np.int32]]) +assert_type(np.frexp(_i32_0d), tuple[np.float64, np.int32]) +assert_type( + np.frexp(_i32_1d), + tuple[ + np.ndarray[tuple[int], np.dtype[np.float64]], + np.ndarray[tuple[int], np.dtype[np.int32]], + ], +) +assert_type( + np.frexp(_i32_2d), tuple[ np.ndarray[tuple[int, int], np.dtype[np.float64]], np.ndarray[tuple[int, int], np.dtype[np.int32]], ], ) -assert_type(np.frexp(_i16_nd), tuple[npt.NDArray[np.float64], npt.NDArray[np.int32]]) +assert_type(np.frexp(_i32_nd), tuple[npt.NDArray[np.float64], npt.NDArray[np.int32]]) assert_type(np.frexp(_f32_0d), tuple[np.float32, np.int32]) assert_type( np.frexp(_f32_1d), @@ -746,10 +802,18 @@ assert_type(np.modf(_py_f_0d), _tuple2[np.float64]) assert_type(np.modf(_py_f_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float64]]]) assert_type(np.modf(_py_f_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float64]]]) -assert_type(np.modf(_i16_0d), _tuple2[np.float64]) -assert_type(np.modf(_i16_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float64]]]) -assert_type(np.modf(_i16_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float64]]]) -assert_type(np.modf(_i16_nd), _tuple2[npt.NDArray[np.float64]]) +assert_type(np.modf(_bool_0d), _tuple2[np.float16]) +assert_type(np.modf(_bool_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float16]]]) +assert_type(np.modf(_bool_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float16]]]) +assert_type(np.modf(_bool_nd), _tuple2[npt.NDArray[np.float16]]) +assert_type(np.modf(_i16_0d), _tuple2[np.float32]) +assert_type(np.modf(_i16_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float32]]]) +assert_type(np.modf(_i16_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float32]]]) +assert_type(np.modf(_i16_nd), _tuple2[npt.NDArray[np.float32]]) +assert_type(np.modf(_i32_0d), _tuple2[np.float64]) +assert_type(np.modf(_i32_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float64]]]) +assert_type(np.modf(_i32_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float64]]]) +assert_type(np.modf(_i32_nd), _tuple2[npt.NDArray[np.float64]]) assert_type(np.modf(_f32_0d), _tuple2[np.float32]) assert_type(np.modf(_f32_1d), _tuple2[np.ndarray[tuple[int], np.dtype[np.float32]]]) assert_type(np.modf(_f32_2d), _tuple2[np.ndarray[tuple[int, int], np.dtype[np.float32]]]) @@ -938,14 +1002,18 @@ assert_type(np.ldexp(_py_f_0d, _py_i_0d), np.float64) assert_type(np.ldexp(_py_f_1d, _py_i_1d), npt.NDArray[np.float64]) assert_type(np.ldexp(_py_f_2d, _py_i_2d), npt.NDArray[np.float64]) -assert_type(np.ldexp(_bool_0d, _bool_0d), np.float64) -assert_type(np.ldexp(_bool_1d, _bool_1d), npt.NDArray[np.float64]) -assert_type(np.ldexp(_bool_2d, _bool_2d), npt.NDArray[np.float64]) -assert_type(np.ldexp(_bool_nd, _bool_nd), npt.NDArray[np.float64]) -assert_type(np.ldexp(_i16_0d, _i16_0d), np.float64) -assert_type(np.ldexp(_i16_1d, _i16_1d), npt.NDArray[np.float64]) -assert_type(np.ldexp(_i16_2d, _i16_2d), npt.NDArray[np.float64]) -assert_type(np.ldexp(_i16_nd, _i16_nd), npt.NDArray[np.float64]) +assert_type(np.ldexp(_bool_0d, _bool_0d), np.float16) +assert_type(np.ldexp(_bool_1d, _bool_1d), npt.NDArray[np.float16]) +assert_type(np.ldexp(_bool_2d, _bool_2d), npt.NDArray[np.float16]) +assert_type(np.ldexp(_bool_nd, _bool_nd), npt.NDArray[np.float16]) +assert_type(np.ldexp(_i16_0d, _i16_0d), np.float32) +assert_type(np.ldexp(_i16_1d, _i16_1d), npt.NDArray[np.float32]) +assert_type(np.ldexp(_i16_2d, _i16_2d), npt.NDArray[np.float32]) +assert_type(np.ldexp(_i16_nd, _i16_nd), npt.NDArray[np.float32]) +assert_type(np.ldexp(_i32_0d, _i32_0d), np.float64) +assert_type(np.ldexp(_i32_1d, _i32_1d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_i32_2d, _i32_2d), npt.NDArray[np.float64]) +assert_type(np.ldexp(_i32_nd, _i32_nd), npt.NDArray[np.float64]) assert_type(np.ldexp(_f32_0d, _i16_0d), np.float32) assert_type(np.ldexp(_f32_1d, _i16_1d), npt.NDArray[np.float32]) assert_type(np.ldexp(_f32_2d, _i16_2d), npt.NDArray[np.float32]) @@ -964,11 +1032,21 @@ assert_type(np.ldexp.outer(_f32_1d, _py_i_1d), npt.NDArray[np.float32]) assert_type(np.ldexp.at(_f32_2d, 1, _py_i_1d), None) -assert_type(np.ldexp.reduce(_i16_1d), npt.NDArray[np.float64] | np.float64) -assert_type(np.ldexp.reduce(_i16_2d), npt.NDArray[np.float64] | np.float64) -assert_type(np.ldexp.reduce(_i16_1d, axis=None), np.float64) -assert_type(np.ldexp.reduce(_i16_1d, keepdims=True), npt.NDArray[np.float64]) -assert_type(np.ldexp.reduce(_i16_1d, out=...), npt.NDArray[np.float64]) -assert_type(np.ldexp.reduce(_i16_1d, out=_f32_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) +assert_type(np.ldexp.reduce(_bool_1d), npt.NDArray[np.float16] | np.float16) +assert_type(np.ldexp.reduce(_bool_2d), npt.NDArray[np.float16] | np.float16) +assert_type(np.ldexp.reduce(_bool_1d, axis=None), np.float16) +assert_type(np.ldexp.reduce(_bool_1d, keepdims=True), npt.NDArray[np.float16]) +assert_type(np.ldexp.reduce(_bool_1d, out=...), npt.NDArray[np.float16]) +assert_type(np.ldexp.reduce(_i16_1d), npt.NDArray[np.float32] | np.float32) +assert_type(np.ldexp.reduce(_i16_2d), npt.NDArray[np.float32] | np.float32) +assert_type(np.ldexp.reduce(_i16_1d, axis=None), np.float32) +assert_type(np.ldexp.reduce(_i16_1d, keepdims=True), npt.NDArray[np.float32]) +assert_type(np.ldexp.reduce(_i16_1d, out=...), npt.NDArray[np.float32]) +assert_type(np.ldexp.reduce(_i32_1d), npt.NDArray[np.float64] | np.float64) +assert_type(np.ldexp.reduce(_i32_2d), npt.NDArray[np.float64] | np.float64) +assert_type(np.ldexp.reduce(_i32_1d, axis=None), np.float64) +assert_type(np.ldexp.reduce(_i32_1d, keepdims=True), npt.NDArray[np.float64]) +assert_type(np.ldexp.reduce(_i32_1d, out=...), npt.NDArray[np.float64]) +assert_type(np.ldexp.reduce(_i32_1d, out=_f32_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) # ldexp doesn't support reduceat / accumulate From 3c398a328bcae1a9c05548f58e7e95351ce7e7fa Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:56:45 -0500 Subject: [PATCH 46/60] PERF: Optimize reduction helper fast paths (#31845) Co-authored-by: Nathan Goldbaum --- .../upcoming_changes/31845.performance.rst | 6 +++ numpy/_core/fromnumeric.py | 45 ++++++++++++------- 2 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 doc/release/upcoming_changes/31845.performance.rst diff --git a/doc/release/upcoming_changes/31845.performance.rst b/doc/release/upcoming_changes/31845.performance.rst new file mode 100644 index 000000000000..0d950fc27798 --- /dev/null +++ b/doc/release/upcoming_changes/31845.performance.rst @@ -0,0 +1,6 @@ +Faster reductions on small arrays +--------------------------------- +``numpy.sum``, ``numpy.prod``, ``numpy.min``, ``numpy.max``, ``numpy.any``, +and ``numpy.all`` are now faster for small arrays. This reduces the +Python-level overhead of calling these reductions, which is most noticeable +when the reduction itself is cheap. diff --git a/numpy/_core/fromnumeric.py b/numpy/_core/fromnumeric.py index ee5e65456178..e6ce77e259e9 100644 --- a/numpy/_core/fromnumeric.py +++ b/numpy/_core/fromnumeric.py @@ -29,6 +29,7 @@ _gentype = types.GeneratorType # save away Python sum _sum_ = sum +_NoValue = np._NoValue array_function_dispatch = functools.partial( overrides.array_function_dispatch, module='numpy') @@ -63,9 +64,20 @@ def _wrapfunc(obj, method, *args, **kwds): return _wrapit(obj, method, *args, **kwds) -def _wrapreduction(obj, ufunc, method, axis, dtype, out, **kwargs): - passkwargs = {k: v for k, v in kwargs.items() - if v is not np._NoValue} +# The positional-only signature and unrolled _NoValue checks (rather than +# **kwargs with a dict comprehension) are deliberate: these helpers are on +# the hot path of every reduction (sum, prod, min, max, any, all), and +# avoiding the creation and iteration of a temporary kwargs dict measurably +# reduces call overhead for small arrays. See gh-31845. +def _wrapreduction(obj, ufunc, method, axis, dtype, out, + keepdims=_NoValue, initial=_NoValue, where=_NoValue, /): + passkwargs = {} + if keepdims is not _NoValue: + passkwargs["keepdims"] = keepdims + if initial is not _NoValue: + passkwargs["initial"] = initial + if where is not _NoValue: + passkwargs["where"] = where if type(obj) is not mu.ndarray: try: @@ -83,10 +95,14 @@ def _wrapreduction(obj, ufunc, method, axis, dtype, out, **kwargs): return ufunc.reduce(obj, axis, dtype, out, **passkwargs) -def _wrapreduction_any_all(obj, ufunc, method, axis, out, **kwargs): +def _wrapreduction_any_all(obj, ufunc, method, axis, out, + keepdims=_NoValue, where=_NoValue, /): # Same as above function, but dtype is always bool (but never passed on) - passkwargs = {k: v for k, v in kwargs.items() - if v is not np._NoValue} + passkwargs = {} + if keepdims is not _NoValue: + passkwargs["keepdims"] = keepdims + if where is not _NoValue: + passkwargs["where"] = where if type(obj) is not mu.ndarray: try: @@ -2486,8 +2502,7 @@ def sum(a, axis=None, dtype=None, out=None, keepdims=np._NoValue, ) return _wrapreduction( - a, np.add, 'sum', axis, dtype, out, - keepdims=keepdims, initial=initial, where=where + a, np.add, 'sum', axis, dtype, out, keepdims, initial, where ) @@ -2600,7 +2615,7 @@ def any(a, axis=None, out=None, keepdims=np._NoValue, *, where=np._NoValue): """ return _wrapreduction_any_all(a, np.logical_or, 'any', axis, out, - keepdims=keepdims, where=where) + keepdims, where) def _all_dispatcher(a, axis=None, out=None, keepdims=None, *, @@ -2695,7 +2710,7 @@ def all(a, axis=None, out=None, keepdims=np._NoValue, *, where=np._NoValue): """ return _wrapreduction_any_all(a, np.logical_and, 'all', axis, out, - keepdims=keepdims, where=where) + keepdims, where) def _cumulative_func(x, func, axis, dtype, out, include_initial): @@ -3184,7 +3199,7 @@ def max(a, axis=None, out=None, keepdims=np._NoValue, initial=np._NoValue, 5 """ return _wrapreduction(a, np.maximum, 'max', axis, None, out, - keepdims=keepdims, initial=initial, where=where) + keepdims, initial, where) @array_function_dispatch(_max_dispatcher) @@ -3201,7 +3216,7 @@ def amax(a, axis=None, out=None, keepdims=np._NoValue, initial=np._NoValue, ndarray.max : equivalent method """ return _wrapreduction(a, np.maximum, 'max', axis, None, out, - keepdims=keepdims, initial=initial, where=where) + keepdims, initial, where) def _min_dispatcher(a, axis=None, out=None, keepdims=None, initial=None, @@ -3322,7 +3337,7 @@ def min(a, axis=None, out=None, keepdims=np._NoValue, initial=np._NoValue, 6 """ return _wrapreduction(a, np.minimum, 'min', axis, None, out, - keepdims=keepdims, initial=initial, where=where) + keepdims, initial, where) @array_function_dispatch(_min_dispatcher) @@ -3339,7 +3354,7 @@ def amin(a, axis=None, out=None, keepdims=np._NoValue, initial=np._NoValue, ndarray.min : equivalent method """ return _wrapreduction(a, np.minimum, 'min', axis, None, out, - keepdims=keepdims, initial=initial, where=where) + keepdims, initial, where) def _prod_dispatcher(a, axis=None, dtype=None, out=None, keepdims=None, @@ -3465,7 +3480,7 @@ def prod(a, axis=None, dtype=None, out=None, keepdims=np._NoValue, 10 """ return _wrapreduction(a, np.multiply, 'prod', axis, dtype, out, - keepdims=keepdims, initial=initial, where=where) + keepdims, initial, where) def _cumprod_dispatcher(a, axis=None, dtype=None, out=None): From 1ee47309925bcbf235c340a033672a2decd13e88 Mon Sep 17 00:00:00 2001 From: Chirag Dhamange <64466827+Diamagnetic@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:35:45 -0400 Subject: [PATCH 47/60] BUG: restrict weak scalar handling in resolve_dtypes to input operands (#31746) --- numpy/_core/src/umath/ufunc_object.c | 13 +++++++++---- numpy/_core/tests/test_ufunc.py | 7 +++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/numpy/_core/src/umath/ufunc_object.c b/numpy/_core/src/umath/ufunc_object.c index 88e2cd641d9d..73ce23992b57 100644 --- a/numpy/_core/src/umath/ufunc_object.c +++ b/numpy/_core/src/umath/ufunc_object.c @@ -6423,8 +6423,8 @@ py_resolve_dtypes_generic(PyUFuncObject *ufunc, npy_bool return_context, DTypes[i] = NPY_DTYPE(descr); Py_INCREF(DTypes[i]); } - /* Explicitly allow int, float, and complex for the "weak" types. */ - else if (descr_obj == (PyObject *)&PyLong_Type) { + /* Explicitly allow int, float, and complex for the "weak" types. */ + else if (descr_obj == (PyObject *)&PyLong_Type && i < ufunc -> nin) { descr = PyArray_DescrFromType(NPY_INTP); dummy_arrays[i] = (PyArrayObject *)PyArray_Empty(0, NULL, descr, 0); if (dummy_arrays[i] == NULL) { @@ -6435,7 +6435,7 @@ py_resolve_dtypes_generic(PyUFuncObject *ufunc, npy_bool return_context, DTypes[i] = &PyArray_PyLongDType; promoting_pyscalars = NPY_TRUE; } - else if (descr_obj == (PyObject *)&PyFloat_Type) { + else if (descr_obj == (PyObject *)&PyFloat_Type && i < ufunc -> nin) { descr = PyArray_DescrFromType(NPY_DOUBLE); dummy_arrays[i] = (PyArrayObject *)PyArray_Empty(0, NULL, descr, 0); if (dummy_arrays[i] == NULL) { @@ -6446,7 +6446,7 @@ py_resolve_dtypes_generic(PyUFuncObject *ufunc, npy_bool return_context, DTypes[i] = &PyArray_PyFloatDType; promoting_pyscalars = NPY_TRUE; } - else if (descr_obj == (PyObject *)&PyComplex_Type) { + else if (descr_obj == (PyObject *)&PyComplex_Type && i < ufunc -> nin) { descr = PyArray_DescrFromType(NPY_CDOUBLE); dummy_arrays[i] = (PyArrayObject *)PyArray_Empty(0, NULL, descr, 0); if (dummy_arrays[i] == NULL) { @@ -6465,6 +6465,11 @@ py_resolve_dtypes_generic(PyUFuncObject *ufunc, npy_bool return_context, goto finish; } } + else if (i >= ufunc -> nin) { + PyErr_SetString(PyExc_TypeError, + "Output descriptors must be NumPy dtypes or None."); + goto finish; + } else { PyErr_SetString(PyExc_TypeError, "Provided dtype must be a valid NumPy dtype, " diff --git a/numpy/_core/tests/test_ufunc.py b/numpy/_core/tests/test_ufunc.py index a078d6fa18d3..dd5c1c8853f5 100644 --- a/numpy/_core/tests/test_ufunc.py +++ b/numpy/_core/tests/test_ufunc.py @@ -3253,6 +3253,13 @@ def test_resolve_dtypes_basic(self): with pytest.raises(TypeError): np.add.resolve_dtypes((i4, f4, None), casting="no") + def test_resolve_dtypes_unary_weak_scalar(self): + assert np.sin.resolve_dtypes((int, None)) == ( + np.dtype("f8"), np.dtype("f8")) + with pytest.raises(TypeError, + match="Output descriptors must be NumPy dtypes or None."): + np.sin.resolve_dtypes((int, int)) + def test_resolve_dtypes_comparison(self): i4 = np.dtype("i4") i8 = np.dtype("i8") From 87926cc49e457724e7af304cd159514ce40dd0b2 Mon Sep 17 00:00:00 2001 From: Charles Harris Date: Mon, 6 Jul 2026 09:16:12 -0600 Subject: [PATCH 48/60] MAINT: Update Cython version check. This updates the minimum Cython version to 3.1.0 to match the version in pyproject.toml. --- meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 533d6c18223f..cb6af31b1ed1 100644 --- a/meson.build +++ b/meson.build @@ -31,8 +31,8 @@ elif cc.get_id() == 'msvc' endif add_project_arguments('/experimental:c11atomics', language: 'c') endif -if not cy.version().version_compare('>=3.0.6') - error('NumPy requires Cython >= 3.0.6') +if not cy.version().version_compare('>=3.1.0') + error('NumPy requires Cython >= 3.1.0') endif py = import('python').find_installation(pure: false) From 69954c2abe32b3a4dab5975b65a130c8facb3828 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 19:04:28 +0200 Subject: [PATCH 49/60] TYP: fix int promotion for ``*sum`` and ``*prod`` (#31876) --- numpy/__init__.pyi | 80 ++++---- numpy/_core/fromnumeric.pyi | 178 ++++++++++++++---- numpy/ma/core.pyi | 18 +- .../typing/tests/data/reveal/fromnumeric.pyi | 57 ++++-- .../typing/tests/data/reveal/ndarray_misc.pyi | 36 ++-- 5 files changed, 250 insertions(+), 119 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index 69c42dfc2314..a29e168e1f93 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -2520,8 +2520,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): # keep in sync with `sum` below (but without `timedelta64`) @override # type: ignore[override] - @overload # ~number - def prod[ScalarT: number]( + @overload # ~inexact + def prod[ScalarT: inexact]( self: NDArray[ScalarT], axis: None = None, dtype: None = None, @@ -2531,8 +2531,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> ScalarT: ... - @overload # ~number, axis: - def prod[ScalarT: number | object_]( + @overload # ~inexact | object_, axis: + def prod[ScalarT: inexact | object_]( self: NDArray[ScalarT], axis: int | tuple[int, ...], dtype: None = None, @@ -2542,8 +2542,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[ScalarT]: ... - @overload # ~number | object_, keepdims=True - def prod[ArrayT: NDArray[number | object_]]( + @overload # ~inexact | object_, keepdims=True + def prod[ArrayT: NDArray[inexact | object_]]( self: ArrayT, axis: int | tuple[int, ...] | None = None, dtype: None = None, @@ -2553,9 +2553,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> ArrayT: ... - @overload # bool_ + @overload # +integer def prod( - self: NDArray[bool_], + self: NDArray[integer | bool_], axis: None = None, dtype: None = None, out: None = None, @@ -2564,9 +2564,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _IntLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> int_: ... - @overload # bool_, axis: + @overload # +integer, axis: def prod( - self: NDArray[bool_], + self: NDArray[integer | bool_], axis: int | tuple[int, ...], dtype: None = None, out: None = None, @@ -2575,9 +2575,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _IntLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[int_]: ... - @overload # bool_, keepdims=True + @overload # +integer, keepdims=True def prod( - self: NDArray[bool_], + self: NDArray[integer | bool_], axis: int | tuple[int, ...] | None = None, dtype: None = None, out: None = None, @@ -2699,8 +2699,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): # keep in sync with `prod` above (but also accept `timedelta64`) @override # type: ignore[override] - @overload # ~number | timedelta64 - def sum[ScalarT: number | timedelta64]( + @overload # ~inexact | timedelta64 + def sum[ScalarT: inexact | timedelta64]( self: NDArray[ScalarT], axis: None = None, dtype: None = None, @@ -2710,8 +2710,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> ScalarT: ... - @overload # ~number | timedelta64, axis: - def sum[ScalarT: number | timedelta64 | object_]( + @overload # ~inexact | timedelta64, axis: + def sum[ScalarT: inexact | timedelta64 | object_]( self: NDArray[ScalarT], axis: int | tuple[int, ...], dtype: None = None, @@ -2721,8 +2721,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[ScalarT]: ... - @overload # ~number | timedelta64 | object_, keepdims=True - def sum[ArrayT: NDArray[number | timedelta64 | object_]]( + @overload # ~inexact | timedelta64 | object_, keepdims=True + def sum[ArrayT: NDArray[inexact | timedelta64 | object_]]( self: ArrayT, axis: int | tuple[int, ...] | None = None, dtype: None = None, @@ -2732,9 +2732,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> ArrayT: ... - @overload # bool_ + @overload # +integer def sum( - self: NDArray[bool_], + self: NDArray[integer | bool_], axis: None = None, dtype: None = None, out: None = None, @@ -2743,9 +2743,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _IntLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> int_: ... - @overload # bool_, axis: + @overload # +integer, axis: def sum( - self: NDArray[bool_], + self: NDArray[integer | bool_], axis: int | tuple[int, ...], dtype: None = None, out: None = None, @@ -2754,9 +2754,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): initial: _IntLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[int_]: ... - @overload # bool_, keepdims=True + @overload # +integer, keepdims=True def sum( - self: NDArray[bool_], + self: NDArray[integer | bool_], axis: int | tuple[int, ...] | None = None, dtype: None = None, out: None = None, @@ -2878,16 +2878,16 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): # keep in sync with `MaskedArray.cumprod` @override # type: ignore[override] - @overload # number | object_ - def cumprod[DTypeT: dtype[number | object_]]( + @overload # inexact | object_ + def cumprod[DTypeT: dtype[inexact | object_]]( self: ndarray[Any, DTypeT], axis: None = None, dtype: None = None, out: None = None, ) -> ndarray[_1D, DTypeT]: ... - @overload # bool_ + @overload # +integer def cumprod( - self: NDArray[bool_], + self: NDArray[integer | bool_], axis: None = None, dtype: None = None, out: None = None, @@ -2922,16 +2922,16 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): dtype: DTypeLike, out: None = None, ) -> ndarray[_1D]: ... - @overload # axis: - def cumprod[ArrayT: NDArray[number | object_]]( + @overload # inexact | object_, axis: + def cumprod[ArrayT: NDArray[inexact | object_]]( self: ArrayT, axis: SupportsIndex, dtype: None = None, out: None = None, ) -> ArrayT: ... - @overload # bool_, axis: + @overload # +integer, axis: def cumprod[ShapeT: _Shape]( - self: ndarray[ShapeT, dtype[bool_]], + self: ndarray[ShapeT, dtype[integer | bool_]], axis: SupportsIndex, dtype: None = None, out: None = None, @@ -2968,16 +2968,16 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): # keep in sync with `MaskedArray.cumsum` @override # type: ignore[override] - @overload # number | timedelta64 | object_ - def cumsum[DTypeT: dtype[number | timedelta64 | object_]]( + @overload # inexact | timedelta64 | object_ + def cumsum[DTypeT: dtype[inexact | timedelta64 | object_]]( self: ndarray[Any, DTypeT], axis: None = None, dtype: None = None, out: None = None, ) -> ndarray[_1D, DTypeT]: ... - @overload # bool_ + @overload # +integer def cumsum( - self: NDArray[bool_], + self: NDArray[integer | bool_], axis: None = None, dtype: None = None, out: None = None, @@ -3012,16 +3012,16 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): dtype: DTypeLike, out: None = None, ) -> ndarray[_1D]: ... - @overload # axis: - def cumsum[ArrayT: NDArray[number | timedelta64 | object_]]( + @overload # inexact | timedelta64 | object_, axis: + def cumsum[ArrayT: NDArray[inexact | timedelta64 | object_]]( self: ArrayT, axis: SupportsIndex, dtype: None = None, out: None = None, ) -> ArrayT: ... - @overload # bool_, axis: + @overload # +integer, axis: def cumsum[ShapeT: _Shape]( - self: ndarray[ShapeT, dtype[bool_]], + self: ndarray[ShapeT, dtype[integer | bool_]], axis: SupportsIndex, dtype: None = None, out: None = None, diff --git a/numpy/_core/fromnumeric.pyi b/numpy/_core/fromnumeric.pyi index 4da4a6b7ddd1..efe37c6643df 100644 --- a/numpy/_core/fromnumeric.pyi +++ b/numpy/_core/fromnumeric.pyi @@ -1029,27 +1029,48 @@ def any[ArrayT: np.ndarray]( # keep in sync with `cumprod` below @overload -def cumsum[ScalarT: np.generic]( +def cumsum[ScalarT: np.inexact | np.timedelta64 | np.object_]( a: _ArrayLike[ScalarT], axis: None = None, dtype: None = None, out: None = None, ) -> _Array1D[ScalarT]: ... @overload -def cumsum[ArrayT: np.ndarray]( +def cumsum[ArrayT: np.ndarray[Any, np.dtype[np.inexact | np.timedelta64 | np.object_] | np.dtypes.StringDType]]( a: ArrayT, axis: SupportsIndex, dtype: None = None, out: None = None, ) -> ArrayT: ... @overload -def cumsum[ScalarT: np.generic]( +def cumsum[ScalarT: np.inexact | np.timedelta64 | np.object_]( a: _ArrayLike[ScalarT], axis: SupportsIndex, dtype: None = None, out: None = None, ) -> NDArray[ScalarT]: ... @overload +def cumsum( + a: _ArrayLikeInt_co, + axis: None = None, + dtype: None = None, + out: None = None, +) -> _Array1D[np.int_]: ... +@overload +def cumsum[ShapeT: _Shape]( + a: np.ndarray[ShapeT, np.dtype[np.bool | np.integer]], + axis: SupportsIndex, + dtype: None = None, + out: None = None, +) -> np.ndarray[ShapeT, np.dtype[np.int_]]: ... +@overload +def cumsum( + a: _ArrayLikeInt_co, + axis: SupportsIndex, + dtype: None = None, + out: None = None, +) -> NDArray[np.int_]: ... +@overload def cumsum( a: ArrayLike, axis: None = None, @@ -1117,7 +1138,7 @@ def cumsum[ArrayT: np.ndarray]( # keep in sync with `cumulative_prod` below @overload -def cumulative_sum[ScalarT: np.generic]( +def cumulative_sum[ScalarT: np.inexact | np.timedelta64 | np.object_]( x: _ArrayLike[ScalarT], /, *, @@ -1127,7 +1148,7 @@ def cumulative_sum[ScalarT: np.generic]( include_initial: bool = False, ) -> _Array1D[ScalarT]: ... @overload -def cumulative_sum[ArrayT: np.ndarray]( +def cumulative_sum[ArrayT: np.ndarray[Any, np.dtype[np.inexact | np.timedelta64 | np.object_] | np.dtypes.StringDType]]( x: ArrayT, /, *, @@ -1137,7 +1158,7 @@ def cumulative_sum[ArrayT: np.ndarray]( include_initial: bool = False, ) -> ArrayT: ... @overload -def cumulative_sum[ScalarT: np.generic]( +def cumulative_sum[ScalarT: np.inexact | np.timedelta64 | np.object_]( x: _ArrayLike[ScalarT], /, *, @@ -1147,6 +1168,36 @@ def cumulative_sum[ScalarT: np.generic]( include_initial: bool = False, ) -> NDArray[ScalarT]: ... @overload +def cumulative_sum( + x: _ArrayLikeInt_co, + /, + *, + axis: None = None, + dtype: None = None, + out: None = None, + include_initial: bool = False, +) -> _Array1D[np.int_]: ... +@overload +def cumulative_sum[ShapeT: _Shape]( + x: np.ndarray[ShapeT, np.dtype[np.bool | np.integer]], + /, + *, + axis: SupportsIndex, + dtype: None = None, + out: None = None, + include_initial: bool = False, +) -> np.ndarray[ShapeT, np.dtype[np.int_]]: ... +@overload +def cumulative_sum( + x: _ArrayLikeInt_co, + /, + *, + axis: SupportsIndex, + dtype: None = None, + out: None = None, + include_initial: bool = False, +) -> NDArray[np.int_]: ... +@overload def cumulative_sum( x: ArrayLike, /, @@ -1766,29 +1817,50 @@ def amin( min = amin -# keep in sync with `cumsum` above +# keep in sync with `cumsum` above (minus `timedelta64` and `StringDType`) @overload -def cumprod[ScalarT: np.number | np.bool | np.object_]( +def cumprod[ScalarT: np.inexact | np.object_]( a: _ArrayLike[ScalarT], axis: None = None, dtype: None = None, out: None = None, ) -> _Array1D[ScalarT]: ... @overload -def cumprod[ArrayT: NDArray[np.number | np.bool | np.object_]]( +def cumprod[ArrayT: NDArray[np.inexact | np.object_]]( a: ArrayT, axis: SupportsIndex, dtype: None = None, out: None = None, ) -> ArrayT: ... @overload -def cumprod[ScalarT: np.number | np.bool | np.object_]( +def cumprod[ScalarT: np.inexact | np.object_]( a: _ArrayLike[ScalarT], axis: SupportsIndex, dtype: None = None, out: None = None, ) -> NDArray[ScalarT]: ... @overload +def cumprod( + a: _ArrayLikeInt_co, + axis: None = None, + dtype: None = None, + out: None = None, +) -> _Array1D[np.int_]: ... +@overload +def cumprod[ShapeT: _Shape]( + a: np.ndarray[ShapeT, np.dtype[np.bool | np.integer]], + axis: SupportsIndex, + dtype: None = None, + out: None = None, +) -> np.ndarray[ShapeT, np.dtype[np.int_]]: ... +@overload +def cumprod( + a: _ArrayLikeInt_co, + axis: SupportsIndex, + dtype: None = None, + out: None = None, +) -> NDArray[np.int_]: ... +@overload def cumprod( a: _ArrayLikeComplex_co | _ArrayLikeObject_co, axis: None = None, @@ -1854,9 +1926,9 @@ def cumprod[ArrayT: np.ndarray]( out: ArrayT, ) -> ArrayT: ... -# keep in sync with `cumulative_sum` above +# keep in sync with `cumulative_sum` above (minus `timedelta64` and `StringDType`) @overload -def cumulative_prod[ScalarT: np.number | np.bool | np.object_]( +def cumulative_prod[ScalarT: np.inexact | np.object_]( x: _ArrayLike[ScalarT], /, *, @@ -1866,7 +1938,7 @@ def cumulative_prod[ScalarT: np.number | np.bool | np.object_]( include_initial: bool = False, ) -> _Array1D[ScalarT]: ... @overload -def cumulative_prod[ArrayT: NDArray[np.number | np.bool | np.object_]]( +def cumulative_prod[ArrayT: NDArray[np.inexact | np.object_]]( x: ArrayT, /, *, @@ -1876,7 +1948,7 @@ def cumulative_prod[ArrayT: NDArray[np.number | np.bool | np.object_]]( include_initial: bool = False, ) -> ArrayT: ... @overload -def cumulative_prod[ScalarT: np.generic]( +def cumulative_prod[ScalarT: np.inexact | np.object_]( x: _ArrayLike[ScalarT], /, *, @@ -1887,7 +1959,37 @@ def cumulative_prod[ScalarT: np.generic]( ) -> NDArray[ScalarT]: ... @overload def cumulative_prod( - x: _ArrayLikeComplex_co | _ArrayLikeObject_co, + x: _ArrayLikeInt_co, + /, + *, + axis: None = None, + dtype: None = None, + out: None = None, + include_initial: bool = False, +) -> _Array1D[np.int_]: ... +@overload +def cumulative_prod[ShapeT: _Shape]( + x: np.ndarray[ShapeT, np.dtype[np.bool | np.integer]], + /, + *, + axis: SupportsIndex, + dtype: None = None, + out: None = None, + include_initial: bool = False, +) -> np.ndarray[ShapeT, np.dtype[np.int_]]: ... +@overload +def cumulative_prod( + x: _ArrayLikeInt_co, + /, + *, + axis: SupportsIndex, + dtype: None = None, + out: None = None, + include_initial: bool = False, +) -> NDArray[np.int_]: ... +@overload +def cumulative_prod( + x: ArrayLike, /, *, axis: None = None, @@ -2074,8 +2176,8 @@ def prod( initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[np.complex128]: ... -@overload # ~number -def prod[ScalarT: np.number]( +@overload # ~inexact +def prod[ScalarT: np.inexact]( a: _ArrayLike[ScalarT], axis: None = None, dtype: None = None, @@ -2084,8 +2186,8 @@ def prod[ScalarT: np.number]( initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> ScalarT: ... -@overload # ~number | timedelta64 | object_, axis: -def prod[ScalarT: np.number | np.object_]( +@overload # ~inexact | timedelta64 | object_, axis: +def prod[ScalarT: np.inexact | np.object_]( a: _ArrayLike[ScalarT], axis: int | tuple[int, ...], dtype: None = None, @@ -2094,8 +2196,8 @@ def prod[ScalarT: np.number | np.object_]( initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[ScalarT]: ... -@overload # ~number | object_, keepdims=True -def prod[ArrayT: NDArray[np.number | np.object_]]( +@overload # ~inexact | object_, keepdims=True +def prod[ArrayT: NDArray[np.inexact | np.object_]]( a: ArrayT, axis: int | tuple[int, ...] | None = None, dtype: None = None, @@ -2105,9 +2207,9 @@ def prod[ArrayT: NDArray[np.number | np.object_]]( initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> ArrayT: ... -@overload # bool_ | +builtins.int +@overload # bool | integer | +builtins.int def prod( - a: _DualArrayLike[np.dtype[np.bool], int], + a: _DualArrayLike[np.dtype[np.bool | np.integer], int], axis: None = None, dtype: None = None, out: None = None, @@ -2115,9 +2217,9 @@ def prod( initial: _IntLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> np.int_: ... -@overload # bool_ | +builtins.int, axis: +@overload # bool | np.integer | +builtins.int, axis: def prod( - a: _DualArrayLike[np.dtype[np.bool], int], + a: _DualArrayLike[np.dtype[np.bool | np.integer], int], axis: int | tuple[int, ...], dtype: None = None, out: None = None, @@ -2125,9 +2227,9 @@ def prod( initial: _IntLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[np.int_]: ... -@overload # bool_, keepdims=True +@overload # bool | np.integer, keepdims=True def prod[ShapeT: _Shape]( - a: np.ndarray[ShapeT, np.dtype[np.bool]], + a: np.ndarray[ShapeT, np.dtype[np.bool | np.integer]], axis: int | tuple[int, ...] | None = None, dtype: None = None, out: None = None, @@ -2316,8 +2418,8 @@ def sum( initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[np.complex128]: ... -@overload # ~number | timedelta64 -def sum[ScalarT: np.number | np.timedelta64]( +@overload # ~inexact | timedelta64 +def sum[ScalarT: np.inexact | np.timedelta64]( a: _ArrayLike[ScalarT], axis: None = None, dtype: None = None, @@ -2326,8 +2428,8 @@ def sum[ScalarT: np.number | np.timedelta64]( initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> ScalarT: ... -@overload # ~number | timedelta64 | object_, axis: -def sum[ScalarT: np.number | np.timedelta64 | np.object_]( +@overload # ~inexact | timedelta64 | object_, axis: +def sum[ScalarT: np.inexact | np.timedelta64 | np.object_]( a: _ArrayLike[ScalarT], axis: int | tuple[int, ...], dtype: None = None, @@ -2337,7 +2439,7 @@ def sum[ScalarT: np.number | np.timedelta64 | np.object_]( where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[ScalarT]: ... @overload # ~number | timedelta64 | object_, keepdims=True -def sum[ArrayT: NDArray[np.number | np.timedelta64 | np.object_]]( +def sum[ArrayT: NDArray[np.inexact | np.timedelta64 | np.object_]]( a: ArrayT, axis: int | tuple[int, ...] | None = None, dtype: None = None, @@ -2347,9 +2449,9 @@ def sum[ArrayT: NDArray[np.number | np.timedelta64 | np.object_]]( initial: _NumberLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> ArrayT: ... -@overload # bool_ | +builtins.int +@overload # bool | integer | +builtins.int def sum( - a: _DualArrayLike[np.dtype[np.bool], int], + a: _DualArrayLike[np.dtype[np.bool | np.integer], int], axis: None = None, dtype: None = None, out: None = None, @@ -2357,9 +2459,9 @@ def sum( initial: _IntLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> np.int_: ... -@overload # bool_ | +builtins.int, axis: +@overload # bool | integer | +builtins.int, axis: def sum( - a: _DualArrayLike[np.dtype[np.bool], int], + a: _DualArrayLike[np.dtype[np.bool | np.integer], int], axis: int | tuple[int, ...], dtype: None = None, out: None = None, @@ -2367,9 +2469,9 @@ def sum( initial: _IntLike_co | _NoValueType = ..., where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[np.int_]: ... -@overload # bool_, keepdims=True +@overload # bool | integer, keepdims=True def sum[ShapeT: _Shape]( - a: np.ndarray[ShapeT, np.dtype[np.bool]], + a: np.ndarray[ShapeT, np.dtype[np.bool | np.integer]], axis: int | tuple[int, ...] | None = None, dtype: None = None, out: None = None, diff --git a/numpy/ma/core.pyi b/numpy/ma/core.pyi index c7b189f2e230..0e1260e30d65 100644 --- a/numpy/ma/core.pyi +++ b/numpy/ma/core.pyi @@ -2073,7 +2073,7 @@ class MaskedArray(ndarray[_ShapeT_co, _DTypeT_co]): # Keep in sync with `ndarray.cumprod` @override # type: ignore[override] @overload - def cumprod[DTypeT: dtype[number | object_]]( + def cumprod[DTypeT: dtype[inexact | object_]]( self: MaskedArray[Any, DTypeT], axis: None = None, dtype: None = None, @@ -2081,7 +2081,7 @@ class MaskedArray(ndarray[_ShapeT_co, _DTypeT_co]): ) -> MaskedArray[tuple[int], DTypeT]: ... @overload # bool_ def cumprod( - self: _MaskedArray[np.bool], + self: _MaskedArray[np.integer | np.bool], axis: None = None, dtype: None = None, out: None = None, @@ -2117,15 +2117,15 @@ class MaskedArray(ndarray[_ShapeT_co, _DTypeT_co]): out: None = None, ) -> _Masked1D[Any]: ... @overload # axis: - def cumprod[ArrayT: _MaskedArray[number | object_]]( + def cumprod[ArrayT: _MaskedArray[np.inexact | object_]]( self: ArrayT, axis: SupportsIndex, dtype: None = None, out: None = None, ) -> ArrayT: ... - @overload # bool_, axis: + @overload # +integer, axis: def cumprod[ShapeT: _Shape]( - self: MaskedArray[ShapeT, np.dtype[np.bool]], + self: MaskedArray[ShapeT, np.dtype[np.integer | np.bool]], axis: SupportsIndex, dtype: None = None, out: None = None, @@ -2163,7 +2163,7 @@ class MaskedArray(ndarray[_ShapeT_co, _DTypeT_co]): # Keep in sync with `ndarray.cumsum` @override # type: ignore[override] @overload - def cumsum[DTypeT: dtype[number | timedelta64 | object_]]( + def cumsum[DTypeT: dtype[inexact | timedelta64 | object_]]( self: MaskedArray[Any, DTypeT], axis: None = None, dtype: None = None, @@ -2171,7 +2171,7 @@ class MaskedArray(ndarray[_ShapeT_co, _DTypeT_co]): ) -> MaskedArray[tuple[int], DTypeT]: ... @overload # bool_ def cumsum( - self: _MaskedArray[np.bool], + self: _MaskedArray[np.integer | np.bool], axis: None = None, dtype: None = None, out: None = None, @@ -2213,9 +2213,9 @@ class MaskedArray(ndarray[_ShapeT_co, _DTypeT_co]): dtype: None = None, out: None = None, ) -> ArrayT: ... - @overload # bool_, axis: + @overload # +integer, axis: def cumsum[ShapeT: _Shape]( - self: MaskedArray[ShapeT, np.dtype[np.bool]], + self: MaskedArray[ShapeT, np.dtype[np.integer | np.bool]], axis: SupportsIndex, dtype: None = None, out: None = None, diff --git a/numpy/typing/tests/data/reveal/fromnumeric.pyi b/numpy/typing/tests/data/reveal/fromnumeric.pyi index af473fd41305..8179bfdb4f45 100644 --- a/numpy/typing/tests/data/reveal/fromnumeric.pyi +++ b/numpy/typing/tests/data/reveal/fromnumeric.pyi @@ -14,6 +14,7 @@ AR_f4_1d: np.ndarray[tuple[int], np.dtype[np.float32]] AR_f4_2d: np.ndarray[tuple[int, int], np.dtype[np.float32]] AR_f4_3d: np.ndarray[tuple[int, int, int], np.dtype[np.float32]] AR_c16: npt.NDArray[np.complex128] +AR_i1: npt.NDArray[np.int8] AR_u8: npt.NDArray[np.uint64] AR_i8: npt.NDArray[np.int64] AR_O: npt.NDArray[np.object_] @@ -242,22 +243,26 @@ assert_type(np.any(AR_f4_2d, keepdims=True), np.ndarray[tuple[int, int], np.dtyp assert_type(np.any(AR_f4_3d, keepdims=True), np.ndarray[tuple[int, int, int], np.dtype[np.bool]]) assert_type(np.any(AR_f4, out=AR_subclass), NDArraySubclass) -assert_type(np.cumsum(b), np.ndarray[tuple[int], np.dtype[np.bool]]) +assert_type(np.cumsum(b), np.ndarray[tuple[int], np.dtype[np.int_]]) assert_type(np.cumsum(f4), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.cumsum(f), np.ndarray[tuple[int]]) -assert_type(np.cumsum(AR_b), np.ndarray[tuple[int], np.dtype[np.bool]]) -assert_type(np.cumsum(AR_b, axis=0), npt.NDArray[np.bool]) +assert_type(np.cumsum(AR_b), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(np.cumsum(AR_b, axis=0), npt.NDArray[np.int_]) +assert_type(np.cumsum(AR_i1), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(np.cumsum(AR_i1, axis=0), npt.NDArray[np.int_]) assert_type(np.cumsum(AR_f4), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.cumsum(AR_f4, axis=0), npt.NDArray[np.float32]) assert_type(np.cumsum(f, dtype=float), np.ndarray[tuple[int]]) assert_type(np.cumsum(f, dtype=np.float64), np.ndarray[tuple[int], np.dtype[np.float64]]) assert_type(np.cumsum(AR_f4, out=AR_subclass), NDArraySubclass) -assert_type(np.cumulative_sum(b), np.ndarray[tuple[int], np.dtype[np.bool]]) +assert_type(np.cumulative_sum(b), np.ndarray[tuple[int], np.dtype[np.int_]]) assert_type(np.cumulative_sum(f4), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.cumulative_sum(f), np.ndarray[tuple[int]]) -assert_type(np.cumulative_sum(AR_b), np.ndarray[tuple[int], np.dtype[np.bool]]) -assert_type(np.cumulative_sum(AR_b, axis=0), npt.NDArray[np.bool]) +assert_type(np.cumulative_sum(AR_b), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(np.cumulative_sum(AR_b, axis=0), npt.NDArray[np.int_]) +assert_type(np.cumulative_sum(AR_i1), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(np.cumulative_sum(AR_i1, axis=0), npt.NDArray[np.int_]) assert_type(np.cumulative_sum(AR_f4), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.cumulative_sum(AR_f4, axis=0), npt.NDArray[np.float32]) assert_type(np.cumulative_sum(f, dtype=float), np.ndarray[tuple[int]]) @@ -342,22 +347,26 @@ assert_type(np.amin(_dtype_list), Any) assert_type(np.amin(_dtype_list, axis=1), npt.NDArray[Any]) assert_type(np.amin(_dtype_list, keepdims=True), npt.NDArray[Any]) -assert_type(np.cumprod(b), np.ndarray[tuple[int], np.dtype[np.bool]]) +assert_type(np.cumprod(b), np.ndarray[tuple[int], np.dtype[np.int_]]) assert_type(np.cumprod(f4), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.cumprod(f), np.ndarray[tuple[int]]) -assert_type(np.cumprod(AR_b), np.ndarray[tuple[int], np.dtype[np.bool]]) -assert_type(np.cumprod(AR_b, axis=0), npt.NDArray[np.bool]) +assert_type(np.cumprod(AR_b), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(np.cumprod(AR_b, axis=0), npt.NDArray[np.int_]) +assert_type(np.cumprod(AR_i1), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(np.cumprod(AR_i1, axis=0), npt.NDArray[np.int_]) assert_type(np.cumprod(AR_f4), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.cumprod(AR_f4, axis=0), npt.NDArray[np.float32]) assert_type(np.cumprod(f, dtype=float), np.ndarray[tuple[int]]) assert_type(np.cumprod(f, dtype=np.float64), np.ndarray[tuple[int], np.dtype[np.float64]]) assert_type(np.cumprod(AR_f4, out=AR_subclass), NDArraySubclass) -assert_type(np.cumulative_prod(b), np.ndarray[tuple[int], np.dtype[np.bool]]) +assert_type(np.cumulative_prod(b), np.ndarray[tuple[int], np.dtype[np.int_]]) assert_type(np.cumulative_prod(f4), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.cumulative_prod(f), np.ndarray[tuple[int]]) -assert_type(np.cumulative_prod(AR_b), np.ndarray[tuple[int], np.dtype[np.bool]]) -assert_type(np.cumulative_prod(AR_b, axis=0), npt.NDArray[np.bool]) +assert_type(np.cumulative_prod(AR_b), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(np.cumulative_prod(AR_b, axis=0), npt.NDArray[np.int_]) +assert_type(np.cumulative_prod(AR_i1), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(np.cumulative_prod(AR_i1, axis=0), npt.NDArray[np.int_]) assert_type(np.cumulative_prod(AR_f4), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.cumulative_prod(AR_f4, axis=0), npt.NDArray[np.float32]) assert_type(np.cumulative_prod(f, dtype=float), np.ndarray[tuple[int]]) @@ -389,10 +398,14 @@ assert_type(np.around(AR_f4_1d), np.ndarray[tuple[int], np.dtype[np.float32]]) assert_type(np.prod(AR_nd), Any) assert_type(np.prod(AR_b), np.int_) -assert_type(np.prod(AR_i8), np.int64) -assert_type(np.prod(AR_i8, axis=0), npt.NDArray[np.int64]) -assert_type(np.prod(AR_i8, keepdims=True), npt.NDArray[np.int64]) -assert_type(np.prod(AR_i8, axis=0, keepdims=True), npt.NDArray[np.int64]) +assert_type(np.prod(AR_i1), np.int_) +assert_type(np.prod(AR_i1, axis=0), npt.NDArray[np.int_]) +assert_type(np.prod(AR_i1, keepdims=True), npt.NDArray[np.int_]) +assert_type(np.prod(AR_i1, axis=0, keepdims=True), npt.NDArray[np.int_]) +assert_type(np.prod(AR_i8), np.int_) +assert_type(np.prod(AR_i8, axis=0), npt.NDArray[np.int_]) +assert_type(np.prod(AR_i8, keepdims=True), npt.NDArray[np.int_]) +assert_type(np.prod(AR_i8, axis=0, keepdims=True), npt.NDArray[np.int_]) assert_type(np.prod(AR_f4), np.float32) assert_type(np.prod(AR_c16), np.complex128) assert_type(np.prod(AR_O), Any) @@ -418,10 +431,14 @@ assert_type(np.prod(AR_f4, out=AR_subclass), NDArraySubclass) # same as above (but with `timedelta64`) assert_type(np.sum(AR_nd), Any) assert_type(np.sum(AR_b), np.int_) -assert_type(np.sum(AR_i8), np.int64) -assert_type(np.sum(AR_i8, axis=0), npt.NDArray[np.int64]) -assert_type(np.sum(AR_i8, keepdims=True), npt.NDArray[np.int64]) -assert_type(np.sum(AR_i8, axis=0, keepdims=True), npt.NDArray[np.int64]) +assert_type(np.sum(AR_i1), np.int_) +assert_type(np.sum(AR_i1, axis=0), npt.NDArray[np.int_]) +assert_type(np.sum(AR_i1, keepdims=True), npt.NDArray[np.int_]) +assert_type(np.sum(AR_i1, axis=0, keepdims=True), npt.NDArray[np.int_]) +assert_type(np.sum(AR_i8), np.int_) +assert_type(np.sum(AR_i8, axis=0), npt.NDArray[np.int_]) +assert_type(np.sum(AR_i8, keepdims=True), npt.NDArray[np.int_]) +assert_type(np.sum(AR_i8, axis=0, keepdims=True), npt.NDArray[np.int_]) assert_type(np.sum(AR_f4), np.float32) assert_type(np.sum(AR_c16), np.complex128) assert_type(np.sum(AR_O), Any) diff --git a/numpy/typing/tests/data/reveal/ndarray_misc.pyi b/numpy/typing/tests/data/reveal/ndarray_misc.pyi index ba718421af88..a5abf798476a 100644 --- a/numpy/typing/tests/data/reveal/ndarray_misc.pyi +++ b/numpy/typing/tests/data/reveal/ndarray_misc.pyi @@ -119,8 +119,10 @@ assert_type(f8.conjugate(), np.float64) assert_type(AR_f8.conjugate(), npt.NDArray[np.float64]) assert_type(B.conjugate(), SubClass) -assert_type(AR_i8.cumprod(), np.ndarray[tuple[int], np.dtype[np.int64]]) -assert_type(AR_i8.cumprod(axis=0), npt.NDArray[np.int64]) +assert_type(AR_u1.cumprod(), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(AR_u1.cumprod(axis=0), npt.NDArray[np.int_]) +assert_type(AR_i8.cumprod(), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(AR_i8.cumprod(axis=0), npt.NDArray[np.int_]) assert_type(AR_f8.cumprod(), np.ndarray[tuple[int], np.dtype[np.float64]]) assert_type(AR_f8.cumprod(axis=0), npt.NDArray[np.float64]) assert_type(AR_f8.cumprod(dtype=np.float32), np.ndarray[tuple[int], np.dtype[np.float32]]) @@ -133,8 +135,10 @@ assert_type(AR_f8_2d.cumprod(dtype=np.float32, axis=0), np.ndarray[tuple[int, in assert_type(AR_any.cumprod(), np.ndarray[tuple[int]]) assert_type(AR_any.cumprod(axis=0), np.ndarray) -assert_type(AR_i8.cumsum(), np.ndarray[tuple[int], np.dtype[np.int64]]) -assert_type(AR_i8.cumsum(axis=0), npt.NDArray[np.int64]) +assert_type(AR_u1.cumsum(), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(AR_u1.cumsum(axis=0), npt.NDArray[np.int_]) +assert_type(AR_i8.cumsum(), np.ndarray[tuple[int], np.dtype[np.int_]]) +assert_type(AR_i8.cumsum(axis=0), npt.NDArray[np.int_]) assert_type(AR_f8.cumsum(), np.ndarray[tuple[int], np.dtype[np.float64]]) assert_type(AR_f8.cumsum(axis=0), npt.NDArray[np.float64]) assert_type(AR_f8.cumsum(dtype=np.float32), np.ndarray[tuple[int], np.dtype[np.float32]]) @@ -151,10 +155,14 @@ assert_type(AR_any.cumsum(axis=0), np.ndarray) assert_type(b1.prod(), np.int_) assert_type(i8.prod(), np.int64) assert_type(f8.prod(), np.float64) -assert_type(AR_i8.prod(), np.int64) -assert_type(AR_i8.prod(keepdims=True), npt.NDArray[np.int64]) -assert_type(AR_i8.prod(axis=0), npt.NDArray[np.int64]) -assert_type(AR_i8.prod(axis=0, keepdims=True), npt.NDArray[np.int64]) +assert_type(AR_u1.prod(), np.int_) +assert_type(AR_u1.prod(keepdims=True), npt.NDArray[np.int_]) +assert_type(AR_u1.prod(axis=0), npt.NDArray[np.int_]) +assert_type(AR_u1.prod(axis=0, keepdims=True), npt.NDArray[np.int_]) +assert_type(AR_i8.prod(), np.int_) +assert_type(AR_i8.prod(keepdims=True), npt.NDArray[np.int_]) +assert_type(AR_i8.prod(axis=0), npt.NDArray[np.int_]) +assert_type(AR_i8.prod(axis=0, keepdims=True), npt.NDArray[np.int_]) assert_type(AR_f8.prod(), np.float64) assert_type(AR_f8.prod(keepdims=True), npt.NDArray[np.float64]) assert_type(AR_f8.prod(axis=0), npt.NDArray[np.float64]) @@ -181,10 +189,14 @@ assert_type(f8.sum(), np.float64) assert_type(m8_ns.sum(), np.timedelta64[int]) assert_type(m8_ms.sum(), np.timedelta64[dt.timedelta]) assert_type(m8_na.sum(), np.timedelta64[None]) -assert_type(AR_i8.sum(), np.int64) -assert_type(AR_i8.sum(keepdims=True), npt.NDArray[np.int64]) -assert_type(AR_i8.sum(axis=0), npt.NDArray[np.int64]) -assert_type(AR_i8.sum(axis=0, keepdims=True), npt.NDArray[np.int64]) +assert_type(AR_u1.sum(), np.int_) +assert_type(AR_u1.sum(keepdims=True), npt.NDArray[np.int_]) +assert_type(AR_u1.sum(axis=0), npt.NDArray[np.int_]) +assert_type(AR_u1.sum(axis=0, keepdims=True), npt.NDArray[np.int_]) +assert_type(AR_i8.sum(), np.int_) +assert_type(AR_i8.sum(keepdims=True), npt.NDArray[np.int_]) +assert_type(AR_i8.sum(axis=0), npt.NDArray[np.int_]) +assert_type(AR_i8.sum(axis=0, keepdims=True), npt.NDArray[np.int_]) assert_type(AR_f8.sum(), np.float64) assert_type(AR_f8.sum(keepdims=True), npt.NDArray[np.float64]) assert_type(AR_f8.sum(axis=0), npt.NDArray[np.float64]) From aea55d1b8f12e8981c4a324465d990af50e4de6e Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 19:06:31 +0200 Subject: [PATCH 50/60] TYP: fix ``random.Generator.choice`` output type for int sequences (#31877) --- numpy/random/_generator.pyi | 15 +++++++++++++-- numpy/typing/tests/data/reveal/random.pyi | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/numpy/random/_generator.pyi b/numpy/random/_generator.pyi index 634aaf68912c..e45990c1db65 100644 --- a/numpy/random/_generator.pyi +++ b/numpy/random/_generator.pyi @@ -688,17 +688,28 @@ class Generator: def permuted[ArrayT: np.ndarray](self, /, x: ArrayLike, *, axis: int | None = None, out: ArrayT) -> ArrayT: ... # - @overload # >=0d int, size=None (default) + @overload # 0d int, size=None (default) def choice( self, /, - a: int | _NestedSequence[int], + a: int, size: None = None, replace: bool = True, p: _ArrayLikeFloat_co | None = None, axis: int = 0, shuffle: bool = True, ) -> int: ... + @overload # >0d int, size=None (default) + def choice( + self, + /, + a: _NestedSequence[int], + size: None = None, + replace: bool = True, + p: _ArrayLikeFloat_co | None = None, + axis: int = 0, + shuffle: bool = True, + ) -> np.int_: ... @overload # >=0d known, size=None (default) def choice[ScalarT: np.generic]( self, diff --git a/numpy/typing/tests/data/reveal/random.pyi b/numpy/typing/tests/data/reveal/random.pyi index 72f8c62f79e0..593f13dcd0a2 100644 --- a/numpy/typing/tests/data/reveal/random.pyi +++ b/numpy/typing/tests/data/reveal/random.pyi @@ -700,6 +700,7 @@ assert_type(def_gen.bit_generator, np.random.BitGenerator) assert_type(def_gen.bytes(2), bytes) assert_type(def_gen.choice(5), int) +assert_type(def_gen.choice([1, 2]), np.int_) assert_type(def_gen.choice(5, 3), npt.NDArray[np.int64]) assert_type(def_gen.choice(5, 3, replace=True), npt.NDArray[np.int64]) assert_type(def_gen.choice(5, 3, p=[1 / 5] * 5), npt.NDArray[np.int64]) From e8ef59b36e14aafe22ace609ba9d805a7277c813 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 19:07:45 +0200 Subject: [PATCH 51/60] TYP: fix Pyright error for ``piecewise`` with all-scalar ``funclist`` (#31878) --- numpy/lib/_function_base_impl.pyi | 6 ++++++ numpy/typing/tests/data/fail/lib_function_base.pyi | 4 ++-- numpy/typing/tests/data/reveal/lib_function_base.pyi | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/numpy/lib/_function_base_impl.pyi b/numpy/lib/_function_base_impl.pyi index 5887d7d496ce..96f3cb70b5b1 100644 --- a/numpy/lib/_function_base_impl.pyi +++ b/numpy/lib/_function_base_impl.pyi @@ -470,6 +470,12 @@ def asarray_chkfinite(a: object, dtype: DTypeLike | None = None, order: _OrderKA # `[condlist]`. And even though the documentation says these should be boolean, in # practice anything that `np.array(condlist, dtype=bool)` accepts will work, i.e. any # array-like. +@overload # this overload is required to avoid pyright rejecting all-scalar `funclist` +def piecewise[ShapeT: _Shape, ScalarT: np.generic]( + x: _Array[ShapeT, ScalarT], + condlist: ArrayLike, + funclist: _SizedIterable[_ScalarLike_co], +) -> _Array[ShapeT, ScalarT]: ... @overload def piecewise[ShapeT: _Shape, ScalarT: np.generic, **Tss]( x: _Array[ShapeT, ScalarT], diff --git a/numpy/typing/tests/data/fail/lib_function_base.pyi b/numpy/typing/tests/data/fail/lib_function_base.pyi index d7be993bf04c..6dd26af01307 100644 --- a/numpy/typing/tests/data/fail/lib_function_base.pyi +++ b/numpy/typing/tests/data/fail/lib_function_base.pyi @@ -24,8 +24,8 @@ np.vectorize(1) # type: ignore[arg-type] np.place(AR_f8, slice(None), 5) # type: ignore[arg-type] np.piecewise(AR_f8, True, [fn_ar_i], "wrong") # type: ignore[call-overload] -np.piecewise(AR_f8, AR_b_list, [fn_none_i]) # type: ignore[call-overload] -np.piecewise(AR_f8, AR_b_list, [fn_ar_i]) # type: ignore[call-overload] +np.piecewise(AR_f8, AR_b_list, [fn_none_i]) # type: ignore[list-item] +np.piecewise(AR_f8, AR_b_list, [fn_ar_i]) # type: ignore[list-item] np.piecewise(AR_f8, AR_b_list, [fn_ar_i], 3.14) # type: ignore[call-overload] np.piecewise(AR_f8, AR_b_list, [fn_ar_i], 42, None) # type: ignore[call-overload] np.piecewise(AR_f8, AR_b_list, [fn_ar_i], 42, _=None) # type: ignore[list-item] diff --git a/numpy/typing/tests/data/reveal/lib_function_base.pyi b/numpy/typing/tests/data/reveal/lib_function_base.pyi index bab43106d1c5..e288237e3bf9 100644 --- a/numpy/typing/tests/data/reveal/lib_function_base.pyi +++ b/numpy/typing/tests/data/reveal/lib_function_base.pyi @@ -103,6 +103,7 @@ assert_type(np.asarray_chkfinite(AR_f8, dtype=np.float64), npt.NDArray[np.float6 assert_type(np.asarray_chkfinite(AR_f8, dtype=float), np.ndarray) # piecewise +assert_type(np.piecewise(AR_f8_1d, AR_b, [-1.0, 1.0]), np.ndarray[tuple[int], np.dtype[np.float64]]) assert_type(np.piecewise(AR_f8_1d, AR_b, [func]), np.ndarray[tuple[int], np.dtype[np.float64]]) assert_type(np.piecewise(AR_f8, AR_b, [func]), npt.NDArray[np.float64]) assert_type(np.piecewise(AR_f8, AR_b, [func_f8]), npt.NDArray[np.float64]) From 271915f5a538a23ea54166553982f483589bb257 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 19:08:44 +0200 Subject: [PATCH 52/60] TYP: fix ``str_.__getitem__`` and ``bytes_.__getitem__`` (#31879) --- numpy/__init__.pyi | 11 +++++++++ numpy/typing/tests/data/reveal/scalars.pyi | 26 ++++++++-------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index a29e168e1f93..0f3ae2f70cf8 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -7511,6 +7511,13 @@ class bytes_(character[bytes], bytes): # type: ignore[misc] @override def __hash__(self, /) -> int: ... + # re-declare inherited `bytes.__getitem__` shadowed by `generic.__getitem__` + @override # type: ignore[override] + @overload + def __getitem__(self, key: SupportsIndex, /) -> int: ... + @overload + def __getitem__(self, key: slice[SupportsIndex | None], /) -> bytes: ... # pyright: ignore[reportIncompatibleMethodOverride] + # def __bytes__(self, /) -> bytes: ... @@ -7524,6 +7531,10 @@ class str_(character[str], str): # type: ignore[misc] @override def __hash__(self, /) -> int: ... + # re-declare inherited `str.__getitem__` shadowed by `generic.__getitem__` + @override + def __getitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> str: ... # type: ignore[override] + # See `numpy._typing._ufunc` for more concrete nin-/nout-specific stubs @final class ufunc: diff --git a/numpy/typing/tests/data/reveal/scalars.pyi b/numpy/typing/tests/data/reveal/scalars.pyi index c56c8e88092c..3ab5bd8fde9b 100644 --- a/numpy/typing/tests/data/reveal/scalars.pyi +++ b/numpy/typing/tests/data/reveal/scalars.pyi @@ -49,8 +49,6 @@ assert_type(u8[()], np.uint64) assert_type(f8[()], np.float64) assert_type(c8[()], np.complex64) assert_type(c16[()], np.complex128) -assert_type(U[()], np.str_) -assert_type(S[()], np.bytes_) assert_type(V[()], np.void) assert_type(b[...], np.ndarray[tuple[()], np.dtype[np.bool]]) @@ -65,10 +63,6 @@ assert_type(c8[...], np.ndarray[tuple[()], np.dtype[np.complex64]]) assert_type(c8[(...,)], np.ndarray[tuple[()], np.dtype[np.complex64]]) assert_type(c16[...], np.ndarray[tuple[()], np.dtype[np.complex128]]) assert_type(c16[(...,)], np.ndarray[tuple[()], np.dtype[np.complex128]]) -assert_type(U[...], np.ndarray[tuple[()], np.dtype[np.str_]]) -assert_type(U[(...,)], np.ndarray[tuple[()], np.dtype[np.str_]]) -assert_type(S[...], np.ndarray[tuple[()], np.dtype[np.bytes_]]) -assert_type(S[(...,)], np.ndarray[tuple[()], np.dtype[np.bytes_]]) assert_type(V[...], np.ndarray[tuple[()], np.dtype[np.void]]) assert_type(V[(...,)], np.ndarray[tuple[()], np.dtype[np.void]]) @@ -113,17 +107,15 @@ assert_type(c16[None2], np.ndarray[tuple[int, int], np.dtype[np.complex128]]) assert_type(c16[None3], np.ndarray[tuple[int, int, int], np.dtype[np.complex128]]) assert_type(c16[None4], np.ndarray[tuple[Any, ...], np.dtype[np.complex128]]) -assert_type(U[None], np.ndarray[tuple[int], np.dtype[np.str_]]) -assert_type(U[None1], np.ndarray[tuple[int], np.dtype[np.str_]]) -assert_type(U[None2], np.ndarray[tuple[int, int], np.dtype[np.str_]]) -assert_type(U[None3], np.ndarray[tuple[int, int, int], np.dtype[np.str_]]) -assert_type(U[None4], np.ndarray[tuple[Any, ...], np.dtype[np.str_]]) - -assert_type(S[None], np.ndarray[tuple[int], np.dtype[np.bytes_]]) -assert_type(S[None1], np.ndarray[tuple[int], np.dtype[np.bytes_]]) -assert_type(S[None2], np.ndarray[tuple[int, int], np.dtype[np.bytes_]]) -assert_type(S[None3], np.ndarray[tuple[int, int, int], np.dtype[np.bytes_]]) -assert_type(S[None4], np.ndarray[tuple[Any, ...], np.dtype[np.bytes_]]) +assert_type(U[0], str) +assert_type(U[:1], str) +assert_type(U[1:], str) +assert_type(U[:], str) + +assert_type(S[0], int) +assert_type(S[:1], bytes) +assert_type(S[1:], bytes) +assert_type(S[:], bytes) assert_type(V[None], np.ndarray[tuple[int], np.dtype[np.void]]) assert_type(V[None1], np.ndarray[tuple[int], np.dtype[np.void]]) From 3fd7439f9f489e8429b998b4ee587b240055dbfa Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 23:12:18 +0200 Subject: [PATCH 53/60] TYP: fix ``std``/``var`` output type for complex input (#31880) --- numpy/__init__.pyi | 48 ++++++------- numpy/_core/fromnumeric.pyi | 72 ++++++------------- .../typing/tests/data/reveal/fromnumeric.pyi | 15 +++- .../typing/tests/data/reveal/ndarray_misc.pyi | 11 ++- 4 files changed, 70 insertions(+), 76 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index 0f3ae2f70cf8..8f258f8be084 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -3201,9 +3201,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): # keep in sync with `ndarray.mean` above @override # type: ignore[override] - @overload # +integer | ~object_ + @overload # ~complex128 | +integer | ~object_ def std( - self: NDArray[integer | bool_ | object_], + self: NDArray[complex128 | integer | bool_ | object_], axis: None = None, dtype: None = None, out: None = None, @@ -3214,9 +3214,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> float64: ... - @overload # +integer, axis: + @overload # ~complex128 | +integer, axis: def std( - self: NDArray[integer | bool_], + self: NDArray[complex128 | integer | bool_], axis: int | tuple[int, ...], dtype: None = None, out: None = None, @@ -3227,9 +3227,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> NDArray[float64]: ... - @overload # +integer, keepdims=True + @overload # ~complex128 | +integer, keepdims=True def std( - self: NDArray[integer | bool_], + self: NDArray[complex128 | integer | bool_], axis: int | tuple[int, ...] | None = None, dtype: None = None, out: None = None, @@ -3240,8 +3240,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> ndarray[_ShapeT_co, dtype[float64]]: ... - @overload # ~inexact | timedelta64 - def std[ScalarT: inexact | timedelta64]( + @overload # ~floating | timedelta64 + def std[ScalarT: floating | timedelta64]( self: NDArray[ScalarT], axis: None = None, dtype: None = None, @@ -3253,8 +3253,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> ScalarT: ... - @overload # ~inexact | timedelta64, axis: - def std[ScalarT: inexact | timedelta64 | object_]( + @overload # ~floating | timedelta64, axis: + def std[ScalarT: floating | timedelta64 | object_]( self: NDArray[ScalarT], axis: int | tuple[int, ...], dtype: None = None, @@ -3266,8 +3266,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> NDArray[ScalarT]: ... - @overload # ~inexact | timedelta64 | object_, keepdims=True - def std[ArrayT: NDArray[inexact | timedelta64 | object_]]( + @overload # ~floating | timedelta64 | object_, keepdims=True + def std[ArrayT: NDArray[floating | timedelta64 | object_]]( self: ArrayT, axis: int | tuple[int, ...] | None = None, dtype: None = None, @@ -3386,9 +3386,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): # keep in sync with `ndarray.std` above @override # type: ignore[override] - @overload # +integer | ~object_ + @overload # ~complex128 | +integer | ~object_ def var( - self: NDArray[integer | bool_ | object_], + self: NDArray[complex128 | integer | bool_ | object_], axis: None = None, dtype: None = None, out: None = None, @@ -3399,9 +3399,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> float64: ... - @overload # +integer, axis: + @overload # ~complex128 | +integer, axis: def var( - self: NDArray[integer | bool_], + self: NDArray[complex128 | integer | bool_], axis: int | tuple[int, ...], dtype: None = None, out: None = None, @@ -3412,9 +3412,9 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> NDArray[float64]: ... - @overload # +integer, keepdims=True + @overload # ~complex128 | +integer, keepdims=True def var( - self: NDArray[integer | bool_], + self: NDArray[complex128 | integer | bool_], axis: int | tuple[int, ...] | None = None, dtype: None = None, out: None = None, @@ -3425,8 +3425,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> ndarray[_ShapeT_co, dtype[float64]]: ... - @overload # ~inexact | timedelta64 - def var[ScalarT: inexact | timedelta64]( + @overload # ~floating | timedelta64 + def var[ScalarT: floating | timedelta64]( self: NDArray[ScalarT], axis: None = None, dtype: None = None, @@ -3438,8 +3438,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> ScalarT: ... - @overload # ~inexact | timedelta64, axis: - def var[ScalarT: inexact | timedelta64 | object_]( + @overload # ~floating | timedelta64, axis: + def var[ScalarT: floating | timedelta64 | object_]( self: NDArray[ScalarT], axis: int | tuple[int, ...], dtype: None = None, @@ -3451,8 +3451,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]): mean: _ArrayLikeNumber_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> NDArray[ScalarT]: ... - @overload # ~inexact | timedelta64 | object_, keepdims=True - def var[ArrayT: NDArray[inexact | timedelta64 | object_]]( + @overload # ~floating | timedelta64 | object_, keepdims=True + def var[ArrayT: NDArray[floating | timedelta64 | object_]]( self: ArrayT, axis: int | tuple[int, ...] | None = None, dtype: None = None, diff --git a/numpy/_core/fromnumeric.pyi b/numpy/_core/fromnumeric.pyi index efe37c6643df..f9705ca0f9f5 100644 --- a/numpy/_core/fromnumeric.pyi +++ b/numpy/_core/fromnumeric.pyi @@ -2759,10 +2759,10 @@ def mean( where: _ArrayLikeBool_co | _NoValueType = ..., ) -> NDArray[Any]: ... -# keep in sync with `mean` above -@overload # +integer | ~object_ | +builtins.float +# +@overload # ~complex128 | +integer | ~object_ | +builtins.float def std( - a: _DualArrayLike[np.dtype[np.integer | np.bool | np.object_], float], + a: _DualArrayLike[np.dtype[np.complex128 | np.integer | np.bool | np.object_], complex], axis: None = None, dtype: None = None, out: None = None, @@ -2773,9 +2773,9 @@ def std( mean: _ArrayLikeFloat_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> np.float64: ... -@overload # +integer | +builtins.float, axis: +@overload # ~complex128 | +integer | +builtins.float, axis: def std( - a: _DualArrayLike[np.dtype[np.integer | np.bool], float], + a: _DualArrayLike[np.dtype[np.complex128 | np.integer | np.bool], complex], axis: int | tuple[int, ...], dtype: None = None, out: None = None, @@ -2788,7 +2788,7 @@ def std( ) -> NDArray[np.float64]: ... @overload # +integer, keepdims=True def std[ShapeT: _Shape]( - a: np.ndarray[ShapeT, np.dtype[np.integer | np.bool]], + a: np.ndarray[ShapeT, np.dtype[np.complex128 | np.integer | np.bool]], axis: int | tuple[int, ...] | None = None, dtype: None = None, out: None = None, @@ -2799,21 +2799,8 @@ def std[ShapeT: _Shape]( mean: _ArrayLikeComplex_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> np.ndarray[ShapeT, np.dtype[np.float64]]: ... -@overload # ~complex (`list` ensures invariance to avoid overlap with the previous overload) -def std( - a: _NestedSequence[list[complex]] | list[complex], - axis: None = None, - dtype: None = None, - out: None = None, - ddof: float = 0, - keepdims: Literal[False] | _NoValueType = ..., - *, - where: _ArrayLikeBool_co | _NoValueType = ..., - mean: _ArrayLikeComplex_co | _NoValueType = ..., - correction: float | _NoValueType = ..., -) -> np.complex128: ... -@overload # ~inexact | timedelta64 -def std[ScalarT: np.inexact | np.timedelta64]( +@overload # ~floating | timedelta64 +def std[ScalarT: np.floating | np.timedelta64]( a: _ArrayLike[ScalarT], axis: None = None, dtype: None = None, @@ -2825,8 +2812,8 @@ def std[ScalarT: np.inexact | np.timedelta64]( mean: _ArrayLikeComplex_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> ScalarT: ... -@overload # ~inexact | timedelta64 | object_, axis: -def std[ScalarT: np.inexact | np.timedelta64 | np.object_]( +@overload # ~floating | timedelta64 | object_, axis: +def std[ScalarT: np.floating | np.timedelta64 | np.object_]( a: _ArrayLike[ScalarT], axis: int | tuple[int, ...], dtype: None = None, @@ -2838,8 +2825,8 @@ def std[ScalarT: np.inexact | np.timedelta64 | np.object_]( mean: _ArrayLikeComplex_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> NDArray[ScalarT]: ... -@overload # ~inexact | timedelta64 | object_, keepdims=True -def std[ArrayT: NDArray[np.inexact | np.timedelta64 | np.object_]]( +@overload # ~floating | timedelta64 | object_, keepdims=True +def std[ArrayT: NDArray[np.floating | np.timedelta64 | np.object_]]( a: ArrayT, axis: int | tuple[int, ...] | None = None, dtype: None = None, @@ -2970,9 +2957,9 @@ def std( ) -> NDArray[Any]: ... # keep in sync with `std` above -@overload # +integer | ~object_ | +builtins.float +@overload # ~complex128 | +integer | ~object_ | +builtins.float def var( - a: _DualArrayLike[np.dtype[np.integer | np.bool | np.object_], float], + a: _DualArrayLike[np.dtype[np.complex128 | np.integer | np.bool | np.object_], complex], axis: None = None, dtype: None = None, out: None = None, @@ -2983,9 +2970,9 @@ def var( mean: _ArrayLikeFloat_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> np.float64: ... -@overload # +integer | +builtins.float, axis: +@overload # ~complex128 | +integer | +builtins.float, axis: def var( - a: _DualArrayLike[np.dtype[np.integer | np.bool], float], + a: _DualArrayLike[np.dtype[np.complex128 | np.integer | np.bool], complex], axis: int | tuple[int, ...], dtype: None = None, out: None = None, @@ -2998,7 +2985,7 @@ def var( ) -> NDArray[np.float64]: ... @overload # +integer, keepdims=True def var[ShapeT: _Shape]( - a: np.ndarray[ShapeT, np.dtype[np.integer | np.bool]], + a: np.ndarray[ShapeT, np.dtype[np.complex128 | np.integer | np.bool]], axis: int | tuple[int, ...] | None = None, dtype: None = None, out: None = None, @@ -3009,21 +2996,8 @@ def var[ShapeT: _Shape]( mean: _ArrayLikeComplex_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> np.ndarray[ShapeT, np.dtype[np.float64]]: ... -@overload # ~complex (`list` ensures invariance to avoid overlap with the previous overload) -def var( - a: _NestedSequence[list[complex]] | list[complex], - axis: None = None, - dtype: None = None, - out: None = None, - ddof: float = 0, - keepdims: Literal[False] | _NoValueType = ..., - *, - where: _ArrayLikeBool_co | _NoValueType = ..., - mean: _ArrayLikeComplex_co | _NoValueType = ..., - correction: float | _NoValueType = ..., -) -> np.complex128: ... -@overload # ~inexact | timedelta64 -def var[ScalarT: np.inexact | np.timedelta64]( +@overload # ~floating | timedelta64 +def var[ScalarT: np.floating | np.timedelta64]( a: _ArrayLike[ScalarT], axis: None = None, dtype: None = None, @@ -3035,8 +3009,8 @@ def var[ScalarT: np.inexact | np.timedelta64]( mean: _ArrayLikeComplex_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> ScalarT: ... -@overload # ~inexact | timedelta64 | object_, axis: -def var[ScalarT: np.inexact | np.timedelta64 | np.object_]( +@overload # ~floating | timedelta64 | object_, axis: +def var[ScalarT: np.floating | np.timedelta64 | np.object_]( a: _ArrayLike[ScalarT], axis: int | tuple[int, ...], dtype: None = None, @@ -3048,8 +3022,8 @@ def var[ScalarT: np.inexact | np.timedelta64 | np.object_]( mean: _ArrayLikeComplex_co | _NoValueType = ..., correction: float | _NoValueType = ..., ) -> NDArray[ScalarT]: ... -@overload # ~inexact | timedelta64 | object_, keepdims=True -def var[ArrayT: NDArray[np.inexact | np.timedelta64 | np.object_]]( +@overload # ~floating | timedelta64 | object_, keepdims=True +def var[ArrayT: NDArray[np.floating | np.timedelta64 | np.object_]]( a: ArrayT, axis: int | tuple[int, ...] | None = None, dtype: None = None, diff --git a/numpy/typing/tests/data/reveal/fromnumeric.pyi b/numpy/typing/tests/data/reveal/fromnumeric.pyi index 8179bfdb4f45..9b84a8987054 100644 --- a/numpy/typing/tests/data/reveal/fromnumeric.pyi +++ b/numpy/typing/tests/data/reveal/fromnumeric.pyi @@ -13,6 +13,7 @@ AR_f4: npt.NDArray[np.float32] AR_f4_1d: np.ndarray[tuple[int], np.dtype[np.float32]] AR_f4_2d: np.ndarray[tuple[int, int], np.dtype[np.float32]] AR_f4_3d: np.ndarray[tuple[int, int, int], np.dtype[np.float32]] +AR_c8: npt.NDArray[np.complex64] AR_c16: npt.NDArray[np.complex128] AR_i1: npt.NDArray[np.int8] AR_u8: npt.NDArray[np.uint64] @@ -502,7 +503,12 @@ assert_type(np.std(AR_i8, axis=0), npt.NDArray[np.float64]) assert_type(np.std(AR_i8, keepdims=True), npt.NDArray[np.float64]) assert_type(np.std(AR_i8, axis=0, keepdims=True), npt.NDArray[np.float64]) assert_type(np.std(AR_f4), np.float32) -assert_type(np.std(AR_c16), np.complex128) +assert_type(np.std(AR_c8), Any) +assert_type(np.std(AR_c8, axis=0), npt.NDArray[Any]) +assert_type(np.std(AR_c8, keepdims=True), npt.NDArray[Any]) +assert_type(np.std(AR_c16), np.float64) +assert_type(np.std(AR_c16, axis=0), npt.NDArray[np.float64]) +assert_type(np.std(AR_c16, keepdims=True), npt.NDArray[np.float64]) assert_type(np.std(AR_O), np.float64) assert_type(np.std(AR_O, axis=0), npt.NDArray[np.object_]) assert_type(np.std(AR_O, keepdims=True), npt.NDArray[np.object_]) @@ -533,7 +539,12 @@ assert_type(np.var(AR_i8, axis=0), npt.NDArray[np.float64]) assert_type(np.var(AR_i8, keepdims=True), npt.NDArray[np.float64]) assert_type(np.var(AR_i8, axis=0, keepdims=True), npt.NDArray[np.float64]) assert_type(np.var(AR_f4), np.float32) -assert_type(np.var(AR_c16), np.complex128) +assert_type(np.var(AR_c8), Any) +assert_type(np.var(AR_c8, axis=0), npt.NDArray[Any]) +assert_type(np.var(AR_c8, keepdims=True), npt.NDArray[Any]) +assert_type(np.var(AR_c16), np.float64) +assert_type(np.var(AR_c16, axis=0), npt.NDArray[np.float64]) +assert_type(np.var(AR_c16, keepdims=True), npt.NDArray[np.float64]) assert_type(np.var(AR_O), np.float64) assert_type(np.var(AR_O, axis=0), npt.NDArray[np.object_]) assert_type(np.var(AR_O, keepdims=True), npt.NDArray[np.object_]) diff --git a/numpy/typing/tests/data/reveal/ndarray_misc.pyi b/numpy/typing/tests/data/reveal/ndarray_misc.pyi index a5abf798476a..868fb5731710 100644 --- a/numpy/typing/tests/data/reveal/ndarray_misc.pyi +++ b/numpy/typing/tests/data/reveal/ndarray_misc.pyi @@ -32,6 +32,7 @@ AR_f8: npt.NDArray[np.float64] AR_i8: npt.NDArray[np.int64] AR_u1: npt.NDArray[np.uint8] AR_c8: npt.NDArray[np.complex64] +AR_c16: npt.NDArray[np.complex128] AR_m: npt.NDArray[np.timedelta64] AR_U: npt.NDArray[np.str_] AR_V: npt.NDArray[np.void] @@ -236,12 +237,16 @@ assert_type(AR_f8_2d.mean(dtype=np.float32, axis=0), npt.NDArray[np.float32]) assert_type(AR_f8_2d.mean(dtype=np.float32, keepdims=True), np.ndarray[tuple[int, int], np.dtype[np.float32]]) assert_type(AR_f8_2d.mean(dtype=np.float32, axis=0, keepdims=True), np.ndarray[tuple[int, int], np.dtype[np.float32]]) -# same as above +# as above, but returns floating for complexfloating input assert_type(f8.std(), Any) assert_type(AR_f8.std(), np.float64) assert_type(AR_f8.std(keepdims=True), npt.NDArray[np.float64]) assert_type(AR_f8.std(axis=0), npt.NDArray[np.float64]) assert_type(AR_f8.std(axis=0, keepdims=True), npt.NDArray[np.float64]) +assert_type(AR_c16.std(), np.float64) +assert_type(AR_c16.std(keepdims=True), npt.NDArray[np.float64]) +assert_type(AR_c16.std(axis=0), npt.NDArray[np.float64]) +assert_type(AR_c16.std(axis=0, keepdims=True), npt.NDArray[np.float64]) assert_type(AR_f8.std(dtype=np.float32), np.float32) assert_type(AR_f8.std(dtype=np.float32, axis=0), npt.NDArray[np.float32]) assert_type(AR_f8.std(dtype=np.float32, keepdims=True), npt.NDArray[np.float32]) @@ -262,6 +267,10 @@ assert_type(AR_f8.var(), np.float64) assert_type(AR_f8.var(keepdims=True), npt.NDArray[np.float64]) assert_type(AR_f8.var(axis=0), npt.NDArray[np.float64]) assert_type(AR_f8.var(axis=0, keepdims=True), npt.NDArray[np.float64]) +assert_type(AR_c16.var(), np.float64) +assert_type(AR_c16.var(keepdims=True), npt.NDArray[np.float64]) +assert_type(AR_c16.var(axis=0), npt.NDArray[np.float64]) +assert_type(AR_c16.var(axis=0, keepdims=True), npt.NDArray[np.float64]) assert_type(AR_f8.var(dtype=np.float32), np.float32) assert_type(AR_f8.var(dtype=np.float32, axis=0), npt.NDArray[np.float32]) assert_type(AR_f8.var(dtype=np.float32, keepdims=True), npt.NDArray[np.float32]) From e0134ce5187b3b85e65d20425ab85b71aa2732c4 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 23:13:40 +0200 Subject: [PATCH 54/60] TYP: fix ``random.RandomState.get_state`` conflicting overloads (#31881) --- numpy/random/mtrand.pyi | 4 ++-- numpy/typing/tests/data/reveal/random.pyi | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/numpy/random/mtrand.pyi b/numpy/random/mtrand.pyi index 066a56545f23..e2215c70fe7d 100644 --- a/numpy/random/mtrand.pyi +++ b/numpy/random/mtrand.pyi @@ -97,9 +97,9 @@ class RandomState: # @overload - def get_state(self, legacy: Literal[False] = False) -> dict[str, Any]: ... + def get_state(self, legacy: Literal[False]) -> dict[str, Any]: ... @overload - def get_state(self, legacy: Literal[True] = True) -> dict[str, Any] | tuple[str, NDArray[np.uint32], int, int, float]: ... + def get_state(self, legacy: Literal[True] = True) -> tuple[str, NDArray[np.uint32], int, int, float]: ... # def set_state(self, state: dict[str, Any] | tuple[str, NDArray[np.uint32], int, int, float]) -> None: ... diff --git a/numpy/typing/tests/data/reveal/random.pyi b/numpy/typing/tests/data/reveal/random.pyi index 593f13dcd0a2..37b920a1b5e2 100644 --- a/numpy/typing/tests/data/reveal/random.pyi +++ b/numpy/typing/tests/data/reveal/random.pyi @@ -1336,7 +1336,7 @@ assert_type(random_st.seed([0, 1]), None) random_st_get_state = random_st.get_state() assert_type(random_st_state, dict[str, Any]) random_st_get_state_legacy = random_st.get_state(legacy=True) -assert_type(random_st_get_state_legacy, dict[str, Any] | tuple[str, npt.NDArray[np.uint32], int, int, float]) +assert_type(random_st_get_state_legacy, tuple[str, npt.NDArray[np.uint32], int, int, float]) assert_type(random_st.set_state(random_st_get_state), None) assert_type(random_st.rand(), float) From c49660f81902026779945988674dda06b10063e8 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 23:14:41 +0200 Subject: [PATCH 55/60] TYP: fix swapped ``linalg.slogdet`` dtypes of ``sign``/``logabsdet`` (#31882) --- numpy/linalg/_linalg.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy/linalg/_linalg.pyi b/numpy/linalg/_linalg.pyi index 8eb3e57cf1e2..cd53d17cf77d 100644 --- a/numpy/linalg/_linalg.pyi +++ b/numpy/linalg/_linalg.pyi @@ -196,8 +196,8 @@ class SVDResult(NamedTuple, Generic[_FloatingT_co, _InexactT_co]): Vh: NDArray[_InexactT_co] class SlogdetResult(NamedTuple, Generic[_FloatingOrArrayT_co, _InexactOrArrayT_co]): - sign: _FloatingOrArrayT_co - logabsdet: _InexactOrArrayT_co + sign: _InexactOrArrayT_co + logabsdet: _FloatingOrArrayT_co # keep in sync with `solve` @overload # ~float64, +float64 From daf9cb36603ee2191c9a94f1a2b1e21f090921da Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 23:15:17 +0200 Subject: [PATCH 56/60] TYP: fix ``datetime64`` unit conversion (#31883) --- numpy/__init__.pyi | 6 +++--- numpy/typing/tests/data/reveal/scalars.pyi | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index 8f258f8be084..22db9ae40e37 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -7332,13 +7332,13 @@ class datetime64(_RealMixin, generic[_DT64ItemT_co], Generic[_DT64ItemT_co]): @overload def __new__(cls, value: _DT64Date, format: _TimeUnitSpec[_DateUnit] = ..., /) -> datetime64[dt.date]: ... @overload - def __new__(cls, value: int | bytes | str | dt.date, format: _TimeUnitSpec[_IntTimeUnit], /) -> datetime64[int]: ... + def __new__(cls, value: int | bytes | str | dt.date | datetime64, format: _TimeUnitSpec[_IntTimeUnit], /) -> datetime64[int]: ... @overload def __new__( - cls, value: int | bytes | str | dt.date, format: _TimeUnitSpec[_NativeTimeUnit], / + cls, value: int | bytes | str | dt.date | datetime64, format: _TimeUnitSpec[_NativeTimeUnit], / ) -> datetime64[dt.datetime]: ... @overload - def __new__(cls, value: int | bytes | str | dt.date, format: _TimeUnitSpec[_DateUnit], /) -> datetime64[dt.date]: ... + def __new__(cls, value: int | bytes | str | dt.date | datetime64, format: _TimeUnitSpec[_DateUnit], /) -> datetime64[dt.date]: ... @overload def __new__(cls, value: bytes | str | dt.date | None, format: _TimeUnitSpec[_TD64Unit] = ..., /) -> Self: ... diff --git a/numpy/typing/tests/data/reveal/scalars.pyi b/numpy/typing/tests/data/reveal/scalars.pyi index 3ab5bd8fde9b..2ad0c8932e43 100644 --- a/numpy/typing/tests/data/reveal/scalars.pyi +++ b/numpy/typing/tests/data/reveal/scalars.pyi @@ -1,3 +1,4 @@ +import datetime as dt from typing import Any, Literal, assert_type import numpy as np @@ -250,6 +251,24 @@ assert_type(int(O), int) assert_type(float(O), float) assert_type(complex(O), complex) +assert_type(np.datetime64(), np.datetime64[None]) +assert_type(np.datetime64("now", "ns"), np.datetime64[int]) +assert_type(np.datetime64("now", "s"), np.datetime64[dt.datetime]) +assert_type(np.datetime64("now", "Y"), np.datetime64[dt.date]) +# unit-swapping +assert_type(np.datetime64(np.datetime64(), "ns"), np.datetime64[int]) +assert_type(np.datetime64(np.datetime64("now", "ns"), "ns"), np.datetime64[int]) +assert_type(np.datetime64(np.datetime64("now", "s"), "ns"), np.datetime64[int]) +assert_type(np.datetime64(np.datetime64("now", "Y"), "ns"), np.datetime64[int]) +assert_type(np.datetime64(np.datetime64(), "s"), np.datetime64[dt.datetime]) +assert_type(np.datetime64(np.datetime64("now", "ns"), "s"), np.datetime64[dt.datetime]) +assert_type(np.datetime64(np.datetime64("now", "s"), "s"), np.datetime64[dt.datetime]) +assert_type(np.datetime64(np.datetime64("now", "Y"), "s"), np.datetime64[dt.datetime]) +assert_type(np.datetime64(np.datetime64(), "Y"), np.datetime64[dt.date]) +assert_type(np.datetime64(np.datetime64("now", "ns"), "Y"), np.datetime64[dt.date]) +assert_type(np.datetime64(np.datetime64("now", "s"), "Y"), np.datetime64[dt.date]) +assert_type(np.datetime64(np.datetime64("now", "Y"), "Y"), np.datetime64[dt.date]) + # These fail fail because of a mypy __new__ bug: # https://github.com/python/mypy/issues/15182 # According to the typing spec, the following statements are valid, see From bb353e86d2cc3c53319258b981f3730f590a2473 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 23:15:56 +0200 Subject: [PATCH 57/60] TYP: fix int promotion for ``linalg.trace`` (#31885) --- numpy/linalg/_linalg.pyi | 52 +++++++++++++---------- numpy/typing/tests/data/reveal/linalg.pyi | 10 ++++- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/numpy/linalg/_linalg.pyi b/numpy/linalg/_linalg.pyi index cd53d17cf77d..1f3f79faecfc 100644 --- a/numpy/linalg/_linalg.pyi +++ b/numpy/linalg/_linalg.pyi @@ -962,43 +962,49 @@ def diagonal(x: ArrayLike, /, *, offset: SupportsIndex = 0) -> np.ndarray: ... def trace( x: _SupportsArray[_JustAnyShape, np.dtype[_to_complex]], /, *, offset: SupportsIndex = 0, dtype: DTypeLike | None = None ) -> Any: ... -@overload # 2d known dtype, dtype=None -def trace[ScalarT: _to_complex](x: _ArrayLike2D[ScalarT], /, *, offset: SupportsIndex = 0, dtype: None = None) -> ScalarT: ... +@overload # 2d known dtype +def trace[ScalarT: np.inexact](x: _ArrayLike2D[ScalarT], /, *, offset: SupportsIndex = 0, dtype: None = None) -> ScalarT: ... @overload # 2d, dtype= -def trace[ScalarT: _to_complex]( - x: _ToArrayComplex_2d, /, *, offset: SupportsIndex = 0, dtype: _DTypeLike[ScalarT] -) -> ScalarT: ... -@overload # 2d bool -def trace(x: _Sequence2D[bool], /, *, offset: SupportsIndex = 0, dtype: None = None) -> np.bool: ... -@overload # 2d int -def trace(x: Sequence[list[int]], /, *, offset: SupportsIndex = 0, dtype: None = None) -> np.int_: ... -@overload # 2d float +def trace[ScalarT: np.number](x: _ToArrayComplex_2d, /, *, offset: SupportsIndex = 0, dtype: _DTypeLike[ScalarT]) -> ScalarT: ... +@overload # 2d +int +def trace( + x: _ArrayLike2D[np.integer | np.bool] | Sequence[Sequence[int]], /, *, offset: SupportsIndex = 0, dtype: None = None +) -> np.int_: ... +@overload # 2d ~float def trace(x: Sequence[list[float]], /, *, offset: SupportsIndex = 0, dtype: None = None) -> np.float64: ... -@overload # 2d complex +@overload # 2d ~complex def trace(x: Sequence[list[complex]], /, *, offset: SupportsIndex = 0, dtype: None = None) -> np.complex128: ... -@overload # 3d known dtype, dtype=None -def trace[DTypeT: np.dtype[_to_complex]]( +@overload # 3d known dtype +def trace[DTypeT: np.dtype[np.inexact]]( x: _SupportsArray[tuple[int, int, int], DTypeT], /, *, offset: SupportsIndex = 0, dtype: None = None ) -> np.ndarray[tuple[int], DTypeT]: ... +@overload # 3d +int +def trace( + x: _SupportsArray[tuple[int, int, int], np.dtype[np.integer | np.bool]], /, *, offset: SupportsIndex = 0, dtype: None = None +) -> np.ndarray[tuple[int], np.dtype[np.int_]]: ... @overload # 3d, dtype= -def trace[ScalarT: _to_complex]( +def trace[ScalarT: np.number]( x: _ToArrayComplex_3d, /, *, offset: SupportsIndex = 0, dtype: _DTypeLike[ScalarT] ) -> _Array1D[ScalarT]: ... -@overload # 3d+ known dtype, dtype=None -def trace[DTypeT: np.dtype[_to_complex]]( +@overload # 3d+ known dtype +def trace[DTypeT: np.dtype[np.inexact]]( x: _SupportsArray[_AtLeast3D, DTypeT], /, *, offset: SupportsIndex = 0, dtype: None = None ) -> np.ndarray[tuple[int, *tuple[Any, ...]], DTypeT]: ... @overload # 3d+, dtype= -def trace[ScalarT: _to_complex]( +def trace[ScalarT: np.inexact]( x: _ArrayLike3ND[_to_complex] | _Sequence3ND[complex], /, *, offset: SupportsIndex = 0, dtype: _DTypeLike[ScalarT] ) -> np.ndarray[tuple[int, *tuple[Any, ...]], np.dtype[ScalarT]]: ... -@overload # 3d+ bool -def trace(x: _Sequence3ND[bool], /, *, offset: SupportsIndex = 0, dtype: None = None) -> NDArray[np.bool]: ... -@overload # 3d+ int -def trace(x: _Sequence2ND[list[int]], /, *, offset: SupportsIndex = 0, dtype: None = None) -> NDArray[np.int_]: ... -@overload # 3d+ float +@overload # 3d+ +integer +def trace( + x: _SupportsArray[_AtLeast3D, np.dtype[np.integer | np.bool]] | _Sequence2ND[Sequence[int]], + /, + *, + offset: SupportsIndex = 0, + dtype: None = None, +) -> NDArray[np.int_]: ... +@overload # 3d+ ~float def trace(x: _Sequence2ND[list[float]], /, *, offset: SupportsIndex = 0, dtype: None = None) -> NDArray[np.float64]: ... -@overload # 3d+ complex +@overload # 3d+ ~complex def trace(x: _Sequence2ND[list[complex]], /, *, offset: SupportsIndex = 0, dtype: None = None) -> NDArray[np.complex128]: ... @overload # fallback def trace(x: _ArrayLikeComplex_co, /, *, offset: SupportsIndex = 0, dtype: DTypeLike | None = None) -> Any: ... diff --git a/numpy/typing/tests/data/reveal/linalg.pyi b/numpy/typing/tests/data/reveal/linalg.pyi index a1155e2bb5ed..d58204a5b1e5 100644 --- a/numpy/typing/tests/data/reveal/linalg.pyi +++ b/numpy/typing/tests/data/reveal/linalg.pyi @@ -16,8 +16,10 @@ type _Array3D[ScalarT: np.generic] = np.ndarray[tuple[int, int, int], np.dtype[S bool_list_1d: list[bool] bool_list_2d: list[list[bool]] +bool_list_3d: list[list[list[bool]]] int_list_1d: list[int] int_list_2d: list[list[int]] +int_list_3d: list[list[list[int]]] float_list_1d: list[float] float_list_2d: list[list[float]] float_list_3d: list[list[list[float]]] @@ -59,6 +61,8 @@ AR_f8_2d: _Array2D[np.float64] AR_f8_3d: _Array3D[np.float64] AR_f8_4d: np.ndarray[tuple[int, int, int, int], np.dtype[np.float64]] +AR_i1_2d: _Array2D[np.int8] +AR_i1_3d: _Array3D[np.int8] AR_f2_2d: _Array2D[np.float16] AR_f4_1d: _Array1D[np.float32] AR_f4_2d: _Array2D[np.float32] @@ -388,15 +392,19 @@ assert_type(np.linalg.diagonal(str_list_2d), npt.NDArray[np.str_]) assert_type(np.linalg.trace(AR_any), Any) assert_type(np.linalg.trace(AR_f4), Any) +assert_type(np.linalg.trace(AR_i1_2d), np.int_) +assert_type(np.linalg.trace(AR_i1_3d), _Array1D[np.int_]) assert_type(np.linalg.trace(AR_f4_2d), np.float32) assert_type(np.linalg.trace(AR_f8_2d), np.float64) assert_type(np.linalg.trace(AR_f4_3d), _Array1D[np.float32]) assert_type(np.linalg.trace(AR_f8_3d), _Array1D[np.float64]) assert_type(np.linalg.trace(AR_f8_4d), np.ndarray[tuple[int, *tuple[Any, ...]], np.dtype[np.float64]]) -assert_type(np.linalg.trace(bool_list_2d), np.bool) +assert_type(np.linalg.trace(bool_list_2d), np.int_) assert_type(np.linalg.trace(int_list_2d), np.int_) assert_type(np.linalg.trace(float_list_2d), np.float64) assert_type(np.linalg.trace(complex_list_2d), np.complex128) +assert_type(np.linalg.trace(bool_list_3d), npt.NDArray[np.int_]) +assert_type(np.linalg.trace(int_list_3d), npt.NDArray[np.int_]) assert_type(np.linalg.trace(float_list_3d), npt.NDArray[np.float64]) assert_type(np.linalg.outer(bool_list_1d, bool_list_1d), _Array2D[np.bool]) From 34b746f1fa2a3417877878642ba42d0f7e7721d2 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Mon, 6 Jul 2026 23:16:54 +0200 Subject: [PATCH 58/60] TYP: fix ``tri{l,u}`` shape-typing for 1d input (#31886) --- numpy/lib/_twodim_base_impl.pyi | 10 ++++++++-- numpy/typing/tests/data/reveal/twodim_base.pyi | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/numpy/lib/_twodim_base_impl.pyi b/numpy/lib/_twodim_base_impl.pyi index af8e3d72c4d8..f6c08b6a5d21 100644 --- a/numpy/lib/_twodim_base_impl.pyi +++ b/numpy/lib/_twodim_base_impl.pyi @@ -40,6 +40,8 @@ type _Int_co = np.integer | np.bool type _Float_co = np.floating | _Int_co type _Number_co = np.number | np.bool +type _AtLeast2D = tuple[int, int, *tuple[int, ...]] # input only + type _Array1D[ScalarT: np.generic] = np.ndarray[tuple[int], np.dtype[ScalarT]] type _Array2D[ScalarT: np.generic] = np.ndarray[tuple[int, int], np.dtype[ScalarT]] # Workaround for mypy's and pyright's lack of compliance with the typing spec for @@ -192,7 +194,9 @@ def tri( # keep in sync with `triu` @overload -def tril[ArrayT: np.ndarray](m: ArrayT, k: int = 0) -> ArrayT: ... +def tril[ArrayT: np.ndarray[_AtLeast2D]](m: ArrayT, k: int = 0) -> ArrayT: ... +@overload +def tril[DTypeT: np.dtype](m: np.ndarray[tuple[int], DTypeT], k: int = 0) -> np.ndarray[tuple[int, int], DTypeT]: ... @overload def tril[ScalarT: np.generic](m: _ArrayLike[ScalarT], k: int = 0) -> NDArray[ScalarT]: ... @overload @@ -200,7 +204,9 @@ def tril(m: ArrayLike, k: int = 0) -> NDArray[Any]: ... # keep in sync with `tril` @overload -def triu[ArrayT: np.ndarray](m: ArrayT, k: int = 0) -> ArrayT: ... +def triu[ArrayT: np.ndarray[_AtLeast2D]](m: ArrayT, k: int = 0) -> ArrayT: ... +@overload +def triu[DTypeT: np.dtype](m: np.ndarray[tuple[int], DTypeT], k: int = 0) -> np.ndarray[tuple[int, int], DTypeT]: ... @overload def triu[ScalarT: np.generic](m: _ArrayLike[ScalarT], k: int = 0) -> NDArray[ScalarT]: ... @overload diff --git a/numpy/typing/tests/data/reveal/twodim_base.pyi b/numpy/typing/tests/data/reveal/twodim_base.pyi index d8c45afe44ab..1abe90e0d7ad 100644 --- a/numpy/typing/tests/data/reveal/twodim_base.pyi +++ b/numpy/typing/tests/data/reveal/twodim_base.pyi @@ -5,6 +5,7 @@ import numpy.typing as npt type _1D = tuple[int] type _2D = tuple[int, int] +type _3D = tuple[int, int, int] type _ND = tuple[Any, ...] type _Indices2D = tuple[ @@ -17,6 +18,7 @@ type _Indices2D = tuple[ _nd_bool: np.ndarray[_ND, np.dtype[np.bool]] _1d_bool: np.ndarray[_1D, np.dtype[np.bool]] _2d_bool: np.ndarray[_2D, np.dtype[np.bool]] +_3d_bool: np.ndarray[_3D, np.dtype[np.bool]] _nd_u64: np.ndarray[_ND, np.dtype[np.uint64]] _nd_i64: np.ndarray[_ND, np.dtype[np.int64]] _nd_f64: np.ndarray[_ND, np.dtype[np.float64]] @@ -89,12 +91,18 @@ assert_type(np.tril(_nd_bool), np.ndarray[_ND, np.dtype[np.bool]]) assert_type(np.tril(_to_nd_bool, k=0), np.ndarray) assert_type(np.tril(_to_1d_bool, k=0), np.ndarray) assert_type(np.tril(_to_2d_bool, k=0), np.ndarray) +assert_type(np.tril(_1d_bool, k=0), np.ndarray[_2D, np.dtype[np.bool]]) +assert_type(np.tril(_2d_bool, k=0), np.ndarray[_2D, np.dtype[np.bool]]) +assert_type(np.tril(_3d_bool, k=0), np.ndarray[_3D, np.dtype[np.bool]]) # triu assert_type(np.triu(_nd_bool), np.ndarray[_ND, np.dtype[np.bool]]) assert_type(np.triu(_to_nd_bool, k=0), np.ndarray) assert_type(np.triu(_to_1d_bool, k=0), np.ndarray) assert_type(np.triu(_to_2d_bool, k=0), np.ndarray) +assert_type(np.triu(_1d_bool, k=0), np.ndarray[_2D, np.dtype[np.bool]]) +assert_type(np.triu(_2d_bool, k=0), np.ndarray[_2D, np.dtype[np.bool]]) +assert_type(np.triu(_3d_bool, k=0), np.ndarray[_3D, np.dtype[np.bool]]) # vander assert_type(np.vander(_nd_bool), np.ndarray[_2D, np.dtype[np.int_]]) From 70815da8a1c510034278e1eeeb046763c482831e Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Tue, 7 Jul 2026 02:08:37 +0200 Subject: [PATCH 59/60] TYP: reject float array-likes in ``random.RandomState.seed`` (#31888) --- numpy/random/mtrand.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpy/random/mtrand.pyi b/numpy/random/mtrand.pyi index e2215c70fe7d..116364284e0d 100644 --- a/numpy/random/mtrand.pyi +++ b/numpy/random/mtrand.pyi @@ -93,7 +93,7 @@ class RandomState: def __reduce__(self) -> tuple[Callable[[BitGenerator], RandomState], tuple[BitGenerator], dict[str, Any]]: ... # - def seed(self, seed: _ArrayLikeFloat_co | None = None) -> None: ... + def seed(self, seed: _ArrayLikeInt_co | None = None) -> None: ... # @overload From 98aa6e9e238623146715ef9c1441abf2e8466e70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:09:06 +0000 Subject: [PATCH 60/60] MAINT: Bump hypothesis in /requirements in the python-deps group Bumps the python-deps group in /requirements with 1 update: [hypothesis](https://github.com/HypothesisWorks/hypothesis). Updates `hypothesis` from 6.155.7 to 6.156.1 - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/v6.155.7...v6.156.1) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.156.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-deps ... Signed-off-by: dependabot[bot] --- requirements/emscripten_test_requirements.txt | 2 +- requirements/test_requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/emscripten_test_requirements.txt b/requirements/emscripten_test_requirements.txt index 0963f89097a7..a375dd0cfd7d 100644 --- a/requirements/emscripten_test_requirements.txt +++ b/requirements/emscripten_test_requirements.txt @@ -1,4 +1,4 @@ -hypothesis==6.155.7 +hypothesis==6.156.1 pytest==9.1.1 tzdata pytest-xdist diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt index 219eda52fd07..b8684cba9592 100644 --- a/requirements/test_requirements.txt +++ b/requirements/test_requirements.txt @@ -1,5 +1,5 @@ Cython -hypothesis==6.155.7 +hypothesis==6.156.1 pytest==9.1.1 pytest-cov==7.1.0 meson