Repository: scipy/scipy
Pull request: DEP: improve message for deprecated private API for `sparse`
<!-- 
Thanks for contributing a pull request! Please ensure that
your PR satisfies the checklist before submitting:
http://scipy.github.io/devdocs/dev/contributor/development_workflow.html#checklist-before-submitting-a-pr

Also, please name and describe your PR as you would write a
commit message:
http://scipy.github.io/devdocs/dev/contributor/development_workflow.html#writing-the-commit-message

Depending on your changes, you can skip CI operations and save time and energy: 
http://scipy.github.io/devdocs/dev/contributor/continuous_integration.html#skipping

Note that we are a team of volunteers; we appreciate your
patience during the review process.

Again, thanks for contributing!
-->

#### Reference issue
<!--Example: Closes -->
Solving a part of issue gh-18279
closes #19241
#### What does this implement/fix?
<!--Please explain your changes.-->
Finish #18279 for `sparse`


#### Additional information
<!--Any additional information you think is important.-->


Problem statement:
BUG: utils from scipy.sparse.sputils are not available from scipy.sparse module
### Describe your issue.

It seems that the deprecation warning of the `scipy.sparse.sputils` is incorrect. The utilities are not available from the `scipy.sparse` module.

This might be on purpose but then the deprecation warning is misleading.

### Reproducing Code Example

```python
In [2]: from scipy.sparse.sputils import get_index_dtype
<ipython-input-2-8d76e9dbe0fa>:1: DeprecationWarning: Please use `get_index_dtype` from the `scipy.sparse` namespace, the `scipy.sparse.sputils` namespace is deprecated.
  from scipy.sparse.sputils import get_index_dtype

In [3]: from scipy.sparse import get_index_dtype
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[3], line 1
----> 1 from scipy.sparse import get_index_dtype

ImportError: cannot import name 'get_index_dtype' from 'scipy.sparse' (/home/glemaitre/mambaforge/envs/dev/lib/python3.10/site-packages/scipy/sparse/__init__.py)
```


### Error message

```shell
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[3], line 1
----> 1 from scipy.sparse import get_index_dtype

ImportError: cannot import name 'get_index_dtype' from 'scipy.sparse' (/home/glemaitre/mambaforge/envs/dev/lib/python3.10/site-packages/scipy/sparse/__init__.py)
```


### SciPy/NumPy/Python version and system information

```shell
In [4]: import scipy

In [5]: scipy.__version__
Out[5]: '1.11.2'
```

Hints from issue discussion:

Commit message: DEP scipy#18279 improve deprecated modules

Patch to review:
diff --git a/scipy/_lib/tests/test_public_api.py b/scipy/_lib/tests/test_public_api.py
index 4446a50af009..19a9bc26e627 100644
--- a/scipy/_lib/tests/test_public_api.py
+++ b/scipy/_lib/tests/test_public_api.py
@@ -339,6 +339,15 @@ def check_importable(module_name):
                           ('scipy.optimize.slsqp', None),
                           ('scipy.optimize.tnc', None),
                           ('scipy.optimize.zeros', None),
+                          ('scipy.sparse.lil', None),
+                          ('scipy.sparse.linalg.dsolve', 'linalg'),
+                          ('scipy.sparse.linalg.eigen', 'linalg'),
+                          ('scipy.sparse.linalg.interface', 'linalg'),
+                          ('scipy.sparse.linalg.isolve', 'linalg'),
+                          ('scipy.sparse.linalg.matfuncs', 'linalg'),
+                          ('scipy.sparse.sparsetools', None),
+                          ('scipy.sparse.spfuncs', None),
+                          ('scipy.sparse.sputils', None),
                           ('scipy.stats.biasedurn', None),
                           ('scipy.stats.kde', None),
                           ('scipy.stats.morestats', None),
@@ -346,6 +355,7 @@ def check_importable(module_name):
                           ('scipy.stats.mstats_extras', 'mstats'),
                           ('scipy.stats.mvn', None),
                           ('scipy.stats.stats', None)])
