Skip to content

Commit 4698a87

Browse files
Merge branch 'master' into patch-3
2 parents fd5bf8b + 16e99de commit 4698a87

File tree

12 files changed

+401
-24
lines changed

12 files changed

+401
-24
lines changed

docs/source/generics.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ Let us illustrate this by few simple examples:
630630
631631
my_circles: list[Circle] = []
632632
add_one(my_circles) # This may appear safe, but...
633-
my_circles[-1].rotate() # ...this will fail, since my_circles[0] is now a Shape, not a Circle
633+
my_circles[0].rotate() # ...this will fail, since my_circles[0] is now a Shape, not a Circle
634634
635635
Another example of invariant type is ``dict``. Most mutable containers
636636
are invariant.

misc/perf_compare.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,15 @@ def heading(s: str) -> None:
3535
print()
3636

3737

38-
def build_mypy(target_dir: str) -> None:
38+
def build_mypy(target_dir: str, multi_file: bool, *, cflags: str | None = None) -> None:
3939
env = os.environ.copy()
4040
env["CC"] = "clang"
4141
env["MYPYC_OPT_LEVEL"] = "2"
4242
env["PYTHONHASHSEED"] = "1"
43+
if multi_file:
44+
env["MYPYC_MULTI_FILE"] = "1"
45+
if cflags is not None:
46+
env["CFLAGS"] = cflags
4347
cmd = [sys.executable, "setup.py", "--use-mypyc", "build_ext", "--inplace"]
4448
subprocess.run(cmd, env=env, check=True, cwd=target_dir)
4549

