Skip to content

Commit 8d94c09

Browse files
authored
Merge branch 'master' into try-finally-await
2 parents 18dbff2 + 02c9766 commit 8d94c09

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+2345
-616
lines changed

.github/workflows/mypy_primer.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
runs-on: ubuntu-latest
2929
strategy:
3030
matrix:
31-
shard-index: [0, 1, 2, 3, 4]
31+
shard-index: [0, 1, 2, 3, 4, 5]
3232
fail-fast: false
3333
timeout-minutes: 60
3434
steps:
@@ -63,7 +63,7 @@ jobs:
6363
mypy_primer \
6464
--repo mypy_to_test \
6565
--new $GITHUB_SHA --old base_commit \
66-
--num-shards 5 --shard-index ${{ matrix.shard-index }} \
66+
--num-shards 6 --shard-index ${{ matrix.shard-index }} \
6767
--debug \
6868
--additional-flags="--debug-serialize" \
6969
--output concise \

misc/profile_self_check.py renamed to misc/profile_check.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
"""Compile mypy using mypyc and profile self-check using perf.
1+
"""Compile mypy using mypyc and profile type checking using perf.
2+
3+
By default does a self check.
24
35
Notes:
46
- Only Linux is supported for now (TODO: add support for other profilers)
@@ -23,6 +25,8 @@
2325
CFLAGS="-O2 -g -fno-omit-frame-pointer"
2426
"""
2527

28+
from __future__ import annotations
29+
2630
import argparse
2731
import glob
2832
import os
@@ -41,24 +45,28 @@
4145
TARGET_DIR = "mypy.profile.tmpdir"
4246

4347

44-
def _profile_self_check(target_dir: str) -> None:
48+
def _profile_type_check(target_dir: str, code: str | None) -> None:
4549
cache_dir = os.path.join(target_dir, ".mypy_cache")
4650
if os.path.exists(cache_dir):
4751
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
52+
args = []
53+
if code is None:
54+
args.extend(["--config-file", "mypy_self_check.ini"])
55+
for pat in "mypy/*.py", "mypy/*/*.py", "mypyc/*.py", "mypyc/test/*.py":
56+
args.extend(glob.glob(pat))
57+
else:
58+
args.extend(["-c", code])
59+
check_cmd = ["python", "-m", "mypy"] + args
60+
cmdline = ["perf", "record", "-g"] + check_cmd
5361
t0 = time.time()
5462
subprocess.run(cmdline, cwd=target_dir, check=True)
5563
elapsed = time.time() - t0
5664
print(f"{elapsed:.2f}s elapsed")
5765

5866

59-
def profile_self_check(target_dir: str) -> None:
67+
def profile_type_check(target_dir: str, code: str | None) -> None:
6068
try:
61-
_profile_self_check(target_dir)
69+
_profile_type_check(target_dir, code)
6270
except subprocess.CalledProcessError:
6371
print("\nProfiling failed! You may missing some permissions.")
6472
print("\nThis may help (note that it has security implications):")
@@ -92,7 +100,7 @@ def main() -> None:
92100
check_requirements()
93101

94102
parser = argparse.ArgumentParser(
95-
description="Compile mypy and profile self checking using 'perf'."
103+
description="Compile mypy and profile type checking using 'perf' (by default, self check)."
96104
)
97105
parser.add_argument(
98106
"--multi-file",
@@ -102,9 +110,17 @@ def main() -> None:
102110
parser.add_argument(
103111
"--skip-compile", action="store_true", help="use compiled mypy from previous run"
104112
)
113+
parser.add_argument(
114+
"-c",
115+
metavar="CODE",
116+
default=None,
117+
type=str,
118+
help="profile type checking Python code fragment instead of mypy self-check",
119+
)
105120
args = parser.parse_args()
106121
multi_file: bool = args.multi_file
107122
skip_compile: bool = args.skip_compile
123+
code: str | None = args.c
108124

109125
target_dir = TARGET_DIR
110126

@@ -116,7 +132,7 @@ def main() -> None:
116132
elif not os.path.isdir(target_dir):
117133
sys.exit("error: Can't find compile mypy from previous run -- can't use --skip-compile")
118134

119-
profile_self_check(target_dir)
135+
profile_type_check(target_dir, code)
120136

121137
print()
122138
print('NOTE: Compile CPython using CFLAGS="-O2 -g -fno-omit-frame-pointer" for good results')

mypy/checker.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ def accept_loop(
618618
if on_enter_body is not None:
619619
on_enter_body()
620620

621-
with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher:
621+
with IterationErrorWatcher(self.msg.errors, iter_errors):
622622
self.accept(body)
623623

624624
partials_new = sum(len(pts.map) for pts in self.partial_types)
@@ -641,10 +641,7 @@ def accept_loop(
641641
if iter == 20:
642642
raise RuntimeError("Too many iterations when checking a loop")
643643

644-
for error_info in watcher.yield_error_infos():
645-
self.msg.fail(*error_info[:2], code=error_info[2])
646-
for note_info in watcher.yield_note_infos(self.options):
647-
self.note(*note_info)
644+
self.msg.iteration_dependent_errors(iter_errors)
648645

649646
# If exit_condition is set, assume it must be False on exit from the loop:
650647
if exit_condition:
@@ -3041,7 +3038,7 @@ def is_noop_for_reachability(self, s: Statement) -> bool:
30413038
if isinstance(s.expr, EllipsisExpr):
30423039
return True
30433040
elif isinstance(s.expr, CallExpr):
3044-
with self.expr_checker.msg.filter_errors():
3041+
with self.expr_checker.msg.filter_errors(filter_revealed_type=True):
30453042
typ = get_proper_type(
30463043
self.expr_checker.accept(
30473044
s.expr, allow_none_return=True, always_allow_any=True
@@ -3141,7 +3138,7 @@ def check_assignment(
31413138
else:
31423139
self.check_getattr_method(signature, lvalue, name)
31433140

3144-
if name == "__slots__":
3141+
if name == "__slots__" and self.scope.active_class() is not None:
31453142
typ = lvalue_type or self.expr_checker.accept(rvalue)
31463143
self.check_slots_definition(typ, lvalue)
31473144
if name == "__match_args__" and inferred is not None:
@@ -3320,6 +3317,12 @@ def get_variable_type_context(self, inferred: Var, rvalue: Expression) -> Type |
33203317
type_contexts.append(base_type)
33213318
# Use most derived supertype as type context if available.
33223319
if not type_contexts:
3320+
if inferred.name == "__slots__" and self.scope.active_class() is not None:
3321+
str_type = self.named_type("builtins.str")
3322+
return self.named_generic_type("typing.Iterable", [str_type])
3323+
if inferred.name == "__all__" and self.scope.is_top_level():
3324+
str_type = self.named_type("builtins.str")
3325+
return self.named_generic_type("typing.Sequence", [str_type])
33233326
return None
33243327
candidate = type_contexts[0]
33253328
for other in type_contexts:
@@ -4969,7 +4972,7 @@ def visit_try_stmt(self, s: TryStmt) -> None:
49694972
if s.finally_body:
49704973
# First we check finally_body is type safe on all abnormal exit paths
49714974
iter_errors = IterationDependentErrors()
4972-
with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher:
4975+
with IterationErrorWatcher(self.msg.errors, iter_errors):
49734976
self.accept(s.finally_body)
49744977

49754978
if s.finally_body:
@@ -4986,13 +4989,9 @@ def visit_try_stmt(self, s: TryStmt) -> None:
49864989
# that follows the try statement.)
49874990
assert iter_errors is not None
49884991
if not self.binder.is_unreachable():
4989-
with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher:
4992+
with IterationErrorWatcher(self.msg.errors, iter_errors):
49904993
self.accept(s.finally_body)
4991-
4992-
for error_info in watcher.yield_error_infos():
4993-
self.msg.fail(*error_info[:2], code=error_info[2])
4994-
for note_info in watcher.yield_note_infos(self.options):
4995-
self.msg.note(*note_info)
4994+
self.msg.iteration_dependent_errors(iter_errors)
49964995

49974996
def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None:
49984997
"""Type check a try statement, ignoring the finally block.
@@ -7280,7 +7279,7 @@ def named_type(self, name: str) -> Instance:
72807279
if isinstance(node, TypeAlias):
72817280
assert isinstance(node.target, Instance) # type: ignore[misc]
72827281
node = node.target.type
7283-
assert isinstance(node, TypeInfo)
7282+
assert isinstance(node, TypeInfo), node
72847283
any_type = AnyType(TypeOfAny.from_omitted_generics)
72857284
return Instance(node, [any_type] * len(node.defn.type_vars))
72867285

