diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index bfe6173b..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,15 +0,0 @@ -recursive-include sparse *.py -recursive-include sparse *.html -recursive-include docs *.py -recursive-include docs *.rst -recursive-include docs *.png - -include setup.py -include README.rst -include LICENSE -include MANIFEST.in -include requirements.txt -recursive-include requirements *.txt - -prune docs/_build -include versioneer.py diff --git a/pyproject.toml b/pyproject.toml index b47d1c02..12e13067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "sparse" dynamic = ["version"] description = "Sparse n-dimensional arrays for the PyData ecosystem" readme = "README.rst" -dependencies = ["numpy>=1.17", "scipy>=0.19", "numba>=0.49"] +dependencies = ["numpy>=1.17", "numba>=0.49"] maintainers = [{ name = "Hameer Abbasi", email = "hameerabbasi@yahoo.com" }] requires-python = ">=3.8" license = { file = "LICENSE" } @@ -28,12 +28,13 @@ classifiers = [ ] [project.optional-dependencies] -docs = ["sphinx", "sphinx_rtd_theme"] +docs = ["sphinx", "sphinx_rtd_theme", "scipy"] tests = [ "dask[array]", "pytest>=3.5", "pytest-cov", "pre-commit", + "scipy", ] tox = ["sparse[tests]", "tox"] all = ["sparse[docs,tox]", "matrepr"] diff --git a/sparse/_common.py b/sparse/_common.py index 27af54fb..e43daeae 100644 --- a/sparse/_common.py +++ b/sparse/_common.py @@ -9,8 +9,6 @@ from numba import literal_unroll import numpy as np -import scipy.sparse -from scipy.sparse import spmatrix from ._coo.common import asCOO from ._sparse_array import SparseArray @@ -22,6 +20,22 @@ ) +def _is_scipy_sparse_obj(x): + """ + Tests if the supplied argument is a SciPy sparse object. + """ + if hasattr(x, "__module__") and x.__module__.startswith("scipy.sparse"): + return True + return False + + +def _is_sparse(x): + """ + Tests if the supplied argument is a SciPy sparse object, or one from this library. + """ + return isinstance(x, SparseArray) or _is_scipy_sparse_obj(x) + + @numba.njit def nan_check(*args): """ @@ -61,7 +75,7 @@ def check_class_nan(test): if isinstance(test, (GCXS, COO)): return nan_check(test.fill_value, test.data) - if isinstance(test, spmatrix): + if _is_scipy_sparse_obj(test): return nan_check(test.data) return nan_check(test) @@ -99,9 +113,9 @@ def tensordot(a, b, axes=2, *, return_type=None): # Please see license at https://github.com/numpy/numpy/blob/main/LICENSE.txt check_zero_fill_value(a, b) - if scipy.sparse.issparse(a): + if _is_scipy_sparse_obj(a): a = GCXS.from_scipy_sparse(a) - if scipy.sparse.issparse(b): + if _is_scipy_sparse_obj(b): b = GCXS.from_scipy_sparse(b) try: @@ -2047,6 +2061,7 @@ def asarray(obj, /, *, dtype=None, format="coo", backend="pydata", device=None, >>> sparse.asarray(x, format="COO") """ + if format not in {"coo", "dok", "gcxs"}: raise ValueError(f"{format} format not supported.") @@ -2065,7 +2080,7 @@ def asarray(obj, /, *, dtype=None, format="coo", backend="pydata", device=None, if isinstance(obj, (COO, DOK, GCXS)): return obj.asformat(format) - if isinstance(obj, spmatrix): + if _is_scipy_sparse_obj(obj): sparse_obj = format_dict[format].from_scipy_sparse(obj) if dtype is None: dtype = sparse_obj.dtype diff --git a/sparse/_compressed/compressed.py b/sparse/_compressed/compressed.py index 01281150..8d10c6ac 100644 --- a/sparse/_compressed/compressed.py +++ b/sparse/_compressed/compressed.py @@ -4,7 +4,6 @@ from functools import reduce import numpy as np -import scipy.sparse as ss from numpy.lib.mixins import NDArrayOperatorsMixin from .._coo.common import linear_loc @@ -140,7 +139,9 @@ def __init__( fill_value=0, idx_dtype=None, ): - if isinstance(arg, ss.spmatrix): + from .._common import _is_scipy_sparse_obj + + if _is_scipy_sparse_obj(arg): arg = self.from_scipy_sparse(arg) if isinstance(arg, np.ndarray): @@ -482,6 +483,7 @@ def to_scipy_sparse(self): ValueError If all the array doesn't zero fill-values. """ + import scipy.sparse check_zero_fill_value(self) @@ -489,9 +491,9 @@ def to_scipy_sparse(self): raise ValueError("Can only convert a 2-dimensional array to a Scipy sparse matrix.") if 0 in self.compressed_axes: - return ss.csr_matrix((self.data, self.indices, self.indptr), shape=self.shape) + return scipy.sparse.csr_matrix((self.data, self.indices, self.indptr), shape=self.shape) - return ss.csc_matrix((self.data, self.indices, self.indptr), shape=self.shape) + return scipy.sparse.csc_matrix((self.data, self.indices, self.indptr), shape=self.shape) def asformat(self, format, **kwargs): """ diff --git a/sparse/_coo/common.py b/sparse/_coo/common.py index b298753f..e25163a5 100644 --- a/sparse/_coo/common.py +++ b/sparse/_coo/common.py @@ -7,7 +7,6 @@ import numba import numpy as np -import scipy.sparse from .._sparse_array import SparseArray from .._utils import ( @@ -43,9 +42,10 @@ def asCOO(x, name="asCOO", check=True): ValueError If ``check`` is true and a dense input is supplied. """ + from .._common import _is_sparse from .core import COO - if check and not isinstance(x, (SparseArray, scipy.sparse.spmatrix)): + if check and not _is_sparse(x): raise ValueError(f"Performing this operation would produce a dense result: {name}") if not isinstance(x, COO): @@ -94,13 +94,14 @@ def kron(a, b): [0, 0, 0, 1, 2, 3, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 2, 3]], dtype=int64) """ + from .._common import _is_sparse from .._umath import _cartesian_product from .core import COO check_zero_fill_value(a, b) - a_sparse = isinstance(a, (SparseArray, scipy.sparse.spmatrix)) - b_sparse = isinstance(b, (SparseArray, scipy.sparse.spmatrix)) + a_sparse = _is_sparse(a) + b_sparse = _is_sparse(b) a_ndim = np.ndim(a) b_ndim = np.ndim(b) @@ -1346,9 +1347,10 @@ def take(x, indices, /, *, axis=None): def _validate_coo_input(x: Any): + from .._common import _is_scipy_sparse_obj from .core import COO - if isinstance(x, scipy.sparse.spmatrix): + if _is_scipy_sparse_obj(x): x = COO.from_scipy_sparse(x) elif not isinstance(x, SparseArray): raise ValueError(f"Input must be an instance of SparseArray, but it's {type(x)}.") diff --git a/sparse/_coo/core.py b/sparse/_coo/core.py index 9b68b6cf..340e64d8 100644 --- a/sparse/_coo/core.py +++ b/sparse/_coo/core.py @@ -8,7 +8,6 @@ import numba import numpy as np -import scipy.sparse from numpy.lib.mixins import NDArrayOperatorsMixin from .._sparse_array import SparseArray @@ -1177,6 +1176,8 @@ def to_scipy_sparse(self): COO.tocsr : Convert to a :obj:`scipy.sparse.csr_matrix`. COO.tocsc : Convert to a :obj:`scipy.sparse.csc_matrix`. """ + import scipy.sparse + check_zero_fill_value(self) if self.ndim != 2: @@ -1187,6 +1188,8 @@ def to_scipy_sparse(self): return result def _tocsr(self): + import scipy.sparse + if self.ndim != 2: raise ValueError("This array must be two-dimensional for this conversion to work.") row, col = self.coords @@ -1557,6 +1560,8 @@ def as_coo(x, shape=None, fill_value=None, idx_dtype=None): COO.from_iter : Convert an iterable to :obj:`COO`. """ + from .._common import _is_scipy_sparse_obj + if hasattr(x, "shape") and shape is not None: raise ValueError("Cannot provide a shape in combination with something that already has a shape.") @@ -1569,7 +1574,7 @@ def as_coo(x, shape=None, fill_value=None, idx_dtype=None): if isinstance(x, np.ndarray) or np.isscalar(x): return COO.from_numpy(x, fill_value=fill_value, idx_dtype=idx_dtype) - if isinstance(x, scipy.sparse.spmatrix): + if _is_scipy_sparse_obj(x): return COO.from_scipy_sparse(x) if isinstance(x, (Iterable, Iterator)): diff --git a/sparse/_dok.py b/sparse/_dok.py index ae587429..95f9b29d 100644 --- a/sparse/_dok.py +++ b/sparse/_dok.py @@ -2,7 +2,6 @@ from numbers import Integral import numpy as np -import scipy.sparse from numpy.lib.mixins import NDArrayOperatorsMixin from ._slicing import normalize_index @@ -92,6 +91,7 @@ class DOK(SparseArray, NDArrayOperatorsMixin): """ def __init__(self, shape, data=None, dtype=None, fill_value=None): + from ._common import _is_scipy_sparse_obj from ._coo import COO self.data = {} @@ -106,7 +106,7 @@ def __init__(self, shape, data=None, dtype=None, fill_value=None): self._make_shallow_copy_of(ar) return - if isinstance(shape, scipy.sparse.spmatrix): + if _is_scipy_sparse_obj(shape): ar = DOK.from_scipy_sparse(shape) self._make_shallow_copy_of(ar) return diff --git a/sparse/_sparse_array.py b/sparse/_sparse_array.py index fbcfd9d2..063eaced 100644 --- a/sparse/_sparse_array.py +++ b/sparse/_sparse_array.py @@ -8,7 +8,6 @@ from typing import Callable import numpy as np -import scipy.sparse as ss from ._umath import elemwise from ._utils import _zero_of_dtype, equivalent, html_table, normalize_axis @@ -298,10 +297,12 @@ def __array_function__(self, func, types, args, kwargs): @staticmethod def _reduce(method, *args, **kwargs): + from ._common import _is_scipy_sparse_obj + assert len(args) == 1 self = args[0] - if isinstance(self, ss.spmatrix): + if _is_scipy_sparse_obj(self): self = type(self).from_scipy_sparse(self) return self.reduce(method, **kwargs) diff --git a/sparse/_umath.py b/sparse/_umath.py index 1162e7fc..25715f1b 100644 --- a/sparse/_umath.py +++ b/sparse/_umath.py @@ -4,7 +4,6 @@ import numba import numpy as np -import scipy.sparse from ._utils import _zero_of_dtype, equivalent, isscalar @@ -399,6 +398,7 @@ def __init__(self, func, *args, **kwargs): **kwargs : dict Extra arguments to pass to the function. """ + from ._common import _is_scipy_sparse_obj from ._compressed import GCXS from ._coo import COO from ._dok import DOK @@ -422,7 +422,7 @@ def __init__(self, func, *args, **kwargs): out_type = COO for arg in args: - if isinstance(arg, scipy.sparse.spmatrix): + if _is_scipy_sparse_obj(arg): processed_args.append(COO.from_scipy_sparse(arg)) elif isscalar(arg) or isinstance(arg, np.ndarray): # Faster and more reliable to pass ()-shaped ndarrays as scalars.