Skip to content

Commit a9f206b

Browse files
authored
Merge branch 'master' into bugfix/st-synthetic-named-expr-in-match
2 parents a24af26 + db67888 commit a9f206b

File tree

134 files changed

+5172
-2158
lines changed

Some content is hidden

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

134 files changed

+5172
-2158
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 \

docs/source/error_code_list.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,35 @@ You can use :py:class:`~collections.abc.Callable` as the type for callable objec
215215
for x in objs:
216216
f(x)
217217
218+
.. _code-metaclass:
219+
220+
Check the validity of a class's metaclass [metaclass]
221+
-----------------------------------------------------
222+
223+
Mypy checks whether the metaclass of a class is valid. The metaclass
224+
must be a subclass of ``type``. Further, the class hierarchy must yield
225+
a consistent metaclass. For more details, see the
226+
`Python documentation <https://docs.python.org/3.13/reference/datamodel.html#determining-the-appropriate-metaclass>`_
227+
228+
Note that mypy's metaclass checking is limited and may produce false-positives.
229+
See also :ref:`limitations`.
230+
231+
Example with an error:
232+
233+
.. code-block:: python
234+
235+
class GoodMeta(type):
236+
pass
237+
238+
class BadMeta:
239+
pass
240+
241+
class A1(metaclass=GoodMeta): # OK
242+
pass
243+
244+
class A2(metaclass=BadMeta): # Error: Metaclasses not inheriting from "type" are not supported [metaclass]
245+
pass
246+
218247
.. _code-var-annotated:
219248

220249
Require annotation if variable type is unclear [var-annotated]

docs/source/metaclasses.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,28 @@ so it's better not to combine metaclasses and class hierarchies:
9090
* ``Self`` is not allowed as annotation in metaclasses as per `PEP 673`_.
9191

