From dbf97df271f3e69f0f52c9fc99e38a03ecadde79 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Sat, 13 Dec 2025 14:26:04 -0800 Subject: [PATCH 01/12] Bump version to 1.19.1+dev --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index f550cd929eb5..eef99f7203cf 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -8,7 +8,7 @@ # - Release versions have the form "1.2.3". # - Dev versions have the form "1.2.3+dev" (PLUS sign to conform to PEP 440). # - Before 1.0 we had the form "0.NNN". -__version__ = "1.19.0" +__version__ = "1.19.1+dev" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) From d503cf87a130449a053fd9ac098be7c9482ea540 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 1 Dec 2025 14:06:09 +0000 Subject: [PATCH 02/12] Fix crash on typevar with forward ref used in other module (#20334) Fixes https://github.com/python/mypy/issues/20326 Type variables with forward references in upper bound are known to be problematic. Existing mechanisms to work with them implicitly assumed that they are used in the same module where they are defined, which is not necessarily the case for "old-style" type variables that can be imported. Note that the simplification I made in `semanal_typeargs.py` would be probably sufficient to fix this, but that would be papering over the real issue, so I am making a bit more principled fix. --- mypy/plugins/proper_plugin.py | 1 + mypy/semanal.py | 2 +- mypy/semanal_typeargs.py | 12 ++-- mypy/typeanal.py | 9 +++ test-data/unit/check-type-aliases.test | 90 ++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 9 deletions(-) diff --git a/mypy/plugins/proper_plugin.py b/mypy/plugins/proper_plugin.py index 0189bfbd22fc..872903ea6b47 100644 --- a/mypy/plugins/proper_plugin.py +++ b/mypy/plugins/proper_plugin.py @@ -108,6 +108,7 @@ def is_special_target(right: ProperType) -> bool: "mypy.types.RequiredType", "mypy.types.ReadOnlyType", "mypy.types.TypeGuardedType", + "mypy.types.PlaceholderType", ): # Special case: these are not valid targets for a type alias and thus safe. # TODO: introduce a SyntheticType base to simplify this? diff --git a/mypy/semanal.py b/mypy/semanal.py index 973a28db0588..f9f0e4d71098 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -4935,7 +4935,7 @@ def get_typevarlike_argument( ) if analyzed is None: # Type variables are special: we need to place them in the symbol table - # soon, even if upper bound is not ready yet. Otherwise avoiding + # soon, even if upper bound is not ready yet. Otherwise, avoiding # a "deadlock" in this common pattern would be tricky: # T = TypeVar('T', bound=Custom[Any]) # class Custom(Generic[T]): diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index 86f8a8700def..9d1ce1fd6080 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -176,12 +176,12 @@ def validate_args( code=codes.VALID_TYPE, ) continue + if self.in_type_alias_expr and isinstance(arg, TypeVarType): + # Type aliases are allowed to use unconstrained type variables + # error will be checked at substitution point. + continue if tvar.values: if isinstance(arg, TypeVarType): - if self.in_type_alias_expr: - # Type aliases are allowed to use unconstrained type variables - # error will be checked at substitution point. - continue arg_values = arg.values if not arg_values: is_error = True @@ -205,10 +205,6 @@ def validate_args( and upper_bound.type.fullname == "builtins.object" ) if not object_upper_bound and not is_subtype(arg, upper_bound): - if self.in_type_alias_expr and isinstance(arg, TypeVarType): - # Type aliases are allowed to use unconstrained type variables - # error will be checked at substitution point. - continue is_error = True self.fail( message_registry.INVALID_TYPEVAR_ARG_BOUND.format( diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 06fa847c5434..3e5f522f3907 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -346,6 +346,15 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) if hook is not None: return hook(AnalyzeTypeContext(t, t, self)) tvar_def = self.tvar_scope.get_binding(sym) + if tvar_def is not None: + # We need to cover special-case explained in get_typevarlike_argument() here, + # since otherwise the deferral will not be triggered if the type variable is + # used in a different module. Using isinstance() should be safe for this purpose. + tvar_params = [tvar_def.upper_bound, tvar_def.default] + if isinstance(tvar_def, TypeVarType): + tvar_params += tvar_def.values + if any(isinstance(tp, PlaceholderType) for tp in tvar_params): + self.api.defer() if isinstance(sym.node, ParamSpecExpr): if tvar_def is None: if self.allow_unbound_tvars: diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 6923b0d8f006..1fb2b038a1a1 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -1351,3 +1351,93 @@ reveal_type(D(x="asdf")) # E: No overload variant of "dict" matches argument ty # N: def __init__(self, arg: Iterable[tuple[str, int]], **kwargs: int) -> dict[str, int] \ # N: Revealed type is "Any" [builtins fixtures/dict.pyi] + +[case testTypeAliasesInCyclicImport1] +import p.aliases + +[file p/__init__.py] +[file p/aliases.py] +from typing_extensions import TypeAlias +from .defs import C, Alias1 + +Alias2: TypeAlias = Alias1[C] + +[file p/defs.py] +from typing import TypeVar +from typing_extensions import TypeAlias +import p.aliases + +C = TypeVar("C", bound="SomeClass") +Alias1: TypeAlias = C + +class SomeClass: + pass +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeAliasesInCyclicImport2] +import p.aliases + +[file p/__init__.py] +[file p/aliases.py] +from typing_extensions import TypeAlias +from .defs import C, Alias1 + +Alias2: TypeAlias = Alias1[C] + +[file p/defs.py] +from typing import TypeVar, Union +from typing_extensions import TypeAlias +import p.aliases + +C = TypeVar("C", bound="SomeClass") +Alias1: TypeAlias = Union[C, int] + +class SomeClass: + pass +[builtins fixtures/tuple.pyi] + +[case testTypeAliasesInCyclicImport3] +import p.aliases + +[file p/__init__.py] +[file p/aliases.py] +from typing_extensions import TypeAlias +from .defs import C, Alias1 + +Alias2: TypeAlias = Alias1[C] + +[file p/defs.py] +from typing import TypeVar +from typing_extensions import TypeAlias +import p.aliases + +C = TypeVar("C", bound="list[SomeClass]") +Alias1: TypeAlias = C + +class SomeClass: + pass +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeAliasesInCyclicImport4] +import p.aliases + +[file p/__init__.py] +[file p/aliases.py] +from typing_extensions import TypeAlias +from .defs import C, Alias1 + +Alias2: TypeAlias = Alias1[C] + +[file p/defs.py] +from typing import TypeVar, Union +from typing_extensions import TypeAlias +import p.aliases + +C = TypeVar("C", bound="list[SomeClass]") +Alias1: TypeAlias = Union[C, int] + +class SomeClass: + pass +[builtins fixtures/tuple.pyi] From c93d917a86993e06dcc88e508f28f4f5199ce1c8 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 30 Nov 2025 16:30:01 +0000 Subject: [PATCH 03/12] Fix crash on star import of redefinition (#20333) Fixes https://github.com/python/mypy/issues/20327 Fix is trivial, do not grab various internal/temporary symbols with star imports. This may create an invalid cross-reference (and is generally dangerous). Likely, this worked previously because we processed all fresh modules in queue, not just the dependencies of current SCC. --- mypy/semanal.py | 4 ++++ test-data/unit/check-incremental.test | 30 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index f9f0e4d71098..1035efb29061 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3195,6 +3195,10 @@ def visit_import_all(self, i: ImportAll) -> None: # namespace is incomplete. self.mark_incomplete("*", i) for name, node in m.names.items(): + if node.no_serialize: + # This is either internal or generated symbol, skip it to avoid problems + # like accidental name conflicts or invalid cross-references. + continue fullname = i_id + "." + name self.set_future_import_flags(fullname) # if '__all__' exists, all nodes not included have had module_public set to diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 56c9cef80f34..170a883ce25d 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -7596,3 +7596,33 @@ X = 0 tmp/a.py:6: error: "object" has no attribute "dtypes" [out2] tmp/a.py:2: error: "object" has no attribute "dtypes" + +[case testStarImportCycleRedefinition] +import m + +[file m.py] +import a + +[file m.py.2] +import a +reveal_type(a.C) + +[file a/__init__.py] +from a.b import * +from a.c import * +x = 1 + +[file a/b.py] +from other import C +from a.c import y +class C: ... # type: ignore + +[file a/c.py] +from other import C +from a import x +y = 1 + +[file other.py] +class C: ... +[out2] +tmp/m.py:2: note: Revealed type is "def () -> other.C" From 3890fc49bf7cc02db04b1e63eb2540aaacdeecc0 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 29 Nov 2025 06:42:03 -0800 Subject: [PATCH 04/12] Fix crash involving Unpack-ed TypeVarTuple (#20323) Fixes #20093 This fixes the crash, but not the false positive (the false positive existed prior to the regression that introduced the crash) --- mypy/typeops.py | 10 ++++++++-- test-data/unit/check-overloading.test | 13 +++++++++++++ test-data/unit/check-typevar-tuple.test | 23 +++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index 050252eb6205..f6646740031d 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -508,7 +508,7 @@ def erase_to_bound(t: Type) -> Type: def callable_corresponding_argument( typ: NormalizedCallableType | Parameters, model: FormalArgument ) -> FormalArgument | None: - """Return the argument a function that corresponds to `model`""" + """Return the argument of a function that corresponds to `model`""" by_name = typ.argument_by_name(model.name) by_pos = typ.argument_by_position(model.pos) @@ -522,17 +522,23 @@ def callable_corresponding_argument( # taking both *args and **args, or a pair of functions like so: # def right(a: int = ...) -> None: ... - # def left(__a: int = ..., *, a: int = ...) -> None: ... + # def left(x: int = ..., /, *, a: int = ...) -> None: ... from mypy.meet import meet_types if ( not (by_name.required or by_pos.required) and by_pos.name is None and by_name.pos is None + # This is not principled, but prevents a crash. It's weird to have a FormalArgument + # that has an UnpackType. + and not isinstance(by_name.typ, UnpackType) + and not isinstance(by_pos.typ, UnpackType) ): return FormalArgument( by_name.name, by_pos.pos, meet_types(by_name.typ, by_pos.typ), False ) + return by_name + return by_name if by_name is not None else by_pos diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index be55a182b87b..1830a0c5ce3c 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -263,6 +263,19 @@ def foo(*args: int | str, **kw: int | Foo) -> None: pass [builtins fixtures/tuple.pyi] + +[case testTypeCheckOverloadImplOverlapVarArgsAndKwargsNever] +from __future__ import annotations +from typing import overload + +@overload # E: Single overload definition, multiple required +def foo(x: int) -> None: ... + +def foo(*args: int, **kw: str) -> None: # E: Overloaded function implementation does not accept all possible arguments of signature 1 + pass +[builtins fixtures/tuple.pyi] + + [case testTypeCheckOverloadWithImplTooSpecificRetType] from typing import overload, Any diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index cb5029ee4e6d..c60d0aec0835 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2716,3 +2716,26 @@ class MyTuple(tuple[Unpack[Union[int, str]]], Generic[Unpack[Ts]]): # E: "Union x: MyTuple[int, str] reveal_type(x[0]) # N: Revealed type is "Any" [builtins fixtures/tuple.pyi] + +[case testHigherOrderFunctionUnpackTypeVarTupleViaParamSpec] +from typing import Callable, ParamSpec, TypeVar, TypeVarTuple, Unpack + +P = ParamSpec("P") +T = TypeVar("T") +Ts = TypeVarTuple("Ts") + +def call(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: + return func(*args, **kwargs) + + +def run(func: Callable[[Unpack[Ts]], T], *args: Unpack[Ts], some_kwarg: str = "asdf") -> T: + raise + + +def foo() -> str: + return "hello" + + +# this is a false positive, but it no longer crashes +call(run, foo, some_kwarg="a") # E: Argument 1 to "call" has incompatible type "def [Ts`-1, T] run(func: def (*Unpack[Ts]) -> T, *args: Unpack[Ts], some_kwarg: str = ...) -> T"; expected "Callable[[Callable[[], str], str], str]" +[builtins fixtures/tuple.pyi] From 70eceea682c041c0d8e8462dffef9c7bb252e014 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:24:20 -0800 Subject: [PATCH 05/12] Fix noncommutative joins with bounded TypeVars (#20345) Fixes #20344 --- mypy/join.py | 13 +++++++++---- mypy/test/testtypes.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/mypy/join.py b/mypy/join.py index 0822ddbfd89a..a074fa522588 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -297,10 +297,15 @@ def visit_erased_type(self, t: ErasedType) -> ProperType: return self.s def visit_type_var(self, t: TypeVarType) -> ProperType: - if isinstance(self.s, TypeVarType) and self.s.id == t.id: - if self.s.upper_bound == t.upper_bound: - return self.s - return self.s.copy_modified(upper_bound=join_types(self.s.upper_bound, t.upper_bound)) + if isinstance(self.s, TypeVarType): + if self.s.id == t.id: + if self.s.upper_bound == t.upper_bound: + return self.s + return self.s.copy_modified( + upper_bound=join_types(self.s.upper_bound, t.upper_bound) + ) + # Fix non-commutative joins + return get_proper_type(join_types(self.s.upper_bound, t.upper_bound)) else: return self.default(self.s) diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index fc68d9aa6eac..f5f4c6797db2 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -1051,6 +1051,35 @@ def test_join_type_type_type_var(self) -> None: self.assert_join(self.fx.type_a, self.fx.t, self.fx.o) self.assert_join(self.fx.t, self.fx.type_a, self.fx.o) + def test_join_type_var_bounds(self) -> None: + tvar1 = TypeVarType( + "tvar1", + "tvar1", + TypeVarId(-100), + [], + self.fx.o, + AnyType(TypeOfAny.from_omitted_generics), + INVARIANT, + ) + any_type = AnyType(TypeOfAny.special_form) + tvar2 = TypeVarType( + "tvar2", + "tvar2", + TypeVarId(-101), + [], + upper_bound=UnionType( + [ + TupleType([any_type], self.fx.std_tuple), + TupleType([any_type, any_type], self.fx.std_tuple), + ] + ), + default=AnyType(TypeOfAny.from_omitted_generics), + variance=INVARIANT, + ) + + self.assert_join(tvar1, tvar2, self.fx.o) + self.assert_join(tvar2, tvar1, self.fx.o) + # There are additional test cases in check-inference.test. # TODO: Function types + varargs and default args. From 8a6eff478416cd3ed3931a6ed77ce61c88ab69e9 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Tue, 9 Dec 2025 02:27:25 -0500 Subject: [PATCH 06/12] [mypyc] fix generator regression with empty tuple (#20371) This PR fixes #20341 --- mypyc/irbuild/builder.py | 4 ++- mypyc/irbuild/for_helpers.py | 42 +++++++++++++++++++++-------- mypyc/test-data/run-generators.test | 8 ++++++ mypyc/test-data/run-loops.test | 9 +++++-- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index 63930123135f..51a02ed5446d 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -990,8 +990,10 @@ def get_sequence_type_from_type(self, target_type: Type) -> RType: elif isinstance(target_type, TypeVarLikeType): return self.get_sequence_type_from_type(target_type.upper_bound) elif isinstance(target_type, TupleType): + items = target_type.items + assert items, "This function does not support empty tuples" # Tuple might have elements of different types. - rtypes = {self.mapper.type_to_rtype(item) for item in target_type.items} + rtypes = set(map(self.mapper.type_to_rtype, items)) if len(rtypes) == 1: return rtypes.pop() else: diff --git a/mypyc/irbuild/for_helpers.py b/mypyc/irbuild/for_helpers.py index 715f5432cd13..33e442935641 100644 --- a/mypyc/irbuild/for_helpers.py +++ b/mypyc/irbuild/for_helpers.py @@ -7,7 +7,7 @@ from __future__ import annotations -from typing import Callable, ClassVar +from typing import Callable, ClassVar, cast from mypy.nodes import ( ARG_POS, @@ -241,25 +241,45 @@ def sequence_from_generator_preallocate_helper( rtype = builder.node_type(sequence_expr) if not (is_sequence_rprimitive(rtype) or isinstance(rtype, RTuple)): return None - sequence = builder.accept(sequence_expr) - length = get_expr_length_value(builder, sequence_expr, sequence, line, use_pyssize_t=True) + if isinstance(rtype, RTuple): # If input is RTuple, box it to tuple_rprimitive for generic iteration # TODO: this can be optimized a bit better with an unrolled ForRTuple helper proper_type = get_proper_type(builder.types[sequence_expr]) assert isinstance(proper_type, TupleType), proper_type - get_item_ops = [ - ( - LoadLiteral(typ.value, object_rprimitive) - if isinstance(typ, LiteralType) - else TupleGet(sequence, i, line) - ) - for i, typ in enumerate(get_proper_types(proper_type.items)) - ] + # the for_loop_helper_with_index crashes for empty tuples, bail out + if not proper_type.items: + return None + + proper_types = get_proper_types(proper_type.items) + + get_item_ops: list[LoadLiteral | TupleGet] + if all(isinstance(typ, LiteralType) for typ in proper_types): + get_item_ops = [ + LoadLiteral(cast(LiteralType, typ).value, object_rprimitive) + for typ in proper_types + ] + + else: + sequence = builder.accept(sequence_expr) + get_item_ops = [ + ( + LoadLiteral(typ.value, object_rprimitive) + if isinstance(typ, LiteralType) + else TupleGet(sequence, i, line) + ) + for i, typ in enumerate(proper_types) + ] + items = list(map(builder.add, get_item_ops)) sequence = builder.new_tuple(items, line) + else: + sequence = builder.accept(sequence_expr) + + length = get_expr_length_value(builder, sequence_expr, sequence, line, use_pyssize_t=True) + target_op = empty_op_llbuilder(length, line) def set_item(item_index: Value) -> None: diff --git a/mypyc/test-data/run-generators.test b/mypyc/test-data/run-generators.test index c8e83173474d..cf1dac7c5733 100644 --- a/mypyc/test-data/run-generators.test +++ b/mypyc/test-data/run-generators.test @@ -936,3 +936,11 @@ def test_generator_override() -> None: assert base1_foo(Base1()) == [1] assert base1_foo(Derived1()) == [2, 3] assert derived1_foo(Derived1()) == [2, 3] + +[case testGeneratorEmptyTuple] +from collections.abc import Generator +from typing import Optional, Union + +def test_compiledGeneratorEmptyTuple() -> None: + jobs: Generator[Optional[str], None, None] = (_ for _ in ()) + assert list(jobs) == [] diff --git a/mypyc/test-data/run-loops.test b/mypyc/test-data/run-loops.test index 3cbb07297e6e..106c2271d326 100644 --- a/mypyc/test-data/run-loops.test +++ b/mypyc/test-data/run-loops.test @@ -1,7 +1,7 @@ # Test cases for "range" objects, "for" and "while" loops (compile and run) [case testFor] -from typing import List, Tuple +from typing import Any, List, Tuple def count(n: int) -> None: for i in range(n): print(i) @@ -21,6 +21,10 @@ def list_iter(l: List[int]) -> None: def tuple_iter(l: Tuple[int, ...]) -> None: for i in l: print(i) +def empty_tuple_iter(l: Tuple[()]) -> None: + i: Any + for i in l: + print(i) def str_iter(l: str) -> None: for i in l: print(i) @@ -39,7 +43,7 @@ def count_down_short() -> None: [file driver.py] from native import ( count, list_iter, list_rev_iter, list_rev_iter_lol, count_between, count_down, count_double, - count_down_short, tuple_iter, str_iter, + count_down_short, tuple_iter, empty_tuple_iter, str_iter, ) count(5) list_iter(list(reversed(range(5)))) @@ -52,6 +56,7 @@ count_down_short() print('==') list_rev_iter_lol(list(reversed(range(5)))) tuple_iter((1, 2, 3)) +empty_tuple_iter(()) str_iter("abc") [out] 0 From a4b31a26788b70c4a2a19adbafa2bbda43dc2e8b Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 9 Dec 2025 07:03:09 -0500 Subject: [PATCH 07/12] Allow `types.NoneType` in match cases (#20383) Fixes https://github.com/python/mypy/issues/20367 --- mypy/checkpattern.py | 3 +++ test-data/unit/check-python310.test | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 3c51c4106909..cafc69490e09 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -46,6 +46,7 @@ Type, TypedDictType, TypeOfAny, + TypeType, TypeVarTupleType, TypeVarType, UninhabitedType, @@ -556,6 +557,8 @@ def visit_class_pattern(self, o: ClassPattern) -> PatternType: fallback = self.chk.named_type("builtins.function") any_type = AnyType(TypeOfAny.unannotated) typ = callable_with_ellipsis(any_type, ret_type=any_type, fallback=fallback) + elif isinstance(p_typ, TypeType) and isinstance(p_typ.item, NoneType): + typ = p_typ.item elif not isinstance(p_typ, AnyType): self.msg.fail( message_registry.CLASS_PATTERN_TYPE_REQUIRED.format( diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 1e27e30d4b04..8bc781d091c3 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -3178,3 +3178,19 @@ match 5: reveal_type(b) # N: Revealed type is "Any" case BlahBlah(c=c): # E: Name "BlahBlah" is not defined reveal_type(c) # N: Revealed type is "Any" + +[case testMatchAllowsNoneTypeAsClass] +import types + +class V: + X = types.NoneType + +def fun(val: str | None): + match val: + case V.X(): + reveal_type(val) # N: Revealed type is "None" + + match val: + case types.NoneType(): + reveal_type(val) # N: Revealed type is "None" +[builtins fixtures/tuple.pyi] From 58d485b4ea4776e0b9d4045b306cb0818ecc2aa6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 9 Dec 2025 14:08:13 +0000 Subject: [PATCH 08/12] Fail with an explicit error on PyPy (#20384) Fixes https://github.com/mypyc/librt/issues/21 Fail with an explicit user-friendly error on PyPy. --- mypy-requirements.txt | 2 +- pyproject.toml | 4 ++-- setup.py | 9 +++++++++ test-requirements.txt | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/mypy-requirements.txt b/mypy-requirements.txt index b0c632dddac5..6984d9a5d070 100644 --- a/mypy-requirements.txt +++ b/mypy-requirements.txt @@ -4,4 +4,4 @@ typing_extensions>=4.6.0 mypy_extensions>=1.0.0 pathspec>=0.9.0 tomli>=1.1.0; python_version<'3.11' -librt>=0.6.2 +librt>=0.6.2; platform_python_implementation != 'PyPy' diff --git a/pyproject.toml b/pyproject.toml index bb41c82b1a3c..fa56caeaa4bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ requires = [ "mypy_extensions>=1.0.0", "pathspec>=0.9.0", "tomli>=1.1.0; python_version<'3.11'", - "librt>=0.6.2", + "librt>=0.6.2; platform_python_implementation != 'PyPy'", # the following is from build-requirements.txt "types-psutil", "types-setuptools", @@ -54,7 +54,7 @@ dependencies = [ "mypy_extensions>=1.0.0", "pathspec>=0.9.0", "tomli>=1.1.0; python_version<'3.11'", - "librt>=0.6.2", + "librt>=0.6.2; platform_python_implementation != 'PyPy'", ] dynamic = ["version"] diff --git a/setup.py b/setup.py index f20c1db5d045..8cba27ae0f85 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ import glob import os import os.path +import platform import sys from typing import TYPE_CHECKING, Any @@ -12,6 +13,14 @@ sys.stderr.write("ERROR: You need Python 3.9 or later to use mypy.\n") exit(1) +if platform.python_implementation() == "PyPy": + sys.stderr.write( + "ERROR: Running mypy on PyPy is not supported yet.\n" + "To type-check a PyPy library please use an equivalent CPython version,\n" + "see https://github.com/mypyc/librt/issues/16 for possible workarounds.\n" + ) + exit(1) + # we'll import stuff from the source tree, let's ensure is on the sys path sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) diff --git a/test-requirements.txt b/test-requirements.txt index 953e7a750c75..d8334108fc1d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -22,7 +22,7 @@ identify==2.6.15 # via pre-commit iniconfig==2.1.0 # via pytest -librt==0.6.2 +librt==0.7.3 ; platform_python_implementation != 'PyPy' # via -r mypy-requirements.txt lxml==6.0.2 ; python_version < "3.15" # via -r test-requirements.in From f60f90fb8872bf722e32aefd548daaf6d8560e05 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 10 Dec 2025 02:02:26 +0000 Subject: [PATCH 09/12] Fail on PyPy in main instead of setup.py (#20389) Follow-up for https://github.com/python/mypy/pull/20384 --- mypy/main.py | 8 ++++++++ setup.py | 9 --------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 7d5721851c3d..5b8f8b5a5476 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -4,6 +4,7 @@ import argparse import os +import platform import subprocess import sys import time @@ -39,6 +40,13 @@ if TYPE_CHECKING: from _typeshed import SupportsWrite +if platform.python_implementation() == "PyPy": + sys.stderr.write( + "ERROR: Running mypy on PyPy is not supported yet.\n" + "To type-check a PyPy library please use an equivalent CPython version,\n" + "see https://github.com/mypyc/librt/issues/16 for possible workarounds.\n" + ) + sys.exit(2) orig_stat: Final = os.stat MEM_PROFILE: Final = False # If True, dump memory profile diff --git a/setup.py b/setup.py index 8cba27ae0f85..f20c1db5d045 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ import glob import os import os.path -import platform import sys from typing import TYPE_CHECKING, Any @@ -13,14 +12,6 @@ sys.stderr.write("ERROR: You need Python 3.9 or later to use mypy.\n") exit(1) -if platform.python_implementation() == "PyPy": - sys.stderr.write( - "ERROR: Running mypy on PyPy is not supported yet.\n" - "To type-check a PyPy library please use an equivalent CPython version,\n" - "see https://github.com/mypyc/librt/issues/16 for possible workarounds.\n" - ) - exit(1) - # we'll import stuff from the source tree, let's ensure is on the sys path sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) From 2b23b507524bf1bd7513eea6f2a16fb91e072cb6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 10 Dec 2025 00:51:04 +0000 Subject: [PATCH 10/12] Serialize raw errors in cache metas (#20372) Fixes https://github.com/python/mypy/issues/20353 This makes us respect e.g. `--output json` for cached files without re-checking the files (which is the desired behavior for users, see issue). This is also a first step towards resolving the "foo defined here" conundrum for parallel checking. The fix is straightforward. The only question was whether to continue using `ErrorTuple`s or switch to a proper class. I decided to keep the tuples for now to minimize the scope of change. Note I am also adjusting generic "JSON" fixed-format helpers to natively support tuples (unlike real JSON). We already use tuples in few other places, so it makes sense to just make it "official" (this format is still internal to mypy obviously). --- mypy/build.py | 69 +++++++++++++++++++++---- mypy/cache.py | 72 +++++++++++++++++++++++---- mypy/errors.py | 25 ++++++---- test-data/unit/check-incremental.test | 10 ++++ 4 files changed, 146 insertions(+), 30 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 853e54e445ac..aee099fed316 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -31,10 +31,17 @@ from librt.internal import cache_version import mypy.semanal_main -from mypy.cache import CACHE_VERSION, CacheMeta, ReadBuffer, WriteBuffer +from mypy.cache import ( + CACHE_VERSION, + CacheMeta, + ReadBuffer, + SerializedError, + WriteBuffer, + write_json, +) from mypy.checker import TypeChecker from mypy.error_formatter import OUTPUT_CHOICES, ErrorFormatter -from mypy.errors import CompileError, ErrorInfo, Errors, report_internal_error +from mypy.errors import CompileError, ErrorInfo, Errors, ErrorTuple, report_internal_error from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort from mypy.indirection import TypeIndirectionVisitor from mypy.messages import MessageBuilder @@ -1869,7 +1876,7 @@ class State: dep_hashes: dict[str, bytes] = {} # List of errors reported for this file last time. - error_lines: list[str] = [] + error_lines: list[SerializedError] = [] # Parent package, its parent, etc. ancestors: list[str] | None = None @@ -3286,9 +3293,13 @@ def find_stale_sccs( scc = order_ascc_ex(graph, ascc) for id in scc: if graph[id].error_lines: - manager.flush_errors( - manager.errors.simplify_path(graph[id].xpath), graph[id].error_lines, False + path = manager.errors.simplify_path(graph[id].xpath) + formatted = manager.errors.format_messages( + path, + deserialize_codes(graph[id].error_lines), + formatter=manager.error_formatter, ) + manager.flush_errors(path, formatted, False) fresh_sccs.append(ascc) else: size = len(ascc.mod_ids) @@ -3492,13 +3503,16 @@ def process_stale_scc(graph: Graph, ascc: SCC, manager: BuildManager) -> None: # Flush errors, and write cache in two phases: first data files, then meta files. meta_tuples = {} errors_by_id = {} + formatted_by_id = {} for id in stale: if graph[id].xpath not in manager.errors.ignored_files: - errors = manager.errors.file_messages( - graph[id].xpath, formatter=manager.error_formatter + errors = manager.errors.file_messages(graph[id].xpath) + formatted = manager.errors.format_messages( + graph[id].xpath, errors, formatter=manager.error_formatter ) - manager.flush_errors(manager.errors.simplify_path(graph[id].xpath), errors, False) + manager.flush_errors(manager.errors.simplify_path(graph[id].xpath), formatted, False) errors_by_id[id] = errors + formatted_by_id[id] = formatted meta_tuples[id] = graph[id].write_cache() graph[id].mark_as_rechecked() for id in stale: @@ -3507,7 +3521,7 @@ def process_stale_scc(graph: Graph, ascc: SCC, manager: BuildManager) -> None: continue meta, meta_file = meta_tuple meta.dep_hashes = [graph[dep].interface_hash for dep in graph[id].dependencies] - meta.error_lines = errors_by_id.get(id, []) + meta.error_lines = serialize_codes(errors_by_id.get(id, [])) write_cache_meta(meta, manager, meta_file) manager.done_sccs.add(ascc.id) @@ -3640,3 +3654,40 @@ def write_undocumented_ref_info( deps_json = get_undocumented_ref_info_json(state.tree, type_map) metastore.write(ref_info_file, json_dumps(deps_json)) + + +def sources_to_bytes(sources: list[BuildSource]) -> bytes: + source_tuples = [(s.path, s.module, s.text, s.base_dir, s.followed) for s in sources] + buf = WriteBuffer() + write_json(buf, {"sources": source_tuples}) + return buf.getvalue() + + +def sccs_to_bytes(sccs: list[SCC]) -> bytes: + scc_tuples = [(list(scc.mod_ids), scc.id, list(scc.deps)) for scc in sccs] + buf = WriteBuffer() + write_json(buf, {"sccs": scc_tuples}) + return buf.getvalue() + + +def serialize_codes(errs: list[ErrorTuple]) -> list[SerializedError]: + return [ + (path, line, column, end_line, end_column, severity, message, code.code if code else None) + for path, line, column, end_line, end_column, severity, message, code in errs + ] + + +def deserialize_codes(errs: list[SerializedError]) -> list[ErrorTuple]: + return [ + ( + path, + line, + column, + end_line, + end_column, + severity, + message, + codes.error_codes.get(code) if code else None, + ) + for path, line, column, end_line, end_column, severity, message, code in errs + ] diff --git a/mypy/cache.py b/mypy/cache.py index ad12fd96f1fa..7755755898c0 100644 --- a/mypy/cache.py +++ b/mypy/cache.py @@ -48,7 +48,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Any, Final, Union +from typing import Any, Final, Optional, Union from typing_extensions import TypeAlias as _TypeAlias from librt.internal import ( @@ -70,7 +70,9 @@ from mypy_extensions import u8 # High-level cache layout format -CACHE_VERSION: Final = 0 +CACHE_VERSION: Final = 1 + +SerializedError: _TypeAlias = tuple[Optional[str], int, int, int, int, str, str, Optional[str]] class CacheMeta: @@ -93,7 +95,7 @@ def __init__( dep_lines: list[int], dep_hashes: list[bytes], interface_hash: bytes, - error_lines: list[str], + error_lines: list[SerializedError], version_id: str, ignore_all: bool, plugin_data: Any, @@ -158,7 +160,7 @@ def deserialize(cls, meta: dict[str, Any], data_file: str) -> CacheMeta | None: dep_lines=meta["dep_lines"], dep_hashes=[bytes.fromhex(dep) for dep in meta["dep_hashes"]], interface_hash=bytes.fromhex(meta["interface_hash"]), - error_lines=meta["error_lines"], + error_lines=[tuple(err) for err in meta["error_lines"]], version_id=meta["version_id"], ignore_all=meta["ignore_all"], plugin_data=meta["plugin_data"], @@ -180,7 +182,7 @@ def write(self, data: WriteBuffer) -> None: write_int_list(data, self.dep_lines) write_bytes_list(data, self.dep_hashes) write_bytes(data, self.interface_hash) - write_str_list(data, self.error_lines) + write_errors(data, self.error_lines) write_str(data, self.version_id) write_bool(data, self.ignore_all) # Plugin data may be not a dictionary, so we use @@ -205,7 +207,7 @@ def read(cls, data: ReadBuffer, data_file: str) -> CacheMeta | None: dep_lines=read_int_list(data), dep_hashes=read_bytes_list(data), interface_hash=read_bytes(data), - error_lines=read_str_list(data), + error_lines=read_errors(data), version_id=read_str(data), ignore_all=read_bool(data), plugin_data=read_json_value(data), @@ -232,6 +234,7 @@ def read(cls, data: ReadBuffer, data_file: str) -> CacheMeta | None: LIST_INT: Final[Tag] = 21 LIST_STR: Final[Tag] = 22 LIST_BYTES: Final[Tag] = 23 +TUPLE_GEN: Final[Tag] = 24 DICT_STR_GEN: Final[Tag] = 30 # Misc classes. @@ -391,7 +394,13 @@ def write_str_opt_list(data: WriteBuffer, value: list[str | None]) -> None: write_str_opt(data, item) -JsonValue: _TypeAlias = Union[None, int, str, bool, list["JsonValue"], dict[str, "JsonValue"]] +Value: _TypeAlias = Union[None, int, str, bool] + +# Our JSON format is somewhat non-standard as we distinguish lists and tuples. +# This is convenient for some internal things, like mypyc plugin and error serialization. +JsonValue: _TypeAlias = Union[ + Value, list["JsonValue"], dict[str, "JsonValue"], tuple["JsonValue", ...] +] def read_json_value(data: ReadBuffer) -> JsonValue: @@ -409,15 +418,16 @@ def read_json_value(data: ReadBuffer) -> JsonValue: if tag == LIST_GEN: size = read_int_bare(data) return [read_json_value(data) for _ in range(size)] + if tag == TUPLE_GEN: + size = read_int_bare(data) + return tuple(read_json_value(data) for _ in range(size)) if tag == DICT_STR_GEN: size = read_int_bare(data) return {read_str_bare(data): read_json_value(data) for _ in range(size)} assert False, f"Invalid JSON tag: {tag}" -# Currently tuples are used by mypyc plugin. They will be normalized to -# JSON lists after a roundtrip. -def write_json_value(data: WriteBuffer, value: JsonValue | tuple[JsonValue, ...]) -> None: +def write_json_value(data: WriteBuffer, value: JsonValue) -> None: if value is None: write_tag(data, LITERAL_NONE) elif isinstance(value, bool): @@ -428,11 +438,16 @@ def write_json_value(data: WriteBuffer, value: JsonValue | tuple[JsonValue, ...] elif isinstance(value, str): write_tag(data, LITERAL_STR) write_str_bare(data, value) - elif isinstance(value, (list, tuple)): + elif isinstance(value, list): write_tag(data, LIST_GEN) write_int_bare(data, len(value)) for val in value: write_json_value(data, val) + elif isinstance(value, tuple): + write_tag(data, TUPLE_GEN) + write_int_bare(data, len(value)) + for val in value: + write_json_value(data, val) elif isinstance(value, dict): write_tag(data, DICT_STR_GEN) write_int_bare(data, len(value)) @@ -457,3 +472,38 @@ def write_json(data: WriteBuffer, value: dict[str, Any]) -> None: for key in sorted(value): write_str_bare(data, key) write_json_value(data, value[key]) + + +def write_errors(data: WriteBuffer, errs: list[SerializedError]) -> None: + write_tag(data, LIST_GEN) + write_int_bare(data, len(errs)) + for path, line, column, end_line, end_column, severity, message, code in errs: + write_tag(data, TUPLE_GEN) + write_str_opt(data, path) + write_int(data, line) + write_int(data, column) + write_int(data, end_line) + write_int(data, end_column) + write_str(data, severity) + write_str(data, message) + write_str_opt(data, code) + + +def read_errors(data: ReadBuffer) -> list[SerializedError]: + assert read_tag(data) == LIST_GEN + result = [] + for _ in range(read_int_bare(data)): + assert read_tag(data) == TUPLE_GEN + result.append( + ( + read_str_opt(data), + read_int(data), + read_int(data), + read_int(data), + read_int(data), + read_str(data), + read_str(data), + read_str_opt(data), + ) + ) + return result diff --git a/mypy/errors.py b/mypy/errors.py index 69e4fb4cf065..ce5c6cc8215f 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -951,7 +951,7 @@ def raise_error(self, use_stdout: bool = True) -> NoReturn: self.new_messages(), use_stdout=use_stdout, module_with_blocker=self.blocker_module() ) - def format_messages( + def format_messages_default( self, error_tuples: list[ErrorTuple], source_lines: list[str] | None ) -> list[str]: """Return a string list that represents the error messages. @@ -1009,24 +1009,28 @@ def format_messages( a.append(" " * (DEFAULT_SOURCE_OFFSET + column) + marker) return a - def file_messages(self, path: str, formatter: ErrorFormatter | None = None) -> list[str]: - """Return a string list of new error messages from a given file. - - Use a form suitable for displaying to the user. - """ + def file_messages(self, path: str) -> list[ErrorTuple]: + """Return an error tuple list of new error messages from a given file.""" if path not in self.error_info_map: return [] error_info = self.error_info_map[path] error_info = [info for info in error_info if not info.hidden] error_info = self.remove_duplicates(self.sort_messages(error_info)) - error_tuples = self.render_messages(error_info) + return self.render_messages(error_info) + def format_messages( + self, path: str, error_tuples: list[ErrorTuple], formatter: ErrorFormatter | None = None + ) -> list[str]: + """Return a string list of new error messages from a given file. + + Use a form suitable for displaying to the user. + """ + self.flushed_files.add(path) if formatter is not None: errors = create_errors(error_tuples) return [formatter.report_error(err) for err in errors] - self.flushed_files.add(path) source_lines = None if self.options.pretty and self.read_source: # Find shadow file mapping and read source lines if a shadow file exists for the given path. @@ -1036,7 +1040,7 @@ def file_messages(self, path: str, formatter: ErrorFormatter | None = None) -> l source_lines = self.read_source(mapped_path) else: source_lines = self.read_source(path) - return self.format_messages(error_tuples, source_lines) + return self.format_messages_default(error_tuples, source_lines) def find_shadow_file_mapping(self, path: str) -> str | None: """Return the shadow file path for a given source file path or None.""" @@ -1058,7 +1062,8 @@ def new_messages(self) -> list[str]: msgs = [] for path in self.error_info_map.keys(): if path not in self.flushed_files: - msgs.extend(self.file_messages(path)) + error_tuples = self.file_messages(path) + msgs.extend(self.format_messages(path, error_tuples)) return msgs def targets(self) -> set[str]: diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 170a883ce25d..fdda5f64284d 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -7626,3 +7626,13 @@ y = 1 class C: ... [out2] tmp/m.py:2: note: Revealed type is "def () -> other.C" + +[case testOutputFormatterIncremental] +# flags2: --output json +def wrong() -> int: + if wrong(): + return 0 +[out] +main:2: error: Missing return statement +[out2] +{"file": "main", "line": 2, "column": 0, "message": "Missing return statement", "hint": null, "code": "return", "severity": "error"} From 20aea0a6ca0710f5427239bdd2fd8e8bf1caf634 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 14 Dec 2025 15:44:59 -0800 Subject: [PATCH 11/12] Update changelog for 1.19.1 (#20414) Also change the header for 1.18 because of https://github.com/python/mypy/issues/19910 --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be81310c6e1..ed5d947cb829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -202,6 +202,17 @@ Related PRs: Please see [git log](https://github.com/python/typeshed/commits/main?after=ebce8d766b41fbf4d83cf47c1297563a9508ff60+0&branch=main&path=stdlib) for full list of standard library typeshed stub changes. +### Mypy 1.19.1 + +- Fix noncommutative joins with bounded TypeVars (Shantanu, PR [20345](https://github.com/python/mypy/pull/20345)) +- Respect output format for cached runs by serializing raw errors in cache metas (Ivan Levkivskyi, PR [20372](https://github.com/python/mypy/pull/20372)) +- Allow `types.NoneType` in match cases (A5rocks, PR [20383](https://github.com/python/mypy/pull/20383)) +- Fix mypyc generator regression with empty tuple (BobTheBuidler, PR [20371](https://github.com/python/mypy/pull/20371)) +- Fix crash involving Unpack-ed TypeVarTuple (Shantanu, PR [20323](https://github.com/python/mypy/pull/20323)) +- Fix crash on star import of redefinition (Ivan Levkivskyi, PR [20333](https://github.com/python/mypy/pull/20333)) +- Fix crash on typevar with forward ref used in other module (Ivan Levkivskyi, PR [20334](https://github.com/python/mypy/pull/20334)) +- Fail with an explicit error on PyPy (Ivan Levkivskyi, PR [20389](https://github.com/python/mypy/pull/20389)) + ### Acknowledgements Thanks to all mypy contributors who contributed to this release: @@ -237,7 +248,7 @@ Thanks to all mypy contributors who contributed to this release: I’d also like to thank my employer, Dropbox, for supporting mypy development. -## Mypy 1.18.1 +## Mypy 1.18 We’ve just uploaded mypy 1.18.1 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). Mypy is a static type checker for Python. This release includes new features, performance From 412c19a6bde31e7afa7f41afdf8356664689ae80 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Sun, 14 Dec 2025 15:46:31 -0800 Subject: [PATCH 12/12] Bump version to 1.19.1 --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index eef99f7203cf..eded284e7941 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -8,7 +8,7 @@ # - Release versions have the form "1.2.3". # - Dev versions have the form "1.2.3+dev" (PLUS sign to conform to PEP 440). # - Before 1.0 we had the form "0.NNN". -__version__ = "1.19.1+dev" +__version__ = "1.19.1" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))