+
 def test_private_but_present_deprecation(module_name, correct_module):
     # gh-18279, gh-17572, gh-17771 noted that deprecation warnings
     # for imports from private modules
diff --git a/scipy/sparse/base.py b/scipy/sparse/base.py
index d4a1ae43c638..913e4ac5ea46 100644
--- a/scipy/sparse/base.py
+++ b/scipy/sparse/base.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _base
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -30,13 +29,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.base is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.base` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_base, name)
+    return _sub_module_deprecation(sub_package="sparse", module="base",
+                                   private_modules=["_base"], all=__all__,
+                                   attribute=name)
\ No newline at end of file
diff --git a/scipy/sparse/bsr.py b/scipy/sparse/bsr.py
index b7666e200a86..f9696db666b9 100644
--- a/scipy/sparse/bsr.py
+++ b/scipy/sparse/bsr.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _bsr
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -16,11 +15,9 @@
     'bsr_transpose',
     'check_shape',
     'csr_matmat_maxnnz',
-    'get_index_dtype',
     'getdata',
     'getdtype',
     'isshape',
-    'isspmatrix',
     'isspmatrix_bsr',
     'spmatrix',
     'to_native',
@@ -34,13 +31,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.bsr is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.bsr` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_bsr, name)
+    return _sub_module_deprecation(sub_package="sparse", module="bsr",
+                                   private_modules=["_bsr"], all=__all__,
+                                   attribute=name)
\ No newline at end of file
diff --git a/scipy/sparse/compressed.py b/scipy/sparse/compressed.py
index a40d2edb0ee4..e6dc8a73e5ab 100644
--- a/scipy/sparse/compressed.py
+++ b/scipy/sparse/compressed.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _compressed
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -19,7 +18,6 @@
     'csr_todense',
     'downcast_intp_index',
     'get_csr_submatrix',
-    'get_index_dtype',
     'get_sum_dtype',
     'getdtype',
     'is_pydata_spmatrix',
@@ -27,9 +25,7 @@
     'isintlike',
     'isscalarlike',
     'isshape',
-    'isspmatrix',
     'operator',
-    'spmatrix',
     'to_native',
     'upcast',
     'upcast_char',
