diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 04de10f57b..51a2ff5d30 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -39,7 +39,7 @@ jobs: cache: 'pip' - name: Cache datasets - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cache diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a27b67ca0b..45b71420d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: with: { enable-cache: false } - id: get-envs run: | - ENVS_JSON=$(NO_COLOR=1 uvx hatch env show --json | jq -c 'to_entries + ENVS_JSON=$(NO_COLOR=1 uvx '--with=virtualenv<21' hatch env show --json | jq -c 'to_entries | map( select(.key | startswith("hatch-test")) | { @@ -55,7 +55,7 @@ jobs: python-version: ${{ matrix.env.python }} - name: Cache downloaded data - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .pytest_cache/d/scanpy-data key: pytest @@ -63,7 +63,7 @@ jobs: - name: Install dependencies run: | echo "::group::Install hatch" - uv tool install hatch + uv tool install hatch '--with=virtualenv<21' echo "::endgroup::" echo "::group::Create environment" hatch -v env create ${{ matrix.env.name }} diff --git a/.readthedocs.yml b/.readthedocs.yml index 3615552cd3..33b6ac7816 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -16,8 +16,8 @@ build: - asdf global uv latest pre_build: # run towncrier to preview the next version’s release notes - - ( find docs/release-notes -regex '[^.]+[.][^.]+.md' | grep -q . ) && uvx hatch run towncrier build --keep || true + - ( find docs/release-notes -regex '[^.]+[.][^.]+.md' | grep -q . ) && uvx "--with=virtualenv<21" hatch run towncrier build --keep || true build: html: - - uvx hatch run docs:build + - uvx "--with=virtualenv<21" hatch run docs:build - mv docs/_build $READTHEDOCS_OUTPUT diff --git a/docs/conf.py b/docs/conf.py index 4537197189..fae0cc4e4f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -263,7 +263,6 @@ def setup(app: Sphinx) -> None: "pandas.core.series.Series": "pandas.Series", # https://github.com/pandas-dev/pandas/issues/63810 "pandas.api.typing.aliases.AnyArrayLike": ("doc", "pandas:reference/aliases"), - "numpy.bool_": "numpy.bool", # Since numpy 2, numpy.bool is the canonical dtype **{ f"numpy.{prefix}typing.{name}": ("py:data", f"numpy.typing.{name}") for name, prefix in product(["ArrayLike", "DTypeLike"], ["", "_"]) diff --git a/pyproject.toml b/pyproject.toml index fd1c7b982a..472ae447ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -217,7 +217,7 @@ lint.pylint.max-positional-args = 5 "legacy_api_wrap.legacy_api".msg = "Use scanpy._compat.old_positionals instead" "numba.jit".msg = "Use `scanpy._compat.njit` instead" "numba.njit".msg = "Use `scanpy._compat.njit` instead" -"numpy.bool".msg = "Use `np.bool_` instead for numpy>=1.24<2 compatibility" +"numpy.bool_".msg = "Use `np.bool` instead" "pandas.api.types.is_categorical_dtype".msg = "Use isinstance(s.dtype, CategoricalDtype) instead" "pandas.value_counts".msg = "Use pd.Series(a).value_counts() instead" "pytest.importorskip".msg = "Use the “@needs” decorator/mark instead" diff --git a/src/scanpy/_utils/__init__.py b/src/scanpy/_utils/__init__.py index 156a2832bc..208a360e90 100644 --- a/src/scanpy/_utils/__init__.py +++ b/src/scanpy/_utils/__init__.py @@ -801,7 +801,7 @@ def select_groups( adata: AnnData, groups_order_subset: Iterable[str] | Literal["all"] = "all", key: str = "groups", -) -> tuple[list[str], NDArray[np.bool_]]: +) -> tuple[list[str], NDArray[np.bool]]: """Get subset of groups in adata.obs[key].""" groups_order = adata.obs[key].cat.categories if f"{key}_masks" in adata.uns: diff --git a/src/scanpy/get/_aggregated.py b/src/scanpy/get/_aggregated.py index a23b41551d..c5750a8193 100644 --- a/src/scanpy/get/_aggregated.py +++ b/src/scanpy/get/_aggregated.py @@ -55,7 +55,7 @@ def __init__( groupby: pd.Categorical, data: Array, *, - mask: NDArray[np.bool_] | None = None, + mask: NDArray[np.bool] | None = None, ) -> None: self.groupby = groupby if (missing := groupby.isna()).any(): @@ -188,7 +188,7 @@ def aggregate( # noqa: PLR0912 func: AggType | Iterable[AggType], *, axis: Literal["obs", 0, "var", 1] | None = None, - mask: NDArray[np.bool_] | str | None = None, + mask: NDArray[np.bool] | str | None = None, dof: int = 1, layer: str | None = None, obsm: str | None = None, @@ -337,7 +337,7 @@ def _aggregate( by: pd.Categorical, func: AggType | Iterable[AggType], *, - mask: NDArray[np.bool_] | None = None, + mask: NDArray[np.bool] | None = None, dof: int = 1, ) -> dict[AggType, np.ndarray | DaskArray]: msg = f"Data type {type(data)} not supported for aggregation" @@ -353,7 +353,7 @@ def aggregate_dask_mean_var( data: DaskArray, by: pd.Categorical, *, - mask: NDArray[np.bool_] | None = None, + mask: NDArray[np.bool] | None = None, dof: int = 1, ) -> MeanVarDict: mean = aggregate_dask(data, by, "mean", mask=mask, dof=dof)["mean"] @@ -374,7 +374,7 @@ def aggregate_dask( by: pd.Categorical, func: AggType | Iterable[AggType], *, - mask: NDArray[np.bool_] | None = None, + mask: NDArray[np.bool] | None = None, dof: int = 1, ) -> dict[AggType, DaskArray]: if not isinstance(data._meta, CSBase | np.ndarray): @@ -465,7 +465,7 @@ def aggregate_array( by: pd.Categorical, func: AggType | Iterable[AggType], *, - mask: NDArray[np.bool_] | None = None, + mask: NDArray[np.bool] | None = None, dof: int = 1, ) -> dict[AggType, np.ndarray]: groupby = Aggregate(groupby=by, data=data, mask=mask) @@ -549,7 +549,7 @@ def _combine_categories( def sparse_indicator( categorical: pd.Categorical, *, - mask: NDArray[np.bool_] | None = None, + mask: NDArray[np.bool] | None = None, weight: NDArray[np.floating] | None = None, ) -> sparse.coo_matrix: if mask is not None and weight is None: diff --git a/src/scanpy/get/get.py b/src/scanpy/get/get.py index c106db8a31..fd5ead5ce6 100644 --- a/src/scanpy/get/get.py +++ b/src/scanpy/get/get.py @@ -475,7 +475,7 @@ def _set_obs_rep( raise AssertionError(msg) -def _check_mask[M: NDArray[np.bool_] | NDArray[np.floating] | pd.Series | None]( +def _check_mask[M: NDArray[np.bool] | NDArray[np.floating] | pd.Series | None]( data: AnnData | np.ndarray | CSBase | DaskArray, mask: str | M, dim: Literal["obs", "var"], diff --git a/src/scanpy/metrics/_common.py b/src/scanpy/metrics/_common.py index 9b5b09fc6b..c9c90e1dc8 100644 --- a/src/scanpy/metrics/_common.py +++ b/src/scanpy/metrics/_common.py @@ -124,7 +124,7 @@ def _(val: pd.DataFrame | pd.Series) -> NDArray: def _vals_heterogeneous[V: NDArray | CSRBase]( vals: V, -) -> tuple[V, NDArray[np.bool_] | slice, NDArray[np.float64]]: +) -> tuple[V, NDArray[np.bool] | slice, NDArray[np.float64]]: """Check that values wont cause issues in computation. Returns new set of vals, and indexer to put values back into result. diff --git a/src/scanpy/plotting/_anndata.py b/src/scanpy/plotting/_anndata.py index 97d82ab7c1..5e4193441a 100755 --- a/src/scanpy/plotting/_anndata.py +++ b/src/scanpy/plotting/_anndata.py @@ -229,7 +229,7 @@ def _check_if_annotations( other_ax_obj, "var" if axis_name == "obs" else "obs" ).index - def is_annotation(needle: pd.Index) -> NDArray[np.bool_]: + def is_annotation(needle: pd.Index) -> NDArray[np.bool]: return needle.isin({None}) | needle.isin(annotations) | needle.isin(names) if not is_annotation(pd.Index([x, y])).all(): @@ -237,8 +237,8 @@ def is_annotation(needle: pd.Index) -> NDArray[np.bool_]: color_idx = pd.Index(colors if colors is not None else []) # Colors are valid - color_valid: NDArray[np.bool_] = np.fromiter( - map(is_color_like, color_idx), dtype=np.bool_, count=len(color_idx) + color_valid: NDArray[np.bool] = np.fromiter( + map(is_color_like, color_idx), dtype=np.bool, count=len(color_idx) ) # Annotation names are valid too color_valid[~color_valid] = is_annotation(color_idx[~color_valid]) diff --git a/src/scanpy/plotting/_tools/scatterplots.py b/src/scanpy/plotting/_tools/scatterplots.py index f4044a9fa0..0b17bdb108 100644 --- a/src/scanpy/plotting/_tools/scatterplots.py +++ b/src/scanpy/plotting/_tools/scatterplots.py @@ -61,7 +61,7 @@ def embedding( # noqa: PLR0912, PLR0913, PLR0915 basis: str, *, color: str | Sequence[str] | None = None, - mask_obs: NDArray[np.bool_] | str | None = None, + mask_obs: NDArray[np.bool] | str | None = None, gene_symbols: str | None = None, use_raw: bool | None = None, sort_order: bool = True, @@ -1204,7 +1204,7 @@ def _get_color_source_vector( adata: AnnData, value_to_plot: str, *, - mask_obs: NDArray[np.bool_] | None = None, + mask_obs: NDArray[np.bool] | None = None, use_raw: bool = False, gene_symbols: str | None = None, layer: str | None = None, diff --git a/src/scanpy/preprocessing/_highly_variable_genes.py b/src/scanpy/preprocessing/_highly_variable_genes.py index 906f0fb16e..95978d0fa5 100644 --- a/src/scanpy/preprocessing/_highly_variable_genes.py +++ b/src/scanpy/preprocessing/_highly_variable_genes.py @@ -313,7 +313,7 @@ def in_bounds( self, mean: NDArray[np.floating] | DaskArray, dispersion_norm: NDArray[np.floating] | DaskArray, - ) -> NDArray[np.bool_] | DaskArray: + ) -> NDArray[np.bool] | DaskArray: return ( (mean > self.min_mean) & (mean < self.max_mean) @@ -482,7 +482,7 @@ def _subset_genes( mean: NDArray[np.float64] | DaskArray, dispersion_norm: NDArray[np.float64] | DaskArray, cutoff: _Cutoffs | int, -) -> NDArray[np.bool_] | DaskArray: +) -> NDArray[np.bool] | DaskArray: """Get boolean mask of genes with normalized dispersion in bounds.""" if isinstance(cutoff, _Cutoffs): dispersion_norm = np.nan_to_num(dispersion_norm) # similar to Seurat diff --git a/src/scanpy/preprocessing/_pca/__init__.py b/src/scanpy/preprocessing/_pca/__init__.py index b370c09d7b..5c6f3b7bf2 100644 --- a/src/scanpy/preprocessing/_pca/__init__.py +++ b/src/scanpy/preprocessing/_pca/__init__.py @@ -66,7 +66,7 @@ def pca( # noqa: PLR0912, PLR0913, PLR0915 chunk_size: int | None = None, random_state: _LegacyRandom = 0, return_info: bool = False, - mask_var: NDArray[np.bool_] | str | None | Empty = _empty, + mask_var: NDArray[np.bool] | str | None | Empty = _empty, use_highly_variable: bool | None = None, dtype: DTypeLike = "float32", key_added: str | None = None, @@ -403,7 +403,7 @@ def pca( # noqa: PLR0912, PLR0913, PLR0915 def _handle_mask_var( adata: AnnData, - mask_var: NDArray[np.bool_] | str | Empty | None, + mask_var: NDArray[np.bool] | str | Empty | None, *, obsm: str | None = None, use_highly_variable: bool | None, diff --git a/src/scanpy/preprocessing/_scale.py b/src/scanpy/preprocessing/_scale.py index 751cecfdc2..d85fd3223e 100644 --- a/src/scanpy/preprocessing/_scale.py +++ b/src/scanpy/preprocessing/_scale.py @@ -78,7 +78,7 @@ def scale[A: _Array]( copy: bool = False, layer: str | None = None, obsm: str | None = None, - mask_obs: NDArray[np.bool_] | str | None = None, + mask_obs: NDArray[np.bool] | str | None = None, ) -> AnnData | A | None: """Scale data to unit variance and zero mean. @@ -148,7 +148,7 @@ def scale_array[A: _Array]( max_value: float | None = None, copy: bool = False, return_mean_std: bool = False, - mask_obs: NDArray[np.bool_] | None = None, + mask_obs: NDArray[np.bool] | None = None, ) -> ( A | tuple[ @@ -175,7 +175,7 @@ def scale_array[A: _Array]( mask_obs = ( # For CSR matrices, default to a set mask to take the `scale_array_masked` path. # This is faster than the maskless `axis_mul_or_truediv` path. - np.ones(x.shape[0], dtype=np.bool_) + np.ones(x.shape[0], dtype=np.bool) if isinstance(x, CSRBase) and mask_obs is None and not zero_center else _check_mask(x, mask_obs, "obs") ) @@ -219,7 +219,7 @@ def scale_array[A: _Array]( def scale_array_masked[A: _Array]( x: A, - mask_obs: NDArray[np.bool_], + mask_obs: NDArray[np.bool], *, zero_center: bool = True, max_value: float | None = None, @@ -268,7 +268,7 @@ def scale_and_clip_csr( data: NDArray[np.floating], *, std: NDArray[np.floating], - mask_obs: NDArray[np.bool_], + mask_obs: NDArray[np.bool], max_value: float | None, ) -> None: for i in numba.prange(len(indptr) - 1): @@ -289,7 +289,7 @@ def scale_anndata( copy: bool = False, layer: str | None = None, obsm: str | None = None, - mask_obs: NDArray[np.bool_] | str | None = None, + mask_obs: NDArray[np.bool] | str | None = None, ) -> AnnData | None: adata = adata.copy() if copy else adata str_mean_std = ("mean", "std") diff --git a/src/scanpy/preprocessing/_scrublet/core.py b/src/scanpy/preprocessing/_scrublet/core.py index a5cb80fd43..b7eb1e4785 100644 --- a/src/scanpy/preprocessing/_scrublet/core.py +++ b/src/scanpy/preprocessing/_scrublet/core.py @@ -89,7 +89,7 @@ class Scrublet: # Fields set by methods - predicted_doublets_: NDArray[np.bool_] | None = field(init=False) + predicted_doublets_: NDArray[np.bool] | None = field(init=False) """(shape: n_cells) Boolean mask of predicted doublets in the observed transcriptomes. """ @@ -354,7 +354,7 @@ def _nearest_neighbor_classifier( if use_approx_neighbors: neighbors = neighbors[:, 1:] # Calculate doublet score based on ratio of simulated cell neighbors vs. observed cell neighbors - doub_neigh_mask: NDArray[np.bool_] = ( + doub_neigh_mask: NDArray[np.bool] = ( manifold.obs["doub_labels"].to_numpy()[neighbors] == "sim" ) n_sim_neigh: NDArray[np.int64] = doub_neigh_mask.sum(axis=1) @@ -403,7 +403,7 @@ def _nearest_neighbor_classifier( def call_doublets( self, *, threshold: float | None = None, verbose: bool = True - ) -> NDArray[np.bool_] | None: + ) -> NDArray[np.bool] | None: """Call trancriptomes as doublets or singlets. Parameters diff --git a/src/scanpy/preprocessing/_simple.py b/src/scanpy/preprocessing/_simple.py index 9eebeb0246..7291545c4e 100644 --- a/src/scanpy/preprocessing/_simple.py +++ b/src/scanpy/preprocessing/_simple.py @@ -853,11 +853,11 @@ def sample( fraction: float | None = None, *, n: int | None = None, - rng: RNGLike | SeedLike | None = 0, + rng: RNGLike | SeedLike | None = None, copy: Literal[False] = False, replace: bool = False, axis: Literal["obs", 0, "var", 1] = "obs", - p: str | NDArray[np.bool_] | NDArray[np.floating] | None = None, + p: str | NDArray[np.bool] | NDArray[np.floating] | None = None, ) -> None: ... @overload def sample( @@ -869,7 +869,7 @@ def sample( copy: Literal[True], replace: bool = False, axis: Literal["obs", 0, "var", 1] = "obs", - p: str | NDArray[np.bool_] | NDArray[np.floating] | None = None, + p: str | NDArray[np.bool] | NDArray[np.floating] | None = None, ) -> AnnData: ... @overload def sample[A: np.ndarray | CSBase | DaskArray]( @@ -881,7 +881,7 @@ def sample[A: np.ndarray | CSBase | DaskArray]( copy: bool = False, replace: bool = False, axis: Literal["obs", 0, "var", 1] = "obs", - p: str | NDArray[np.bool_] | NDArray[np.floating] | None = None, + p: str | NDArray[np.bool] | NDArray[np.floating] | None = None, ) -> tuple[A, NDArray[np.int64]]: ... def sample( # noqa: PLR0912 data: AnnData | np.ndarray | CSBase | DaskArray, @@ -892,7 +892,7 @@ def sample( # noqa: PLR0912 copy: bool = False, replace: bool = False, axis: Literal["obs", 0, "var", 1] = "obs", - p: str | NDArray[np.bool_] | NDArray[np.floating] | None = None, + p: str | NDArray[np.bool] | NDArray[np.floating] | None = None, ) -> AnnData | None | tuple[np.ndarray | CSBase | DaskArray, NDArray[np.int64]]: r"""Sample observations or variables with or without replacement. diff --git a/src/scanpy/tools/_rank_genes_groups.py b/src/scanpy/tools/_rank_genes_groups.py index 9c1f012c83..afc2e9c5c5 100644 --- a/src/scanpy/tools/_rank_genes_groups.py +++ b/src/scanpy/tools/_rank_genes_groups.py @@ -84,8 +84,8 @@ def _tiecorrect(rankvals: NDArray[np.number]) -> NDArray[np.float64]: def _ranks( x: NDArray[np.number] | CSBase, /, - mask_obs: NDArray[np.bool_] | None = None, - mask_obs_rest: NDArray[np.bool_] | None = None, + mask_obs: NDArray[np.bool] | None = None, + mask_obs_rest: NDArray[np.bool] | None = None, ) -> Generator[tuple[NDArray[np.float64], int, int], None, None]: n_genes = x.shape[1] @@ -125,7 +125,7 @@ def __init__( groups: Iterable[str] | Literal["all"], groupby: str, *, - mask_var: NDArray[np.bool_] | None = None, + mask_var: NDArray[np.bool] | None = None, reference: Literal["rest"] | str = "rest", use_raw: bool = True, layer: str | None = None, @@ -507,7 +507,7 @@ def rank_genes_groups( # noqa: PLR0912, PLR0913, PLR0915 adata: AnnData, groupby: str, *, - mask_var: NDArray[np.bool_] | str | None = None, + mask_var: NDArray[np.bool] | str | None = None, use_raw: bool | None = None, groups: Literal["all"] | Iterable[str] = "all", reference: str = "rest", diff --git a/src/scanpy/tools/_utils_clustering.py b/src/scanpy/tools/_utils_clustering.py index c64e92bf65..eb6dc2ecb9 100644 --- a/src/scanpy/tools/_utils_clustering.py +++ b/src/scanpy/tools/_utils_clustering.py @@ -19,7 +19,7 @@ def rename_groups( *, key_added: str | None, restrict_categories: Iterable[str], - restrict_indices: NDArray[np.bool_], + restrict_indices: NDArray[np.bool], groups: NDArray, ) -> pd.Series[str]: key_added = f"{restrict_key}_R" if key_added is None else key_added @@ -36,7 +36,7 @@ def restrict_adjacency( *, restrict_categories: Sequence[str], adjacency: CSBase, -) -> tuple[CSBase, NDArray[np.bool_]]: +) -> tuple[CSBase, NDArray[np.bool]]: if not isinstance(restrict_categories[0], str): msg = "You need to use strings to label categories, e.g. '1' instead of 1." raise ValueError(msg) diff --git a/src/testing/scanpy/_helpers/__init__.py b/src/testing/scanpy/_helpers/__init__.py index d84a9bed18..37ab9fb258 100644 --- a/src/testing/scanpy/_helpers/__init__.py +++ b/src/testing/scanpy/_helpers/__init__.py @@ -186,7 +186,7 @@ def maybe_dask_process_context(): dask.config.set(scheduler=prev_scheduler) -def random_mask(n: int, *, rng: np.random.Generator | None = None) -> NDArray[np.bool_]: +def random_mask(n: int, *, rng: np.random.Generator | None = None) -> NDArray[np.bool]: """Generate a random mask. Makes sure that at least 2 mask entries are True and at least 2 are False. diff --git a/tests/test_preprocessing.py b/tests/test_preprocessing.py index 29a71cbfc8..82087ed6fe 100644 --- a/tests/test_preprocessing.py +++ b/tests/test_preprocessing.py @@ -164,7 +164,7 @@ def test_sample( axis: Literal[0, 1], f_or_n: float | int, # noqa: PYI041 replace: bool, - ps: dict[Literal["obs", "var"], NDArray[np.bool_] | None], + ps: dict[Literal["obs", "var"], NDArray[np.bool] | None], ): adata = AnnData(array_type(np.ones((200, 10)))) p = ps["obs" if axis == 0 else "var"] diff --git a/tests/test_utils.py b/tests/test_utils.py index b82deea324..02e37dcabc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -173,7 +173,7 @@ def test_check_nonnegative_integers(array_type, array_value, expected): # compute received = received.compute() assert not isinstance(received, DaskArray) - if isinstance(received, np.bool_): + if isinstance(received, np.bool): # convert to python bool received = received.item() assert received is expected