Repository: Shoobx/mypy-zope

Issue:
Caching errors with latest version of mypy
Hi.  With the latest version of mypy and mypy-zope we're getting mypy errors that I think are cache related. The error goes away after `rm -rf .mypy_cache`

Here's the smallest repro I could make:

src/zg/foo.py:
```
from zg.bar import ZGScriptProtocol

print(ZGScriptProtocol.transport)
```

src/zg/bar.py:
```
from twisted.internet.protocol import ProcessProtocol

class ZGScriptProtocol(ProcessProtocol):
    pass
```


I am able to reliably hit the problem by running the following commands:
```
$ rm -rf .mypy_cache
$ mypy src/zg/foo.py 
Success: no issues found in 1 source file
$ mypy src/zg/foo.py src/zg/bar.py 
src/zg/bar.py:3: error: Cannot determine consistent method resolution order (MRO) for "ZGScriptProtocol"
src/zg/foo.py:3: error: "Type[ZGScriptProtocol]" has no attribute "transport"
```


```
$ mypy --version
mypy 0.971 (compiled: yes)
```

`mypy-zope==0.3.9` and `twisted[tls]==22.4.0`


Discussion:

Patch:
diff --git a/src/mypy_zope/plugin.py b/src/mypy_zope/plugin.py
index 312c0b5..46bdc1b 100644
--- a/src/mypy_zope/plugin.py
+++ b/src/mypy_zope/plugin.py
@@ -695,7 +695,7 @@ class ZopeInterfacePlugin(Plugin):
         # there is a decorator for the class that will create a "type promotion",
         # but ensure this only gets applied a single time per interface.
         promote = Instance(iface, [])
-        if not any(ti._promote == promote for ti in impl.mro):
+        if not any(promote in ti._promote for ti in impl.mro):
             faketi = TypeInfo(SymbolTable(), iface.defn, iface.module_name)
             faketi._promote = [promote]
             impl.mro.append(faketi)


Test patch:
diff --git a/tests/samples/forward_reference_to_implementer.py b/tests/samples/forward_reference_to_implementer.py
new file mode 100644
index 0000000..ce8d846
--- /dev/null
+++ b/tests/samples/forward_reference_to_implementer.py
@@ -0,0 +1,32 @@
+"""
+Reproduces a bug where MROs were incorrectly computed for implementers
+    https://github.com/Shoobx/mypy-zope/issues/34
+    https://github.com/Shoobx/mypy-zope/issues/76
+"""
+
+from zope.interface import implementer, Interface
+
+
+class IProtocol(Interface):
+    pass
+
+
+def main() -> None:
+    class Factory:
+        # It seems important for "Protocol" to show up as an attribute annotation to
+        # trigger the bug(!?)
+        protocol: "Protocol"
+
+    @implementer(IProtocol)
+    class Protocol:
+        pass
+
+
+if __name__ == '__main__':
+    main()
+
+"""
+Expect no errors. A specific test checks that we correct compute the MRO of `Protocol`.
+<output>
+</output>
+"""
diff --git a/tests/test_mro_calculation.py b/tests/test_mro_calculation.py
new file mode 100644
index 0000000..a051dcc
--- /dev/null
+++ b/tests/test_mro_calculation.py
@@ -0,0 +1,68 @@
+import os.path
+from typing import Optional, List
+
+import pytest
+from mypy import options, build
+from mypy.build import State
+from mypy.modulefinder import BuildSource
+from mypy.nodes import SymbolTableNode, TypeInfo
+
+HERE = os.path.abspath(os.path.dirname(__file__))
+SAMPLES_DIR = os.path.join(HERE, "samples")
+
+
+@pytest.fixture(scope="session")
+def mypy_cache_dir(tmp_path_factory):
+    tdir = tmp_path_factory.mktemp('.mypy_cahe')
+    print("Setup cache", str(tdir))
+    return str(tdir)
+
+
+def test_mro_computation_in_forward_reference_to_implementer(mypy_cache_dir: str) -> None:
+    sample_name = "forward_reference_to_implementer"
+
+    opts = options.Options()
+    opts.show_traceback = True
+    opts.namespace_packages = True
+    opts.cache_dir = mypy_cache_dir
+    opts.plugins = ['mypy_zope:plugin']
+    # Config file is needed to load plugins, it doesn't not exist and is not
+    # supposed to.
+    opts.config_file = 'not_existing_config.ini'
+
+    samplefile = os.path.join(SAMPLES_DIR, f"{sample_name}.py")
+    base_dir = os.path.dirname(samplefile)
+    with open(samplefile) as f:
+        source = BuildSource(
+            None,
+            module=sample_name,
+            text=f.read(),
+            base_dir=base_dir,
+        )
+    result = build.build(sources=[source], options=opts)
+    assert result.errors == []
+
+    # Result.graph is a map from module name to state objects.
+    state: State = result.graph[sample_name]
+
+    # Find Mypy's representation of the Protocol class.
+    node: Optional[SymbolTableNode] = None
+    for fullname, symbol_table_node, _type_info in state.tree.local_definitions():
+        # Use startswith(...) rather than a direct comparison
+        # because the typename includes a line number at the end
+        if fullname.startswith(f"{sample_name}.Protocol"):
+            node = symbol_table_node
+            break
+
+    assert node is not None, f"Failed to find `Protocol` class in mypy's state for {samplefile}"
+
+    mro: List[TypeInfo] = node.node.mro
+    # Expected: [
+    #   <TypeInfo forward_reference_to_implementer.Protocol@21>,
+    #   <TypeInfo builtins.object>,
+    #   <TypeInfo forward_reference_to_implementer.IProtocol>,
+    # ]
+    assert len(mro) == 3
+    assert mro[0].fullname.startswith(f"{sample_name}.Protocol")
+    assert mro[1].fullname == "builtins.object"
+    assert mro[2].fullname == f"{sample_name}.IProtocol"