Skip to content

Commit 07119e0

Browse files
Merge branch 'master' into fix_match_callable
2 parents 9a3e374 + a0665e1 commit 07119e0

File tree

12 files changed

+241
-39
lines changed

12 files changed

+241
-39
lines changed

misc/log_trace_check.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Compile mypy using mypyc with trace logging enabled, and collect a trace.
2+
3+
The trace log can be used to analyze low-level performance bottlenecks.
4+
5+
By default does a self check as the workload.
6+
7+
This works on all supported platforms, unlike some of our other performance tools.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import argparse
13+
import glob
14+
import os
15+
import shutil
16+
import subprocess
17+
import sys
18+
import time
19+
20+
from perf_compare import build_mypy, clone
21+
22+
# Generated files, including binaries, go under this directory to avoid overwriting user state.
23+
TARGET_DIR = "mypy.log_trace.tmpdir"
24+
25+
26+
def perform_type_check(target_dir: str, code: str | None) -> None:
27+
cache_dir = os.path.join(target_dir, ".mypy_cache")
28+
if os.path.exists(cache_dir):
29+
shutil.rmtree(cache_dir)
30+
args = []
31+
if code is None:
32+
args.extend(["--config-file", "mypy_self_check.ini"])
33+
for pat in "mypy/*.py", "mypy/*/*.py", "mypyc/*.py", "mypyc/test/*.py":
34+
args.extend(glob.glob(pat))
35+
else:
36+
args.extend(["-c", code])
37+
check_cmd = ["python", "-m", "mypy"] + args
38+
t0 = time.time()
39+
subprocess.run(check_cmd, cwd=target_dir, check=True)
40+
elapsed = time.time() - t0
41+
print(f"{elapsed:.2f}s elapsed")
42+
43+
44+
def main() -> None:
45+
parser = argparse.ArgumentParser(
46+
description="Compile mypy and collect a trace log while type checking (by default, self check)."
47+
)
48+
parser.add_argument(
49+
"--multi-file",
50+
action="store_true",
51+
help="compile mypy into one C file per module (to reduce RAM use during compilation)",
52+
)
53+
parser.add_argument(
54+
"--skip-compile", action="store_true", help="use compiled mypy from previous run"
55+
)
56+
parser.add_argument(
57+
"-c",
58+
metavar="CODE",
59+
default=None,
60+
type=str,
61+
help="type check Python code fragment instead of mypy self-check",
62+
)
63+
args = parser.parse_args()
64+
multi_file: bool = args.multi_file
65+
skip_compile: bool = args.skip_compile
66+
code: str | None = args.c
67+
68+
target_dir = TARGET_DIR
69+
70+
if not skip_compile:
71+
clone(target_dir, "HEAD")
72+
73+
print(f"Building mypy in {target_dir} with trace logging enabled...")
74+
build_mypy(target_dir, multi_file, log_trace=True, opt_level="0")
75+
elif not os.path.isdir(target_dir):
76+
sys.exit("error: Can't find compile mypy from previous run -- can't use --skip-compile")
77+
78+
perform_type_check(target_dir, code)
79+
80+
trace_fnam = os.path.join(target_dir, "mypyc_trace.txt")
81+
print(f"Generated event trace log in {trace_fnam}")
82+
83+
84+
if __name__ == "__main__":
85+
main()

misc/perf_compare.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,22 @@ def heading(s: str) -> None:
3737
print()
3838

3939

40-
def build_mypy(target_dir: str, multi_file: bool, *, cflags: str | None = None) -> None:
40+
def build_mypy(
41+
target_dir: str,
42+
multi_file: bool,
43+
*,
44+
cflags: str | None = None,
45+
log_trace: bool = False,
46+
opt_level: str = "2",
47+
) -> None:
4148
env = os.environ.copy()
4249
env["CC"] = "clang"
43-
env["MYPYC_OPT_LEVEL"] = "2"
50+
env["MYPYC_OPT_LEVEL"] = opt_level
4451
env["PYTHONHASHSEED"] = "1"
4552
if multi_file:
4653
env["MYPYC_MULTI_FILE"] = "1"
54+
if log_trace:
55+
env["MYPYC_LOG_TRACE"] = "1"
4756
if cflags is not None:
4857
env["CFLAGS"] = cflags
4958
cmd = [sys.executable, "setup.py", "--use-mypyc", "build_ext", "--inplace"]

