From 775fa3b082dfcaa2cbd5c03704ba6758a08845f1 Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Tue, 21 Jan 2025 18:07:32 -0800 Subject: [PATCH] Move scipy to optional requirements (#1233) --- .github/workflows/run_coverage.yml | 2 +- .readthedocs.yaml | 2 +- CHANGELOG.md | 1 + pyproject.toml | 4 ++-- src/hdmf/common/sparse.py | 22 +++++++++++++++++----- tests/unit/common/test_sparse.py | 22 +++++++++++++++++++--- tox.ini | 2 +- 7 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.github/workflows/run_coverage.yml b/.github/workflows/run_coverage.yml index ee1f9ff91..330bb7aba 100644 --- a/.github/workflows/run_coverage.yml +++ b/.github/workflows/run_coverage.yml @@ -56,7 +56,7 @@ jobs: - name: Install package with optional dependencies if: ${{ matrix.opt_req }} run: | - python -m pip install ".[test,tqdm,zarr,termset]" + python -m pip install ".[test,tqdm,sparse,zarr,termset]" - name: Run tests and generate coverage report run: | diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b752396f4..f17c323b1 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -24,7 +24,7 @@ formats: all # Optionally set the version of Python and requirements required to build your docs python: install: - - path: .[docs,tqdm,zarr,termset] # path to the package relative to the root + - path: .[docs,tqdm,sparse,zarr,termset] # path to the package relative to the root # Optionally include all submodules submodules: diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ab56940..8c72205cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Importing from `hdmf.build.map` is no longer supported. Import from `hdmf.build` instead. @rly [#1221](https://github.com/hdmf-dev/hdmf/pull/1221) - Python 3.8 has reached end of life. Dropped support for Python 3.8 and add support for Python 3.13. @mavaylon1 [#1209](https://github.com/hdmf-dev/hdmf/pull/1209) - Support for Zarr is limited to versions < 3. @rly [#1229](https://github.com/hdmf-dev/hdmf/pull/1229) +- Scipy is no longer a required dependency. Users using the `CSRMatrix` data type should install `scipy` separately or with `pip install "hdmf[sparse]"`. @rly [#1140](https://github.com/hdmf-dev/hdmf/pull/1140) ### Changed - Added checks to ensure that group and dataset spec names and default names do not contain slashes. @bendichter [#1219](https://github.com/hdmf-dev/hdmf/pull/1219) diff --git a/pyproject.toml b/pyproject.toml index 5308543d4..5a58b6cef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,13 +35,13 @@ dependencies = [ 'numpy>=1.19.3', "pandas>=1.2.0", "ruamel.yaml>=0.16", - "scipy>=1.7", ] dynamic = ["version"] [project.optional-dependencies] tqdm = ["tqdm>=4.41.0"] zarr = ["zarr>=2.12.0,<3"] +sparse = ["scipy>=1.7"] termset = [ "linkml-runtime>=1.5.5", "schemasheets>=0.1.23", @@ -70,7 +70,7 @@ docs = [ ] # all possible dependencies -all = ["hdmf[tqdm,zarr,termset,test,docs]"] +all = ["hdmf[tqdm,zarr,sparse,termset,test,docs]"] [project.urls] "Homepage" = "https://github.com/hdmf-dev/hdmf" diff --git a/src/hdmf/common/sparse.py b/src/hdmf/common/sparse.py index db38d12e8..0dd7d9654 100644 --- a/src/hdmf/common/sparse.py +++ b/src/hdmf/common/sparse.py @@ -1,4 +1,11 @@ -import scipy.sparse as sps +try: + from scipy.sparse import csr_matrix + SCIPY_INSTALLED = True +except ImportError: + SCIPY_INSTALLED = False + class csr_matrix: # dummy class to prevent import errors + pass + from . import register_class from ..container import Container from ..utils import docval, popargs, to_uint_array, get_data_shape, AllowPositional @@ -7,7 +14,7 @@ @register_class('CSRMatrix') class CSRMatrix(Container): - @docval({'name': 'data', 'type': (sps.csr_matrix, 'array_data'), + @docval({'name': 'data', 'type': (csr_matrix, 'array_data'), 'doc': 'the data to use for this CSRMatrix or CSR data array.' 'If passing CSR data array, *indices*, *indptr*, and *shape* must also be provided'}, {'name': 'indices', 'type': 'array_data', 'doc': 'CSR index array', 'default': None}, @@ -16,13 +23,17 @@ class CSRMatrix(Container): {'name': 'name', 'type': str, 'doc': 'the name to use for this when storing', 'default': 'csr_matrix'}, allow_positional=AllowPositional.WARNING) def __init__(self, **kwargs): + if not SCIPY_INSTALLED: + raise ImportError( + "scipy must be installed to use CSRMatrix. Please install scipy using `pip install scipy`." + ) data, indices, indptr, shape = popargs('data', 'indices', 'indptr', 'shape', kwargs) super().__init__(**kwargs) - if not isinstance(data, sps.csr_matrix): + if not isinstance(data, csr_matrix): temp_shape = get_data_shape(data) temp_ndim = len(temp_shape) if temp_ndim == 2: - data = sps.csr_matrix(data) + data = csr_matrix(data) elif temp_ndim == 1: if any(_ is None for _ in (indptr, indices, shape)): raise ValueError("Must specify 'indptr', 'indices', and 'shape' arguments when passing data array.") @@ -31,9 +42,10 @@ def __init__(self, **kwargs): shape = self.__check_arr(shape, 'shape') if len(shape) != 2: raise ValueError("'shape' argument must specify two and only two dimensions.") - data = sps.csr_matrix((data, indices, indptr), shape=shape) + data = csr_matrix((data, indices, indptr), shape=shape) else: raise ValueError("'data' argument cannot be ndarray of dimensionality > 2.") + # self.__data is a scipy.sparse.csr_matrix self.__data = data @staticmethod diff --git a/tests/unit/common/test_sparse.py b/tests/unit/common/test_sparse.py index 7d94231f4..720f1f473 100644 --- a/tests/unit/common/test_sparse.py +++ b/tests/unit/common/test_sparse.py @@ -1,10 +1,25 @@ from hdmf.common import CSRMatrix from hdmf.testing import TestCase, H5RoundTripMixin - -import scipy.sparse as sps import numpy as np +import unittest + +try: + import scipy.sparse as sps + SCIPY_INSTALLED = True +except ImportError: + SCIPY_INSTALLED = False + + +@unittest.skipIf(SCIPY_INSTALLED, "scipy is installed") +class TestCSRMatrixNoScipy(TestCase): + def test_import_error(self): + data = np.array([[1, 0, 2], [0, 0, 3], [4, 5, 6]]) + with self.assertRaises(ImportError): + CSRMatrix(data=data) + +@unittest.skipIf(not SCIPY_INSTALLED, "scipy is not installed") class TestCSRMatrix(TestCase): def test_from_sparse_matrix(self): @@ -153,7 +168,7 @@ def test_array_bad_dim(self): with self.assertRaisesWith(ValueError, msg): CSRMatrix(data=data, indices=indices, indptr=indptr, shape=shape) - +@unittest.skipIf(not SCIPY_INSTALLED, "scipy is not installed") class TestCSRMatrixRoundTrip(H5RoundTripMixin, TestCase): def setUpContainer(self): @@ -164,6 +179,7 @@ def setUpContainer(self): return CSRMatrix(data=data, indices=indices, indptr=indptr, shape=shape) +@unittest.skipIf(not SCIPY_INSTALLED, "scipy is not installed") class TestCSRMatrixRoundTripFromLists(H5RoundTripMixin, TestCase): """Test that CSRMatrix works with lists as well""" diff --git a/tox.ini b/tox.ini index 775cb7592..4caa68a4b 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,7 @@ extras = # which optional dependency set(s) to use (default: none) pytest: test gallery: doc - optional: tqdm,zarr,termset + optional: tqdm,sparse,zarr,termset commands = # commands to run for every environment python --version # print python version for debugging