9292
.. _PEP 673: https://peps.python.org/pep-0673/#valid-locations-for-self
93+
94+
For some builtin types, mypy may think their metaclass is :py:class:`abc.ABCMeta`
95+
even if it is :py:class:`type` at runtime. In those cases, you can either:
96+
97+
* use :py:class:`abc.ABCMeta` instead of :py:class:`type` as the
98+
superclass of your metaclass if that works in your use-case
99+
* mute the error with ``# type: ignore[metaclass]``
100+
101+
.. code-block:: python
102+
103+
import abc
104+
105+
assert type(tuple) is type # metaclass of tuple is type at runtime
106+
107+
# The problem:
108+
class M0(type): pass
109+
class A0(tuple, metaclass=M0): pass # Mypy Error: metaclass conflict
110+
111+
# Option 1: use ABCMeta instead of type
112+
class M1(abc.ABCMeta): pass
113+
class A1(tuple, metaclass=M1): pass
114+
115+
# Option 2: mute the error
116+
class M2(type): pass
117+
class A2(tuple, metaclass=M2): pass # type: ignore[metaclass]

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: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ def accept_loop(
623623
if on_enter_body is not None:
624624
on_enter_body()
625625

626-
with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher:
626+
with IterationErrorWatcher(self.msg.errors, iter_errors):
627627
self.accept(body)
628628

629629
partials_new = sum(len(pts.map) for pts in self.partial_types)
@@ -646,10 +646,7 @@ def accept_loop(
646646
if iter == 20:
647647
raise RuntimeError("Too many iterations when checking a loop")
648648

649-
for error_info in watcher.yield_error_infos():
650-
self.msg.fail(*error_info[:2], code=error_info[2])
651-
for note_info in watcher.yield_note_infos(self.options):
652-
self.note(*note_info)
649+
self.msg.iteration_dependent_errors(iter_errors)
653650

654651
# If exit_condition is set, assume it must be False on exit from the loop:
655652
if exit_condition:
@@ -2964,7 +2961,11 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
29642961
"Metaclass conflict: the metaclass of a derived class must be "
29652962
"a (non-strict) subclass of the metaclasses of all its bases",
29662963
typ,
2964+
code=codes.METACLASS,
29672965
)
2966+
explanation = typ.explain_metaclass_conflict()
2967+
if explanation:
2968+
self.note(explanation, typ, code=codes.METACLASS)
29682969

29692970
def visit_import_from(self, node: ImportFrom) -> None:
29702971
for name, _ in node.names:
@@ -3042,7 +3043,7 @@ def is_noop_for_reachability(self, s: Statement) -> bool:
30423043
if isinstance(s.expr, EllipsisExpr):
30433044
return True
30443045
elif isinstance(s.expr, CallExpr):
3045-
with self.expr_checker.msg.filter_errors():
3046+
with self.expr_checker.msg.filter_errors(filter_revealed_type=True):
30463047
typ = get_proper_type(
30473048
self.expr_checker.accept(
30483049
s.expr, allow_none_return=True, always_allow_any=True
@@ -3142,7 +3143,7 @@ def check_assignment(
31423143
else:
31433144
self.check_getattr_method(signature, lvalue, name)
31443145

3145-
if name == "__slots__":
3146+
if name == "__slots__" and self.scope.active_class() is not None:
31463147
typ = lvalue_type or self.expr_checker.accept(rvalue)
31473148
self.check_slots_definition(typ, lvalue)
31483149
if name == "__match_args__" and inferred is not None:
@@ -3321,6 +3322,12 @@ def get_variable_type_context(self, inferred: Var, rvalue: Expression) -> Type |
33213322
type_contexts.append(base_type)
33223323
# Use most derived supertype as type context if available.
33233324
if not type_contexts:
3325+
if inferred.name == "__slots__" and self.scope.active_class() is not None:
3326+
str_type = self.named_type("builtins.str")
3327+
return self.named_generic_type("typing.Iterable", [str_type])
3328+
if inferred.name == "__all__" and self.scope.is_top_level():
3329+
str_type = self.named_type("builtins.str")
3330+
return self.named_generic_type("typing.Sequence", [str_type])
33243331
return None
33253332
candidate = type_contexts[0]
33263333
for other in type_contexts:
@@ -4970,7 +4977,7 @@ def visit_try_stmt(self, s: TryStmt) -> None:
49704977
if s.finally_body:
49714978
# First we check finally_body is type safe on all abnormal exit paths
49724979
iter_errors = IterationDependentErrors()
4973-
with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher:
4980+
with IterationErrorWatcher(self.msg.errors, iter_errors):
49744981
self.accept(s.finally_body)
49754982

49764983
if s.finally_body:
@@ -4987,13 +4994,9 @@ def visit_try_stmt(self, s: TryStmt) -> None:
49874994
# that follows the try statement.)
49884995
assert iter_errors is not None
49894996
if not self.binder.is_unreachable():
4990-
with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher:
4997+
with IterationErrorWatcher(self.msg.errors, iter_errors):
49914998
self.accept(s.finally_body)
4992-
4993-
for error_info in watcher.yield_error_infos():
4994-
self.msg.fail(*error_info[:2], code=error_info[2])
4995-
for note_info in watcher.yield_note_infos(self.options):
4996-
self.msg.note(*note_info)
4999+
self.msg.iteration_dependent_errors(iter_errors)
49975000

49985001
def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None:
49995002
"""Type check a try statement, ignoring the finally block.
@@ -7303,7 +7306,7 @@ def named_type(self, name: str) -> Instance:
73037306
if isinstance(node, TypeAlias):
73047307
assert isinstance(node.target, Instance) # type: ignore[misc]
73057308
node = node.target.type
7306-
assert isinstance(node, TypeInfo)
7309+
assert isinstance(node, TypeInfo), node
73077310
any_type = AnyType(TypeOfAny.from_omitted_generics)
73087311
return Instance(node, [any_type] * len(node.defn.type_vars))
73097312

@@ -7322,7 +7325,7 @@ def lookup_typeinfo(self, fullname: str) -> TypeInfo:
73227325
# Assume that the name refers to a class.
73237326
sym = self.lookup_qualified(fullname)
73247327
node = sym.node
7325-
assert isinstance(node, TypeInfo)
7328+
assert isinstance(node, TypeInfo), node
73267329
return node
73277330

73287331
def type_type(self) -> Instance:
@@ -7913,6 +7916,9 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool:
79137916
def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type:
79147917
return self.expr_checker.accept(node, type_context=type_context)
79157918

7919+
def is_defined_in_stub(self, typ: Instance, /) -> bool:
7920+
return self.modules[typ.type.module_name].is_stub
7921+
79167922
def check_deprecated(self, node: Node | None, context: Context) -> None:
79177923
"""Warn if deprecated and not directly imported with a `from` statement."""
79187924
if isinstance(node, Decorator):

mypy/checker_shared.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ def checking_await_set(self) -> Iterator[None]:
277277
def get_precise_awaitable_type(self, typ: Type, local_errors: ErrorWatcher) -> Type | None:
278278
raise NotImplementedError
279279

280+
@abstractmethod
281+
def is_defined_in_stub(self, typ: Instance, /) -> bool:
282+
raise NotImplementedError
283+
280284

281285
class CheckerScope:
282286
# We keep two stacks combined, to maintain the relative order
@@ -334,6 +338,10 @@ def current_self_type(self) -> Instance | TupleType | None:
334338
return fill_typevars(item)
335339
return None
336340

341+
def is_top_level(self) -> bool:
342+
"""Is current scope top-level (no classes or functions)?"""
343+
return len(self.stack) == 1
344+
337345
@contextmanager
338346
def push_function(self, item: FuncItem) -> Iterator[None]:
339347
self.stack.append(item)

mypy/checkexpr.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2684,9 +2684,12 @@ def check_arg(
26842684
context=context,
26852685
outer_context=outer_context,
26862686
)
2687-
self.msg.incompatible_argument_note(
2688-
original_caller_type, callee_type, context, parent_error=error
2689-
)
2687+
if not caller_kind.is_star():
2688+
# For *args and **kwargs this note would be incorrect - we're comparing
2689+
# iterable/mapping type with union of relevant arg types.
2690+
self.msg.incompatible_argument_note(
2691+
original_caller_type, callee_type, context, parent_error=error
2692+
)
26902693
if not self.msg.prefer_simple_messages():
26912694
self.chk.check_possible_missing_await(
26922695
caller_type, callee_type, context, error.code

mypy/checkmember.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,23 +1477,18 @@ def bind_self_fast(method: F, original_type: Type | None = None) -> F:
14771477
items = [bind_self_fast(c, original_type) for c in method.items]
14781478
return cast(F, Overloaded(items))
14791479
assert isinstance(method, CallableType)
1480-
func: CallableType = method
1481-
if not func.arg_types:
1480+
if not method.arg_types:
14821481
# Invalid method, return something.
14831482
return method
1484-
if func.arg_kinds[0] in (ARG_STAR, ARG_STAR2):
1483+
if method.arg_kinds[0] in (ARG_STAR, ARG_STAR2):
14851484
# See typeops.py for details.
14861485
return method
1487-
original_type = get_proper_type(original_type)
1488-
if isinstance(original_type, CallableType) and original_type.is_type_obj():
1489-
original_type = TypeType.make_normalized(original_type.ret_type)
1490-
res = func.copy_modified(
1491-
arg_types=func.arg_types[1:],
1492-
arg_kinds=func.arg_kinds[1:],
1493-
arg_names=func.arg_names[1:],
1486+
return method.copy_modified(
1487+
arg_types=method.arg_types[1:],
1488+
arg_kinds=method.arg_kinds[1:],
1489+
arg_names=method.arg_names[1:],
14941490
is_bound=True,
14951491
)
1496-
return cast(F, res)
14971492

14981493

14991494
def has_operator(typ: Type, op_method: str, named_type: Callable[[str], Instance]) -> bool:

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/errorcodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ def __hash__(self) -> int:
270270
"General",
271271
default_enabled=False,
272272
)
273+
METACLASS: Final[ErrorCode] = ErrorCode("metaclass", "Ensure that metaclass is valid", "General")
273274

274275
# Syntax errors are often blocking.
275276
SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General")

0 commit comments

Comments
 (0)