mypy/checkexpr.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,6 +1671,10 @@ def check_call(
16711671
object_type,
16721672
original_type=callee,
16731673
)
1674+
elif isinstance(callee, UninhabitedType):
1675+
ret = UninhabitedType()
1676+
ret.ambiguous = callee.ambiguous
1677+
return callee, ret
16741678
else:
16751679
return self.msg.not_callable(callee, context), AnyType(TypeOfAny.from_error)
16761680

mypy/checkmember.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
TypeVarLikeType,
7070
TypeVarTupleType,
7171
TypeVarType,
72+
UninhabitedType,
7273
UnionType,
7374
get_proper_type,
7475
)
@@ -268,6 +269,10 @@ def _analyze_member_access(
268269
if not mx.suppress_errors:
269270
mx.msg.deleted_as_rvalue(typ, mx.context)
270271
return AnyType(TypeOfAny.from_error)
272+
elif isinstance(typ, UninhabitedType):
273+
attr_type = UninhabitedType()
274+
attr_type.ambiguous = typ.ambiguous
275+
return attr_type
271276
return report_missing_attribute(mx.original_type, typ, name, mx)
272277

273278

mypy/expandtype.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from collections.abc import Iterable, Mapping, Sequence
3+
from collections.abc import Iterable, Mapping
44
from typing import Final, TypeVar, cast, overload
55

66
from mypy.nodes import ARG_STAR, FakeInfo, Var
@@ -209,7 +209,11 @@ def visit_erased_type(self, t: ErasedType) -> Type:
209209
return t
210210

211211
def visit_instance(self, t: Instance) -> Type:
212-
args = self.expand_types_with_unpack(list(t.args))
212+
if len(t.args) == 0:
213+
# TODO: Why do we need to create a copy here?
214+
return t.copy_modified()
215+
216+
args = self.expand_type_tuple_with_unpack(t.args)
213217

214218
if isinstance(t.type, FakeInfo):
215219
# The type checker expands function definitions and bodies
@@ -431,7 +435,7 @@ def visit_overloaded(self, t: Overloaded) -> Type:
431435
items.append(new_item)
432436
return Overloaded(items)
433437

434-
def expand_types_with_unpack(self, typs: Sequence[Type]) -> list[Type]:
438+
def expand_type_list_with_unpack(self, typs: list[Type]) -> list[Type]:
435439
"""Expands a list of types that has an unpack."""
436440
items: list[Type] = []
437441
for item in typs:
@@ -441,8 +445,19 @@ def expand_types_with_unpack(self, typs: Sequence[Type]) -> list[Type]:
441445
items.append(item.accept(self))
442446
return items
443447

448+
def expand_type_tuple_with_unpack(self, typs: tuple[Type, ...]) -> list[Type]:
449+
"""Expands a tuple of types that has an unpack."""
450+
# Micro-optimization: Specialized variant of expand_type_list_with_unpack
451+
items: list[Type] = []
452+
for item in typs:
453+
if isinstance(item, UnpackType) and isinstance(item.type, TypeVarTupleType):
454+
items.extend(self.expand_unpack(item))
455+
else:
456+
items.append(item.accept(self))
457+
return items
458+
444459
def visit_tuple_type(self, t: TupleType) -> Type:
445-
items = self.expand_types_with_unpack(t.items)
460+
items = self.expand_type_list_with_unpack(t.items)
446461
if len(items) == 1:
447462
# Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...]
448463
item = items[0]
@@ -510,7 +525,7 @@ def visit_type_type(self, t: TypeType) -> Type:
510525
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
511526
# Target of the type alias cannot contain type variables (not bound by the type
512527
# alias itself), so we just expand the arguments.
513-
args = self.expand_types_with_unpack(t.args)
528+
args = self.expand_type_list_with_unpack(t.args)
514529
# TODO: normalize if target is Tuple, and args are [*tuple[X, ...]]?
515530
return t.copy_modified(args=args)
516531

mypy/indirection.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,29 @@ def find_modules(self, typs: Iterable[types.Type]) -> set[str]:
3232
self.modules = set()
3333
self.seen_fullnames = set()
3434
self.seen_aliases = set()
35-
self._visit(typs)
35+
for typ in typs:
36+
self._visit(typ)
3637
return self.modules
3738

38-
def _visit(self, typ_or_typs: types.Type | Iterable[types.Type]) -> None:
39-
typs = [typ_or_typs] if isinstance(typ_or_typs, types.Type) else typ_or_typs
39+
def _visit(self, typ: types.Type) -> None:
40+
if isinstance(typ, types.TypeAliasType):
41+
# Avoid infinite recursion for recursive type aliases.
42+
if typ not in self.seen_aliases:
43+
self.seen_aliases.add(typ)
44+
typ.accept(self)
45+
46+
def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None:
47+
# Micro-optimization: Specialized version of _visit for lists
48+
for typ in typs:
49+
if isinstance(typ, types.TypeAliasType):
50+
# Avoid infinite recursion for recursive type aliases.
51+
if typ in self.seen_aliases:
52+
continue
53+
self.seen_aliases.add(typ)
54+
typ.accept(self)
55+
56+
def _visit_type_list(self, typs: list[types.Type]) -> None:
57+
# Micro-optimization: Specialized version of _visit for tuples
4058
for typ in typs:
4159
if isinstance(typ, types.TypeAliasType):
4260
# Avoid infinite recursion for recursive type aliases.
@@ -50,7 +68,7 @@ def _visit_module_name(self, module_name: str) -> None:
5068
self.modules.update(split_module_names(module_name))
5169

5270
def visit_unbound_type(self, t: types.UnboundType) -> None:
53-
self._visit(t.args)
71+
self._visit_type_tuple(t.args)
5472

5573
def visit_any(self, t: types.AnyType) -> None:
5674
pass
@@ -68,7 +86,7 @@ def visit_deleted_type(self, t: types.DeletedType) -> None:
6886
pass
6987

7088
def visit_type_var(self, t: types.TypeVarType) -> None:
71-
self._visit(t.values)
89+
self._visit_type_list(t.values)
7290
self._visit(t.upper_bound)
7391
self._visit(t.default)
7492

@@ -84,10 +102,10 @@ def visit_unpack_type(self, t: types.UnpackType) -> None:
84102
t.type.accept(self)
85103

86104
def visit_parameters(self, t: types.Parameters) -> None:
87-
self._visit(t.arg_types)
105+
self._visit_type_list(t.arg_types)
88106

89107
def visit_instance(self, t: types.Instance) -> None:
90-
self._visit(t.args)
108+
self._visit_type_tuple(t.args)
91109
if t.type:
92110
# Uses of a class depend on everything in the MRO,
93111
# as changes to classes in the MRO can add types to methods,
@@ -98,7 +116,7 @@ def visit_instance(self, t: types.Instance) -> None:
98116
self._visit_module_name(t.type.metaclass_type.type.module_name)
99117

100118
def visit_callable_type(self, t: types.CallableType) -> None:
101-
self._visit(t.arg_types)
119+
self._visit_type_list(t.arg_types)
102120
self._visit(t.ret_type)
103121
if t.definition is not None:
104122
fullname = t.definition.fullname
@@ -107,22 +125,22 @@ def visit_callable_type(self, t: types.CallableType) -> None:
107125
self.seen_fullnames.add(fullname)
108126

109127
def visit_overloaded(self, t: types.Overloaded) -> None:
110-
self._visit(t.items)
128+
self._visit_type_list(list(t.items))
111129
self._visit(t.fallback)
112130

113131
def visit_tuple_type(self, t: types.TupleType) -> None:
114-
self._visit(t.items)
132+
self._visit_type_list(t.items)
115133
self._visit(t.partial_fallback)
116134

117135
def visit_typeddict_type(self, t: types.TypedDictType) -> None:
118-
self._visit(t.items.values())
136+
self._visit_type_list(list(t.items.values()))
119137
self._visit(t.fallback)
120138

121139
def visit_literal_type(self, t: types.LiteralType) -> None:
122140
self._visit(t.fallback)
123141

124142
def visit_union_type(self, t: types.UnionType) -> None:
125-
self._visit(t.items)
143+
self._visit_type_list(t.items)
126144

127145
def visit_partial_type(self, t: types.PartialType) -> None:
128146
pass

mypy/plugin.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -846,26 +846,51 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
846846
return deps
847847

848848
def get_type_analyze_hook(self, fullname: str) -> Callable[[AnalyzeTypeContext], Type] | None:
849-
return self._find_hook(lambda plugin: plugin.get_type_analyze_hook(fullname))
849+
# Micro-optimization: Inline iteration over plugins
850+
for plugin in self._plugins:
851+
hook = plugin.get_type_analyze_hook(fullname)
852+
if hook is not None:
853+
return hook
854+
return None
850855

851856
def get_function_signature_hook(
852857
self, fullname: str
853858
) -> Callable[[FunctionSigContext], FunctionLike] | None:
854-
return self._find_hook(lambda plugin: plugin.get_function_signature_hook(fullname))
859+
# Micro-optimization: Inline iteration over plugins
860+
for plugin in self._plugins:
861+
hook = plugin.get_function_signature_hook(fullname)
862+
if hook is not None:
863+
return hook
864+
return None
855865

856866
def get_function_hook(self, fullname: str) -> Callable[[FunctionContext], Type] | None:
857867
return self._find_hook(lambda plugin: plugin.get_function_hook(fullname))
858868

859869
def get_method_signature_hook(
860870
self, fullname: str
861871
) -> Callable[[MethodSigContext], FunctionLike] | None:
862-
return self._find_hook(lambda plugin: plugin.get_method_signature_hook(fullname))
872+
# Micro-optimization: Inline iteration over plugins
873+
for plugin in self._plugins:
874+
hook = plugin.get_method_signature_hook(fullname)
875+
if hook is not None:
876+
return hook
877+
return None
863878

864879
def get_method_hook(self, fullname: str) -> Callable[[MethodContext], Type] | None:
865-
return self._find_hook(lambda plugin: plugin.get_method_hook(fullname))
880+
# Micro-optimization: Inline iteration over plugins
881+
for plugin in self._plugins:
882+
hook = plugin.get_method_hook(fullname)
883+
if hook is not None:
884+
return hook
885+
return None
866886

867887
def get_attribute_hook(self, fullname: str) -> Callable[[AttributeContext], Type] | None:
868-
return self._find_hook(lambda plugin: plugin.get_attribute_hook(fullname))
888+
# Micro-optimization: Inline iteration over plugins
889+
for plugin in self._plugins:
890+
hook = plugin.get_attribute_hook(fullname)
891+
if hook is not None:
892+
return hook
893+
return None
869894

870895
def get_class_attribute_hook(self, fullname: str) -> Callable[[AttributeContext], Type] | None:
871896
return self._find_hook(lambda plugin: plugin.get_class_attribute_hook(fullname))
@@ -897,6 +922,6 @@ def get_dynamic_class_hook(
897922
def _find_hook(self, lookup: Callable[[Plugin], T]) -> T | None:
898923
for plugin in self._plugins:
899924
hook = lookup(plugin)
900-
if hook:
925+
if hook is not None:
901926
return hook
902927
return None

mypy/server/astmerge.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -345,13 +345,11 @@ def visit_type_alias(self, node: TypeAlias) -> None:
345345
def fixup(self, node: SN) -> SN:
346346
if node in self.replacements:
347347
new = self.replacements[node]
348-
skip_slots: tuple[str, ...] = ()
349348
if isinstance(node, TypeInfo) and isinstance(new, TypeInfo):
350349
# Special case: special_alias is not exposed in symbol tables, but may appear
351350
# in external types (e.g. named tuples), so we need to update it manually.
352-
skip_slots = ("special_alias",)
353351
replace_object_state(new.special_alias, node.special_alias)
354-
replace_object_state(new, node, skip_slots=skip_slots)
352+
replace_object_state(new, node, skip_slots=_get_ignored_slots(new))
355353
return cast(SN, new)
356354
return node
357355

@@ -556,9 +554,16 @@ def replace_nodes_in_symbol_table(
556554
if node.node in replacements:
557555
new = replacements[node.node]
558556
old = node.node
559-
# Needed for TypeInfo, see comment in fixup() above.
560-
replace_object_state(new, old, skip_slots=("special_alias",))
557+
replace_object_state(new, old, skip_slots=_get_ignored_slots(new))
561558
node.node = new
562559
if isinstance(node.node, (Var, TypeAlias)):
563560
# Handle them here just in case these aren't exposed through the AST.
564561
node.node.accept(NodeReplaceVisitor(replacements))
562+
563+
564+
def _get_ignored_slots(node: SymbolNode) -> tuple[str, ...]:
565+
if isinstance(node, OverloadedFuncDef):
566+
return ("setter",)
567+
if isinstance(node, TypeInfo):
568+
return ("special_alias",)
569+
return ()

0 commit comments

Comments
 (0)