From 1fc18b58afe2bbd347158091f960e23f559e8d42 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 8 May 2025 16:09:43 +0200 Subject: [PATCH 1/6] Switch to np.bool_ --- pyproject.toml | 14 ++++++++++++-- src/fast_array_utils/stats/__init__.py | 4 ++-- src/fast_array_utils/stats/_is_constant.py | 16 ++++++++-------- tests/test_stats.py | 8 ++++---- typings/cupy/_core/core.pyi | 2 +- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51657f6..33f63ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] build-backend = "hatchling.build" -requires = [ "hatch-docstring-description>=1.1.1", "hatch-fancy-pypi-readme", "hatch-vcs", "hatchling" ] +requires = [ "hatch-docstring-description>=1.1.1", "hatch-fancy-pypi-readme", "hatch-min-requirements", "hatch-vcs", "hatchling" ] [project] name = "fast-array-utils" @@ -18,7 +18,7 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dynamic = [ "description", "readme", "version" ] -dependencies = [ "numpy" ] +dependencies = [ "numpy>=1.25.2" ] optional-dependencies.accel = [ "numba" ] optional-dependencies.doc = [ "furo", @@ -61,6 +61,7 @@ path = "README.rst" start-after = ".. begin" [tool.hatch.metadata.hooks.docstring-description] +[tool.hatch.metadata.hooks.min_requirements] [tool.hatch.build.targets.wheel] packages = [ "src/testing", "src/fast_array_utils" ] @@ -87,11 +88,19 @@ overrides.matrix.extras.dependencies = [ { if = [ "full" ], value = "scipy-stubs" }, { if = [ "full" ], value = "scikit-learn" }, ] +overrides.matrix.resolution.features = [ + { if = [ "lowest" ], value = "min-reqs" }, # feature added by hatch-min-requirements +] [[tool.hatch.envs.hatch-test.matrix]] python = [ "3.13", "3.12", "3.11" ] extras = [ "full", "default", "min" ] +[[tool.hatch.envs.hatch-test.matrix]] +python = [ "3.11" ] +extras = [ "default" ] +resolution = [ "lowest" ] + [tool.ruff] line-length = 160 namespace-packages = [ "src/testing" ] @@ -130,6 +139,7 @@ lint.isort.known-first-party = [ "fast_array_utils" ] lint.isort.lines-after-imports = 2 lint.isort.required-imports = [ "from __future__ import annotations" ] lint.pydocstyle.convention = "numpy" +lint.flake8-tidy-imports.banned-api."numpy.bool".msg = "Use `np.bool_` instead for numpy>=1.24<2 compatibility" [tool.pytest.ini_options] addopts = [ diff --git a/src/fast_array_utils/stats/__init__.py b/src/fast_array_utils/stats/__init__.py index f7b7c5b..a99c9a4 100644 --- a/src/fast_array_utils/stats/__init__.py +++ b/src/fast_array_utils/stats/__init__.py @@ -29,7 +29,7 @@ @overload def is_constant(x: NDArray[Any] | types.CSBase | types.CupyArray, /, *, axis: None = None) -> bool: ... @overload -def is_constant(x: NDArray[Any] | types.CSBase, /, *, axis: Literal[0, 1]) -> NDArray[np.bool]: ... +def is_constant(x: NDArray[Any] | types.CSBase, /, *, axis: Literal[0, 1]) -> NDArray[np.bool_]: ... @overload def is_constant(x: types.CupyArray, /, *, axis: Literal[0, 1]) -> types.CupyArray: ... @overload @@ -41,7 +41,7 @@ def is_constant( /, *, axis: Literal[0, 1, None] = None, -) -> bool | NDArray[np.bool] | types.CupyArray | types.DaskArray: +) -> bool | NDArray[np.bool_] | types.CupyArray | types.DaskArray: """Check whether values in array are constant. Parameters diff --git a/src/fast_array_utils/stats/_is_constant.py b/src/fast_array_utils/stats/_is_constant.py index ecb6620..0ca3c1b 100644 --- a/src/fast_array_utils/stats/_is_constant.py +++ b/src/fast_array_utils/stats/_is_constant.py @@ -25,12 +25,12 @@ def is_constant_( /, *, axis: Literal[0, 1, None] = None, -) -> bool | NDArray[np.bool] | types.CupyArray | types.DaskArray: # pragma: no cover +) -> bool | NDArray[np.bool_] | types.CupyArray | types.DaskArray: # pragma: no cover raise NotImplementedError @is_constant_.register(np.ndarray | types.CupyArray) # type: ignore[call-overload,misc] -def _is_constant_ndarray(a: NDArray[Any] | types.CupyArray, /, *, axis: Literal[0, 1, None] = None) -> bool | NDArray[np.bool] | types.CupyArray: +def _is_constant_ndarray(a: NDArray[Any] | types.CupyArray, /, *, axis: Literal[0, 1, None] = None) -> bool | NDArray[np.bool_] | types.CupyArray: # Should eventually support nd, not now. match axis: case None: @@ -41,13 +41,13 @@ def _is_constant_ndarray(a: NDArray[Any] | types.CupyArray, /, *, axis: Literal[ return _is_constant_rows(a) -def _is_constant_rows(a: NDArray[Any] | types.CupyArray) -> NDArray[np.bool] | types.CupyArray: +def _is_constant_rows(a: NDArray[Any] | types.CupyArray) -> NDArray[np.bool_] | types.CupyArray: b = np.broadcast_to(a[:, 0][:, np.newaxis], a.shape) - return cast("NDArray[np.bool]", (a == b).all(axis=1)) + return cast("NDArray[np.bool_]", (a == b).all(axis=1)) @is_constant_.register(types.CSBase) # type: ignore[call-overload,misc] -def _is_constant_cs(a: types.CSBase, /, *, axis: Literal[0, 1, None] = None) -> bool | NDArray[np.bool]: +def _is_constant_cs(a: types.CSBase, /, *, axis: Literal[0, 1, None] = None) -> bool | NDArray[np.bool_]: from . import is_constant if len(a.shape) == 1: # pragma: no cover @@ -68,9 +68,9 @@ def _is_constant_cs(a: types.CSBase, /, *, axis: Literal[0, 1, None] = None) -> @numba.njit(cache=True) -def _is_constant_cs_major(a: types.CSBase, shape: tuple[int, int]) -> NDArray[np.bool]: +def _is_constant_cs_major(a: types.CSBase, shape: tuple[int, int]) -> NDArray[np.bool_]: n = len(a.indptr) - 1 - result = np.ones(n, dtype=np.bool) + result = np.ones(n, dtype=np.bool_) for i in numba.prange(n): start = a.indptr[i] stop = a.indptr[i + 1] @@ -89,7 +89,7 @@ def _is_constant_dask(a: types.DaskArray, /, *, axis: Literal[0, 1, None] = None from . import is_constant if axis is not None: - return da.map_blocks(partial(is_constant, axis=axis), a, drop_axis=axis, meta=np.array([], dtype=np.bool)) + return da.map_blocks(partial(is_constant, axis=axis), a, drop_axis=axis, meta=np.array([], dtype=np.bool_)) rv = ( (a == a[0, 0].compute()).all() diff --git a/tests/test_stats.py b/tests/test_stats.py index 9cde8d5..dd727ea 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -24,7 +24,7 @@ Array: TypeAlias = CpuArray | GpuArray | DiskArray | types.CSDataset | types.DaskArray - DTypeIn = np.float32 | np.float64 | np.int32 | np.bool + DTypeIn = np.float32 | np.float64 | np.int32 | np.bool_ DTypeOut = np.float32 | np.float64 | np.int64 NdAndAx: TypeAlias = tuple[Literal[1], Literal[None]] | tuple[Literal[2], Literal[0, 1, None]] @@ -81,7 +81,7 @@ def axis(ndim_and_axis: NdAndAx) -> Literal[0, 1, None]: return ndim_and_axis[1] -@pytest.fixture(params=[np.float32, np.float64, np.int32, np.bool]) +@pytest.fixture(params=[np.float32, np.float64, np.int32, np.bool_]) def dtype_in(request: pytest.FixtureRequest, array_type: ArrayType) -> type[DTypeIn]: dtype = cast("type[DTypeIn]", request.param) inner_cls = array_type.inner.cls if array_type.inner else array_type.cls @@ -152,7 +152,7 @@ def test_sum( if dtype_arg is not None: assert sum_.dtype == dtype_arg, (sum_.dtype, dtype_arg) - elif dtype_in in {np.bool, np.int32}: + elif dtype_in in {np.bool_, np.int32}: assert sum_.dtype == np.int64 else: assert sum_.dtype == dtype_in @@ -276,7 +276,7 @@ def test_is_constant( x = array_type(x_data, dtype=np.float64) result = stats.is_constant(x, axis=axis) if isinstance(result, types.DaskArray): - result = cast("NDArray[np.bool] | bool", result.compute()) + result = cast("NDArray[np.bool_] | bool", result.compute()) if isinstance(result, types.CupyArray | types.CupyCSMatrix): result = result.get() if isinstance(expected, list): diff --git a/typings/cupy/_core/core.pyi b/typings/cupy/_core/core.pyi index f8d459e..1ace0e6 100644 --- a/typings/cupy/_core/core.pyi +++ b/typings/cupy/_core/core.pyi @@ -32,7 +32,7 @@ class ndarray: @property def T(self) -> Self: ... # noqa: N802 @overload - def all(self, axis: None = None) -> np.bool: ... + def all(self, axis: None = None) -> np.bool_: ... @overload def all(self, axis: int) -> ndarray: ... def reshape(self, shape: tuple[int, ...] | int) -> ndarray: ... From e72ff3b78a01c4d913766e90a76cbcad207d29f8 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 8 May 2025 16:18:42 +0200 Subject: [PATCH 2/6] Fix almost all --- tests/test_numpy_scipy_sparse.py | 8 ++++---- tests/test_stats.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_numpy_scipy_sparse.py b/tests/test_numpy_scipy_sparse.py index 14675eb..72dcb95 100644 --- a/tests/test_numpy_scipy_sparse.py +++ b/tests/test_numpy_scipy_sparse.py @@ -66,10 +66,10 @@ def test_copy( assert mat.indptr.ctypes.data != copied.indptr.ctypes.data # check that the array contents and dtypes are the same assert mat.shape == copied.shape - np.testing.assert_equal(copied.toarray(), mat.toarray(), strict=True) - np.testing.assert_equal(copied.data, mat.data, strict=True) - np.testing.assert_equal(copied.indices, mat.indices, strict=not downcasts_idx(mat)) - np.testing.assert_equal(copied.indptr, mat.indptr, strict=not downcasts_idx(mat)) + np.testing.assert_array_equal(copied.toarray(), mat.toarray(), strict=True) + np.testing.assert_array_equal(copied.data, mat.data, strict=True) + np.testing.assert_array_equal(copied.indices, mat.indices, strict=not downcasts_idx(mat)) + np.testing.assert_array_equal(copied.indptr, mat.indptr, strict=not downcasts_idx(mat)) def downcasts_idx(mat: types.CSBase) -> bool: diff --git a/tests/test_stats.py b/tests/test_stats.py index dd727ea..889efb0 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -210,7 +210,7 @@ def test_mean_var( mean, var = mean.get(), var.get() mean_expected = np.mean(np_arr, axis=axis) # type: ignore[arg-type] - var_expected = np.var(np_arr, axis=axis, correction=1) # type: ignore[arg-type] + var_expected = np.var(np_arr, axis=axis, ddof=1) # type: ignore[arg-type] np.testing.assert_array_equal(mean, mean_expected) np.testing.assert_array_almost_equal(var, var_expected) # type: ignore[arg-type] From 1410076214dfbc0e27315637b78cf923d7524831 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 14:21:26 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyproject.toml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 33f63ec..658571f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,12 @@ [build-system] build-backend = "hatchling.build" -requires = [ "hatch-docstring-description>=1.1.1", "hatch-fancy-pypi-readme", "hatch-min-requirements", "hatch-vcs", "hatchling" ] +requires = [ + "hatch-docstring-description>=1.1.1", + "hatch-fancy-pypi-readme", + "hatch-min-requirements", + "hatch-vcs", + "hatchling", +] [project] name = "fast-array-utils" @@ -133,13 +139,14 @@ lint.per-file-ignores."typings/**/*.pyi" = [ "A002", "F403", "F405", "N801" ] # lint.allowed-confusables = [ "×", "’" ] lint.flake8-bugbear.extend-immutable-calls = [ "testing.fast_array_utils.Flags" ] lint.flake8-copyright.notice-rgx = "SPDX-License-Identifier: MPL-2\\.0" +lint.flake8-tidy-imports.banned-api."numpy.bool".msg = "Use `np.bool_` instead for numpy>=1.24<2 compatibility" + lint.flake8-type-checking.exempt-modules = [ ] lint.flake8-type-checking.strict = true lint.isort.known-first-party = [ "fast_array_utils" ] lint.isort.lines-after-imports = 2 lint.isort.required-imports = [ "from __future__ import annotations" ] lint.pydocstyle.convention = "numpy" -lint.flake8-tidy-imports.banned-api."numpy.bool".msg = "Use `np.bool_` instead for numpy>=1.24<2 compatibility" [tool.pytest.ini_options] addopts = [ From 5ff5dd672446fb01efb612c9e86ce8b25b26fac4 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 8 May 2025 16:48:33 +0200 Subject: [PATCH 4/6] Doctest fixes --- pyproject.toml | 2 +- src/fast_array_utils/stats/__init__.py | 6 +++--- src/testing/fast_array_utils/_private.py | 11 +++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 src/testing/fast_array_utils/_private.py diff --git a/pyproject.toml b/pyproject.toml index 658571f..8b57797 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,6 @@ lint.allowed-confusables = [ "×", "’" ] lint.flake8-bugbear.extend-immutable-calls = [ "testing.fast_array_utils.Flags" ] lint.flake8-copyright.notice-rgx = "SPDX-License-Identifier: MPL-2\\.0" lint.flake8-tidy-imports.banned-api."numpy.bool".msg = "Use `np.bool_` instead for numpy>=1.24<2 compatibility" - lint.flake8-type-checking.exempt-modules = [ ] lint.flake8-type-checking.strict = true lint.isort.known-first-party = [ "fast_array_utils" ] @@ -150,6 +149,7 @@ lint.pydocstyle.convention = "numpy" [tool.pytest.ini_options] addopts = [ + "-ptesting.fast_array_utils._private", "--import-mode=importlib", "--strict-markers", "--doctest-modules", diff --git a/src/fast_array_utils/stats/__init__.py b/src/fast_array_utils/stats/__init__.py index a99c9a4..60dcae6 100644 --- a/src/fast_array_utils/stats/__init__.py +++ b/src/fast_array_utils/stats/__init__.py @@ -118,7 +118,7 @@ def mean( ... [0, 0, 0], ... ]) >>> mean(x) - np.float64(0.5) + 0.5 >>> mean(x, axis=0) array([0. , 0.5, 1. ]) >>> mean(x, axis=1) @@ -184,7 +184,7 @@ def mean_var( ... [0, 0, 0], ... ]) >>> mean_var(x) # doctest: +FLOAT_CMP - (np.float64(0.5), np.float64(0.5833333333333334)) + (0.5, 0.5833333333333334) >>> mean_var(x, axis=0) (array([0. , 0.5, 1. ]), array([0. , 0.25, 1. ])) >>> mean_var(x, axis=1) @@ -251,7 +251,7 @@ def sum( ... [0, 0, 0], ... ]) >>> sum(x) - np.int64(3) + 3 >>> sum(x, axis=0) array([0, 1, 2]) >>> sum(x, axis=1) diff --git a/src/testing/fast_array_utils/_private.py b/src/testing/fast_array_utils/_private.py new file mode 100644 index 0000000..0a1f8b0 --- /dev/null +++ b/src/testing/fast_array_utils/_private.py @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: MPL-2.0 +from __future__ import annotations + +import numpy as np +import pytest + + +@pytest.fixture(autouse=True) +def _set_numpy_print() -> None: # TODO(flying-sheep): #97 remove once we depend on numpy >=2 + if int(np.__version__.split(".", 1)[0]) > 1: + np.set_printoptions(legacy="1.25") From f52d20cf798078f9ea56ad61045a3fda4730e71b Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 8 May 2025 17:13:10 +0200 Subject: [PATCH 5/6] Fix docs more --- docs/conf.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2fbe32c..71ac592 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,6 +8,8 @@ from pathlib import Path from typing import TYPE_CHECKING +from docutils.nodes import Text + if TYPE_CHECKING: from docutils.nodes import TextElement, reference @@ -82,6 +84,7 @@ source_directory="docs/", ) +_np_aliases = {"bool_": "bool"} _np_nocls = {"float64": "attr"} _optional_types = { "CupyArray": "cupy.ndarray", @@ -94,30 +97,31 @@ } -def find_type_alias(name: str) -> tuple[str, str] | tuple[None, None]: +def find_type_alias(name: str) -> tuple[str, str, str | None] | tuple[None, None, None]: """Find a type alias.""" import numpy.typing as npt from fast_array_utils import types, typing if name in typing.__all__: - return "data", f"fast_array_utils.typing.{name}" + return "data", f"fast_array_utils.typing.{name}", None if name.startswith("types.") and name[6:] in {*types.__all__, *_optional_types}: if path := _optional_types.get(name[6:]): - return "class", path - return "data", f"fast_array_utils.{name}" + return "class", path, None + return "data", f"fast_array_utils.{name}", None if name.startswith("np."): - return _np_nocls.get(name[3:], "class"), f"numpy.{name[3:]}" + name = _np_aliases.get(name[3:], name[3:]) + return _np_nocls.get(name, "class"), f"numpy.{name}", f"np.{name}" if name in npt.__all__: - return "data", f"numpy.typing.{name}" - return None, None + return "data", f"numpy.typing.{name}", None + return None, None, None def resolve_type_aliases(app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: TextElement) -> reference | None: """Resolve :class: references to our type aliases as :attr: instead.""" if (node["refdomain"], node["reftype"]) != ("py", "class"): return None - typ, target = find_type_alias(node["reftarget"]) + typ, target, name = find_type_alias(node["reftarget"]) if typ is None or target is None: return None if target.startswith("fast_array_utils."): @@ -131,6 +135,8 @@ def resolve_type_aliases(app: Sphinx, env: BuildEnvironment, node: pending_xref, if ref is None: msg = f"Could not resolve {typ} {target} (from {node['reftarget']})" raise AssertionError(msg) + if name: + ref.children[:] = [Text(name)] return ref From 0bf60d34eb63de9e3c9267ff51633949ff94d80f Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 9 May 2025 16:26:23 +0200 Subject: [PATCH 6/6] fix bounds --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 853e1f1..e3868c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ optional-dependencies.doc = [ "sphinx-autofixture", ] optional-dependencies.full = [ "dask", "fast-array-utils[accel,sparse]", "h5py", "zarr" ] -optional-dependencies.sparse = [ "scipy>=1.8" ] +optional-dependencies.sparse = [ "scipy>=1.11" ] optional-dependencies.test = [ "anndata", "fast-array-utils[accel,test-min]", @@ -104,7 +104,7 @@ extras = [ "full", "min" ] [[tool.hatch.envs.hatch-test.matrix]] python = [ "3.11" ] -extras = [ "default" ] +extras = [ "full" ] resolution = [ "lowest" ] [tool.ruff]