@@ -110,6 +114,12 @@ def main() -> None:
110114
action="store_true",
111115
help="measure incremental run (fully cached)",
112116
)
117+
parser.add_argument(
118+
"--multi-file",
119+
default=False,
120+
action="store_true",
121+
help="compile each mypy module to a separate C file (reduces RAM use)",
122+
)
113123
parser.add_argument(
114124
"--dont-setup",
115125
default=False,
@@ -127,9 +137,9 @@ def main() -> None:
127137
parser.add_argument(
128138
"-j",
129139
metavar="N",
130-
default=8,
140+
default=4,
131141
type=int,
132-
help="set maximum number of parallel builds (default=8)",
142+
help="set maximum number of parallel builds (default=4) -- high numbers require a lot of RAM!",
133143
)
134144
parser.add_argument(
135145
"-r",
@@ -155,6 +165,7 @@ def main() -> None:
155165
args = parser.parse_args()
156166
incremental: bool = args.incremental
157167
dont_setup: bool = args.dont_setup
168+
multi_file: bool = args.multi_file
158169
commits = args.commit
159170
num_runs: int = args.num_runs + 1
160171
max_workers: int = args.j
@@ -185,7 +196,9 @@ def main() -> None:
185196
print("(This will take a while...)")
186197

187198
with ThreadPoolExecutor(max_workers=max_workers) as executor:
188-
futures = [executor.submit(build_mypy, target_dir) for target_dir in target_dirs]
199+
futures = [
200+
executor.submit(build_mypy, target_dir, multi_file) for target_dir in target_dirs
201+
]
189202
for future in as_completed(futures):
190203
future.result()
191204

misc/profile_self_check.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""Compile mypy using mypyc and profile self-check using perf.
2+
3+
Notes:
4+
- Only Linux is supported for now (TODO: add support for other profilers)
5+
- The profile is collected at C level
6+
- It includes C functions compiled by mypyc and CPython runtime functions
7+
- The names of mypy functions are mangled to C names, but usually it's clear what they mean
8+
- Generally CPyDef_ prefix for native functions and CPyPy_ prefix for wrapper functions
9+
- It's important to compile CPython using special flags (see below) to get good results
10+
- Generally use the latest Python feature release (or the most recent beta if supported by mypyc)
11+
- The tool prints a command that can be used to analyze the profile afterwards
12+
13+
You may need to adjust kernel parameters temporarily, e.g. this (note that this has security
14+
implications):
15+
16+
sudo sysctl kernel.perf_event_paranoid=-1
17+
18+
This is the recommended way to configure CPython for profiling:
19+
20+
./configure \
21+
--enable-optimizations \
22+
--with-lto \
23+
CFLAGS="-O2 -g -fno-omit-frame-pointer"
24+
"""
25+
26+
import argparse
27+
import glob
28+
import os
29+
import shutil
30+
import subprocess
31+
import sys
32+
import time
33+
34+
from perf_compare import build_mypy, clone
35+
36+
# Use these C compiler flags when compiling mypy (important). Note that it's strongly recommended
37+
# to also compile CPython using similar flags, but we don't enforce it in this script.
38+
CFLAGS = "-O2 -fno-omit-frame-pointer -g"
39+
40+
# Generated files, including binaries, go under this directory to avoid overwriting user state.
41+
TARGET_DIR = "mypy.profile.tmpdir"
42+
43+
44+
def _profile_self_check(target_dir: str) -> None:
45+
cache_dir = os.path.join(target_dir, ".mypy_cache")
46+
if os.path.exists(cache_dir):
47+
shutil.rmtree(cache_dir)
48+
files = []
49+
for pat in "mypy/*.py", "mypy/*/*.py", "mypyc/*.py", "mypyc/test/*.py":
50+
files.extend(glob.glob(pat))
51+
self_check_cmd = ["python", "-m", "mypy", "--config-file", "mypy_self_check.ini"] + files
52+
cmdline = ["perf", "record", "-g"] + self_check_cmd
53+
t0 = time.time()
54+
subprocess.run(cmdline, cwd=target_dir, check=True)
55+
elapsed = time.time() - t0
56+
print(f"{elapsed:.2f}s elapsed")
57+
58+
59+
def profile_self_check(target_dir: str) -> None:
60+
try:
61+
_profile_self_check(target_dir)
62+
except subprocess.CalledProcessError:
63+
print("\nProfiling failed! You may missing some permissions.")
64+
print("\nThis may help (note that it has security implications):")
65+
print(" sudo sysctl kernel.perf_event_paranoid=-1")
66+
sys.exit(1)
67+
68+
69+
def check_requirements() -> None:
70+
if sys.platform != "linux":
71+
# TODO: How to make this work on other platforms?
72+
sys.exit("error: Only Linux is supported")
73+
74+
try:
75+
subprocess.run(["perf", "-h"], capture_output=True)
76+
except (subprocess.CalledProcessError, FileNotFoundError):
77+
print("error: The 'perf' profiler is not installed")
78+
sys.exit(1)
79+
80+
try:
81+
subprocess.run(["clang", "--version"], capture_output=True)
82+
except (subprocess.CalledProcessError, FileNotFoundError):
83+
print("error: The clang compiler is not installed")
84+
sys.exit(1)
85+
86+
if not os.path.isfile("mypy_self_check.ini"):
87+
print("error: Run this in the mypy repository root")
88+
sys.exit(1)
89+
90+
91+
def main() -> None:
92+
check_requirements()
93+
94+
parser = argparse.ArgumentParser(
95+
description="Compile mypy and profile self checking using 'perf'."
96+
)
97+
parser.add_argument(
98+
"--multi-file",
99+
action="store_true",
100+
help="compile mypy into one C file per module (to reduce RAM use during compilation)",
101+
)
102+
parser.add_argument(
103+
"--skip-compile", action="store_true", help="use compiled mypy from previous run"
104+
)
105+
args = parser.parse_args()
106+
multi_file: bool = args.multi_file
107+
skip_compile: bool = args.skip_compile
108+
109+
target_dir = TARGET_DIR
110+
111+
if not skip_compile:
112+
clone(target_dir, "HEAD")
113+
114+
print(f"Building mypy in {target_dir}...")
115+
build_mypy(target_dir, multi_file, cflags=CFLAGS)
116+
elif not os.path.isdir(target_dir):
117+
sys.exit("error: Can't find compile mypy from previous run -- can't use --skip-compile")
118+
119+
profile_self_check(target_dir)
120+
121+
print()
122+
print('NOTE: Compile CPython using CFLAGS="-O2 -g -fno-omit-frame-pointer" for good results')
123+
print()
124+
print("CPU profile collected. You can now analyze the profile:")
125+
print(f" perf report -i {target_dir}/perf.data ")
126+
127+
128+
if __name__ == "__main__":
129+
main()

mypy/checker.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6185,6 +6185,7 @@ def find_isinstance_check_helper(
61856185
self.lookup_type(expr),
61866186
[TypeRange(node.callee.type_is, is_upper_bound=False)],
61876187
expr,
6188+
consider_runtime_isinstance=False,
61886189
),
61896190
)
61906191
elif isinstance(node, ComparisonExpr):
@@ -7612,11 +7613,19 @@ def conditional_types_with_intersection(
76127613
type_ranges: list[TypeRange] | None,
76137614
ctx: Context,
76147615
default: None = None,
7616+
*,
7617+
consider_runtime_isinstance: bool = True,
76157618
) -> tuple[Type | None, Type | None]: ...
76167619

76177620
@overload
76187621
def conditional_types_with_intersection(
7619-
self, expr_type: Type, type_ranges: list[TypeRange] | None, ctx: Context, default: Type
7622+
self,
7623+
expr_type: Type,
7624+
type_ranges: list[TypeRange] | None,
7625+
ctx: Context,
7626+
default: Type,
7627+
*,
7628+
consider_runtime_isinstance: bool = True,
76207629
) -> tuple[Type, Type]: ...
76217630

76227631
def conditional_types_with_intersection(
@@ -7625,8 +7634,15 @@ def conditional_types_with_intersection(
76257634
type_ranges: list[TypeRange] | None,
76267635
ctx: Context,
76277636
default: Type | None = None,
7637+
*,
7638+
consider_runtime_isinstance: bool = True,
76287639
) -> tuple[Type | None, Type | None]:
7629-
initial_types = conditional_types(expr_type, type_ranges, default)
7640+
initial_types = conditional_types(
7641+
expr_type,
7642+
type_ranges,
7643+
default,
7644+
consider_runtime_isinstance=consider_runtime_isinstance,
7645+
)
76307646
# For some reason, doing "yes_map, no_map = conditional_types_to_typemaps(...)"
76317647
# doesn't work: mypyc will decide that 'yes_map' is of type None if we try.
76327648
yes_type: Type | None = initial_types[0]
@@ -7938,18 +7954,30 @@ def visit_type_var(self, t: TypeVarType) -> None:
79387954

79397955
@overload
79407956
def conditional_types(
7941-
current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: None = None
7957+
current_type: Type,
7958+
proposed_type_ranges: list[TypeRange] | None,
7959+
default: None = None,
7960+
*,
7961+
consider_runtime_isinstance: bool = True,
79427962
) -> tuple[Type | None, Type | None]: ...
79437963

79447964

79457965
@overload
79467966
def conditional_types(
7947-
current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type
7967+
current_type: Type,
7968+
proposed_type_ranges: list[TypeRange] | None,
7969+
default: Type,
7970+
*,
7971+
consider_runtime_isinstance: bool = True,
79487972
) -> tuple[Type, Type]: ...
79497973

79507974

79517975
def conditional_types(
7952-
current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type | None = None
7976+
current_type: Type,
7977+
proposed_type_ranges: list[TypeRange] | None,
7978+
default: Type | None = None,
7979+
*,
7980+
consider_runtime_isinstance: bool = True,
79537981
) -> tuple[Type | None, Type | None]:
79547982
"""Takes in the current type and a proposed type of an expression.
79557983
@@ -7991,7 +8019,11 @@ def conditional_types(
79918019
if not type_range.is_upper_bound
79928020
]
79938021
)
7994-
remaining_type = restrict_subtype_away(current_type, proposed_precise_type)
8022+
remaining_type = restrict_subtype_away(
8023+
current_type,
8024+
proposed_precise_type,
8025+
consider_runtime_isinstance=consider_runtime_isinstance,
8026+
)
79958027
return proposed_type, remaining_type
79968028
else:
79978029
# An isinstance check, but we don't understand the type

mypy/checkexpr.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ class ExpressionChecker(ExpressionVisitor[Type], ExpressionCheckerSharedApi):
318318
strfrm_checker: StringFormatterChecker
319319
plugin: Plugin
320320

321+
_arg_infer_context_cache: ArgumentInferContext | None
322+
321323
def __init__(
322324
self,
323325
chk: mypy.checker.TypeChecker,
@@ -352,6 +354,8 @@ def __init__(
352354
self.is_callee = False
353355
type_state.infer_polymorphic = not self.chk.options.old_type_inference
354356

357+
self._arg_infer_context_cache = None
358+
355359
def reset(self) -> None:
356360
self.resolved_type = {}
357361

@@ -2277,9 +2281,11 @@ def infer_function_type_arguments_pass2(
22772281
return callee_type, inferred_args
22782282

22792283
def argument_infer_context(self) -> ArgumentInferContext:
2280-
return ArgumentInferContext(
2281-
self.chk.named_type("typing.Mapping"), self.chk.named_type("typing.Iterable")
2282-
)
2284+
if self._arg_infer_context_cache is None:
2285+
self._arg_infer_context_cache = ArgumentInferContext(
2286+
self.chk.named_type("typing.Mapping"), self.chk.named_type("typing.Iterable")
2287+
)
2288+
return self._arg_infer_context_cache
22832289

22842290
def get_arg_infer_passes(
22852291
self,

mypy/subtypes.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2073,7 +2073,7 @@ def try_restrict_literal_union(t: UnionType, s: Type) -> list[Type] | None:
20732073
return new_items
20742074

20752075

2076-
def restrict_subtype_away(t: Type, s: Type) -> Type:
2076+
def restrict_subtype_away(t: Type, s: Type, *, consider_runtime_isinstance: bool = True) -> Type:
20772077
"""Return t minus s for runtime type assertions.
20782078
20792079
If we can't determine a precise result, return a supertype of the
@@ -2087,16 +2087,27 @@ def restrict_subtype_away(t: Type, s: Type) -> Type:
20872087
new_items = try_restrict_literal_union(p_t, s)
20882088
if new_items is None:
20892089
new_items = [
2090-
restrict_subtype_away(item, s)
2090+
restrict_subtype_away(
2091+
item, s, consider_runtime_isinstance=consider_runtime_isinstance
2092+
)
20912093
for item in p_t.relevant_items()
2092-
if (isinstance(get_proper_type(item), AnyType) or not covers_at_runtime(item, s))
20932094
]
2094-
return UnionType.make_union(new_items)
2095+
return UnionType.make_union(
2096+
[item for item in new_items if not isinstance(get_proper_type(item), UninhabitedType)]
2097+
)
20952098
elif isinstance(p_t, TypeVarType):
20962099
return p_t.copy_modified(upper_bound=restrict_subtype_away(p_t.upper_bound, s))
2097-
elif covers_at_runtime(t, s):
2098-
return UninhabitedType()
2100+
2101+
if consider_runtime_isinstance:
2102+
if covers_at_runtime(t, s):
2103+
return UninhabitedType()
2104+
else:
2105+
return t
20992106
else:
2107+
if is_proper_subtype(t, s, ignore_promotions=True):
2108+
return UninhabitedType()
2109+
if is_proper_subtype(t, s, ignore_promotions=True, erase_instances=True):
2110+
return UninhabitedType()
21002111
return t
21012112

21022113

@@ -2106,7 +2117,8 @@ def covers_at_runtime(item: Type, supertype: Type) -> bool:
21062117
supertype = get_proper_type(supertype)
21072118

21082119
# Since runtime type checks will ignore type arguments, erase the types.
2109-
supertype = erase_type(supertype)
2120+
if not (isinstance(supertype, CallableType) and supertype.is_type_obj()):
2121+
supertype = erase_type(supertype)
21102122
if is_proper_subtype(
21112123
erase_type(item), supertype, ignore_promotions=True, erase_instances=True
21122124
):

mypy/suggestions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ def visit_instance(self, t: Instance) -> str:
852852
if self.module:
853853
parts = obj.split(".") # need to split the object part if it is a nested class
854854
tree = self.graph[self.module].tree
855-
if tree and parts[0] in tree.names:
855+
if tree and parts[0] in tree.names and mod not in tree.names:
856856
mod = self.module
857857

858858
if (mod, obj) == ("builtins", "tuple"):

mypy/typeanal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,8 +1109,8 @@ def visit_callable_type(
11091109
variables = t.variables
11101110
else:
11111111
variables, _ = self.bind_function_type_variables(t, t)
1112-
type_guard = self.anal_type_guard(t.ret_type)
1113-
type_is = self.anal_type_is(t.ret_type)
1112+
type_guard = self.anal_type_guard(t.ret_type) if t.type_guard is None else t.type_guard
1113+
type_is = self.anal_type_is(t.ret_type) if t.type_is is None else t.type_is
11141114

11151115
arg_kinds = t.arg_kinds
11161116
arg_types = []

0 commit comments

Comments
 (0)