Skip to content

Commit 03b53cb

Browse files
committed
Using unbounded_type to access class object of a type annotation.
1 parent 6b68661 commit 03b53cb

File tree

4 files changed

+94
-7
lines changed

4 files changed

+94
-7
lines changed

mypyc/irbuild/classdef.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ def add_non_ext_class_attr_ann(
634634
if builder.current_module == type_info.module_name and stmt.line < type_info.line:
635635
typ = builder.load_str(type_info.fullname)
636636
else:
637-
typ = load_type(builder, type_info, stmt.line)
637+
typ = load_type(builder, type_info, stmt.unanalyzed_type, stmt.line)
638638

639639
if typ is None:
640640
# FIXME: if get_type_info is not provided, don't fall back to stmt.type?
@@ -650,7 +650,7 @@ def add_non_ext_class_attr_ann(
650650
# actually a forward reference due to the __annotations__ future?
651651
typ = builder.load_str(stmt.unanalyzed_type.original_str_expr)
652652
elif isinstance(ann_type, Instance):
653-
typ = load_type(builder, ann_type.type, stmt.line)
653+
typ = load_type(builder, ann_type.type, stmt.unanalyzed_type, stmt.line)
654654
else:
655655
typ = builder.add(LoadAddress(type_object_op.type, type_object_op.src, stmt.line))
656656

mypyc/irbuild/function.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
TypeInfo,
3030
Var,
3131
)
32-
from mypy.types import CallableType, get_proper_type
32+
from mypy.types import CallableType, Type, UnboundType, get_proper_type
3333
from mypyc.common import LAMBDA_NAME, PROPSET_PREFIX, SELF_NAME
3434
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
3535
from mypyc.ir.func_ir import (
@@ -802,15 +802,37 @@ def get_func_target(builder: IRBuilder, fdef: FuncDef) -> AssignmentTarget:
802802
return builder.add_local_reg(fdef, object_rprimitive)
803803

804804

805-
def load_type(builder: IRBuilder, typ: TypeInfo, line: int) -> Value:
805+
def load_type(builder: IRBuilder, typ: TypeInfo, unbounded_type: Type | None, line: int) -> Value:
806+
# typ.fullname contains the module where the class object was defined. However, it is possible that the class
807+
# object's module was not imported in the file currently being compiled. So, we use unbounded_type.name (if provided
808+
# by caller) to load the class object through one of the imported modules.
809+
# Example: for `json.JSONDecoder`, typ.fullname is `json.decoder.JSONDecoder` but the Python file may import `json`
810+
# not `json.decoder`.
811+
# Another corner case: The Python file being compiled imports mod1 and has a type hint `mod1.OuterClass.InnerClass`.
812+
# But, mod1/__init__.py might import OuterClass like this: `from mod2.mod3 import OuterClass`. In this case,
813+
# typ.fullname is `mod2.mod3.OuterClass.InnerClass` and `unbounded_type.name` is `mod1.OuterClass.InnerClass`. So,
814+
# we must use unbounded_type.name to load the class object.
815+
# See issue mypy/mypy#1087.
816+
load_attr_path = (
817+
unbounded_type.name if isinstance(unbounded_type, UnboundType) else typ.fullname
818+
).removesuffix(f".{typ.name}")
806819
if typ in builder.mapper.type_to_ir:
807820
class_ir = builder.mapper.type_to_ir[typ]
808821
class_obj = builder.builder.get_native_type(class_ir)
809822
elif typ.fullname in builtin_names:
810823
builtin_addr_type, src = builtin_names[typ.fullname]
811824
class_obj = builder.add(LoadAddress(builtin_addr_type, src, line))
812-
elif typ.module_name in builder.imports:
813-
loaded_module = builder.load_module(typ.module_name)
825+
# This elif-condition finds the longest import that matches the load_attr_path.
826+
elif module_name := max((i for i in builder.imports if load_attr_path.startswith(i)), key=len):
827+
# Load the imported module.
828+
loaded_module = builder.load_module(module_name)
829+
# Recursively load attributes of the imported module. These may be submodules, classes or any other object.
830+
for attr in (
831+
load_attr_path.removeprefix(f"{module_name}.").split(".")
832+
if load_attr_path != module_name
833+
else []
834+
):
835+
loaded_module = builder.py_get_attr(loaded_module, attr, line)
814836
class_obj = builder.builder.get_attr(
815837
loaded_module, typ.name, object_rprimitive, line, borrow=False
816838
)
@@ -1039,7 +1061,7 @@ def maybe_insert_into_registry_dict(builder: IRBuilder, fitem: FuncDef) -> None:
10391061
)
10401062
registry = load_singledispatch_registry(builder, dispatch_func_obj, line)
10411063
for typ in types:
1042-
loaded_type = load_type(builder, typ, line)
1064+
loaded_type = load_type(builder, typ, None, line)
10431065
builder.primitive_op(dict_set_item_op, [registry, loaded_type, to_insert], line)
10441066
dispatch_cache = builder.builder.get_attr(
10451067
dispatch_func_obj, "dispatch_cache", dict_rprimitive, line

mypyc/test-data/run-classes.test

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,22 @@ assert hasattr(c, 'x')
7878

7979
[case testTypedDictWithFields]
8080
import collections
81+
import json
8182
from typing import TypedDict
8283
class C(TypedDict):
8384
x: collections.deque
85+
spam: json.JSONDecoder
8486
[file driver.py]
8587
from native import C
8688
from collections import deque
89+
from json import JSONDecoder
8790

8891
print(C.__annotations__["x"] is deque)
92+
print(C.__annotations__["spam"] is JSONDecoder)
8993
[typing fixtures/typing-full.pyi]
9094
[out]
9195
True
96+
True
9297

9398
[case testClassWithDeletableAttributes]
9499
from typing import Any, cast

mypyc/test/test_function.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import unittest
2+
from unittest.mock import MagicMock, call, patch
3+
4+
from mypy.nodes import TypeInfo
5+
from mypy.types import UnboundType
6+
from mypyc.ir.rtypes import object_rprimitive
7+
from mypyc.irbuild.builder import IRBuilder
8+
from mypyc.irbuild.function import load_type
9+
from mypyc.irbuild.ll_builder import LowLevelIRBuilder
10+
from mypyc.irbuild.mapper import Mapper
11+
12+
13+
class TestFunction(unittest.TestCase):
14+
def setUp(self) -> None:
15+
self.builder = MagicMock(spec=IRBuilder)
16+
self.builder.configure_mock(
17+
mapper=MagicMock(spec=Mapper), builder=MagicMock(spec=LowLevelIRBuilder)
18+
)
19+
self.builder.mapper.configure_mock(type_to_ir=[])
20+
self.typ = MagicMock(spec=TypeInfo)
21+
self.unbounded_type = MagicMock(spec=UnboundType)
22+
self.line = 10
23+
24+
@patch("mypyc.irbuild.function.builtin_names", {})
25+
def test_load_type_from_imported_module(self) -> None:
26+
self.typ.fullname = "json.decoder.JSONDecoder"
27+
self.typ.name = "JSONDecoder"
28+
self.unbounded_type.name = "json.JSONDecoder"
29+
self.builder.imports = {"json": "json"}
30+
self.builder.load_module.return_value = "json_module"
31+
self.builder.builder.get_attr.return_value = "JSONDecoder_class"
32+
result = load_type(self.builder, self.typ, self.unbounded_type, self.line)
33+
self.builder.load_module.assert_called_once_with("json")
34+
self.builder.py_get_attr.assert_not_called()
35+
self.builder.builder.get_attr.assert_called_once_with(
36+
"json_module", "JSONDecoder", object_rprimitive, self.line, borrow=False
37+
)
38+
self.assertEqual(result, "JSONDecoder_class")
39+
40+
@patch("mypyc.irbuild.function.builtin_names", {})
41+
def test_load_type_with_deep_nesting(self) -> None:
42+
self.typ.fullname = "mod1.mod2.mod3.OuterType.InnerType"
43+
self.typ.name = "InnerType"
44+
self.unbounded_type.name = "mod4.mod5.mod6.OuterType.InnerType"
45+
self.builder.imports = {"mod4.mod5": "mod4.mod5"}
46+
self.builder.load_module.return_value = "mod4.mod5_module"
47+
self.builder.py_get_attr.side_effect = ["mod4.mod5.mod6_module", "OuterType_class"]
48+
self.builder.builder.get_attr.return_value = "InnerType_class"
49+
result = load_type(self.builder, self.typ, self.unbounded_type, self.line)
50+
self.builder.load_module.assert_called_once_with("mod4.mod5")
51+
self.builder.py_get_attr.assert_has_calls(
52+
[
53+
call("mod4.mod5_module", "mod6", self.line),
54+
call("mod4.mod5.mod6_module", "OuterType", self.line),
55+
]
56+
)
57+
self.builder.builder.get_attr.assert_called_once_with(
58+
"OuterType_class", "InnerType", object_rprimitive, self.line, borrow=False
59+
)
60+
self.assertEqual(result, "InnerType_class")

0 commit comments

Comments
 (0)