Repository: scipy/scipy

Issue:
BUG: A sparse matrix raises a ValueError when `__rmul__` with a value as `None`
### Describe your issue.

A sparse matrix multiplied with a value of an unsupported operand type raises TypeError. However, in SciPy 1.8.0rc1, a sparse matrix raises ValueError when it is called `__rmul__` with a value that is not interpretable as an array,  e.g. `None`. It should raise TypeError instead as `__mul__` does.


### Reproducing Code Example

```python
import numpy as np
import scipy.sparse as sp
m = np.eye(3)
m = sp.coo_matrix(m)
None * m
```


### Error message

```shell
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/ext-mtakagi/.pyenv/versions/nvidia/lib/python3.9/site-packages/scipy/sparse/_base.py", line 610, in __rmul__
    return self._rmul_dispatch(other)
  File "/home/ext-mtakagi/.pyenv/versions/nvidia/lib/python3.9/site-packages/scipy/sparse/_base.py", line 607, in _rmul_dispatch
    return (self.transpose() @ tr).transpose()
  File "/home/ext-mtakagi/.pyenv/versions/nvidia/lib/python3.9/site-packages/scipy/sparse/_base.py", line 618, in __matmul__
    raise ValueError("Scalar operands are not allowed, "
ValueError: Scalar operands are not allowed, use '*' instead
```


### SciPy/NumPy/Python version information

1.8.0rc1 1.22.0rc1 sys.version_info(major=3, minor=9, micro=7, releaselevel='final', serial=0)


Discussion:
Thanks for reporting this bug, @takagi.   You are correct, that should be a `TypeError`.
I'm adding new issues that should be fixed for 1.8.0rc2 (or 1.8.0 final) to the 1.8.0 milestone.  If a different procedure is preferred, let me know.
xref: https://github.com/cupy/cupy/issues/6233
Thanks, it is definitely fine for us.
The problem is that `None` can be coerced into an array:

```
In [2]: np.array(None)
Out[2]: array(None, dtype=object)
```

I can add a specific check for None, but it sounds like you want a wider class of objects excluded?

I you *only* want None to be excluded, I can easily add that back in with a special check.
Here's the offending code from `_base.py`:

```python
        # If it's a list or whatever, treat it like a matrix
        other_a = np.asanyarray(other)
```
How about reverting this line https://github.com/scipy/scipy/pull/14822/files#diff-8aed1ba485f8d01bc74679865b6ca8fa04efde8b6fef5a57f587b61eb7a8734cR607 to

```python
return (self.transpose() * tr).transpose()
```
or
```python
return self.transpose()._mul_dispatch(tr).transpose()
```

We cannot revert that line, or it will break for arrays. I think by the time we get to the point where we think `array(None, dtype=object)` is another array we should interact with, we're in trouble already. I think this goes back to an issue with NumPy where it is a bit over-eager in converting objects to arrays.  @seberg Thoughts?


I got
```
>>> np.eye(2) * sparse.csr_array(np.arange(6).reshape(2, 3))
array([[0., 1., 2.],
       [3., 4., 5.]])
>>> np.__version__, scipy.__version__
('1.22.0rc2', '1.8.0rc1')
```
but I expect `dense * sparse` computes
- elementwise mul if both operands are "array"
- matmul if any operand is a "matrix"

I think the problem is that `isscalar` is not just not very useful.  You can't know that the result `asarray` result won't be 0-D, and if it is you have to go into the normal multiply path.  (There might be more subtleties, in that `None * m` used to fail earlier and not try to go to object arrays, but that seems OK?)
So, you can't commit to using `@` quite there, calling the `_mul_dispatcher` may well be reasonable.  By that time, the other object got a chance to deal with it, so converting it to a dense array should not be terrible (but that could be an object array of course).
> but I expect `dense * sparse` computes
> 
> * elementwise mul if both operands are "array"
> * matmul if any operand is a "matrix"

This is a bug, for sure!
OK, figured it out: rmul is missing from the array interface :roll_eyes: 

Resolving patch:
diff --git a/scipy/sparse/_base.py b/scipy/sparse/_base.py
--- a/scipy/sparse/_base.py
+++ b/scipy/sparse/_base.py
@@ -508,13 +508,17 @@ def __rsub__(self,other):  # other - self
             return NotImplemented
 
     def _mul_dispatch(self, other):
-        """interpret other and call one of the following
+        """`np.matrix`-compatible mul, i.e. `dot` or `NotImplemented`
 
+        interpret other and call one of the following
         self._mul_scalar()
         self._mul_vector()
         self._mul_multivector()
         self._mul_sparse_matrix()
         """
+        # This method has to be different from `__mul__` because it is also
+        # called by sparse array classes via matmul, while their mul is
+        # elementwise.
 
         M, N = self.shape
 
@@ -600,14 +604,17 @@ def _mul_sparse_matrix(self, other):
 
     def _rmul_dispatch(self, other):
         if isscalarlike(other):
-            return self._mul_dispatch(other)
+            return self._mul_scalar(other)
         else:
             # Don't use asarray unless we have to
             try:
                 tr = other.transpose()
             except AttributeError:
                 tr = np.asarray(other).transpose()
-            return (self.transpose() @ tr).transpose()
+            ret = self.transpose()._mul_dispatch(tr)
+            if ret is NotImplemented:
+                return NotImplemented
+            return ret.transpose()
 
     def __rmul__(self, other):  # other * self
         return self._rmul_dispatch(other)


Test patch:
diff --git a/scipy/sparse/tests/test_base.py b/scipy/sparse/tests/test_base.py
--- a/scipy/sparse/tests/test_base.py
+++ b/scipy/sparse/tests/test_base.py
@@ -1355,6 +1355,12 @@ def check(dtype):
         for dtype in self.math_dtypes:
             check(dtype)
 
+    # github issue #15210
+    def test_rmul_scalar_type_error(self):
+        datsp = self.datsp_dtypes[np.float64]
+        with assert_raises(TypeError):
+            None * datsp
+
     def test_add(self):
         def check(dtype):
             dat = self.dat_dtypes[dtype]