Repository: sympy/sympy
Pull request: @/__matmul__ now only performs matrix multiplication
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->
Add check to allow __matmul__ to perform only matrix multiplication and throws ValueError if
scalars are used as operators or operands.

#### References to other Issues or PRs

This PR will fix #13766 .

#### Brief description of what is fixed or changed
```
>>> B = Matrix([[2, 3], [1, 2]])
>>> 2@B
```
and
```
>>> B@2
```
now throws ValueError: Scalar operands are not allowed, use '*' instead

Problem statement:
@ (__matmul__) should fail if one argument is not a matrix
```
>>> A = Matrix([[1, 2], [3, 4]])
>>> B = Matrix([[2, 3], [1, 2]])
>>> A@B
Matrix([
[ 4,  7],
[10, 17]])
>>> 2@B
Matrix([
[4, 6],
[2, 4]])
```

Right now `@` (`__matmul__`) just copies `__mul__`, but it should actually only work if the multiplication is actually a matrix multiplication. 

This is also how NumPy works

```
>>> import numpy as np
>>> a = np.array([[1, 2], [3, 4]])
>>> 2*a
array([[2, 4],
       [6, 8]])
>>> 2@a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Scalar operands are not allowed, use '*' instead
```

Hints from issue discussion:
Note to anyone fixing this: `@`/`__matmul__` only works in Python 3.5+. 
I would like to work on this issue.

Commit message: @/__matmul__ now only performs matrix multiplication

Patch to review:
diff --git a/sympy/matrices/common.py b/sympy/matrices/common.py
index 8c4e21fcfbe0..3061f9a8a591 100644
--- a/sympy/matrices/common.py
+++ b/sympy/matrices/common.py
@@ -1973,6 +1973,13 @@ def __div__(self, other):
 
     @call_highest_priority('__rmatmul__')
     def __matmul__(self, other):
+        other = _matrixify(other)
+        # matrix-like objects can have shapes.  This is our check.
+        # Non matrice operands will throw error
+        if not(hasattr(other, 'shape')):
+            raise ValueError("Scalar operands are not allowed, use '*' instead");
+            return
+
         return self.__mul__(other)
 
     @call_highest_priority('__rmul__')
@@ -2066,6 +2073,12 @@ def __radd__(self, other):
 
     @call_highest_priority('__matmul__')
     def __rmatmul__(self, other):
+        other = _matrixify(other)
+        # matrix-like objects can have shapes.  This is our check.
+        # Non matrice operands will throw error
+        if not(hasattr(other, 'shape')):
+            raise ValueError("Scalar operands are not allowed, use '*' instead");
+            return
         return self.__rmul__(other)
 
     @call_highest_priority('__mul__')
diff --git a/sympy/matrices/tests/test_commonmatrix.py b/sympy/matrices/tests/test_commonmatrix.py
index a7554d92956c..17e76291508b 100644
--- a/sympy/matrices/tests/test_commonmatrix.py
+++ b/sympy/matrices/tests/test_commonmatrix.py
@@ -674,6 +674,13 @@ def test_multiplication():
         assert c[1, 0] == 3*5
         assert c[1, 1] == 0
 
+def test_matmul():
+    a = Matrix([[1, 2], [3, 4]])
+    b = Matrix([[2, 3], [1, 2]])
+    raises(ValueError, lambda: a@2)
+    raises(ValueError, lambda: 2@a)
+        
+
 def test_power():
     raises(NonSquareMatrixError, lambda: Matrix((1, 2))**2)
 


Code review comments:
diff_hunk: @@ -1973,6 +1973,13 @@ def __div__(self, other):
 
     @call_highest_priority('__rmatmul__')
     def __matmul__(self, other):
+        other = _matrixify(other)
+        # matrix-like objects can have shapes.  This is our check.
+        # Non matrice operands will throw error
+        if not(hasattr(other, 'shape')):
+            raise ValueError("Scalar operands are not allowed, use '*' instead");
path: sympy/matrices/common.py
text: @user1:
Don't put semicolons at the end of lines.

diff_hunk: @@ -1973,6 +1973,13 @@ def __div__(self, other):
 
     @call_highest_priority('__rmatmul__')
     def __matmul__(self, other):
+        other = _matrixify(other)
+        # matrix-like objects can have shapes.  This is our check.
+        # Non matrice operands will throw error
+        if not(hasattr(other, 'shape')):
+            raise ValueError("Scalar operands are not allowed, use '*' instead");
+            return
path: sympy/matrices/common.py
text: @user1:
Remove the `return`.