Skip to content

Commit f341c60

Browse files
author
David Robertson
committed
TEST CASE
1 parent 235ae02 commit f341c60

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Reproduces a bug where MROs were incorrectly computed for implementers
3+
https://github.com/Shoobx/mypy-zope/issues/34
4+
https://github.com/Shoobx/mypy-zope/issues/76
5+
"""
6+
7+
from zope.interface import implementer, Interface
8+
9+
10+
class IProtocol(Interface):
11+
pass
12+
13+
14+
def main() -> None:
15+
class Factory:
16+
# It seems important for "Protocol" to show up as an attribute annotation to
17+
# trigger the bug(!?)
18+
protocol: "Protocol"
19+
20+
@implementer(IProtocol)
21+
class Protocol:
22+
pass
23+
24+
25+
if __name__ == '__main__':
26+
main()
27+
28+
"""
29+
Expect no errors. A specific test checks that we correct compute the MRO of `Protocol`.
30+
<output>
31+
</output>
32+
"""

tests/test_mro_calculation.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import os.path
2+
from typing import Optional, List
3+
4+
import pytest
5+
from mypy import options, build
6+
from mypy.build import State
7+
from mypy.modulefinder import BuildSource
8+
from mypy.nodes import SymbolTableNode, TypeInfo
9+
10+
HERE = os.path.abspath(os.path.dirname(__file__))
11+
SAMPLES_DIR = os.path.join(HERE, "samples")
12+
13+
14+
@pytest.fixture(scope="session")
15+
def mypy_cache_dir(tmp_path_factory):
16+
tdir = tmp_path_factory.mktemp('.mypy_cahe')
17+
print("Setup cache", str(tdir))
18+
return str(tdir)
19+
20+
21+
def test_mro_computation_in_forward_reference_to_implementer(mypy_cache_dir: str) -> None:
22+
sample_name = "forward_reference_to_implementer"
23+
24+
opts = options.Options()
25+
opts.show_traceback = True
26+
opts.namespace_packages = True
27+
opts.cache_dir = mypy_cache_dir
28+
opts.plugins = ['mypy_zope:plugin']
29+
# Config file is needed to load plugins, it doesn't not exist and is not
30+
# supposed to.
31+
opts.config_file = 'not_existing_config.ini'
32+
33+
samplefile = os.path.join(SAMPLES_DIR, f"{sample_name}.py")
34+
base_dir = os.path.dirname(samplefile)
35+
with open(samplefile) as f:
36+
source = BuildSource(
37+
None,
38+
module=sample_name,
39+
text=f.read(),
40+
base_dir=base_dir,
41+
)
42+
result = build.build(sources=[source], options=opts)
43+
assert result.errors == []
44+
45+
# Result.graph is a map from module name to state objects.
46+
state: State = result.graph[sample_name]
47+
48+
# Find Mypy's representation of the Protocol class.
49+
node: Optional[SymbolTableNode] = None
50+
for fullname, symbol_table_node, _type_info in state.tree.local_definitions():
51+
# Use startswith(...) rather than a direct comparison
52+
# because the typename includes a line number at the end
53+
if fullname.startswith(f"{sample_name}.Protocol"):
54+
node = symbol_table_node
55+
break
56+
57+
assert node is not None, f"Failed to find `Protocol` class in mypy's state for {samplefile}"
58+
59+
mro: List[TypeInfo] = node.node.mro
60+
# Expected: [
61+
# <TypeInfo forward_reference_to_implementer.Protocol@21>,
62+
# <TypeInfo builtins.object>,
63+
# <TypeInfo forward_reference_to_implementer.IProtocol>,
64+
# ]
65+
assert len(mro) == 3
66+
assert mro[0].fullname.startswith(f"{sample_name}.Protocol")
67+
assert mro[1].fullname == "builtins.object"
68+
assert mro[2].fullname == f"{sample_name}.IProtocol"

0 commit comments

Comments
 (0)