@@ -7299,7 +7298,7 @@ def lookup_typeinfo(self, fullname: str) -> TypeInfo:
72997298
# Assume that the name refers to a class.
73007299
sym = self.lookup_qualified(fullname)
73017300
node = sym.node
7302-
assert isinstance(node, TypeInfo)
7301+
assert isinstance(node, TypeInfo), node
73037302
return node
73047303

73057304
def type_type(self) -> Instance:

mypy/checker_shared.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ def current_self_type(self) -> Instance | TupleType | None:
334334
return fill_typevars(item)
335335
return None
336336

337+
def is_top_level(self) -> bool:
338+
"""Is current scope top-level (no classes or functions)?"""
339+
return len(self.stack) == 1
340+
337341
@contextmanager
338342
def push_function(self, item: FuncItem) -> Iterator[None]:
339343
self.stack.append(item)

mypy/checkpattern.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -796,9 +796,9 @@ def get_var(expr: Expression) -> Var:
796796
Warning: this in only true for expressions captured by a match statement.
797797
Don't call it from anywhere else
798798
"""
799-
assert isinstance(expr, NameExpr)
799+
assert isinstance(expr, NameExpr), expr
800800
node = expr.node
801-
assert isinstance(node, Var)
801+
assert isinstance(node, Var), node
802802
return node
803803

804804

mypy/errors.py

Lines changed: 46 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from mypy.nodes import Context
1616
from mypy.options import Options
1717
from mypy.scope import Scope
18+
from mypy.types import Type
1819
from mypy.util import DEFAULT_SOURCE_OFFSET, is_typeshed_file
1920
from mypy.version import __version__ as mypy_version
2021

@@ -166,18 +167,24 @@ class ErrorWatcher:
166167
out by one of the ErrorWatcher instances.
167168
"""
168169

