Skip to content

Commit 246e461

Browse files
Merge branch 'master' into fix_match_case_union_argument
2 parents 7be0e87 + a0665e1 commit 246e461

File tree

9 files changed

+171
-19
lines changed

9 files changed

+171
-19
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/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/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 ()

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,14 @@ addopts = "-nauto --strict-markers --strict-config"
222222
# treat xpasses as test failures so they get converted to regular tests as soon as possible
223223
xfail_strict = true
224224

225+
# Force warnings as errors
226+
filterwarnings = [
227+
"error",
228+
# Some testcases may contain code that emits SyntaxWarnings, and they are not yet
229+
# handled consistently in 3.14 (PEP 765)
230+
"default::SyntaxWarning",
231+
]
232+
225233
[tool.coverage.run]
226234
branch = true
227235
source = ["mypy"]

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,15 @@ def run(self) -> None:
145145
opt_level = os.getenv("MYPYC_OPT_LEVEL", "3")
146146
debug_level = os.getenv("MYPYC_DEBUG_LEVEL", "1")
147147
force_multifile = os.getenv("MYPYC_MULTI_FILE", "") == "1"
148+
log_trace = bool(int(os.getenv("MYPYC_LOG_TRACE", "0")))
148149
ext_modules = mypycify(
149150
mypyc_targets + ["--config-file=mypy_bootstrap.ini"],
150151
opt_level=opt_level,
151152
debug_level=debug_level,
152153
# Use multi-file compilation mode on windows because without it
153154
# our Appveyor builds run out of memory sometimes.
154155
multi_file=sys.platform == "win32" or force_multifile,
156+
log_trace=log_trace,
155157
)
156158

157159
else:

test-data/unit/check-unreachable-code.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,3 +1587,19 @@ x = 0 # not unreachable
15871587

15881588
f2: Callable[[], NoReturn] = lambda: foo()
15891589
x = 0 # not unreachable
1590+
1591+
[case testAttributeNoReturn]
1592+
# flags: --warn-unreachable
1593+
from typing import Optional, NoReturn, TypeVar
1594+
1595+
def foo() -> NoReturn:
1596+
raise
1597+
1598+
T = TypeVar("T")
1599+
def bar(x: Optional[list[T]] = None) -> T:
1600+
...
1601+
1602+
reveal_type(bar().attr) # N: Revealed type is "Never"
1603+
1 # not unreachable
1604+
reveal_type(foo().attr) # N: Revealed type is "Never"
1605+
1 # E: Statement is unreachable

0 commit comments

Comments
 (0)