@@ -42,13 +38,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.compressed is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.compressed` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_compressed, name)
+    return _sub_module_deprecation(sub_package="sparse", module="compressed",
+                                   private_modules=["_compressed"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/construct.py b/scipy/sparse/construct.py
index 647d11fa226e..85a284c93eac 100644
--- a/scipy/sparse/construct.py
+++ b/scipy/sparse/construct.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _construct
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -41,13 +40,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.construct is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.construct` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_construct, name)
+    return _sub_module_deprecation(sub_package="sparse", module="construct",
+                                   private_modules=["_construct"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/coo.py b/scipy/sparse/coo.py
index 92f6e83d8cef..bda2da3d09a6 100644
--- a/scipy/sparse/coo.py
+++ b/scipy/sparse/coo.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _coo
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -15,11 +14,9 @@
     'coo_tocsr',
     'coo_todense',
     'downcast_intp_index',
-    'get_index_dtype',
     'getdata',
     'getdtype',
     'isshape',
-    'isspmatrix',
     'isspmatrix_coo',
     'operator',
     'spmatrix',
@@ -35,13 +32,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.coo is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.coo` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_coo, name)
+    return _sub_module_deprecation(sub_package="sparse", module="coo",
+                                   private_modules=["_coo"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/csc.py b/scipy/sparse/csc.py
index 1a941a647b97..d140b841e072 100644
--- a/scipy/sparse/csc.py
+++ b/scipy/sparse/csc.py
@@ -2,15 +2,13 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _csc
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
     'csc_matrix',
     'csc_tocsr',
     'expandptr',
-    'get_index_dtype',
     'isspmatrix_csc',
     'spmatrix',
     'upcast',
@@ -22,13 +20,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.csc is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.csc` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_csc, name)
+    return _sub_module_deprecation(sub_package="sparse", module="csc",
+                                   private_modules=["_csc"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/csr.py b/scipy/sparse/csr.py
index 8c6dc63d2176..86bb1e072ebe 100644
--- a/scipy/sparse/csr.py
+++ b/scipy/sparse/csr.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _csr
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -12,7 +11,6 @@
     'csr_tobsr',
     'csr_tocsc',
     'get_csr_submatrix',
-    'get_index_dtype',
     'isspmatrix_csr',
     'spmatrix',
     'upcast',
@@ -24,13 +22,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.csr is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.csr` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_csr, name)
+    return _sub_module_deprecation(sub_package="sparse", module="csr",
+                                   private_modules=["_csr"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/data.py b/scipy/sparse/data.py
index f720816e82bb..73ca31804925 100644
--- a/scipy/sparse/data.py
+++ b/scipy/sparse/data.py
@@ -2,16 +2,11 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _data
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
     'isscalarlike',
-    'matrix',
-    'name',
-    'npfunc',
-    'spmatrix',
     'validateaxis',
 ]
 
@@ -21,13 +16,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.data is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.data` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_data, name)
+    return _sub_module_deprecation(sub_package="sparse", module="data",
+                                   private_modules=["_data"], all=__all__,
+                                   attribute=name)
\ No newline at end of file
diff --git a/scipy/sparse/dia.py b/scipy/sparse/dia.py
index fc129c989df7..f79abd39f114 100644
--- a/scipy/sparse/dia.py
+++ b/scipy/sparse/dia.py
@@ -2,19 +2,16 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _dia
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
     'check_shape',
     'dia_matrix',
     'dia_matvec',
-    'get_index_dtype',
     'get_sum_dtype',
     'getdtype',
     'isshape',
-    'isspmatrix',
     'isspmatrix_dia',
     'spmatrix',
     'upcast_char',
@@ -27,13 +24,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.dia is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.dia` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_dia, name)
+    return _sub_module_deprecation(sub_package="sparse", module="dia",
+                                   private_modules=["_dia"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/dok.py b/scipy/sparse/dok.py
index d4a60ebe7be9..847824456eaa 100644
--- a/scipy/sparse/dok.py
+++ b/scipy/sparse/dok.py
@@ -2,21 +2,18 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _dok
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
     'IndexMixin',
     'check_shape',
     'dok_matrix',
-    'get_index_dtype',
     'getdtype',
     'isdense',
     'isintlike',
     'isscalarlike',
     'isshape',
-    'isspmatrix',
     'isspmatrix_dok',
     'itertools',
     'spmatrix',
@@ -30,13 +27,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.dok is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.dok` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_dok, name)
+    return _sub_module_deprecation(sub_package="sparse", module="dok",
+                                   private_modules=["_dok"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/extract.py b/scipy/sparse/extract.py
index ebe920e70177..be5e161b6f99 100644
--- a/scipy/sparse/extract.py
+++ b/scipy/sparse/extract.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _extract
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -19,13 +18,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.extract is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.extract` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_extract, name)
+    return _sub_module_deprecation(sub_package="sparse", module="extract",
+                                   private_modules=["_extract"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/lil.py b/scipy/sparse/lil.py
index e354393cc003..31e5f20e4887 100644
--- a/scipy/sparse/lil.py
+++ b/scipy/sparse/lil.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _lil
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -12,11 +11,9 @@
     'bisect_left',
     'check_reshape_kwargs',
     'check_shape',
-    'get_index_dtype',
     'getdtype',
     'isscalarlike',
     'isshape',
-    'isspmatrix',
     'isspmatrix_lil',
     'lil_matrix',
     'spmatrix',
@@ -29,13 +26,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.lil is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.lil` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_lil, name)
+    return _sub_module_deprecation(sub_package="sparse", module="lil",
+                                   private_modules=["_lil"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/linalg/dsolve.py b/scipy/sparse/linalg/dsolve.py
index 922484211000..a4ddb056152b 100644
--- a/scipy/sparse/linalg/dsolve.py
+++ b/scipy/sparse/linalg/dsolve.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse.linalg` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _dsolve
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -20,19 +19,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__ and name not in dsolve_modules:
-        raise AttributeError(
-            "scipy.sparse.linalg.dsolve is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse.linalg instead.")
-
-    if name in dsolve_modules:
-        msg = (f'The module `scipy.sparse.linalg.dsolve.{name}` is '
-               'deprecated. All public names must be imported directly from '
-               'the `scipy.sparse.linalg` namespace.')
-    else:
-        msg = (f"Please use `{name}` from the `scipy.sparse.linalg` namespace,"
-               " the `scipy.sparse.linalg.eigen` namespace is deprecated.")
-
-    warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_dsolve, name)
+    return _sub_module_deprecation(sub_package="sparse.linalg", module="dsolve",
+                                private_modules=["_dsolve"], all=__all__,
+                                attribute=name)
diff --git a/scipy/sparse/linalg/eigen.py b/scipy/sparse/linalg/eigen.py
index e89cfac77d8e..8ca2ddb3e332 100644
--- a/scipy/sparse/linalg/eigen.py
+++ b/scipy/sparse/linalg/eigen.py
@@ -2,13 +2,14 @@
 # Use the `scipy.sparse.linalg` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _eigen
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
-    'ArpackError', 'ArpackNoConvergence',
-    'eigs', 'eigsh', 'lobpcg', 'svds', 'arpack', 'test'
+    'ArpackError', 
+    'ArpackNoConvergence',
+    'eigs', 
+    'eigsh', 'lobpcg', 'svds', 'arpack', 'test'
 ]
 
 eigen_modules = ['arpack']
@@ -19,19 +20,7 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__ and name not in eigen_modules:
-        raise AttributeError(
-            "scipy.sparse.linalg.eigen is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse.linalg instead.")
-
-    if name in eigen_modules:
-        msg = (f'The module `scipy.sparse.linalg.eigen.{name}` is '
-               'deprecated. All public names must be imported directly from '
-               'the `scipy.sparse.linalg` namespace.')
-    else:
-        msg = (f"Please use `{name}` from the `scipy.sparse.linalg` namespace,"
-               " the `scipy.sparse.linalg.eigen` namespace is deprecated.")
-
-    warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_eigen, name)
+    return _sub_module_deprecation(sub_package="sparse.linalg", module="eigen",
+                                private_modules=["_eigen"], all=__all__,
+                                attribute=name)
+
diff --git a/scipy/sparse/linalg/interface.py b/scipy/sparse/linalg/interface.py
index 6e1329d5a8c2..44ec7197e52a 100644
--- a/scipy/sparse/linalg/interface.py
+++ b/scipy/sparse/linalg/interface.py
@@ -2,14 +2,18 @@
 # Use the `scipy.sparse.linalg` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _interface
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
-    'LinearOperator', 'aslinearoperator',
-    'isspmatrix', 'isshape', 'isintlike', 'asmatrix',
-    'is_pydata_spmatrix', 'MatrixLinearOperator', 'IdentityOperator'
+    'LinearOperator', 
+    'aslinearoperator',
+    'isshape', 
+    'isintlike', 
+    'asmatrix',
+    'is_pydata_spmatrix', 
+    'MatrixLinearOperator', 
+    'IdentityOperator'
 ]
 
 
@@ -18,13 +22,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.linalg.interface is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse.linalg instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse.linalg` namespace, "
-                  "the `scipy.sparse.linalg.interface` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_interface, name)
+    return _sub_module_deprecation(sub_package="sparse.linalg", module="interface",
+                                   private_modules=["_interface"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/linalg/isolve.py b/scipy/sparse/linalg/isolve.py
index f113cd2d4c7d..debcb986a192 100644
--- a/scipy/sparse/linalg/isolve.py
+++ b/scipy/sparse/linalg/isolve.py
@@ -2,14 +2,25 @@
 # Use the `scipy.sparse.linalg` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _isolve
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
-    'bicg', 'bicgstab', 'cg', 'cgs', 'gcrotmk', 'gmres',
-    'lgmres', 'lsmr', 'lsqr',
-    'minres', 'qmr', 'tfqmr', 'utils', 'iterative', 'test'
+    'bicg', 
+    'bicgstab', 
+    'cg', 
+    'cgs', 
+    'gcrotmk', 
+    'gmres',
+    'lgmres', 
+    'lsmr',
+    'lsqr',
+    'minres',
+    'qmr',
+    'tfqmr', 
+    'utils', 
+    'iterative', 
+    'test'
 ]
 
 
@@ -18,13 +29,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.linalg.isolve is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse.linalg instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse.linalg` namespace, "
-                  "the `scipy.sparse.linalg.isolve` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_isolve, name)
+    return _sub_module_deprecation(sub_package="sparse.linalg", module="isolve",
+                                   private_modules=["_isolve"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/linalg/matfuncs.py b/scipy/sparse/linalg/matfuncs.py
index 71d848c5a251..7d78fcc4527d 100644
--- a/scipy/sparse/linalg/matfuncs.py
+++ b/scipy/sparse/linalg/matfuncs.py
@@ -2,14 +2,20 @@
 # Use the `scipy.sparse.linalg` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _matfuncs
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
-    'expm', 'inv', 'solve', 'solve_triangular',
-    'isspmatrix', 'spsolve', 'is_pydata_spmatrix', 'LinearOperator',
-    'UPPER_TRIANGULAR', 'MatrixPowerOperator', 'ProductOperator'
+    'expm', 
+    'inv', 
+    'solve', 
+    'solve_triangular',
+    'spsolve', 
+    'is_pydata_spmatrix', 
+    'LinearOperator',
+    'UPPER_TRIANGULAR', 
+    'MatrixPowerOperator', 
+    'ProductOperator'
 ]
 
 
@@ -18,13 +24,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.linalg.matfuncs is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse.linalg instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse.linalg` namespace, "
-                  "the `scipy.sparse.linalg.matfuncs` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_matfuncs, name)
+    return _sub_module_deprecation(sub_package="sparse.linalg", module="matfuncs",
+                                   private_modules=["_matfuncs"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/sparsetools.py b/scipy/sparse/sparsetools.py
index aba16edab347..47ac80adae71 100644
--- a/scipy/sparse/sparsetools.py
+++ b/scipy/sparse/sparsetools.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _sparsetools
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -94,13 +93,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.sparsetools is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.sparsetools` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_sparsetools, name)
+    return _sub_module_deprecation(sub_package="sparse", module="sparsetools",
+                                   private_modules=["_sparsetools"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/spfuncs.py b/scipy/sparse/spfuncs.py
index 9c65bfd47934..dc4dbb3eae77 100644
--- a/scipy/sparse/spfuncs.py
+++ b/scipy/sparse/spfuncs.py
@@ -2,13 +2,14 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _spfuncs
+from scipy._lib.deprecation import _sub_module_deprecation
+
 
 
 __all__ = [  # noqa: F822
-    'isspmatrix_csr', 'csr_matrix', 'isspmatrix_csc', 'csr_count_blocks',
-    'estimate_blocksize', 'count_blocks'
+    'csr_count_blocks',
+    'estimate_blocksize', 
+    'count_blocks'
 ]
 
 
@@ -17,13 +18,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.spfuncs is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.spfuncs` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_spfuncs, name)
+    return _sub_module_deprecation(sub_package="sparse", module="spfuncs",
+                                   private_modules=["_spfuncs"], all=__all__,
+                                   attribute=name)
diff --git a/scipy/sparse/sputils.py b/scipy/sparse/sputils.py
index ed96d653a318..bdacb42dd0fb 100644
--- a/scipy/sparse/sputils.py
+++ b/scipy/sparse/sputils.py
@@ -2,8 +2,7 @@
 # Use the `scipy.sparse` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _sputils
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
@@ -40,13 +39,6 @@ def __dir__():
 
 
 def __getattr__(name):
-    if name not in __all__:
-        raise AttributeError(
-            "scipy.sparse.sputils is deprecated and has no attribute "
-            f"{name}. Try looking in scipy.sparse instead.")
-
-    warnings.warn(f"Please use `{name}` from the `scipy.sparse` namespace, "
-                  "the `scipy.sparse.sputils` namespace is deprecated.",
-                  category=DeprecationWarning, stacklevel=2)
-
-    return getattr(_sputils, name)
+    return _sub_module_deprecation(sub_package="sparse", module="sputils",
+                                   private_modules=["_sputils"], all=__all__,
+                                   attribute=name)


Code review comments:
diff_hunk: @@ -2,14 +2,25 @@
 # Use the `scipy.sparse.linalg` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _isolve
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
-    'bicg', 'bicgstab', 'cg', 'cgs', 'gcrotmk', 'gmres',
-    'lgmres', 'lsmr', 'lsqr',
-    'minres', 'qmr', 'tfqmr', 'utils', 'iterative', 'test'
+    'bicg', 
+    'bicgstab', 
+    'cg', 
+    'cgs', 
+    'gcrotmk', 
+    'gmres',
+    'lgmres', 
+    'lsmr',
+    'lsqr',
+    'minres',
+    'qmr',
+    'tfqmr', 
+    'utils', 
+    'iterative', 
+    'test'
path: scipy/sparse/linalg/isolve.py
text: @user1:
and here

@user1:
again please revert this change it makes it harder to tell if anything has been removed

diff_hunk: @@ -2,13 +2,14 @@
 # Use the `scipy.sparse.linalg` namespace for importing the functions
 # included below.
 
-import warnings
-from . import _eigen
+from scipy._lib.deprecation import _sub_module_deprecation
 
 
 __all__ = [  # noqa: F822
-    'ArpackError', 'ArpackNoConvergence',
-    'eigs', 'eigsh', 'lobpcg', 'svds', 'arpack', 'test'
+    'ArpackError', 
+    'ArpackNoConvergence',
+    'eigs', 
+    'eigsh', 'lobpcg', 'svds', 'arpack', 'test'
path: scipy/sparse/linalg/eigen.py
text: @user1:
can you revert the change here please

diff_hunk: @@ -339,13 +339,23 @@ def check_importable(module_name):
                           ('scipy.optimize.slsqp', None),
                           ('scipy.optimize.tnc', None),
                           ('scipy.optimize.zeros', None),
+                          ('scipy.sparse.lil', None),
+                          ('scipy.sparse.linalg.dsolve', 'linalg'),
+                          ('scipy.sparse.linalg.eigen', 'linalg'),
+                          ('scipy.sparse.linalg.interface', 'linalg'),
+                          ('scipy.sparse.linalg.isolve', 'linalg'),
+                          ('scipy.sparse.linalg.matfuncs', 'linalg'),
+                          ('scipy.sparse.sparsetools', None),
+                          ('scipy.sparse.spfuncs', None),
+                          ('scipy.sparse.sputils', None),
                           ('scipy.stats.biasedurn', None),
                           ('scipy.stats.kde', None),
                           ('scipy.stats.morestats', None),
                           ('scipy.stats.mstats_basic', 'mstats'),
                           ('scipy.stats.mstats_extras', 'mstats'),
                           ('scipy.stats.mvn', None),
                           ('scipy.stats.stats', None)])
+
path: scipy/_lib/tests/test_public_api.py
text: @user1:
```suggestion
```