Repository: python/mypy

Issue:
Metaclass operators not respected by TypeVar bounds
Using `mypy 0.590`, running `mypy` on the following code:

(EDIT: See https://github.com/python/mypy/issues/4929#issuecomment-383166651 for a smaller test case)

```python
import ctypes
from typing import Type, TypeVar

S = TypeVar("S", bound=ctypes.Structure)

def f(x: Type[ctypes.Structure]) -> ctypes.Array[ctypes.Structure]:
    return (x * 0)()
def g(x: Type[S]) -> ctypes.Array[S]:
    return (x * 0)()
```

fails with

```
test.py:9: error: Unsupported operand types for * ("Type[S]" and "int")
test.py:9: error: "int" not callable
```

But AFAICT, `g` should be perfectly valid -- because it has a bound of `ctypes.Structure` (which supports multiplication with integers), it should also support multiplication with integers.


Discussion:
But does the ctypes.Structure type support multiplication with integers?

Also, it doesn't look like the ctypes stubs has a `__mul__` implementation for Structure.
There is `__mul__` and `__rmul__` on a metaclass. This may be a real bug in mypy, operators are special cased (probably old code) instead of re-using `checkmember`, therefore corner cases (like access via bound via metaclass) is not treated correctly unlike the latter one. This needs some refactoring I think. I left a TODO in code while previously fixing a similar corner case, it looks like we need to implement that TODO.
There are a lot of missing test cases for "magic" methods ... I'm putting together a PR for adding test cases, but it's not high on my priority list.
@JelleZijlstra @ilevkivskyi To be more precise, the inheritance looks like:

```python

class _CDataMeta(type):
    # By default mypy complains about the following two methods, because strictly speaking cls
    # might not be a Type[_CT]. However this can never actually happen, because the only class that
    # uses _CDataMeta as its metaclass is _CData. So it's safe to ignore the errors here.
    def __mul__(cls: Type[_CT], other: int) -> Type[Array[_CT]]: ...  # type: ignore
    def __rmul__(cls: Type[_CT], other: int) -> Type[Array[_CT]]: ...  # type: ignore
class _CData(metaclass=_CDataMeta): ...
class _StructUnionMeta(_CDataMeta): ...
class _StructUnionBase(_CData, metaclass=_StructUnionMeta): ...
class Structure(_StructUnionBase): ...
```

A more minimal reproduction:

```python
from typing import Type, TypeVar

class MetaClass(type):
    def __mul__(cls, other: int) -> str:
        return ""

class Test(metaclass=MetaClass):
    pass

S = TypeVar("S", bound=Test)

def f(x: Type[Test]) -> str:
    return x * 0
def g(x: Type[S]) -> str:
    return x * 0
```

returns

```
test.py:15: error: Incompatible return value type (got "int", expected "str")
test.py:15: error: Unsupported operand types for * ("Type[S]" and "int")
```

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -2431,6 +2431,10 @@ def has_member(self, typ: Type, member: str) -> bool:
         """Does type have member with the given name?"""
         # TODO: refactor this to use checkmember.analyze_member_access, otherwise
         # these two should be carefully kept in sync.
+        if isinstance(typ, TypeVarType):
+            typ = typ.upper_bound
+        if isinstance(typ, TupleType):
+            typ = typ.fallback
         if isinstance(typ, Instance):
             return typ.type.has_readable_member(member)
         if isinstance(typ, CallableType) and typ.is_type_obj():
@@ -2440,14 +2444,17 @@ def has_member(self, typ: Type, member: str) -> bool:
         elif isinstance(typ, UnionType):
             result = all(self.has_member(x, member) for x in typ.relevant_items())
             return result
-        elif isinstance(typ, TupleType):
-            return self.has_member(typ.fallback, member)
         elif isinstance(typ, TypeType):
             # Type[Union[X, ...]] is always normalized to Union[Type[X], ...],
             # so we don't need to care about unions here.
-            if isinstance(typ.item, Instance) and typ.item.type.metaclass_type is not None:
-                return self.has_member(typ.item.type.metaclass_type, member)
-            if isinstance(typ.item, AnyType):
+            item = typ.item
+            if isinstance(item, TypeVarType):
+                item = item.upper_bound
+            if isinstance(item, TupleType):
+                item = item.fallback
+            if isinstance(item, Instance) and item.type.metaclass_type is not None:
+                return self.has_member(item.type.metaclass_type, member)
+            if isinstance(item, AnyType):
                 return True
             return False
         else:


Test patch:
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -3459,6 +3459,23 @@ class Concrete(metaclass=Meta):
 reveal_type(Concrete + X())  # E: Revealed type is 'builtins.str'
 Concrete + "hello"  # E: Unsupported operand types for + ("Type[Concrete]" and "str")
 
+[case testMetaclassOperatorTypeVar]
+from typing import Type, TypeVar
+
+class MetaClass(type):
+    def __mul__(cls, other: int) -> str:
+        return ""
+
+class Test(metaclass=MetaClass):
+    pass
+
+S = TypeVar("S", bound=Test)
+
+def f(x: Type[Test]) -> str:
+    return x * 0
+def g(x: Type[S]) -> str:
+    return reveal_type(x * 0)  # E: Revealed type is 'builtins.str'
+
 [case testMetaclassGetitem]
 class M(type):
     def __getitem__(self, key) -> int: return 1
diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test
--- a/test-data/unit/check-generics.test
+++ b/test-data/unit/check-generics.test
@@ -1604,17 +1604,33 @@ def f(a: T, b: T) -> T:
         return b
 [builtins fixtures/ops.pyi]
 
-[case testTypeVariableTypeLessThan]
+[case testTypeVarLessThan]
 from typing import TypeVar
 T = TypeVar('T')
 def f(a: T, b: T) -> T:
-    if a < b:
+    if a < b:  # E: Unsupported left operand type for < ("T")
         return a
     else:
         return b
 [builtins fixtures/ops.pyi]
-[out]
-main:4: error: Unsupported left operand type for < ("T")
+
+[case testTypeVarReversibleOperator]
+from typing import TypeVar
+class A:
+    def __mul__(cls, other: int) -> str: return ""
+T = TypeVar("T", bound=A)
+def f(x: T) -> str:
+    return reveal_type(x * 0)  # E: Revealed type is 'builtins.str'
+
+[case testTypeVarReversibleOperatorTuple]
+from typing import TypeVar, Tuple
+class A(Tuple[int, int]):
+    def __mul__(cls, other: Tuple[int, int]) -> str: return ""
+T = TypeVar("T", bound=A)
+def f(x: T) -> str:
+    return reveal_type(x * (1, 2) )  # E: Revealed type is 'builtins.str'
+
+[builtins fixtures/tuple.pyi]
 
 
 -- Subtyping generic callables