Repository: python/mypy

Issue:
Mypy fails to calculate MRO in situations with import cycles.
In some situations mypy can't determine MRO, for example consider this test case:
```
[case testMultipleInheritanceCycle]
import b
[file a.py]
from b import B
class A: ...
class C(A, B): ...
class D(C): ...
class Other: ...
[file b.py]
from a import Other
class B: ...
[out]
```
that fails with
```
tmp/a.py:4: error: Cannot determine consistent method resolution order (MRO) for "D"
```
Note that changing order of files in the cycle (for example by importing `a` in `main`) removes the error.

In this case the actual MRO is consistent (works at runtime), while the import cycle is caused by an unrelated class (used for annotations in real stubs).

I think this is high priority because this is just a time bomb waiting to explode in some real code bases.


Discussion:

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1143,11 +1143,9 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
             # Give it an MRO consisting of just the class itself and object.
             defn.info.mro = [defn.info, self.object_type().type]
             return
-        calculate_class_mro(defn, self.fail_blocker)
-        # If there are cyclic imports, we may be missing 'object' in
-        # the MRO. Fix MRO if needed.
-        if info.mro and info.mro[-1].fullname() != 'builtins.object':
-            info.mro.append(self.object_type().type)
+        # TODO: Ideally we should move MRO calculation to a later stage, but this is
+        # not easy, see issue #5536.
+        calculate_class_mro(defn, self.fail_blocker, self.object_type)
 
     def update_metaclass(self, defn: ClassDef) -> None:
         """Lookup for special metaclass declarations, and update defn fields accordingly.
@@ -3430,21 +3428,28 @@ def refers_to_class_or_function(node: Expression) -> bool:
             isinstance(node.node, (TypeInfo, FuncDef, OverloadedFuncDef)))
 
 
-def calculate_class_mro(defn: ClassDef, fail: Callable[[str, Context], None]) -> None:
+def calculate_class_mro(defn: ClassDef, fail: Callable[[str, Context], None],
+                        obj_type: Optional[Callable[[], Instance]] = None) -> None:
+    """Calculate method resolution order for a class.
+
+    `obj_type` may be omitted in the third pass when all classes are already analyzed.
+    It exists just to fill in empty base class list during second pass in case of
+    an import cycle.
+    """
     try:
-        calculate_mro(defn.info)
+        calculate_mro(defn.info, obj_type)
     except MroError:
         fail("Cannot determine consistent method resolution order "
              '(MRO) for "%s"' % defn.name, defn)
         defn.info.mro = []
 
 
-def calculate_mro(info: TypeInfo) -> None:
+def calculate_mro(info: TypeInfo, obj_type: Optional[Callable[[], Instance]] = None) -> None:
     """Calculate and set mro (method resolution order).
 
     Raise MroError if cannot determine mro.
     """
-    mro = linearize_hierarchy(info)
+    mro = linearize_hierarchy(info, obj_type)
     assert mro, "Could not produce a MRO at all for %s" % (info,)
     info.mro = mro
     # The property of falling back to Any is inherited.
@@ -3456,15 +3461,22 @@ class MroError(Exception):
     """Raised if a consistent mro cannot be determined for a class."""
 
 
-def linearize_hierarchy(info: TypeInfo) -> List[TypeInfo]:
+def linearize_hierarchy(info: TypeInfo,
+                        obj_type: Optional[Callable[[], Instance]] = None) -> List[TypeInfo]:
     # TODO describe
     if info.mro:
         return info.mro
     bases = info.direct_base_classes()
+    if (not bases and info.fullname() != 'builtins.object' and
+            obj_type is not None):
+        # Second pass in import cycle, add a dummy `object` base class,
+        # otherwise MRO calculation may spuriously fail.
+        # MRO will be re-calculated for real in the third pass.
+        bases = [obj_type().type]
     lin_bases = []
     for base in bases:
         assert base is not None, "Cannot linearize bases for %s %s" % (info.fullname(), bases)
-        lin_bases.append(linearize_hierarchy(base))
+        lin_bases.append(linearize_hierarchy(base, obj_type))
     lin_bases.append(bases)
     return [info] + merge(lin_bases)
 


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
@@ -4877,3 +4877,39 @@ class C:
 reveal_type(C.foo)  # E: Revealed type is 'builtins.int*'
 reveal_type(C().foo)  # E: Revealed type is 'builtins.int*'
 [out]
+
+[case testMultipleInheritanceCycle]
+import b
+[file a.py]
+from b import B
+class A: ...
+class C(A, B): ...
+class D(C): ...
+class Other: ...
+[file b.py]
+from a import Other
+class B: ...
+[out]
+
+[case testMultipleInheritanceCycle2]
+import b
+[file a.py]
+from b import B
+class A: ...
+class C(A, B): ...
+class D(C): ...
+class Other: ...
+a: A
+b: B
+c: C
+d: D
+d = A()  # E: Incompatible types in assignment (expression has type "A", variable has type "D")
+d = B()  # E: Incompatible types in assignment (expression has type "B", variable has type "D")
+d = C()  # E: Incompatible types in assignment (expression has type "C", variable has type "D")
+a = D()
+b = D()
+c = D()
+[file b.py]
+from a import Other
+class B: ...
+[out]