170+
# public attribute for the special treatment of `reveal_type` by
171+
# `MessageBuilder.reveal_type`:
172+
filter_revealed_type: bool
173+
169174
def __init__(
170175
self,
171176
errors: Errors,
172177
*,
173178
filter_errors: bool | Callable[[str, ErrorInfo], bool] = False,
174179
save_filtered_errors: bool = False,
175180
filter_deprecated: bool = False,
181+
filter_revealed_type: bool = False,
176182
) -> None:
177183
self.errors = errors
178184
self._has_new_errors = False
179185
self._filter = filter_errors
180186
self._filter_deprecated = filter_deprecated
187+
self.filter_revealed_type = filter_revealed_type
181188
self._filtered: list[ErrorInfo] | None = [] if save_filtered_errors else None
182189

183190
def __enter__(self) -> Self:
@@ -236,15 +243,41 @@ class IterationDependentErrors:
236243
# the error report occurs but really all unreachable lines.
237244
unreachable_lines: list[set[int]]
238245

239-
# One set of revealed types for each `reveal_type` statement. Each created set can
240-
# grow during the iteration. Meaning of the tuple items: function_or_member, line,
241-
# column, end_line, end_column:
242-
revealed_types: dict[tuple[str | None, int, int, int, int], set[str]]
246+
# One list of revealed types for each `reveal_type` statement. Each created list
247+
# can grow during the iteration. Meaning of the tuple items: line, column,
248+
# end_line, end_column:
249+
revealed_types: dict[tuple[int, int, int | None, int | None], list[Type]]
243250

244251
def __init__(self) -> None:
245252
self.uselessness_errors = []
246253
self.unreachable_lines = []
247-
self.revealed_types = defaultdict(set)
254+
self.revealed_types = defaultdict(list)
255+
256+
def yield_uselessness_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]:
257+
"""Report only those `unreachable`, `redundant-expr`, and `redundant-casts`
258+
errors that could not be ruled out in any iteration step."""
259+
260+
persistent_uselessness_errors = set()
261+
for candidate in set(chain(*self.uselessness_errors)):
262+
if all(
263+
(candidate in errors) or (candidate[2] in lines)
264+
for errors, lines in zip(self.uselessness_errors, self.unreachable_lines)
265+
):
266+
persistent_uselessness_errors.add(candidate)
267+
for error_info in persistent_uselessness_errors:
268+
context = Context(line=error_info[2], column=error_info[3])
269+
context.end_line = error_info[4]
270+
context.end_column = error_info[5]
271+
yield error_info[1], context, error_info[0]
272+
273+
def yield_revealed_type_infos(self) -> Iterator[tuple[list[Type], Context]]:
274+
"""Yield all types revealed in at least one iteration step."""
275+
276+
for note_info, types in self.revealed_types.items():
277+
context = Context(line=note_info[0], column=note_info[1])
278+
context.end_line = note_info[2]
279+
context.end_column = note_info[3]
280+
yield types, context
248281

