From f02c757d00e407c677495d098899f950a8a4928b Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 25 May 2025 13:56:18 -0700 Subject: [PATCH 1/7] test --- test-data/unit/check-generics.test | 52 +++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 767b55efcac2..b815982b01d7 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -2,8 +2,8 @@ -- -------------------- -[case testGenericMethodReturnType] from typing import TypeVar, Generic +[case testGenericMethodReturnType] T = TypeVar('T') a: A[B] b: B @@ -3563,3 +3563,53 @@ def foo(x: T): reveal_type(C) # N: Revealed type is "Overload(def [T, S] (x: builtins.int, y: S`-1) -> __main__.C[__main__.Int[S`-1]], def [T, S] (x: builtins.str, y: S`-1) -> __main__.C[__main__.Str[S`-1]])" reveal_type(C(0, x)) # N: Revealed type is "__main__.C[__main__.Int[T`-1]]" reveal_type(C("yes", x)) # N: Revealed type is "__main__.C[__main__.Str[T`-1]]" + +[case testDeterminismFromJoinOrderingInSolver] +# Used to fail non-deterministically +# https://github.com/python/mypy/issues/19121 +from __future__ import annotations +from typing import Generic, Iterable, Iterator, Self, TypeVar + +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") +_T_co = TypeVar("_T_co", covariant=True) + +class Base(Iterable[_T1]): + def __iter__(self) -> Iterator[_T1]: ... +class A(Base[_T1]): ... +class B(Base[_T1]): ... +class C(Base[_T1]): ... +class D(Base[_T1]): ... +class E(Base[_T1]): ... + +class zip2(Generic[_T_co]): + def __new__( + cls, + iter1: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + ) -> zip2[tuple[_T1, _T2, _T3]]: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... + +def draw( + colors1: A[str] | B[str] | C[int] | D[int | str], + colors2: A[str] | B[str] | C[int] | D[int | str], + colors3: A[str] | B[str] | C[int] | D[int | str], +) -> None: + for c1, c2, c3 in zip2(colors1, colors2, colors3): + reveal_type(c1) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(c2) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(c3) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def draw_again( + colors1: B[str] | A[int | str] | C[str] | E[int] | D[int], + colors2: B[str] | A[int | str] | C[str] | E[int] | D[int], + colors3: B[str] | A[int | str] | C[str] | E[int] | D[int], +) -> None: + for c1, c2, c3 in zip2(colors1, colors2, colors3): + reveal_type(c1) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(c2) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(c3) # N: Revealed type is "Union[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] From 62c03999f4dada530c82db202cbdd7d947abb13f Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 25 May 2025 14:37:38 -0700 Subject: [PATCH 2/7] fix --- mypy/join.py | 2 +- mypy/solve.py | 40 +++++++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/mypy/join.py b/mypy/join.py index ac01d11d11d6..e2de36312d26 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import overload +from typing import Iterable, overload import mypy.typeops from mypy.expandtype import expand_type diff --git a/mypy/solve.py b/mypy/solve.py index 57988790a727..d4da352fbdaa 100644 --- a/mypy/solve.py +++ b/mypy/solve.py @@ -9,7 +9,7 @@ from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints, neg_op from mypy.expandtype import expand_type from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort -from mypy.join import join_types +from mypy.join import join_type_list from mypy.meet import meet_type_list, meet_types from mypy.subtypes import is_subtype from mypy.typeops import get_all_type_vars @@ -247,10 +247,16 @@ def solve_iteratively( return solutions +def _join_sorted_key(t: Type) -> int: + t = get_proper_type(t) + if isinstance(t, UnionType): + return -1 + return 0 + + def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: """Solve constraints by finding by using meets of upper bounds, and joins of lower bounds.""" - bottom: Type | None = None - top: Type | None = None + candidate: Type | None = None # Filter out previous results of failed inference, they will only spoil the current pass... @@ -267,19 +273,27 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: candidate.ambiguous = True return candidate + bottom: Type | None = None + top: Type | None = None + # Process each bound separately, and calculate the lower and upper # bounds based on constraints. Note that we assume that the constraint # targets do not have constraint references. - for target in lowers: - if bottom is None: - bottom = target - else: - if type_state.infer_unions: - # This deviates from the general mypy semantics because - # recursive types are union-heavy in 95% of cases. - bottom = UnionType.make_union([bottom, target]) - else: - bottom = join_types(bottom, target) + if type_state.infer_unions: + # This deviates from the general mypy semantics because + # recursive types are union-heavy in 95% of cases. + bottom = UnionType.make_union(list(lowers)) + else: + # The order of lowers is non-deterministic. + # We attempt to sort lowers because joins are non-associative. For instance: + # join(join(int, str), int | str) == join(object, int | str) == object + # join(int, join(str, int | str)) == join(int, int | str) == int | str + # Note that joins in theory should be commutative, but in practice some bugs mean this is + # also a source of non-deterministic type checking results. + sorted_lowers = sorted(lowers, key=_join_sorted_key) + bottom = join_type_list(sorted_lowers) + if isinstance(bottom, UninhabitedType): + bottom = None for target in uppers: if top is None: From 45fa530791cf92e84deeb91848ed562888d120d7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 25 May 2025 21:57:43 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/join.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/join.py b/mypy/join.py index e2de36312d26..ac01d11d11d6 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Iterable, overload +from typing import overload import mypy.typeops from mypy.expandtype import expand_type From 8b8a3a7019a3ef10b5d5a0d68e5302f23626a1ba Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 25 May 2025 14:58:29 -0700 Subject: [PATCH 4/7] oops --- test-data/unit/check-generics.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index b815982b01d7..22bcee3bc5ee 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -2,8 +2,8 @@ -- -------------------- -from typing import TypeVar, Generic [case testGenericMethodReturnType] +from typing import TypeVar, Generic T = TypeVar('T') a: A[B] b: B From 18562b032bda52ff6497c3acecacd051a6f829c3 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 25 May 2025 15:14:57 -0700 Subject: [PATCH 5/7] fix --- mypy/solve.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mypy/solve.py b/mypy/solve.py index d4da352fbdaa..023a32dbd04b 100644 --- a/mypy/solve.py +++ b/mypy/solve.py @@ -291,9 +291,8 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: # Note that joins in theory should be commutative, but in practice some bugs mean this is # also a source of non-deterministic type checking results. sorted_lowers = sorted(lowers, key=_join_sorted_key) - bottom = join_type_list(sorted_lowers) - if isinstance(bottom, UninhabitedType): - bottom = None + if sorted_lowers: + bottom = join_type_list(sorted_lowers) for target in uppers: if top is None: From ae7b8689ea9b976c4db724b542653771a8b5fabb Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 25 May 2025 15:35:03 -0700 Subject: [PATCH 6/7] makes other test cases a bit better --- test-data/unit/check-recursive-types.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 00d5489e515a..7f6e181a16ca 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -54,7 +54,7 @@ reveal_type(flatten([1, [2, [3]]])) # N: Revealed type is "builtins.list[builti class Bad: ... x: Nested[int] = [1, [2, [3]]] -x = [1, [Bad()]] # E: List item 0 has incompatible type "Bad"; expected "Union[int, Nested[int]]" +x = [1, [Bad()]] # E: List item 1 has incompatible type "List[Bad]"; expected "Union[int, Nested[int]]" [builtins fixtures/isinstancelist.pyi] [case testRecursiveAliasGenericInferenceNested] @@ -605,7 +605,7 @@ class NT(NamedTuple, Generic[T]): class A: ... class B(A): ... -nti: NT[int] = NT(key=0, value=NT(key=1, value=A())) # E: Argument "value" to "NT" has incompatible type "A"; expected "Union[int, NT[int]]" +nti: NT[int] = NT(key=0, value=NT(key=1, value=A())) # E: Argument "value" to "NT" has incompatible type "NT[A]"; expected "Union[int, NT[int]]" reveal_type(nti) # N: Revealed type is "Tuple[builtins.int, Union[builtins.int, ...], fallback=__main__.NT[builtins.int]]" nta: NT[A] From b2fc336e39522fbf72545a601b063756617b7983 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 25 May 2025 23:55:54 -0700 Subject: [PATCH 7/7] remove the extra test case --- test-data/unit/check-generics.test | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 22bcee3bc5ee..35357f8c930f 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -3602,14 +3602,4 @@ def draw( reveal_type(c1) # N: Revealed type is "Union[builtins.int, builtins.str]" reveal_type(c2) # N: Revealed type is "Union[builtins.int, builtins.str]" reveal_type(c3) # N: Revealed type is "Union[builtins.int, builtins.str]" - -def draw_again( - colors1: B[str] | A[int | str] | C[str] | E[int] | D[int], - colors2: B[str] | A[int | str] | C[str] | E[int] | D[int], - colors3: B[str] | A[int | str] | C[str] | E[int] | D[int], -) -> None: - for c1, c2, c3 in zip2(colors1, colors2, colors3): - reveal_type(c1) # N: Revealed type is "Union[builtins.int, builtins.str]" - reveal_type(c2) # N: Revealed type is "Union[builtins.int, builtins.str]" - reveal_type(c3) # N: Revealed type is "Union[builtins.int, builtins.str]" [builtins fixtures/tuple.pyi]