Repository: python/mypy

Issue:
superclass information can be lost in import cycles
The following test case fails:

```
[case testSuperclassInImportCycle]
import a
import d
a.A().f(d.D())
[file a.py]
if 0:
    import d
class B: pass
class C(B): pass
class A:
    def f(self, x: B) -> None: pass
[file d.py]
import a
class D(a.C): pass
```

with the error `Argument 1 to "f" of "A" has incompatible type "D"; expected "B"`.

This test case is a reduction from mypy itself (`a = mypy.types`, `A = Type`, `B = TypeVisitor`, `C = TypeTranslator`, `D` is any type transformation defined in another module `d`). The test does not fail if the mypy-import of module `d` in module `a` is removed. Nor does it fail if the order of the imports in the main module is reversed. I found this counterintuitive at first, but upon reflection I guess it's because the order of traversing imports changes from `main(a(d()))` to `main(d(a()))`, so whichever module is imported first is actually processed second.

I got as far as determining that something causes the MRO of `D` to be computed before the base classes of `C` are known. So the MRO of `D` ends up being computed as `[D, C, object]`, which does not contain `B`. Seems that we could know when processing `D` that it has a base class in a module `a` that we are still in the middle of processing, and update the MRO of `D` when we finish processing `a`.

(#481 is the general ticket for issues involving import cycles.)



Discussion:

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -30,7 +30,10 @@
    it will reject Dict[int].  We don't do this in the second pass,
    since we infer the type argument counts of classes during this
    pass, and it is possible to refer to classes defined later in a
-   file, which would not have the type argument count set yet.
+   file, which would not have the type argument count set yet. This
+   pass also recomputes the method resolution order of each class, in
+   case one of its bases belongs to a module involved in an import
+   loop.
 
 Semantic analysis of types is implemented in module mypy.typeanal.
 
@@ -41,7 +44,7 @@
 """
 
 from typing import (
-    List, Dict, Set, Tuple, cast, Any, overload, TypeVar, Union, Optional
+    List, Dict, Set, Tuple, cast, Any, overload, TypeVar, Union, Optional, Callable
 )
 
 from mypy.nodes import (
@@ -753,22 +756,17 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
             obj = self.object_type()
             defn.base_types.insert(0, obj)
         defn.info.bases = defn.base_types
+        # Calculate the MRO. It might be incomplete at this point if
+        # the bases of defn include classes imported from other
+        # modules in an import loop. We'll recompute it in ThirdPass.
         if not self.verify_base_classes(defn):
             defn.info.mro = []
             return
-        try:
-            defn.info.calculate_mro()
-        except MroError:
-            self.fail("Cannot determine consistent method resolution order "
-                      '(MRO) for "%s"' % defn.name, defn)
-            defn.info.mro = []
-        else:
-            # If there are cyclic imports, we may be missing 'object' in
-            # the MRO. Fix MRO if needed.
-            if defn.info.mro[-1].fullname() != 'builtins.object':
-                defn.info.mro.append(self.object_type().type)
-        # The property of falling back to Any is inherited.
-        defn.info.fallback_to_any = any(baseinfo.fallback_to_any for baseinfo in defn.info.mro)
+        calculate_class_mro(defn, self.fail)
+        # If there are cyclic imports, we may be missing 'object' in
+        # the MRO. Fix MRO if needed.
+        if defn.info.mro and defn.info.mro[-1].fullname() != 'builtins.object':
+            defn.info.mro.append(self.object_type().type)
 
     def expr_to_analyzed_type(self, expr: Node) -> Type:
         if isinstance(expr, CallExpr):
@@ -2437,6 +2435,12 @@ def visit_func_def(self, fdef: FuncDef) -> None:
     def visit_class_def(self, tdef: ClassDef) -> None:
         for type in tdef.info.bases:
             self.analyze(type)
+        # Recompute MRO now that we have analyzed all modules, to pick
+        # up superclasses of bases imported from other modules in an
+        # import loop. (Only do so if we succeeded the first time.)
+        if tdef.info.mro:
+            tdef.info.mro = []  # Force recomputation
+            calculate_class_mro(tdef, self.fail)
         super().visit_class_def(tdef)
 
     def visit_decorator(self, dec: Decorator) -> None:
@@ -2569,6 +2573,17 @@ def refers_to_class_or_function(node: Node) -> bool:
                                                   OverloadedFuncDef)))
 
 
+def calculate_class_mro(defn: ClassDef, fail: Callable[[str, Context], None]) -> None:
+    try:
+        defn.info.calculate_mro()
+    except MroError:
+        fail("Cannot determine consistent method resolution order "
+             '(MRO) for "%s"' % defn.name, defn)
+        defn.info.mro = []
+    # The property of falling back to Any is inherited.
+    defn.info.fallback_to_any = any(baseinfo.fallback_to_any for baseinfo in defn.info.mro)
+
+
 def find_duplicate(list: List[T]) -> T:
     """If the list has duplicates, return one of the duplicates.
 


Test patch:
diff --git a/mypy/test/data/check-modules.test b/mypy/test/data/check-modules.test
--- a/mypy/test/data/check-modules.test
+++ b/mypy/test/data/check-modules.test
@@ -739,3 +739,33 @@ y = 42
 [builtins fixtures/module.py]
 [out]
 tmp/main.py:4: error: Unsupported left operand type for + ("int")
+
+[case testSuperclassInImportCycle]
+import a
+import d
+a.A().f(d.D())
+[file a.py]
+if 0:
+    import d
+class B: pass
+class C(B): pass
+class A:
+    def f(self, x: B) -> None: pass
+[file d.py]
+import a
+class D(a.C): pass
+
+[case testSuperclassInImportCycleReversedImports]
+import d
+import a
+a.A().f(d.D())
+[file a.py]
+if 0:
+    import d
+class B: pass
+class C(B): pass
+class A:
+    def f(self, x: B) -> None: pass
+[file d.py]
+import a
+class D(a.C): pass