249282

250283
class IterationErrorWatcher(ErrorWatcher):
@@ -287,53 +320,8 @@ def on_error(self, file: str, info: ErrorInfo) -> bool:
287320
iter_errors.unreachable_lines[-1].update(range(info.line, info.end_line + 1))
288321
return True
289322

290-
if info.code == codes.MISC and info.message.startswith("Revealed type is "):
291-
key = info.function_or_member, info.line, info.column, info.end_line, info.end_column
292-
types = info.message.split('"')[1]
293-
if types.startswith("Union["):
294-
iter_errors.revealed_types[key].update(types[6:-1].split(", "))
295-
else:
296-
iter_errors.revealed_types[key].add(types)
297-
return True
298-
299323
return super().on_error(file, info)
300324

301-
def yield_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]:
302-
"""Report only those `unreachable`, `redundant-expr`, and `redundant-casts`
303-
errors that could not be ruled out in any iteration step."""
304-
305-
persistent_uselessness_errors = set()
306-
iter_errors = self.iteration_dependent_errors
307-
for candidate in set(chain(*iter_errors.uselessness_errors)):
308-
if all(
309-
(candidate in errors) or (candidate[2] in lines)
310-
for errors, lines in zip(
311-
iter_errors.uselessness_errors, iter_errors.unreachable_lines
312-
)
313-
):
314-
persistent_uselessness_errors.add(candidate)
315-
for error_info in persistent_uselessness_errors:
316-
context = Context(line=error_info[2], column=error_info[3])
317-
context.end_line = error_info[4]
318-
context.end_column = error_info[5]
319-
yield error_info[1], context, error_info[0]
320-
321-
def yield_note_infos(self, options: Options) -> Iterator[tuple[str, Context]]:
322-
"""Yield all types revealed in at least one iteration step."""
323-
324-
for note_info, types in self.iteration_dependent_errors.revealed_types.items():
325-
sorted_ = sorted(types, key=lambda typ: typ.lower())
326-
if len(types) == 1:
327-
revealed = sorted_[0]
328-
elif options.use_or_syntax():
329-
revealed = " | ".join(sorted_)
330-
else:
331-
revealed = f"Union[{', '.join(sorted_)}]"
332-
context = Context(line=note_info[1], column=note_info[2])
333-
context.end_line = note_info[3]
334-
context.end_column = note_info[4]
335-
yield f'Revealed type is "{revealed}"', context
336-
337325

338326
class Errors:
339327
"""Container for compile errors.
@@ -596,18 +584,19 @@ def _add_error_info(self, file: str, info: ErrorInfo) -> None:
596584
if info.code in (IMPORT, IMPORT_UNTYPED, IMPORT_NOT_FOUND):
597585
self.seen_import_error = True
598586

587+
def get_watchers(self) -> Iterator[ErrorWatcher]:
588+
"""Yield the `ErrorWatcher` stack from top to bottom."""
589+
i = len(self._watchers)
590+
while i > 0:
591+
i -= 1
592+
yield self._watchers[i]
593+
599594
def _filter_error(self, file: str, info: ErrorInfo) -> bool:
600595
"""
601596
process ErrorWatcher stack from top to bottom,
602597
stopping early if error needs to be filtered out
603598
"""
604-
i = len(self._watchers)
605-
while i > 0:
606-
i -= 1
607-
w = self._watchers[i]
608-
if w.on_error(file, info):
609-
return True
610-
return False
599+
return any(w.on_error(file, info) for w in self.get_watchers())
611600

612601
def add_error_info(self, info: ErrorInfo) -> None:
613602
file, lines = info.origin

0 commit comments

Comments
 (0)