From 245cba30d43f1659a150a1883d7e823bf8ad5dfa Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Wed, 13 Aug 2025 13:10:39 +0200 Subject: [PATCH 01/12] rework starargs with union argument --- mypy/argmap.py | 100 ++++++++ mypy/checkexpr.py | 6 + test-data/unit/check-kwargs.test | 316 +++++++++++++++++++++++++ test-data/unit/fixtures/primitives.pyi | 3 +- 4 files changed, 424 insertions(+), 1 deletion(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index 28fad1f093dd..af7d7da117a7 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -4,18 +4,22 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Callable +from typing_extensions import TypeGuard from mypy import nodes from mypy.maptype import map_instance_to_supertype +from mypy.typeops import make_simplified_union from mypy.types import ( AnyType, Instance, ParamSpecType, + ProperType, TupleType, Type, TypedDictType, TypeOfAny, TypeVarTupleType, + UnionType, UnpackType, get_proper_type, ) @@ -54,6 +58,16 @@ def map_actuals_to_formals( elif actual_kind == nodes.ARG_STAR: # We need to know the actual type to map varargs. actualt = get_proper_type(actual_arg_type(ai)) + + # Special case for union of equal sized tuples. + if ( + isinstance(actualt, UnionType) + and actualt.items + and is_equal_sized_tuples( + proper_types := [get_proper_type(t) for t in actualt.items] + ) + ): + actualt = proper_types[0] if isinstance(actualt, TupleType): # A tuple actual maps to a fixed number of formals. for _ in range(len(actualt.items)): @@ -171,6 +185,15 @@ def __init__(self, context: ArgumentInferContext) -> None: # Type context for `*` and `**` arg kinds. self.context = context + def __eq__(self, other: object) -> bool: + if isinstance(other, ArgTypeExpander): + return ( + self.tuple_index == other.tuple_index + and self.kwargs_used == other.kwargs_used + and self.context == other.context + ) + return NotImplemented + def expand_actual_type( self, actual_type: Type, @@ -193,6 +216,66 @@ def expand_actual_type( original_actual = actual_type actual_type = get_proper_type(actual_type) if actual_kind == nodes.ARG_STAR: + if isinstance(actual_type, UnionType): + proper_types = [get_proper_type(t) for t in actual_type.items] + # special case: union of equal sized tuples. (e.g. `tuple[int, int] | tuple[None, None]`) + if is_equal_sized_tuples(proper_types): + # transform union of tuples into a tuple of unions + # e.g. tuple[A, B, C] | tuple[None, None, None] -> tuple[A | None, B | None, C | None] + tuple_args: list[Type] = [ + make_simplified_union(items) + for items in zip(*(t.items for t in proper_types)) + ] + actual_type = TupleType( + tuple_args, + # use Iterable[A | B | C] as the fallback type + fallback=Instance( + self.context.iterable_type.type, [UnionType.make_union(tuple_args)] + ), + ) + else: + # reinterpret all union items as iterable types (if possible) + # and return the union of the iterable item types results. + from mypy.subtypes import is_subtype + + iterable_type = self.context.iterable_type + + def as_iterable_type(t: Type) -> Type: + """Map a type to the iterable supertype if it is a subtype.""" + p_t = get_proper_type(t) + if isinstance(p_t, Instance) and is_subtype(t, iterable_type): + return map_instance_to_supertype(p_t, iterable_type.type) + if isinstance(p_t, TupleType): + # Convert tuple[A, B, C] to Iterable[A | B | C]. + return Instance(iterable_type.type, [make_simplified_union(p_t.items)]) + return t + + # create copies of self for each item in the union + sub_expanders = [ + ArgTypeExpander(context=self.context) for _ in actual_type.items + ] + for expander in sub_expanders: + expander.tuple_index = int(self.tuple_index) + expander.kwargs_used = set(self.kwargs_used) + + candidate_type = make_simplified_union( + [ + e.expand_actual_type( + as_iterable_type(item), + actual_kind, + formal_name, + formal_kind, + allow_unpack, + ) + for e, item in zip(sub_expanders, actual_type.items) + ] + ) + assert all(expander == sub_expanders[0] for expander in sub_expanders) + # carry over the new state if all sub-expanders are the same state + self.tuple_index = int(sub_expanders[0].tuple_index) + self.kwargs_used = set(sub_expanders[0].kwargs_used) + return candidate_type + if isinstance(actual_type, TypeVarTupleType): # This code path is hit when *Ts is passed to a callable and various # special-handling didn't catch this. The best thing we can do is to use @@ -265,3 +348,20 @@ def expand_actual_type( else: # No translation for other kinds -- 1:1 mapping. return original_actual + + +def is_equal_sized_tuples(types: Sequence[ProperType]) -> TypeGuard[Sequence[TupleType]]: + """Check if all types are tuples of the same size.""" + if not types: + return True + + iterator = iter(types) + first = next(iterator) + if not isinstance(first, TupleType): + return False + size = first.length() + + for item in iterator: + if not isinstance(item, TupleType) or item.length() != size: + return False + return True diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 9752a5e68638..0e796ea36d39 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -27,6 +27,7 @@ freshen_function_type_vars, ) from mypy.infer import ArgumentInferContext, infer_function_type_arguments, infer_type_arguments +from mypy.join import join_type_list from mypy.literals import literal from mypy.maptype import map_instance_to_supertype from mypy.meet import is_overlapping_types, narrow_declared_type @@ -5227,6 +5228,11 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type: ctx = None tt = self.accept(item.expr, ctx) tt = get_proper_type(tt) + if isinstance(tt, UnionType): + # Coercing union to join allows better inference in some + # special cases like `tuple[A, B] | tuple[C, D]` + tt = get_proper_type(join_type_list(tt.items)) + if isinstance(tt, TupleType): if find_unpack_in_list(tt.items) is not None: if seen_unpack_in_items: diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 689553445e9d..441c5deae622 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -498,6 +498,322 @@ f(**m) g(**m) # E: Argument 1 to "g" has incompatible type "**Mapping[str, object]"; expected "int" [builtins fixtures/dict.pyi] +[case testTupleExpressionWithUnionStarArgs] +from typing import Union, List, Set, Tuple + +class A: pass +class B: pass +class C: pass +class D: pass + +# union of two fixed size types +def test_fixed_size_union() -> None: + # same size tuples + x1: Union[tuple[A, B], tuple[None, None]] + x2: Union[tuple[A, B], tuple[None, *tuple[None]]] + x3: Union[tuple[A, B], tuple[None, None, *tuple[()]]] + # different size tuples + y1: Union[tuple[A, B], tuple[None, None, None]] + y2: Union[tuple[A, B], tuple[None]] + reveal_type( (*x1,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" + reveal_type( (*x2,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" + reveal_type( (*x3,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" + reveal_type( (*y1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" + reveal_type( (*y2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" + +# union of mixed and variable size types +def test_mixed_variable_size_union() -> None: + z1: Union[tuple[A, B, C], tuple[None, ...]] + reveal_type( (*z1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, __main__.C, None], ...]" + +# union of two variable size types +def test_variable_size_union() -> None: + # same + subtype + tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]] + ll1: Union[List[Union[A, None]], List[A]] + ss1: Union[Set[Union[A, None]], Set[A]] + # mixed + subtype + tl1: Union[Tuple[Union[A, None], ...], List[A]] + ts1: Union[Tuple[Union[A, None], ...], Set[A]] + ls1: Union[List[Union[A, None]], Set[A]] + # same + not subtype + tt2: Union[Tuple[A, ...], Tuple[B, ...]] + ll2: Union[List[A], List[B]] + ss2: Union[Set[A], Set[B]] + # mixed + not subtype + tl2: Union[Tuple[A, ...], List[B]] + ts2: Union[Tuple[A, ...], Set[B]] + ls2: Union[List[A], Set[B]] + + reveal_type( (*tt1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( (*ll1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( (*ss1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( (*tl1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( (*ts1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( (*ls1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( (*tt2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( (*ll2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( (*ss2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( (*tl2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( (*ts2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( (*ls2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" +[builtins fixtures/primitives.pyi] + + +[case testListExpressionWithUnionStarArgs] +from typing import Union, List, Set, Tuple + +class A: pass +class B: pass +class C: pass +class D: pass + +# union of two fixed size types +def test_fixed_size_union() -> None: + # same size tuples + x1: Union[tuple[A, B], tuple[None, None, None]] + x2: Union[tuple[A, B], tuple[None, *tuple[None], None]] + x3: Union[tuple[A, B], tuple[None, None, None, *tuple[()]]] + # different size tuples + y1: Union[tuple[A, B], tuple[None, None, None]] + y2: Union[tuple[A, B], tuple[None]] + reveal_type( [*x1] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" + reveal_type( [*x2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" + reveal_type( [*x3] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" + reveal_type( [*y1] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" + reveal_type( [*y2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" + +# union of mixed and variable size types +def test_mixed_variable_size_union() -> None: + z1: Union[tuple[A, B, C], tuple[None, ...]] + reveal_type( [*z1] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, __main__.C, None]]" + +# union of two variable size types +def test_variable_size_union() -> None: + # same + subtype + tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]] + ll1: Union[List[Union[A, None]], List[A]] + ss1: Union[Set[Union[A, None]], Set[A]] + # mixed + subtype + tl1: Union[Tuple[Union[A, None], ...], List[A]] + ts1: Union[Tuple[Union[A, None], ...], Set[A]] + ls1: Union[List[Union[A, None]], Set[A]] + # same + not subtype + tt2: Union[Tuple[A, ...], Tuple[B, ...]] + ll2: Union[List[A], List[B]] + ss2: Union[Set[A], Set[B]] + # mixed + not subtype + tl2: Union[Tuple[A, ...], List[B]] + ts2: Union[Tuple[A, ...], Set[B]] + ls2: Union[List[A], Set[B]] + + reveal_type( [*tt1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None]]" + reveal_type( [*ll1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None]]" + reveal_type( [*ss1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None]]" + reveal_type( [*tl1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None]]" + reveal_type( [*ts1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None]]" + reveal_type( [*ls1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None]]" + reveal_type( [*tt2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B]]" + reveal_type( [*ll2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B]]" + reveal_type( [*ss2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B]]" + reveal_type( [*tl2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B]]" + reveal_type( [*ts2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B]]" + reveal_type( [*ls2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B]]" +[builtins fixtures/primitives.pyi] + + +[case testSetExpressionWithUnionStarArgs] +from typing import Union, List, Set, Tuple + +class A: pass +class B: pass +class C: pass +class D: pass + +# union of two fixed size types +def test_fixed_size_union() -> None: + # same size tuples + x1: Union[tuple[A, B], tuple[None, None, None]] + x2: Union[tuple[A, B], tuple[None, *tuple[None], None]] + x3: Union[tuple[A, B], tuple[None, None, None, *tuple[()]]] + # different size tuples + y1: Union[tuple[A, B], tuple[None, None, None]] + y2: Union[tuple[A, B], tuple[None]] + reveal_type( {*x1} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" + reveal_type( {*x2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" + reveal_type( {*x3} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" + reveal_type( {*y1} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" + reveal_type( {*y2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" + +# union of mixed and variable size types +def test_mixed_variable_size_union() -> None: + z1: Union[tuple[A, B, C], tuple[None, ...]] + reveal_type( {*z1} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, __main__.C, None]]" + +# union of two variable size types +def test_variable_size_union() -> None: + # same + subtype + tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]] + ll1: Union[List[Union[A, None]], List[A]] + ss1: Union[Set[Union[A, None]], Set[A]] + # mixed + subtype + tl1: Union[Tuple[Union[A, None], ...], List[A]] + ts1: Union[Tuple[Union[A, None], ...], Set[A]] + ls1: Union[List[Union[A, None]], Set[A]] + # same + not subtype + tt2: Union[Tuple[A, ...], Tuple[B, ...]] + ll2: Union[List[A], List[B]] + ss2: Union[Set[A], Set[B]] + # mixed + not subtype + tl2: Union[Tuple[A, ...], List[B]] + ts2: Union[Tuple[A, ...], Set[B]] + ls2: Union[List[A], Set[B]] + + reveal_type( {*tt1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None]]" + reveal_type( {*ll1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None]]" + reveal_type( {*ss1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None]]" + reveal_type( {*tl1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None]]" + reveal_type( {*ts1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None]]" + reveal_type( {*ls1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None]]" + reveal_type( {*tt2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B]]" + reveal_type( {*ll2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B]]" + reveal_type( {*ss2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B]]" + reveal_type( {*tl2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B]]" + reveal_type( {*ts2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B]]" + reveal_type( {*ls2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B]]" +[builtins fixtures/primitives.pyi] + + +[case testStarArgsWithUnion] +from typing import Union + +class A: pass +class B: pass +class C: pass +class D: pass + +def f(a: Union[A, None], b: Union[B, None], c: Union[C, None]) -> None: pass +def g(a: Union[A, None], b: Union[B, None], c: Union[C, None], d: Union[D, None] = None) -> None: pass +x: Union[tuple[A, B, C], tuple[None, None, None]] +f(*x) +g(*x) +[builtins fixtures/tuple.pyi] + + +[case testStarArgsWithUnionSameTupleLengths] +from typing import Union + +class A: pass +class B: pass +class C: pass +class D: pass + +def test_good_case() -> None: + def f(a: Union[A, None], b: Union[B, None], c: Union[C, None]) -> None: pass + def g(a: Union[A, None], b: Union[B, None], c: Union[C, None], d: Union[D, None] = None) -> None: pass + x: Union[tuple[A, B, C], tuple[None, None, None]] + y: tuple[Union[A, None], Union[B, None], Union[C, None]] + f(*x) + f(*y) + g(*x) + g(*y) + +def test_bad_case() -> None: + def f(a: Union[A, None], b: Union[B, None], c: Union[C, None]) -> None: pass + def g(a: Union[A, None], b: Union[B, None], c: Union[C, None], d: Union[D, None] = None) -> None: pass + x: Union[tuple[A, A, A], tuple[None, None, None]] + y: tuple[Union[A, None], Union[A, None], Union[A, None]] + f(*x) # E: Argument 1 to "f" has incompatible type "*Union[tuple[A, A, A], tuple[None, None, None]]"; expected "Optional[B]" \ + # E: Argument 1 to "f" has incompatible type "*Union[tuple[A, A, A], tuple[None, None, None]]"; expected "Optional[C]" + f(*y) # E: Argument 1 to "f" has incompatible type "*tuple[Optional[A], Optional[A], Optional[A]]"; expected "Optional[B]" \ + # E: Argument 1 to "f" has incompatible type "*tuple[Optional[A], Optional[A], Optional[A]]"; expected "Optional[C]" + g(*x) # E: Argument 1 to "g" has incompatible type "*Union[tuple[A, A, A], tuple[None, None, None]]"; expected "Optional[B]" \ + # E: Argument 1 to "g" has incompatible type "*Union[tuple[A, A, A], tuple[None, None, None]]"; expected "Optional[C]" + g(*y) # E: Argument 1 to "g" has incompatible type "*tuple[Optional[A], Optional[A], Optional[A]]"; expected "Optional[B]" \ + # E: Argument 1 to "g" has incompatible type "*tuple[Optional[A], Optional[A], Optional[A]]"; expected "Optional[C]" + +[builtins fixtures/tuple.pyi] + + +[case testStarArgsWithUnionSameTupleLengthsOverload] +from typing import Union, overload + +class A: pass +class B: pass +class C: pass +class D: pass + +@overload +def f(a: A, b: B, c: C) -> None: ... +@overload +def f(a: None, b: None, c: None) -> None: ... +def f(a: Union[A, None], b: Union[B, None], c: Union[C, None]) -> None: pass + +def test_case( + good: Union[tuple[A, B, C], tuple[None, None, None]], + bad1: Union[tuple[None, B, C], tuple[A, None, None]], + bad2: tuple[Union[A, None], Union[B, None], Union[C, None]], +) -> None: + f(*good) + f(*bad1) # E: Argument 1 to "f" has incompatible type "*Union[tuple[None, B, C], tuple[A, None, None]]"; expected "A" \ + # E: Argument 1 to "f" has incompatible type "*Union[tuple[None, B, C], tuple[A, None, None]]"; expected "B" \ + # E: Argument 1 to "f" has incompatible type "*Union[tuple[None, B, C], tuple[A, None, None]]"; expected "C" + f(*bad2) # E: Argument 1 to "f" has incompatible type "*tuple[Optional[A], Optional[B], Optional[C]]"; expected "A" \ + # E: Argument 1 to "f" has incompatible type "*tuple[Optional[A], Optional[B], Optional[C]]"; expected "B" \ + # E: Argument 1 to "f" has incompatible type "*tuple[Optional[A], Optional[B], Optional[C]]"; expected "C" + +[builtins fixtures/tuple.pyi] + + + +[case testStarArgsWithUnionDifferentTupleLengths] +from typing import Union + +class A: pass +class B: pass +class C: pass +class D: pass + +# 1. Good case: +def test_good_case() -> None: + def f(a: Union[A, None], b: Union[A, None], c: Union[A, None] = None) -> None: pass + def g(a: Union[A, None], b: Union[A, None], c: Union[A, None] = None, d: Union[A, None] = None) -> None: pass + x: Union[tuple[A, A, A], tuple[None, None]] + y: tuple[Union[A, None], ...] + f(*x) + f(*y) + g(*x) + g(*y) + +# 2. Bad case +def test_bad_case() -> None: + def f(a: Union[A, None], b: Union[B, None], c: Union[C, None]) -> None: pass + def g(a: Union[A, None], b: Union[A, None], c: Union[A, None] = None, d: Union[D, None] = None) -> None: pass + x: Union[tuple[A, B, C], tuple[None, None]] + y: tuple[Union[A, B, C, None], ...] + # TODO: Show the coerced join type. + f(*x) # E: Argument 1 to "f" has incompatible type "*Union[tuple[A, B, C], tuple[None, None]]"; expected "Optional[A]" \ + # E: Argument 1 to "f" has incompatible type "*Union[tuple[A, B, C], tuple[None, None]]"; expected "Optional[B]" \ + # E: Argument 1 to "f" has incompatible type "*Union[tuple[A, B, C], tuple[None, None]]"; expected "Optional[C]" + f(*y) # E: Argument 1 to "f" has incompatible type "*tuple[Union[A, B, C, None], ...]"; expected "Optional[A]" \ + # E: Argument 1 to "f" has incompatible type "*tuple[Union[A, B, C, None], ...]"; expected "Optional[B]" \ + # E: Argument 1 to "f" has incompatible type "*tuple[Union[A, B, C, None], ...]"; expected "Optional[C]" + g(*x) # E: Argument 1 to "g" has incompatible type "*Union[tuple[A, B, C], tuple[None, None]]"; expected "Optional[A]" \ + # E: Argument 1 to "g" has incompatible type "*Union[tuple[A, B, C], tuple[None, None]]"; expected "Optional[D]" + g(*y) # E: Argument 1 to "g" has incompatible type "*tuple[Union[A, B, C, None], ...]"; expected "Optional[A]" \ + # E: Argument 1 to "g" has incompatible type "*tuple[Union[A, B, C, None], ...]"; expected "Optional[D]" +[builtins fixtures/tuple.pyi] + + +[case testListExpressionRecursiveType] +from typing import Union + +NESTED = Union[str, list[NESTED]] +x: Union[list[Union[NESTED, None]], list[NESTED]] +reveal_type([*x]) # N: Revealed type is "builtins.list[Union[builtins.str, builtins.list[Union[builtins.str, builtins.list[...]]], None]]" +[builtins fixtures/list.pyi] + + [case testPassingEmptyDictWithStars] def f(): pass def g(x=1): pass diff --git a/test-data/unit/fixtures/primitives.pyi b/test-data/unit/fixtures/primitives.pyi index 2f8623c79b9f..6459589510c9 100644 --- a/test-data/unit/fixtures/primitives.pyi +++ b/test-data/unit/fixtures/primitives.pyi @@ -47,8 +47,9 @@ class memoryview(Sequence[int]): def __iter__(self) -> Iterator[int]: pass def __contains__(self, other: object) -> bool: pass def __getitem__(self, item: int) -> int: pass -class tuple(Generic[T]): +class tuple(Iterable[T]): def __contains__(self, other: object) -> bool: pass + def __iter__(self) -> Iterator[T]: pass class list(Sequence[T]): def append(self, v: T) -> None: pass def __iter__(self) -> Iterator[T]: pass From f2d254c0f6cde6c3bf51ed2f84004789d5471ecb Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 14 Aug 2025 10:27:23 +0200 Subject: [PATCH 02/12] Use typing.Unpack in tests --- test-data/unit/check-kwargs.test | 87 +++++++++++++++++--------------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 441c5deae622..ab2f2980c57a 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -499,35 +499,38 @@ g(**m) # E: Argument 1 to "g" has incompatible type "**Mapping[str, object]"; e [builtins fixtures/dict.pyi] [case testTupleExpressionWithUnionStarArgs] -from typing import Union, List, Set, Tuple +from typing import Union, List, Set, Tuple, Unpack class A: pass class B: pass class C: pass class D: pass -# union of two fixed size types -def test_fixed_size_union() -> None: + +def test_union_same_size_tuple() -> None: # same size tuples x1: Union[tuple[A, B], tuple[None, None]] - x2: Union[tuple[A, B], tuple[None, *tuple[None]]] - x3: Union[tuple[A, B], tuple[None, None, *tuple[()]]] - # different size tuples - y1: Union[tuple[A, B], tuple[None, None, None]] - y2: Union[tuple[A, B], tuple[None]] + x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]] + x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]] reveal_type( (*x1,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" reveal_type( (*x2,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" reveal_type( (*x3,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" + +def test_union_different_size_tuple() -> None: + y1: Union[tuple[A, B], tuple[None, None, None]] + y2: Union[tuple[A, B], tuple[None]] + y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]] reveal_type( (*y1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" reveal_type( (*y2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" + reveal_type( (*y2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" -# union of mixed and variable size types -def test_mixed_variable_size_union() -> None: +def test_union_fixed_size_and_variadic_tuple() -> None: z1: Union[tuple[A, B, C], tuple[None, ...]] + z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]] reveal_type( (*z1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, __main__.C, None], ...]" + reveal_type( (*z2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, __main__.C, None], ...]" -# union of two variable size types -def test_variable_size_union() -> None: +def test_union_variable_size_tuples() -> None: # same + subtype tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]] ll1: Union[List[Union[A, None]], List[A]] @@ -561,35 +564,37 @@ def test_variable_size_union() -> None: [case testListExpressionWithUnionStarArgs] -from typing import Union, List, Set, Tuple +from typing import Union, List, Set, Tuple, Unpack class A: pass class B: pass class C: pass class D: pass -# union of two fixed size types -def test_fixed_size_union() -> None: +def test_union_same_size_tuple() -> None: # same size tuples - x1: Union[tuple[A, B], tuple[None, None, None]] - x2: Union[tuple[A, B], tuple[None, *tuple[None], None]] - x3: Union[tuple[A, B], tuple[None, None, None, *tuple[()]]] - # different size tuples + x1: Union[tuple[A, B], tuple[None, None]] + x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]] + x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]] + reveal_type( [*x1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None, __main__.B]]" + reveal_type( [*x2] ) # N: Revealed type is "builtins.list[Union[__main__.A, None, __main__.B]]" + reveal_type( [*x3] ) # N: Revealed type is "builtins.list[Union[__main__.A, None, __main__.B]]" + +def test_union_different_size_tuple() -> None: y1: Union[tuple[A, B], tuple[None, None, None]] y2: Union[tuple[A, B], tuple[None]] - reveal_type( [*x1] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" - reveal_type( [*x2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" - reveal_type( [*x3] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" + y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]] reveal_type( [*y1] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" reveal_type( [*y2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" + reveal_type( [*y2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" -# union of mixed and variable size types -def test_mixed_variable_size_union() -> None: +def test_union_fixed_size_and_variadic_tuple() -> None: z1: Union[tuple[A, B, C], tuple[None, ...]] + z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]] reveal_type( [*z1] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, __main__.C, None]]" + reveal_type( [*z2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, __main__.C, None]]" -# union of two variable size types -def test_variable_size_union() -> None: +def test_union_variable_size_tuples() -> None: # same + subtype tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]] ll1: Union[List[Union[A, None]], List[A]] @@ -623,35 +628,37 @@ def test_variable_size_union() -> None: [case testSetExpressionWithUnionStarArgs] -from typing import Union, List, Set, Tuple +from typing import Union, List, Set, Tuple, Unpack class A: pass class B: pass class C: pass class D: pass -# union of two fixed size types -def test_fixed_size_union() -> None: +def test_union_same_size_tuple() -> None: # same size tuples - x1: Union[tuple[A, B], tuple[None, None, None]] - x2: Union[tuple[A, B], tuple[None, *tuple[None], None]] - x3: Union[tuple[A, B], tuple[None, None, None, *tuple[()]]] - # different size tuples + x1: Union[tuple[A, B], tuple[None, None]] + x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]] + x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]] + reveal_type( {*x1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None, __main__.B]]" + reveal_type( {*x2} ) # N: Revealed type is "builtins.set[Union[__main__.A, None, __main__.B]]" + reveal_type( {*x3} ) # N: Revealed type is "builtins.set[Union[__main__.A, None, __main__.B]]" + +def test_union_different_size_tuple() -> None: y1: Union[tuple[A, B], tuple[None, None, None]] y2: Union[tuple[A, B], tuple[None]] - reveal_type( {*x1} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" - reveal_type( {*x2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" - reveal_type( {*x3} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" + y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]] reveal_type( {*y1} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" reveal_type( {*y2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" + reveal_type( {*y2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" -# union of mixed and variable size types -def test_mixed_variable_size_union() -> None: +def test_union_fixed_size_and_variadic_tuple() -> None: z1: Union[tuple[A, B, C], tuple[None, ...]] + z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]] reveal_type( {*z1} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, __main__.C, None]]" + reveal_type( {*z2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, __main__.C, None]]" -# union of two variable size types -def test_variable_size_union() -> None: +def test_union_variable_size_tuples() -> None: # same + subtype tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]] ll1: Union[List[Union[A, None]], List[A]] From df7b3f0409a5ec07cad3afbb02fcb64250e701e5 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 14 Aug 2025 19:50:49 +0200 Subject: [PATCH 03/12] reworked argmapper with star args --- mypy/argmap.py | 317 ++++++++++++++++++++++--------- mypy/checkexpr.py | 36 +++- mypy/infer.py | 1 + test-data/unit/check-kwargs.test | 265 +++++++++++++++----------- 4 files changed, 414 insertions(+), 205 deletions(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index af7d7da117a7..f2ece5efa2ab 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -3,14 +3,15 @@ from __future__ import annotations from collections.abc import Sequence -from typing import TYPE_CHECKING, Callable -from typing_extensions import TypeGuard +from typing import TYPE_CHECKING, Callable, cast +from typing_extensions import NewType, TypeGuard from mypy import nodes from mypy.maptype import map_instance_to_supertype from mypy.typeops import make_simplified_union from mypy.types import ( AnyType, + CallableType, Instance, ParamSpecType, ProperType, @@ -18,9 +19,12 @@ Type, TypedDictType, TypeOfAny, + TypeVarId, TypeVarTupleType, + TypeVarType, UnionType, UnpackType, + flatten_nested_tuples, get_proper_type, ) @@ -28,6 +32,10 @@ from mypy.infer import ArgumentInferContext +IterableType = NewType("IterableType", Instance) +"""Represents an instance of `Iterable[T]`.""" + + def map_actuals_to_formals( actual_kinds: list[nodes.ArgKind], actual_names: Sequence[str | None] | None, @@ -216,92 +224,41 @@ def expand_actual_type( original_actual = actual_type actual_type = get_proper_type(actual_type) if actual_kind == nodes.ARG_STAR: - if isinstance(actual_type, UnionType): - proper_types = [get_proper_type(t) for t in actual_type.items] - # special case: union of equal sized tuples. (e.g. `tuple[int, int] | tuple[None, None]`) - if is_equal_sized_tuples(proper_types): - # transform union of tuples into a tuple of unions - # e.g. tuple[A, B, C] | tuple[None, None, None] -> tuple[A | None, B | None, C | None] - tuple_args: list[Type] = [ - make_simplified_union(items) - for items in zip(*(t.items for t in proper_types)) - ] - actual_type = TupleType( - tuple_args, - # use Iterable[A | B | C] as the fallback type - fallback=Instance( - self.context.iterable_type.type, [UnionType.make_union(tuple_args)] - ), - ) - else: - # reinterpret all union items as iterable types (if possible) - # and return the union of the iterable item types results. - from mypy.subtypes import is_subtype - - iterable_type = self.context.iterable_type - - def as_iterable_type(t: Type) -> Type: - """Map a type to the iterable supertype if it is a subtype.""" - p_t = get_proper_type(t) - if isinstance(p_t, Instance) and is_subtype(t, iterable_type): - return map_instance_to_supertype(p_t, iterable_type.type) - if isinstance(p_t, TupleType): - # Convert tuple[A, B, C] to Iterable[A | B | C]. - return Instance(iterable_type.type, [make_simplified_union(p_t.items)]) - return t - - # create copies of self for each item in the union - sub_expanders = [ - ArgTypeExpander(context=self.context) for _ in actual_type.items - ] - for expander in sub_expanders: - expander.tuple_index = int(self.tuple_index) - expander.kwargs_used = set(self.kwargs_used) - - candidate_type = make_simplified_union( - [ - e.expand_actual_type( - as_iterable_type(item), - actual_kind, - formal_name, - formal_kind, - allow_unpack, - ) - for e, item in zip(sub_expanders, actual_type.items) - ] - ) - assert all(expander == sub_expanders[0] for expander in sub_expanders) - # carry over the new state if all sub-expanders are the same state - self.tuple_index = int(sub_expanders[0].tuple_index) - self.kwargs_used = set(sub_expanders[0].kwargs_used) - return candidate_type - - if isinstance(actual_type, TypeVarTupleType): - # This code path is hit when *Ts is passed to a callable and various - # special-handling didn't catch this. The best thing we can do is to use - # the upper bound. - actual_type = get_proper_type(actual_type.upper_bound) - if isinstance(actual_type, Instance) and actual_type.args: - from mypy.subtypes import is_subtype - - if is_subtype(actual_type, self.context.iterable_type): - return map_instance_to_supertype( - actual_type, self.context.iterable_type.type - ).args[0] - else: - # We cannot properly unpack anything other - # than `Iterable` type with `*`. - # Just return `Any`, other parts of code would raise - # a different error for improper use. - return AnyType(TypeOfAny.from_error) - elif isinstance(actual_type, TupleType): + # parse *args as one of the following: + # IterableType | TupleType | ParamSpecType | AnyType + star_args = self.parse_star_args_type(actual_type) + # star_args = actual_type + + # print(f"expand_actual_type: {actual_type=} {star_args=}") + + # if isinstance(star_args, TypeVarTupleType): + # # This code path is hit when *Ts is passed to a callable and various + # # special-handling didn't catch this. The best thing we can do is to use + # # the upper bound. + # star_args = get_proper_type(star_args.upper_bound) + # if isinstance(star_args, Instance) and star_args.args: + # from mypy.subtypes import is_subtype + # + # if is_subtype(star_args, self.context.iterable_type): + # return map_instance_to_supertype( + # star_args, self.context.iterable_type.type + # ).args[0] + # else: + # # We cannot properly unpack anything other + # # than `Iterable` type with `*`. + # # Just return `Any`, other parts of code would raise + # # a different error for improper use. + # return AnyType(TypeOfAny.from_error) + if self.is_iterable_type(star_args): + return star_args.args[0] + elif isinstance(star_args, TupleType): # Get the next tuple item of a tuple *arg. - if self.tuple_index >= len(actual_type.items): + if self.tuple_index >= len(star_args.items): # Exhausted a tuple -- continue to the next *args. self.tuple_index = 1 else: self.tuple_index += 1 - item = actual_type.items[self.tuple_index - 1] + item = star_args.items[self.tuple_index - 1] if isinstance(item, UnpackType) and not allow_unpack: # An unpack item that doesn't have special handling, use upper bound as above. unpacked = get_proper_type(item.type) @@ -315,9 +272,9 @@ def as_iterable_type(t: Type) -> Type: ) item = fallback.args[0] return item - elif isinstance(actual_type, ParamSpecType): + elif isinstance(star_args, ParamSpecType): # ParamSpec is valid in *args but it can't be unpacked. - return actual_type + return star_args else: return AnyType(TypeOfAny.from_error) elif actual_kind == nodes.ARG_STAR2: @@ -349,19 +306,197 @@ def as_iterable_type(t: Type) -> Type: # No translation for other kinds -- 1:1 mapping. return original_actual + def is_iterable(self, typ: Type) -> bool: + from mypy.subtypes import is_subtype + + return is_subtype(typ, self.context.iterable_type) + + def is_iterable_instance_subtype(self, typ: Type) -> TypeGuard[Instance]: + from mypy.subtypes import is_subtype + + p_t = get_proper_type(typ) + return ( + isinstance(p_t, Instance) + and bool(p_t.args) + and is_subtype(p_t, self.context.iterable_type) + ) + + def is_iterable_type(self, typ: Type) -> TypeGuard[IterableType]: + """Check if the type is an Iterable[T] or a subtype of it.""" + p_t = get_proper_type(typ) + return isinstance(p_t, Instance) and p_t.type == self.context.iterable_type.type + + def as_iterable_type(self, typ: Type) -> IterableType | AnyType: + """Reinterpret a type as Iterable[T], or return AnyType if not possible.""" + p_t = get_proper_type(typ) + if self.is_iterable_type(p_t): + return p_t + elif self.is_iterable_instance_subtype(p_t): + cls = self.context.iterable_type.type + return cast(IterableType, map_instance_to_supertype(p_t, cls)) + elif isinstance(p_t, UnionType): + # If the type is a union, map each item to the iterable supertype. + # the return the combined iterable type Iterable[A] | Iterable[B] -> Iterable[A | B] + converted_types = [self.as_iterable_type(get_proper_type(item)) for item in p_t.items] + # if an item could not be interpreted as Iterable[T], we return AnyType + if all(self.is_iterable_type(it) for it in converted_types): + # all items are iterable, return Iterable[T₁ | T₂ | ... | Tₙ] + iterable_types = cast(list[IterableType], converted_types) + arg = make_simplified_union([it.args[0] for it in iterable_types]) + return self.make_iterable_type(arg) + return AnyType(TypeOfAny.from_error) + elif isinstance(p_t, TupleType): + # maps tuple[A, B, C] -> Iterable[A | B | C] + # note: proper_elements may contain UnpackType, for instance with + # tuple[None, *tuple[None, ...]].. + proper_elements = [get_proper_type(t) for t in flatten_nested_tuples(p_t.items)] + args: list[Type] = [] + for p_e in proper_elements: + if isinstance(p_e, UnpackType): + r = self.as_iterable_type(p_e) + if self.is_iterable_type(r): + args.append(r.args[0]) + else: + args.append(r) + else: + args.append(p_e) + return self.make_iterable_type(make_simplified_union(args)) + if isinstance(p_t, UnpackType): + return self.as_iterable_type(p_t.type) + if isinstance(p_t, (TypeVarType, TypeVarTupleType)): + return self.as_iterable_type(p_t.upper_bound) + # fallback: use the solver to reinterpret the type as Iterable[T] + if self.is_iterable(p_t): + return self._solve_as_iterable(p_t) + return AnyType(TypeOfAny.from_error) + + def make_iterable_type(self, arg: Type) -> IterableType: + value = Instance(self.context.iterable_type.type, [arg]) + return cast(IterableType, value) + + def parse_star_args_type( + self, typ: Type + ) -> TupleType | IterableType | ParamSpecType | AnyType: + """Parse the type of a *args argument. + + Returns one TupleType, IterableType, ParamSpecType or AnyType. + """ + p_t = get_proper_type(typ) + if isinstance(p_t, (TupleType, ParamSpecType, AnyType)): + # just return the type as-is + return p_t + elif isinstance(p_t, TypeVarTupleType): + return self.parse_star_args_type(p_t.upper_bound) + elif isinstance(p_t, UnionType): + proper_items = [get_proper_type(t) for t in p_t.items] + # consider 2 cases: + # 1. Union of equal sized tuples, e.g. tuple[A, B] | tuple[None, None] + # In this case transform union of same-sized tuples into a tuple of unions + # e.g. tuple[A, B] | tuple[None, None] -> tuple[A | None, B | None] + if is_equal_sized_tuples(proper_items): + + tuple_args: list[Type] = [ + make_simplified_union(items) for items in zip(*(t.items for t in proper_items)) + ] + actual_type = TupleType( + tuple_args, + # use Iterable[A | B | C] as the fallback type + fallback=Instance( + self.context.iterable_type.type, [UnionType.make_union(tuple_args)] + ), + ) + return actual_type + # 2. Union of iterable types, e.g. Iterable[A] | Iterable[B] + # In this case return Iterable[A | B] + # Note that this covers unions of differently sized tuples as well. + else: + converted_types = [self.as_iterable_type(p_i) for p_i in proper_items] + if all(self.is_iterable_type(it) for it in converted_types): + # all items are iterable, return Iterable[T₁ | T₂ | ... | Tₙ] + iterables = cast(list[IterableType], converted_types) + arg = make_simplified_union([it.args[0] for it in iterables]) + return self.make_iterable_type(arg) + else: + # some items in the union are not iterable, return AnyType + return AnyType(TypeOfAny.from_error) + elif self.is_iterable_type(parsed := self.as_iterable_type(p_t)): + # in all other cases, we try to reinterpret the type as Iterable[T] + return parsed + return AnyType(TypeOfAny.from_error) + + def _solve_as_iterable(self, typ: Type) -> IterableType | AnyType: + r"""Use the solver to cast a type as Iterable[T]. + + Returns the type as-is if solving fails. + """ + from mypy.constraints import infer_constraints_for_callable + from mypy.nodes import ARG_POS + from mypy.solve import solve_constraints + + iterable_kind = self.context.iterable_type.type + + # We first create an upcast function: + # def [T] (Iterable[T]) -> Iterable[T]: ... + # and then solve for T, given the input type as the argument. + T = TypeVarType( + "T", + "T", + TypeVarId(-1), + values=[], + upper_bound=AnyType(TypeOfAny.special_form), + default=AnyType(TypeOfAny.special_form), + ) + target = Instance(iterable_kind, [T]) + + upcast_callable = CallableType( + variables=[T], + arg_types=[target], + arg_kinds=[ARG_POS], + arg_names=[None], + ret_type=T, + fallback=self.context.function_type, + ) + constraints = infer_constraints_for_callable( + upcast_callable, [typ], [ARG_POS], [None], [[0]], context=self.context + ) + + (sol,), _ = solve_constraints([T], constraints) + + if sol is None: # solving failed, return AnyType fallback + return AnyType(TypeOfAny.from_error) + return self.make_iterable_type(sol) + def is_equal_sized_tuples(types: Sequence[ProperType]) -> TypeGuard[Sequence[TupleType]]: - """Check if all types are tuples of the same size.""" + """Check if all types are tuples of the same size. + + We use `flatten_nested_tuples` to deal with nested tuples. + Note that the result may still contain + """ if not types: return True iterator = iter(types) - first = next(iterator) - if not isinstance(first, TupleType): + typ = next(iterator) + if not isinstance(typ, TupleType): + return False + flattened_elements = flatten_nested_tuples(typ.items) + if any( + isinstance(get_proper_type(member), (UnpackType, TypeVarTupleType)) + for member in flattened_elements + ): + # this can happen e.g. with tuple[int, *tuple[int, ...], int] return False - size = first.length() + size = len(flattened_elements) - for item in iterator: - if not isinstance(item, TupleType) or item.length() != size: + for typ in iterator: + if not isinstance(typ, TupleType): + return False + flattened_elements = flatten_nested_tuples(typ.items) + if len(flattened_elements) != size or any( + isinstance(get_proper_type(member), (UnpackType, TypeVarTupleType)) + for member in flattened_elements + ): + # this can happen e.g. with tuple[int, *tuple[int, ...], int] return False return True diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 0e796ea36d39..293b3f4e4b12 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -27,7 +27,6 @@ freshen_function_type_vars, ) from mypy.infer import ArgumentInferContext, infer_function_type_arguments, infer_type_arguments -from mypy.join import join_type_list from mypy.literals import literal from mypy.maptype import map_instance_to_supertype from mypy.meet import is_overlapping_types, narrow_declared_type @@ -2286,7 +2285,9 @@ def infer_function_type_arguments_pass2( def argument_infer_context(self) -> ArgumentInferContext: if self._arg_infer_context_cache is None: self._arg_infer_context_cache = ArgumentInferContext( - self.chk.named_type("typing.Mapping"), self.chk.named_type("typing.Iterable") + self.chk.named_type("typing.Mapping"), + self.chk.named_type("typing.Iterable"), + self.chk.named_type("builtins.function"), ) return self._arg_infer_context_cache @@ -2671,6 +2672,30 @@ def check_arg( original_caller_type = get_proper_type(original_caller_type) callee_type = get_proper_type(callee_type) + if isinstance(callee_type, UnpackType) and not isinstance(caller_type, UnpackType): + # it can happen that the caller_type got expanded. + # since this is from a callable definition, it should be one of the following: + # - TupleType, TypeVarTupleType, or a variable length tuple Instance. + unpack_arg = get_proper_type(callee_type.type) + if isinstance(unpack_arg, TypeVarTupleType): + # substitute with upper bound of the TypeVarTuple + unpack_arg = get_proper_type(unpack_arg.upper_bound) + # note: not using elif, since in the future upper bound may be a finite tuple + if isinstance(unpack_arg, Instance) and unpack_arg.type.fullname == "builtins.tuple": + callee_type = get_proper_type(unpack_arg.args[0]) + elif isinstance(unpack_arg, TupleType): + # this branch should currently never hit, but it may hit in the future, + # if it will ever be allowed to upper bound TypeVarTuple with a tuple type. + elements = flatten_nested_tuples(unpack_arg.items) + if m < len(elements): + # pick the corresponding item from the tuple + callee_type = get_proper_type(elements[m]) + else: + self.chk.fail(message_registry.TUPLE_INDEX_OUT_OF_RANGE, context) + return + else: + raise TypeError(f"did not expect unpack_arg to be of type {type(unpack_arg)=}") + if isinstance(caller_type, DeletedType): self.msg.deleted_as_rvalue(caller_type, context) # Only non-abstract non-protocol class can be given where Type[...] is expected... @@ -5228,10 +5253,9 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type: ctx = None tt = self.accept(item.expr, ctx) tt = get_proper_type(tt) - if isinstance(tt, UnionType): - # Coercing union to join allows better inference in some - # special cases like `tuple[A, B] | tuple[C, D]` - tt = get_proper_type(join_type_list(tt.items)) + # convert tt to one of TupleType, IterableType, AnyType or + mapper = ArgTypeExpander(self.argument_infer_context()) + tt = mapper.parse_star_args_type(tt) if isinstance(tt, TupleType): if find_unpack_in_list(tt.items) is not None: diff --git a/mypy/infer.py b/mypy/infer.py index cdc43797d3b1..70bb126f3aae 100644 --- a/mypy/infer.py +++ b/mypy/infer.py @@ -28,6 +28,7 @@ class ArgumentInferContext(NamedTuple): mapping_type: Instance iterable_type: Instance + function_type: Instance def infer_function_type_arguments( diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index ab2f2980c57a..5439d57a9c7c 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -499,7 +499,7 @@ g(**m) # E: Argument 1 to "g" has incompatible type "**Mapping[str, object]"; e [builtins fixtures/dict.pyi] [case testTupleExpressionWithUnionStarArgs] -from typing import Union, List, Set, Tuple, Unpack +from typing import Union, List, Set, Tuple, Unpack, TypeVarTuple class A: pass class B: pass @@ -507,47 +507,55 @@ class C: pass class D: pass -def test_union_same_size_tuple() -> None: - # same size tuples - x1: Union[tuple[A, B], tuple[None, None]] - x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]] - x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]] +def test_union_same_size_tuple( + x1: Union[tuple[A, B], tuple[None, None]], + x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]], + x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]], + xx: tuple[Union[A, None], Union[B, None]], # reference type +) -> None: reveal_type( (*x1,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" reveal_type( (*x2,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" reveal_type( (*x3,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" + reveal_type( (*xx,) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" -def test_union_different_size_tuple() -> None: - y1: Union[tuple[A, B], tuple[None, None, None]] - y2: Union[tuple[A, B], tuple[None]] - y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]] +def test_union_different_size_tuple( + y1: Union[tuple[A, B], tuple[None, None, None]], + y2: Union[tuple[A, B], tuple[None]], + y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]], + yy: tuple[Union[A, B, None], ...], # reference type +) -> None: reveal_type( (*y1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" reveal_type( (*y2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" reveal_type( (*y2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" + reveal_type( (*yy,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" -def test_union_fixed_size_and_variadic_tuple() -> None: - z1: Union[tuple[A, B, C], tuple[None, ...]] - z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]] +def test_union_fixed_size_and_variadic_tuple( + z1: Union[tuple[A, B, C], tuple[None, ...]], + z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]], + zz: tuple[Union[A, B, C, None], ...], # reference type +) -> None: reveal_type( (*z1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, __main__.C, None], ...]" reveal_type( (*z2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, __main__.C, None], ...]" + reveal_type( (*zz,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, __main__.C, None], ...]" -def test_union_variable_size_tuples() -> None: +def test_union_variable_size_tuples( # same + subtype - tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]] - ll1: Union[List[Union[A, None]], List[A]] - ss1: Union[Set[Union[A, None]], Set[A]] + tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]], + ll1: Union[List[Union[A, None]], List[A]], + ss1: Union[Set[Union[A, None]], Set[A]], # mixed + subtype - tl1: Union[Tuple[Union[A, None], ...], List[A]] - ts1: Union[Tuple[Union[A, None], ...], Set[A]] - ls1: Union[List[Union[A, None]], Set[A]] + tl1: Union[Tuple[Union[A, None], ...], List[A]], + ts1: Union[Tuple[Union[A, None], ...], Set[A]], + ls1: Union[List[Union[A, None]], Set[A]], # same + not subtype - tt2: Union[Tuple[A, ...], Tuple[B, ...]] - ll2: Union[List[A], List[B]] - ss2: Union[Set[A], Set[B]] + tt2: Union[Tuple[A, ...], Tuple[B, ...]], + ll2: Union[List[A], List[B]], + ss2: Union[Set[A], Set[B]], # mixed + not subtype - tl2: Union[Tuple[A, ...], List[B]] - ts2: Union[Tuple[A, ...], Set[B]] - ls2: Union[List[A], Set[B]] - + tl2: Union[Tuple[A, ...], List[B]], + ts2: Union[Tuple[A, ...], Set[B]], + ls2: Union[List[A], Set[B]], +) -> None: reveal_type( (*tt1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" reveal_type( (*ll1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" reveal_type( (*ss1,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" @@ -560,58 +568,70 @@ def test_union_variable_size_tuples() -> None: reveal_type( (*tl2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" reveal_type( (*ts2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" reveal_type( (*ls2,) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + +NESTED = Union[str, list[NESTED]] +def test_union_recursive(x: Union[list[Union[NESTED, None]], list[NESTED]]) -> None: + reveal_type( (*x,) ) # N: Revealed type is "builtins.tuple[Union[builtins.str, builtins.list[Union[builtins.str, builtins.list[...]]], None], ...]" [builtins fixtures/primitives.pyi] [case testListExpressionWithUnionStarArgs] -from typing import Union, List, Set, Tuple, Unpack +from typing import Union, List, Set, Tuple, Unpack, TypeVarTuple class A: pass class B: pass class C: pass class D: pass -def test_union_same_size_tuple() -> None: - # same size tuples - x1: Union[tuple[A, B], tuple[None, None]] - x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]] - x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]] +def test_union_same_size_tuple( + x1: Union[tuple[A, B], tuple[None, None]], + x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]], + x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]], + xx: tuple[Union[A, None], Union[B, None]], # reference type +) -> None: reveal_type( [*x1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None, __main__.B]]" reveal_type( [*x2] ) # N: Revealed type is "builtins.list[Union[__main__.A, None, __main__.B]]" reveal_type( [*x3] ) # N: Revealed type is "builtins.list[Union[__main__.A, None, __main__.B]]" + reveal_type( [*xx] ) # N: Revealed type is "builtins.list[Union[__main__.A, None, __main__.B]]" -def test_union_different_size_tuple() -> None: - y1: Union[tuple[A, B], tuple[None, None, None]] - y2: Union[tuple[A, B], tuple[None]] - y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]] +def test_union_different_size_tuple( + y1: Union[tuple[A, B], tuple[None, None, None]], + y2: Union[tuple[A, B], tuple[None]], + y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]], + yy: tuple[Union[A, B, None], ...], # reference type +) -> None: reveal_type( [*y1] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" reveal_type( [*y2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" reveal_type( [*y2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" + reveal_type( [*yy] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, None]]" -def test_union_fixed_size_and_variadic_tuple() -> None: - z1: Union[tuple[A, B, C], tuple[None, ...]] - z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]] +def test_union_fixed_size_and_variadic_tuple( + z1: Union[tuple[A, B, C], tuple[None, ...]], + z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]], + zz: tuple[Union[A, B, C, None], ...], # reference type +) -> None: reveal_type( [*z1] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, __main__.C, None]]" reveal_type( [*z2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, __main__.C, None]]" + reveal_type( [*zz] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B, __main__.C, None]]" -def test_union_variable_size_tuples() -> None: +def test_union_variable_size_tuples( # same + subtype - tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]] - ll1: Union[List[Union[A, None]], List[A]] - ss1: Union[Set[Union[A, None]], Set[A]] + tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]], + ll1: Union[List[Union[A, None]], List[A]], + ss1: Union[Set[Union[A, None]], Set[A]], # mixed + subtype - tl1: Union[Tuple[Union[A, None], ...], List[A]] - ts1: Union[Tuple[Union[A, None], ...], Set[A]] - ls1: Union[List[Union[A, None]], Set[A]] + tl1: Union[Tuple[Union[A, None], ...], List[A]], + ts1: Union[Tuple[Union[A, None], ...], Set[A]], + ls1: Union[List[Union[A, None]], Set[A]], # same + not subtype - tt2: Union[Tuple[A, ...], Tuple[B, ...]] - ll2: Union[List[A], List[B]] - ss2: Union[Set[A], Set[B]] + tt2: Union[Tuple[A, ...], Tuple[B, ...]], + ll2: Union[List[A], List[B]], + ss2: Union[Set[A], Set[B]], # mixed + not subtype - tl2: Union[Tuple[A, ...], List[B]] - ts2: Union[Tuple[A, ...], Set[B]] - ls2: Union[List[A], Set[B]] - + tl2: Union[Tuple[A, ...], List[B]], + ts2: Union[Tuple[A, ...], Set[B]], + ls2: Union[List[A], Set[B]], +) -> None: reveal_type( [*tt1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None]]" reveal_type( [*ll1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None]]" reveal_type( [*ss1] ) # N: Revealed type is "builtins.list[Union[__main__.A, None]]" @@ -624,58 +644,70 @@ def test_union_variable_size_tuples() -> None: reveal_type( [*tl2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B]]" reveal_type( [*ts2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B]]" reveal_type( [*ls2] ) # N: Revealed type is "builtins.list[Union[__main__.A, __main__.B]]" + +NESTED = Union[str, list[NESTED]] +def test_union_recursive(x: Union[list[Union[NESTED, None]], list[NESTED]]) -> None: + reveal_type( [*x] ) # N: Revealed type is "builtins.list[Union[builtins.str, builtins.list[Union[builtins.str, builtins.list[...]]], None]]" [builtins fixtures/primitives.pyi] [case testSetExpressionWithUnionStarArgs] -from typing import Union, List, Set, Tuple, Unpack +from typing import Union, List, Set, Tuple, Unpack, TypeVarTuple class A: pass class B: pass class C: pass class D: pass -def test_union_same_size_tuple() -> None: - # same size tuples - x1: Union[tuple[A, B], tuple[None, None]] - x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]] - x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]] +def test_union_same_size_tuple( + x1: Union[tuple[A, B], tuple[None, None]], + x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]], + x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]], + xx: tuple[Union[A, None], Union[B, None]], # reference type +) -> None: reveal_type( {*x1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None, __main__.B]]" reveal_type( {*x2} ) # N: Revealed type is "builtins.set[Union[__main__.A, None, __main__.B]]" reveal_type( {*x3} ) # N: Revealed type is "builtins.set[Union[__main__.A, None, __main__.B]]" + reveal_type( {*xx} ) # N: Revealed type is "builtins.set[Union[__main__.A, None, __main__.B]]" -def test_union_different_size_tuple() -> None: - y1: Union[tuple[A, B], tuple[None, None, None]] - y2: Union[tuple[A, B], tuple[None]] - y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]] +def test_union_different_size_tuple( + y1: Union[tuple[A, B], tuple[None, None, None]], + y2: Union[tuple[A, B], tuple[None]], + y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]], + yy: tuple[Union[A, B, None], ...], # reference type +) -> None: reveal_type( {*y1} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" reveal_type( {*y2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" reveal_type( {*y2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" + reveal_type( {*yy} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, None]]" -def test_union_fixed_size_and_variadic_tuple() -> None: - z1: Union[tuple[A, B, C], tuple[None, ...]] - z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]] +def test_union_fixed_size_and_variadic_tuple( + z1: Union[tuple[A, B, C], tuple[None, ...]], + z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]], + zz: tuple[Union[A, B, C, None], ...], # reference type +) -> None: reveal_type( {*z1} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, __main__.C, None]]" reveal_type( {*z2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, __main__.C, None]]" + reveal_type( {*zz} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B, __main__.C, None]]" -def test_union_variable_size_tuples() -> None: +def test_union_variable_size_tuples( # same + subtype - tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]] - ll1: Union[List[Union[A, None]], List[A]] - ss1: Union[Set[Union[A, None]], Set[A]] + tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]], + ll1: Union[List[Union[A, None]], List[A]], + ss1: Union[Set[Union[A, None]], Set[A]], # mixed + subtype - tl1: Union[Tuple[Union[A, None], ...], List[A]] - ts1: Union[Tuple[Union[A, None], ...], Set[A]] - ls1: Union[List[Union[A, None]], Set[A]] + tl1: Union[Tuple[Union[A, None], ...], List[A]], + ts1: Union[Tuple[Union[A, None], ...], Set[A]], + ls1: Union[List[Union[A, None]], Set[A]], # same + not subtype - tt2: Union[Tuple[A, ...], Tuple[B, ...]] - ll2: Union[List[A], List[B]] - ss2: Union[Set[A], Set[B]] + tt2: Union[Tuple[A, ...], Tuple[B, ...]], + ll2: Union[List[A], List[B]], + ss2: Union[Set[A], Set[B]], # mixed + not subtype - tl2: Union[Tuple[A, ...], List[B]] - ts2: Union[Tuple[A, ...], Set[B]] - ls2: Union[List[A], Set[B]] - + tl2: Union[Tuple[A, ...], List[B]], + ts2: Union[Tuple[A, ...], Set[B]], + ls2: Union[List[A], Set[B]], +) -> None: reveal_type( {*tt1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None]]" reveal_type( {*ll1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None]]" reveal_type( {*ss1} ) # N: Revealed type is "builtins.set[Union[__main__.A, None]]" @@ -688,6 +720,28 @@ def test_union_variable_size_tuples() -> None: reveal_type( {*tl2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B]]" reveal_type( {*ts2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B]]" reveal_type( {*ls2} ) # N: Revealed type is "builtins.set[Union[__main__.A, __main__.B]]" + +NESTED = Union[str, list[NESTED]] +def test_union_recursive(x: Union[list[Union[NESTED, None]], list[NESTED]]) -> None: + reveal_type( {*x} ) # N: Revealed type is "builtins.set[Union[builtins.str, builtins.list[Union[builtins.str, builtins.list[...]]], None]]" + +[builtins fixtures/primitives.pyi] + + +[case testStarArgsWithUnionTypeVarTuple-xfail] +# https://github.com/python/mypy/issues/19659 +from typing import Union, TypeVarTuple + +class A: pass +Ts = TypeVarTuple('Ts') + +def test_union_typevartuple( + tt1: Union[tuple[A, *Ts, A], tuple[None, None, None]], +) -> None: + # technically, this should be ``typ[A | None | Union[*Ts]]`` + reveal_type( (*tt1,) ) # N: Revealed type is "builtins.tuple[Any, ...]" + reveal_type( [*tt1] ) # N: Revealed type is "builtins.list[Any]" + reveal_type( {*tt1} ) # N: Revealed type is "builtins.set[Any]" [builtins fixtures/primitives.pyi] @@ -701,9 +755,12 @@ class D: pass def f(a: Union[A, None], b: Union[B, None], c: Union[C, None]) -> None: pass def g(a: Union[A, None], b: Union[B, None], c: Union[C, None], d: Union[D, None] = None) -> None: pass -x: Union[tuple[A, B, C], tuple[None, None, None]] -f(*x) -g(*x) + +def test( + x: Union[tuple[A, B, C], tuple[None, None, None]], +) -> None: + f(*x) + g(*x) [builtins fixtures/tuple.pyi] @@ -715,21 +772,23 @@ class B: pass class C: pass class D: pass -def test_good_case() -> None: +def test_good_case( + x: Union[tuple[A, B, C], tuple[None, None, None]], + y: tuple[Union[A, None], Union[B, None], Union[C, None]], +) -> None: def f(a: Union[A, None], b: Union[B, None], c: Union[C, None]) -> None: pass def g(a: Union[A, None], b: Union[B, None], c: Union[C, None], d: Union[D, None] = None) -> None: pass - x: Union[tuple[A, B, C], tuple[None, None, None]] - y: tuple[Union[A, None], Union[B, None], Union[C, None]] f(*x) f(*y) g(*x) g(*y) -def test_bad_case() -> None: +def test_bad_case( + x: Union[tuple[A, A, A], tuple[None, None, None]], + y: tuple[Union[A, None], Union[A, None], Union[A, None]], +) -> None: def f(a: Union[A, None], b: Union[B, None], c: Union[C, None]) -> None: pass def g(a: Union[A, None], b: Union[B, None], c: Union[C, None], d: Union[D, None] = None) -> None: pass - x: Union[tuple[A, A, A], tuple[None, None, None]] - y: tuple[Union[A, None], Union[A, None], Union[A, None]] f(*x) # E: Argument 1 to "f" has incompatible type "*Union[tuple[A, A, A], tuple[None, None, None]]"; expected "Optional[B]" \ # E: Argument 1 to "f" has incompatible type "*Union[tuple[A, A, A], tuple[None, None, None]]"; expected "Optional[C]" f(*y) # E: Argument 1 to "f" has incompatible type "*tuple[Optional[A], Optional[A], Optional[A]]"; expected "Optional[B]" \ @@ -781,23 +840,23 @@ class B: pass class C: pass class D: pass -# 1. Good case: -def test_good_case() -> None: +def test_good_case( + x: Union[tuple[A, A, A], tuple[None, None]], + y: tuple[Union[A, None], ...], +) -> None: def f(a: Union[A, None], b: Union[A, None], c: Union[A, None] = None) -> None: pass def g(a: Union[A, None], b: Union[A, None], c: Union[A, None] = None, d: Union[A, None] = None) -> None: pass - x: Union[tuple[A, A, A], tuple[None, None]] - y: tuple[Union[A, None], ...] f(*x) f(*y) g(*x) g(*y) -# 2. Bad case -def test_bad_case() -> None: +def test_bad_case( + x: Union[tuple[A, B, C], tuple[None, None]], + y: tuple[Union[A, B, C, None], ...], +) -> None: def f(a: Union[A, None], b: Union[B, None], c: Union[C, None]) -> None: pass def g(a: Union[A, None], b: Union[A, None], c: Union[A, None] = None, d: Union[D, None] = None) -> None: pass - x: Union[tuple[A, B, C], tuple[None, None]] - y: tuple[Union[A, B, C, None], ...] # TODO: Show the coerced join type. f(*x) # E: Argument 1 to "f" has incompatible type "*Union[tuple[A, B, C], tuple[None, None]]"; expected "Optional[A]" \ # E: Argument 1 to "f" has incompatible type "*Union[tuple[A, B, C], tuple[None, None]]"; expected "Optional[B]" \ @@ -811,16 +870,6 @@ def test_bad_case() -> None: # E: Argument 1 to "g" has incompatible type "*tuple[Union[A, B, C, None], ...]"; expected "Optional[D]" [builtins fixtures/tuple.pyi] - -[case testListExpressionRecursiveType] -from typing import Union - -NESTED = Union[str, list[NESTED]] -x: Union[list[Union[NESTED, None]], list[NESTED]] -reveal_type([*x]) # N: Revealed type is "builtins.list[Union[builtins.str, builtins.list[Union[builtins.str, builtins.list[...]]], None]]" -[builtins fixtures/list.pyi] - - [case testPassingEmptyDictWithStars] def f(): pass def g(x=1): pass From 5e239a3a5f27e3b2972b4398155574cd9c85af7f Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 15 Aug 2025 10:12:57 +0200 Subject: [PATCH 04/12] always use the solver meachnism --- mypy/argmap.py | 164 +++++++++++++------------------ test-data/unit/check-kwargs.test | 13 ++- 2 files changed, 80 insertions(+), 97 deletions(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index f2ece5efa2ab..a8c4c3795f08 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -75,6 +75,7 @@ def map_actuals_to_formals( proper_types := [get_proper_type(t) for t in actualt.items] ) ): + # pick an arbitrary member actualt = proper_types[0] if isinstance(actualt, TupleType): # A tuple actual maps to a fixed number of formals. @@ -193,15 +194,6 @@ def __init__(self, context: ArgumentInferContext) -> None: # Type context for `*` and `**` arg kinds. self.context = context - def __eq__(self, other: object) -> bool: - if isinstance(other, ArgTypeExpander): - return ( - self.tuple_index == other.tuple_index - and self.kwargs_used == other.kwargs_used - and self.context == other.context - ) - return NotImplemented - def expand_actual_type( self, actual_type: Type, @@ -227,29 +219,8 @@ def expand_actual_type( # parse *args as one of the following: # IterableType | TupleType | ParamSpecType | AnyType star_args = self.parse_star_args_type(actual_type) - # star_args = actual_type - - # print(f"expand_actual_type: {actual_type=} {star_args=}") - - # if isinstance(star_args, TypeVarTupleType): - # # This code path is hit when *Ts is passed to a callable and various - # # special-handling didn't catch this. The best thing we can do is to use - # # the upper bound. - # star_args = get_proper_type(star_args.upper_bound) - # if isinstance(star_args, Instance) and star_args.args: - # from mypy.subtypes import is_subtype - # - # if is_subtype(star_args, self.context.iterable_type): - # return map_instance_to_supertype( - # star_args, self.context.iterable_type.type - # ).args[0] - # else: - # # We cannot properly unpack anything other - # # than `Iterable` type with `*`. - # # Just return `Any`, other parts of code would raise - # # a different error for improper use. - # return AnyType(TypeOfAny.from_error) - if self.is_iterable_type(star_args): + + if self.is_iterable_instance_type(star_args): return star_args.args[0] elif isinstance(star_args, TupleType): # Get the next tuple item of a tuple *arg. @@ -321,30 +292,75 @@ def is_iterable_instance_subtype(self, typ: Type) -> TypeGuard[Instance]: and is_subtype(p_t, self.context.iterable_type) ) - def is_iterable_type(self, typ: Type) -> TypeGuard[IterableType]: + def is_iterable_instance_type(self, typ: Type) -> TypeGuard[IterableType]: """Check if the type is an Iterable[T] or a subtype of it.""" p_t = get_proper_type(typ) return isinstance(p_t, Instance) and p_t.type == self.context.iterable_type.type + def _make_iterable_instance_type(self, arg: Type) -> IterableType: + value = Instance(self.context.iterable_type.type, [arg]) + return cast(IterableType, value) + + def _solve_as_iterable(self, typ: Type) -> IterableType | AnyType: + r"""Use the solver to cast a type as Iterable[T]. + + Returns `AnyType` if solving fails. + """ + from mypy.constraints import infer_constraints_for_callable + from mypy.nodes import ARG_POS + from mypy.solve import solve_constraints + + iterable_kind = self.context.iterable_type.type + + # We first create an upcast function: + # def [T] (Iterable[T]) -> Iterable[T]: ... + # and then solve for T, given the input type as the argument. + T = TypeVarType( + "T", + "T", + TypeVarId(-1), + values=[], + upper_bound=AnyType(TypeOfAny.special_form), + default=AnyType(TypeOfAny.special_form), + ) + target = Instance(iterable_kind, [T]) + + upcast_callable = CallableType( + variables=[T], + arg_types=[target], + arg_kinds=[ARG_POS], + arg_names=[None], + ret_type=T, + fallback=self.context.function_type, + ) + constraints = infer_constraints_for_callable( + upcast_callable, [typ], [ARG_POS], [None], [[0]], context=self.context + ) + + (sol,), _ = solve_constraints([T], constraints) + + if sol is None: # solving failed, return AnyType fallback + return AnyType(TypeOfAny.from_error) + return self._make_iterable_instance_type(sol) + def as_iterable_type(self, typ: Type) -> IterableType | AnyType: """Reinterpret a type as Iterable[T], or return AnyType if not possible.""" p_t = get_proper_type(typ) - if self.is_iterable_type(p_t): + if self.is_iterable_instance_type(p_t) or isinstance(p_t, AnyType): return p_t - elif self.is_iterable_instance_subtype(p_t): - cls = self.context.iterable_type.type - return cast(IterableType, map_instance_to_supertype(p_t, cls)) elif isinstance(p_t, UnionType): # If the type is a union, map each item to the iterable supertype. # the return the combined iterable type Iterable[A] | Iterable[B] -> Iterable[A | B] converted_types = [self.as_iterable_type(get_proper_type(item)) for item in p_t.items] - # if an item could not be interpreted as Iterable[T], we return AnyType - if all(self.is_iterable_type(it) for it in converted_types): + + if any(not self.is_iterable_instance_type(it) for it in converted_types): + # if any item could not be interpreted as Iterable[T], we return AnyType + return AnyType(TypeOfAny.from_error) + else: # all items are iterable, return Iterable[T₁ | T₂ | ... | Tₙ] iterable_types = cast(list[IterableType], converted_types) arg = make_simplified_union([it.args[0] for it in iterable_types]) - return self.make_iterable_type(arg) - return AnyType(TypeOfAny.from_error) + return self._make_iterable_instance_type(arg) elif isinstance(p_t, TupleType): # maps tuple[A, B, C] -> Iterable[A | B | C] # note: proper_elements may contain UnpackType, for instance with @@ -354,26 +370,24 @@ def as_iterable_type(self, typ: Type) -> IterableType | AnyType: for p_e in proper_elements: if isinstance(p_e, UnpackType): r = self.as_iterable_type(p_e) - if self.is_iterable_type(r): + if self.is_iterable_instance_type(r): args.append(r.args[0]) else: + # this *should* never happen args.append(r) else: args.append(p_e) - return self.make_iterable_type(make_simplified_union(args)) - if isinstance(p_t, UnpackType): + return self._make_iterable_instance_type(make_simplified_union(args)) + elif isinstance(p_t, UnpackType): return self.as_iterable_type(p_t.type) - if isinstance(p_t, (TypeVarType, TypeVarTupleType)): + elif isinstance(p_t, (TypeVarType, TypeVarTupleType)): return self.as_iterable_type(p_t.upper_bound) - # fallback: use the solver to reinterpret the type as Iterable[T] - if self.is_iterable(p_t): + elif self.is_iterable(p_t): + # TODO: add a 'fast path' (needs measurement) that uses the map_instance_to_supertype + # mechanism? (Only if it works: gh-19662) return self._solve_as_iterable(p_t) return AnyType(TypeOfAny.from_error) - def make_iterable_type(self, arg: Type) -> IterableType: - value = Instance(self.context.iterable_type.type, [arg]) - return cast(IterableType, value) - def parse_star_args_type( self, typ: Type ) -> TupleType | IterableType | ParamSpecType | AnyType: @@ -411,61 +425,19 @@ def parse_star_args_type( # Note that this covers unions of differently sized tuples as well. else: converted_types = [self.as_iterable_type(p_i) for p_i in proper_items] - if all(self.is_iterable_type(it) for it in converted_types): + if all(self.is_iterable_instance_type(it) for it in converted_types): # all items are iterable, return Iterable[T₁ | T₂ | ... | Tₙ] iterables = cast(list[IterableType], converted_types) arg = make_simplified_union([it.args[0] for it in iterables]) - return self.make_iterable_type(arg) + return self._make_iterable_instance_type(arg) else: # some items in the union are not iterable, return AnyType return AnyType(TypeOfAny.from_error) - elif self.is_iterable_type(parsed := self.as_iterable_type(p_t)): + elif self.is_iterable_instance_type(parsed := self.as_iterable_type(p_t)): # in all other cases, we try to reinterpret the type as Iterable[T] return parsed return AnyType(TypeOfAny.from_error) - def _solve_as_iterable(self, typ: Type) -> IterableType | AnyType: - r"""Use the solver to cast a type as Iterable[T]. - - Returns the type as-is if solving fails. - """ - from mypy.constraints import infer_constraints_for_callable - from mypy.nodes import ARG_POS - from mypy.solve import solve_constraints - - iterable_kind = self.context.iterable_type.type - - # We first create an upcast function: - # def [T] (Iterable[T]) -> Iterable[T]: ... - # and then solve for T, given the input type as the argument. - T = TypeVarType( - "T", - "T", - TypeVarId(-1), - values=[], - upper_bound=AnyType(TypeOfAny.special_form), - default=AnyType(TypeOfAny.special_form), - ) - target = Instance(iterable_kind, [T]) - - upcast_callable = CallableType( - variables=[T], - arg_types=[target], - arg_kinds=[ARG_POS], - arg_names=[None], - ret_type=T, - fallback=self.context.function_type, - ) - constraints = infer_constraints_for_callable( - upcast_callable, [typ], [ARG_POS], [None], [[0]], context=self.context - ) - - (sol,), _ = solve_constraints([T], constraints) - - if sol is None: # solving failed, return AnyType fallback - return AnyType(TypeOfAny.from_error) - return self.make_iterable_type(sol) - def is_equal_sized_tuples(types: Sequence[ProperType]) -> TypeGuard[Sequence[TupleType]]: """Check if all types are tuples of the same size. diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 5439d57a9c7c..1964299793dd 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -724,7 +724,6 @@ def test_union_variable_size_tuples( NESTED = Union[str, list[NESTED]] def test_union_recursive(x: Union[list[Union[NESTED, None]], list[NESTED]]) -> None: reveal_type( {*x} ) # N: Revealed type is "builtins.set[Union[builtins.str, builtins.list[Union[builtins.str, builtins.list[...]]], None]]" - [builtins fixtures/primitives.pyi] @@ -870,6 +869,18 @@ def test_bad_case( # E: Argument 1 to "g" has incompatible type "*tuple[Union[A, B, C, None], ...]"; expected "Optional[D]" [builtins fixtures/tuple.pyi] + +[case testListExpressionWithListSubtypeStarArgs] +# https://github.com/python/mypy/issues/19662 +class MyList(list[int]): ... + +def test(x: MyList, y: list[int]) -> None: + reveal_type( [*x] ) # N: Revealed type is "builtins.list[builtins.int]" + reveal_type( [*y] ) # N: Revealed type is "builtins.list[builtins.int]" +[builtins fixtures/list.pyi] + + + [case testPassingEmptyDictWithStars] def f(): pass def g(x=1): pass From 6cbb237ab06c6f5cf8538f567f9125b87c399234 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 15 Aug 2025 11:55:43 +0200 Subject: [PATCH 05/12] fixed infer_constraints --- mypy/argmap.py | 26 ++++--- mypy/checkexpr.py | 27 +++---- mypy/constraints.py | 19 ++--- mypy/infer.py | 1 + test-data/unit/check-kwargs.test | 120 ++++++++++++++++++++++++++++--- 5 files changed, 155 insertions(+), 38 deletions(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index a8c4c3795f08..7d8fe1c527f9 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -34,6 +34,8 @@ IterableType = NewType("IterableType", Instance) """Represents an instance of `Iterable[T]`.""" +TupleInstance = NewType("TupleInstance", Instance) +"""Represents an instance of `builtins.tuple[T, ...]`.""" def map_actuals_to_formals( @@ -218,18 +220,18 @@ def expand_actual_type( if actual_kind == nodes.ARG_STAR: # parse *args as one of the following: # IterableType | TupleType | ParamSpecType | AnyType - star_args = self.parse_star_args_type(actual_type) + star_args_type = self.parse_star_args_type(actual_type) - if self.is_iterable_instance_type(star_args): - return star_args.args[0] - elif isinstance(star_args, TupleType): + if self.is_iterable_instance_type(star_args_type): + return star_args_type.args[0] + elif isinstance(star_args_type, TupleType): # Get the next tuple item of a tuple *arg. - if self.tuple_index >= len(star_args.items): + if self.tuple_index >= len(star_args_type.items): # Exhausted a tuple -- continue to the next *args. self.tuple_index = 1 else: self.tuple_index += 1 - item = star_args.items[self.tuple_index - 1] + item = star_args_type.items[self.tuple_index - 1] if isinstance(item, UnpackType) and not allow_unpack: # An unpack item that doesn't have special handling, use upper bound as above. unpacked = get_proper_type(item.type) @@ -243,9 +245,9 @@ def expand_actual_type( ) item = fallback.args[0] return item - elif isinstance(star_args, ParamSpecType): + elif isinstance(star_args_type, ParamSpecType): # ParamSpec is valid in *args but it can't be unpacked. - return star_args + return star_args_type else: return AnyType(TypeOfAny.from_error) elif actual_kind == nodes.ARG_STAR2: @@ -301,6 +303,10 @@ def _make_iterable_instance_type(self, arg: Type) -> IterableType: value = Instance(self.context.iterable_type.type, [arg]) return cast(IterableType, value) + def _make_tuple_instance_type(self, arg: Type) -> TupleInstance: + value = Instance(self.context.tuple_type.type, [arg]) + return cast(TupleInstance, value) + def _solve_as_iterable(self, typ: Type) -> IterableType | AnyType: r"""Use the solver to cast a type as Iterable[T]. @@ -391,9 +397,9 @@ def as_iterable_type(self, typ: Type) -> IterableType | AnyType: def parse_star_args_type( self, typ: Type ) -> TupleType | IterableType | ParamSpecType | AnyType: - """Parse the type of a *args argument. + """Parse the type of a ``*args`` argument. - Returns one TupleType, IterableType, ParamSpecType or AnyType. + Returns one of TupleType, TupleInstance or AnyType. """ p_t = get_proper_type(typ) if isinstance(p_t, (TupleType, ParamSpecType, AnyType)): diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 293b3f4e4b12..e4ce3d80ee8a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2288,6 +2288,7 @@ def argument_infer_context(self) -> ArgumentInferContext: self.chk.named_type("typing.Mapping"), self.chk.named_type("typing.Iterable"), self.chk.named_type("builtins.function"), + self.chk.named_type("builtins.tuple"), ) return self._arg_infer_context_cache @@ -5251,33 +5252,35 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type: ctx = ctx_item.type else: ctx = None - tt = self.accept(item.expr, ctx) - tt = get_proper_type(tt) - # convert tt to one of TupleType, IterableType, AnyType or - mapper = ArgTypeExpander(self.argument_infer_context()) - tt = mapper.parse_star_args_type(tt) - - if isinstance(tt, TupleType): - if find_unpack_in_list(tt.items) is not None: + + arg_type_expander = ArgTypeExpander(self.argument_infer_context()) + original_arg_type = self.accept(item.expr, ctx) + # convert arg type to one of TupleType, IterableType, AnyType or + star_args_type = arg_type_expander.parse_star_args_type(original_arg_type) + + if isinstance(star_args_type, TupleType): + if find_unpack_in_list(star_args_type.items) is not None: if seen_unpack_in_items: # Multiple unpack items are not allowed in tuples, # fall back to instance type. return self.check_lst_expr(e, "builtins.tuple", "") else: seen_unpack_in_items = True - items.extend(tt.items) + items.extend(star_args_type.items) # Note: this logic depends on full structure match in tuple_context_matches(). if unpack_in_context: j += 1 else: # If there is an unpack in expressions, but not in context, this will # result in an error later, just do something predictable here. - j += len(tt.items) + j += len(star_args_type.items) else: if allow_precise_tuples and not seen_unpack_in_items: # Handle (x, *y, z), where y is e.g. tuple[Y, ...]. - if isinstance(tt, Instance) and self.chk.type_is_iterable(tt): - item_type = self.chk.iterable_item_type(tt, e) + if isinstance(star_args_type, Instance) and self.chk.type_is_iterable( + star_args_type + ): + item_type = self.chk.iterable_item_type(star_args_type, e) mapped = self.chk.named_generic_type("builtins.tuple", [item_type]) items.append(UnpackType(mapped)) seen_unpack_in_items = True diff --git a/mypy/constraints.py b/mypy/constraints.py index 6416791fa74a..812a12a8a9ee 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -167,15 +167,18 @@ def infer_constraints_for_callable( allow_unpack=True, ) - if arg_kinds[actual] != ARG_STAR or isinstance( - get_proper_type(actual_arg_type), TupleType - ): - actual_types.append(expanded_actual) + if arg_kinds[actual] == ARG_STAR: + # Use the expanded form, one of TupleType | IterableType | ParamSpecType | AnyType + star_args_type = mapper.parse_star_args_type(actual_arg_type) + if isinstance(star_args_type, TupleType): + actual_types.append(expanded_actual) + else: + # If we are expanding an iterable inside * actual, append a homogeneous item instead + actual_types.append( + UnpackType(tuple_instance.copy_modified(args=[expanded_actual])) + ) else: - # If we are expanding an iterable inside * actual, append a homogeneous item instead - actual_types.append( - UnpackType(tuple_instance.copy_modified(args=[expanded_actual])) - ) + actual_types.append(expanded_actual) if isinstance(unpacked_type, TypeVarTupleType): constraints.append( diff --git a/mypy/infer.py b/mypy/infer.py index 70bb126f3aae..9678631fdbfb 100644 --- a/mypy/infer.py +++ b/mypy/infer.py @@ -29,6 +29,7 @@ class ArgumentInferContext(NamedTuple): mapping_type: Instance iterable_type: Instance function_type: Instance + tuple_type: Instance def infer_function_type_arguments( diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 1964299793dd..af4c42fcd016 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -498,7 +498,7 @@ f(**m) g(**m) # E: Argument 1 to "g" has incompatible type "**Mapping[str, object]"; expected "int" [builtins fixtures/dict.pyi] -[case testTupleExpressionWithUnionStarArgs] +[case testTupleConversionWithUnionStarArgs] from typing import Union, List, Set, Tuple, Unpack, TypeVarTuple class A: pass @@ -506,6 +506,84 @@ class B: pass class C: pass class D: pass +Ts = TypeVarTuple('Ts') +def as_tuple(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: + return (*args,) + +def test_union_same_size_tuple( + x1: Union[tuple[A, B], tuple[None, None]], + x2: Union[tuple[A, B], tuple[None, Unpack[tuple[None]]]], + x3: Union[tuple[A, B], tuple[None, None, Unpack[tuple[()]]]], + xx: tuple[Union[A, None], Union[B, None]], # reference type +) -> None: + reveal_type( as_tuple(*x1) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" + reveal_type( as_tuple(*x2) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" + reveal_type( as_tuple(*x3) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" + reveal_type( as_tuple(*xx) ) # N: Revealed type is "tuple[Union[__main__.A, None], Union[__main__.B, None]]" + +def test_union_different_size_tuple( + y1: Union[tuple[A, B], tuple[None, None, None]], + y2: Union[tuple[A, B], tuple[None]], + y3: Union[tuple[A, B], tuple[None, None, None, Unpack[tuple[()]]]], + yy: tuple[Union[A, B, None], ...], # reference type +) -> None: + reveal_type( as_tuple(*y1) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" + reveal_type( as_tuple(*y2) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" + reveal_type( as_tuple(*y2) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" + reveal_type( as_tuple(*yy) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, None], ...]" + +def test_union_fixed_size_and_variadic_tuple( + z1: Union[tuple[A, B, C], tuple[None, ...]], + z2: Union[tuple[A, B, C], tuple[None, Unpack[tuple[None, ...]], None]], + zz: tuple[Union[A, B, C, None], ...], # reference type +) -> None: + reveal_type( as_tuple(*z1) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, __main__.C, None], ...]" + reveal_type( as_tuple(*z2) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, __main__.C, None], ...]" + reveal_type( as_tuple(*zz) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B, __main__.C, None], ...]" + +def test_union_variable_size_tuples( + # same + subtype + tt1: Union[Tuple[Union[A, None], ...], Tuple[A, ...]], + ll1: Union[List[Union[A, None]], List[A]], + ss1: Union[Set[Union[A, None]], Set[A]], + # mixed + subtype + tl1: Union[Tuple[Union[A, None], ...], List[A]], + ts1: Union[Tuple[Union[A, None], ...], Set[A]], + ls1: Union[List[Union[A, None]], Set[A]], + # same + not subtype + tt2: Union[Tuple[A, ...], Tuple[B, ...]], + ll2: Union[List[A], List[B]], + ss2: Union[Set[A], Set[B]], + # mixed + not subtype + tl2: Union[Tuple[A, ...], List[B]], + ts2: Union[Tuple[A, ...], Set[B]], + ls2: Union[List[A], Set[B]], +) -> None: + reveal_type( as_tuple(*tt1) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( as_tuple(*ll1) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( as_tuple(*ss1) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( as_tuple(*tl1) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( as_tuple(*ts1) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( as_tuple(*ls1) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, None], ...]" + reveal_type( as_tuple(*tt2) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( as_tuple(*ll2) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( as_tuple(*ss2) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( as_tuple(*tl2) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( as_tuple(*ts2) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + reveal_type( as_tuple(*ls2) ) # N: Revealed type is "builtins.tuple[Union[__main__.A, __main__.B], ...]" + +NESTED = Union[str, list[NESTED]] +def test_union_recursive(x: Union[list[Union[NESTED, None]], list[NESTED]]) -> None: + reveal_type( as_tuple(*x) ) # N: Revealed type is "builtins.tuple[Union[builtins.str, builtins.list[Union[builtins.str, builtins.list[...]]], None], ...]" +[builtins fixtures/primitives.pyi] + +[case testTupleExpressionWithUnionStarArgs] +from typing import Union, List, Set, Tuple, Unpack + +class A: pass +class B: pass +class C: pass +class D: pass def test_union_same_size_tuple( x1: Union[tuple[A, B], tuple[None, None]], @@ -576,7 +654,7 @@ def test_union_recursive(x: Union[list[Union[NESTED, None]], list[NESTED]]) -> N [case testListExpressionWithUnionStarArgs] -from typing import Union, List, Set, Tuple, Unpack, TypeVarTuple +from typing import Union, List, Set, Tuple, Unpack class A: pass class B: pass @@ -652,7 +730,7 @@ def test_union_recursive(x: Union[list[Union[NESTED, None]], list[NESTED]]) -> N [case testSetExpressionWithUnionStarArgs] -from typing import Union, List, Set, Tuple, Unpack, TypeVarTuple +from typing import Union, List, Set, Tuple, Unpack class A: pass class B: pass @@ -727,7 +805,7 @@ def test_union_recursive(x: Union[list[Union[NESTED, None]], list[NESTED]]) -> N [builtins fixtures/primitives.pyi] -[case testStarArgsWithUnionTypeVarTuple-xfail] +[case testStarArgsWithPaddedTypeVarTuple] # https://github.com/python/mypy/issues/19659 from typing import Union, TypeVarTuple @@ -735,14 +813,40 @@ class A: pass Ts = TypeVarTuple('Ts') def test_union_typevartuple( - tt1: Union[tuple[A, *Ts, A], tuple[None, None, None]], + padded_tuple: tuple[A, *Ts, A], + padded_union: Union[tuple[A, *Ts, A], tuple[None, None, None]], ) -> None: # technically, this should be ``typ[A | None | Union[*Ts]]`` - reveal_type( (*tt1,) ) # N: Revealed type is "builtins.tuple[Any, ...]" - reveal_type( [*tt1] ) # N: Revealed type is "builtins.list[Any]" - reveal_type( {*tt1} ) # N: Revealed type is "builtins.set[Any]" + reveal_type( (*padded_tuple,) ) # N: Revealed type is "tuple[__main__.A, Unpack[Ts`-1], __main__.A]" + reveal_type( [*padded_tuple] ) # N: Revealed type is "builtins.list[builtins.object]" + reveal_type( {*padded_tuple} ) # N: Revealed type is "builtins.set[builtins.object]" + reveal_type( (*padded_union,) ) # N: Revealed type is "builtins.tuple[builtins.object, ...]" + reveal_type( [*padded_union] ) # N: Revealed type is "builtins.list[builtins.object]" + reveal_type( {*padded_union} ) # N: Revealed type is "builtins.set[builtins.object]" [builtins fixtures/primitives.pyi] +[case testStarArgsWithParamSpec] +# https://github.com/python/mypy/issues/19663 +from typing import Union, ParamSpec, Callable + +P = ParamSpec('P') + +def test_paramspec( + a: A, # E: Name "A" is not defined + dummy: Callable[P, None], # ensure P is bound + /, + *args: P.args, + **kwargs: P.kwargs, +) -> None: + # technically, this should be ``typ[A | None | Union[*Ts]]`` + reveal_type( args ) # N: Revealed type is "P.args`-1" + reveal_type( (*args,) ) # N: Revealed type is "builtins.tuple[P.args`-1, ...]" + reveal_type( [*args] ) # N: Revealed type is "builtins.list[P.args`-1]" + reveal_type( {*args} ) # N: Revealed type is "builtins.set[P.args`-1]" + + # test padded tuple expression + reveal_type( (a, *args, a) ) # N: Revealed type is "builtins.tuple[__main__.A, Unpack[builtins.tuple[P.args`-1, ...]], __main__.A]" +[builtins fixtures/primitives.pyi] [case testStarArgsWithUnion] from typing import Union From 115bf00a02582f0e85d71d046774597ff3d702e3 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 15 Aug 2025 14:44:09 +0200 Subject: [PATCH 06/12] cleanup --- mypy/argmap.py | 16 ------- mypy/checkexpr.py | 5 +-- mypy/constraints.py | 16 +++---- mypy/infer.py | 1 - test-data/unit/check-kwargs.test | 62 +++++++++++++++------------- test-data/unit/lib-stub/builtins.pyi | 8 +++- 6 files changed, 50 insertions(+), 58 deletions(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index 7d8fe1c527f9..89f33e684bee 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -34,8 +34,6 @@ IterableType = NewType("IterableType", Instance) """Represents an instance of `Iterable[T]`.""" -TupleInstance = NewType("TupleInstance", Instance) -"""Represents an instance of `builtins.tuple[T, ...]`.""" def map_actuals_to_formals( @@ -284,16 +282,6 @@ def is_iterable(self, typ: Type) -> bool: return is_subtype(typ, self.context.iterable_type) - def is_iterable_instance_subtype(self, typ: Type) -> TypeGuard[Instance]: - from mypy.subtypes import is_subtype - - p_t = get_proper_type(typ) - return ( - isinstance(p_t, Instance) - and bool(p_t.args) - and is_subtype(p_t, self.context.iterable_type) - ) - def is_iterable_instance_type(self, typ: Type) -> TypeGuard[IterableType]: """Check if the type is an Iterable[T] or a subtype of it.""" p_t = get_proper_type(typ) @@ -303,10 +291,6 @@ def _make_iterable_instance_type(self, arg: Type) -> IterableType: value = Instance(self.context.iterable_type.type, [arg]) return cast(IterableType, value) - def _make_tuple_instance_type(self, arg: Type) -> TupleInstance: - value = Instance(self.context.tuple_type.type, [arg]) - return cast(TupleInstance, value) - def _solve_as_iterable(self, typ: Type) -> IterableType | AnyType: r"""Use the solver to cast a type as Iterable[T]. diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e4ce3d80ee8a..6a035b07c19e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2288,7 +2288,6 @@ def argument_infer_context(self) -> ArgumentInferContext: self.chk.named_type("typing.Mapping"), self.chk.named_type("typing.Iterable"), self.chk.named_type("builtins.function"), - self.chk.named_type("builtins.tuple"), ) return self._arg_infer_context_cache @@ -5252,12 +5251,10 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type: ctx = ctx_item.type else: ctx = None - - arg_type_expander = ArgTypeExpander(self.argument_infer_context()) original_arg_type = self.accept(item.expr, ctx) # convert arg type to one of TupleType, IterableType, AnyType or + arg_type_expander = ArgTypeExpander(self.argument_infer_context()) star_args_type = arg_type_expander.parse_star_args_type(original_arg_type) - if isinstance(star_args_type, TupleType): if find_unpack_in_list(star_args_type.items) is not None: if seen_unpack_in_items: diff --git a/mypy/constraints.py b/mypy/constraints.py index 812a12a8a9ee..28dc6fbb4661 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1014,13 +1014,15 @@ def visit_instance(self, template: Instance) -> list[Constraint]: if isinstance(item, UnpackType): unpacked = get_proper_type(item.type) if isinstance(unpacked, TypeVarTupleType): - # Cannot infer anything for T from [T, ...] <: *Ts - continue - assert ( - isinstance(unpacked, Instance) - and unpacked.type.fullname == "builtins.tuple" - ) - item = unpacked.args[0] + # Iterable[T] :> tuple[*Ts] => T :> Union[*Ts] + # Since Union[*Ts] is currently not available, use Any instead. + item = AnyType(TypeOfAny.from_omitted_generics) + else: + assert ( + isinstance(unpacked, Instance) + and unpacked.type.fullname == "builtins.tuple" + ) + item = unpacked.args[0] cb = infer_constraints(template.args[0], item, SUPERTYPE_OF) res.extend(cb) return res diff --git a/mypy/infer.py b/mypy/infer.py index 9678631fdbfb..70bb126f3aae 100644 --- a/mypy/infer.py +++ b/mypy/infer.py @@ -29,7 +29,6 @@ class ArgumentInferContext(NamedTuple): mapping_type: Instance iterable_type: Instance function_type: Instance - tuple_type: Instance def infer_function_type_arguments( diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index af4c42fcd016..1ff8b38bc412 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -498,6 +498,20 @@ f(**m) g(**m) # E: Argument 1 to "g" has incompatible type "**Mapping[str, object]"; expected "int" [builtins fixtures/dict.pyi] +[case testStarArgsTupleExpressionVersusTypeVarTuple] +# flags: --enable-incomplete-feature=PreciseTupleTypes +# https://github.com/python/mypy/issues/19665 +from typing import Unpack, TypeVarTuple + +Ts = TypeVarTuple('Ts') + +def as_tuple(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: return args + +def test(one: int, many: tuple[int, ...]) -> None: + reveal_type( (one, *many, one) ) # N: Revealed type is "tuple[builtins.int, Unpack[builtins.tuple[builtins.int, ...]], builtins.int]" + reveal_type( as_tuple(one, *many, one) ) # N: Revealed type is "tuple[builtins.int, Unpack[builtins.tuple[builtins.int, ...]], builtins.int]" +[builtins fixtures/tuple.pyi] + [case testTupleConversionWithUnionStarArgs] from typing import Union, List, Set, Tuple, Unpack, TypeVarTuple @@ -805,47 +819,42 @@ def test_union_recursive(x: Union[list[Union[NESTED, None]], list[NESTED]]) -> N [builtins fixtures/primitives.pyi] -[case testStarArgsWithPaddedTypeVarTuple] -# https://github.com/python/mypy/issues/19659 -from typing import Union, TypeVarTuple +[case testStarArgsWithPaddedTupleArgument] +from typing import Union, TypeVarTuple, Unpack class A: pass Ts = TypeVarTuple('Ts') -def test_union_typevartuple( - padded_tuple: tuple[A, *Ts, A], - padded_union: Union[tuple[A, *Ts, A], tuple[None, None, None]], -) -> None: +def test_padded_tuple(padded_tuple: tuple[A, Unpack[Ts], A]) -> None: # technically, this should be ``typ[A | None | Union[*Ts]]`` reveal_type( (*padded_tuple,) ) # N: Revealed type is "tuple[__main__.A, Unpack[Ts`-1], __main__.A]" reveal_type( [*padded_tuple] ) # N: Revealed type is "builtins.list[builtins.object]" reveal_type( {*padded_tuple} ) # N: Revealed type is "builtins.set[builtins.object]" + +def test_padded_union(padded_union: Union[tuple[A, Unpack[Ts], A], tuple[None, None, None]]) -> None: reveal_type( (*padded_union,) ) # N: Revealed type is "builtins.tuple[builtins.object, ...]" reveal_type( [*padded_union] ) # N: Revealed type is "builtins.list[builtins.object]" reveal_type( {*padded_union} ) # N: Revealed type is "builtins.set[builtins.object]" [builtins fixtures/primitives.pyi] -[case testStarArgsWithParamSpec] -# https://github.com/python/mypy/issues/19663 -from typing import Union, ParamSpec, Callable -P = ParamSpec('P') +[case testStarArgsWithPaddedTupleArgumentUpcast] +# https://github.com/python/mypy/issues/19659 +from typing import Iterable, TypeVarTuple, Unpack, Union, TypeVar -def test_paramspec( - a: A, # E: Name "A" is not defined - dummy: Callable[P, None], # ensure P is bound - /, - *args: P.args, - **kwargs: P.kwargs, -) -> None: - # technically, this should be ``typ[A | None | Union[*Ts]]`` - reveal_type( args ) # N: Revealed type is "P.args`-1" - reveal_type( (*args,) ) # N: Revealed type is "builtins.tuple[P.args`-1, ...]" - reveal_type( [*args] ) # N: Revealed type is "builtins.list[P.args`-1]" - reveal_type( {*args} ) # N: Revealed type is "builtins.set[P.args`-1]" +class A: pass +Ts = TypeVarTuple('Ts') +T = TypeVar('T') + +def upcast(arg: Iterable[T]) -> Iterable[T]: return arg - # test padded tuple expression - reveal_type( (a, *args, a) ) # N: Revealed type is "builtins.tuple[__main__.A, Unpack[builtins.tuple[P.args`-1, ...]], __main__.A]" +def test_padded_tuple(padded_tuple: tuple[A, Unpack[Ts], A]) -> None: + as_iterable = upcast(padded_tuple) + reveal_type(as_iterable) # N: Revealed type is "typing.Iterable[Any]" + +def test_padded_union(padded_union: Union[tuple[A, Unpack[Ts], A], tuple[None, None, None]]) -> None: + as_iterable = upcast(padded_union) + reveal_type(as_iterable) # N: Revealed type is "typing.Iterable[Any]" [builtins fixtures/primitives.pyi] [case testStarArgsWithUnion] @@ -973,7 +982,6 @@ def test_bad_case( # E: Argument 1 to "g" has incompatible type "*tuple[Union[A, B, C, None], ...]"; expected "Optional[D]" [builtins fixtures/tuple.pyi] - [case testListExpressionWithListSubtypeStarArgs] # https://github.com/python/mypy/issues/19662 class MyList(list[int]): ... @@ -983,8 +991,6 @@ def test(x: MyList, y: list[int]) -> None: reveal_type( [*y] ) # N: Revealed type is "builtins.list[builtins.int]" [builtins fixtures/list.pyi] - - [case testPassingEmptyDictWithStars] def f(): pass def g(x=1): pass diff --git a/test-data/unit/lib-stub/builtins.pyi b/test-data/unit/lib-stub/builtins.pyi index 17d519cc8eea..f2955d96c480 100644 --- a/test-data/unit/lib-stub/builtins.pyi +++ b/test-data/unit/lib-stub/builtins.pyi @@ -23,13 +23,17 @@ class function: __name__: str class ellipsis: pass -from typing import Generic, Iterator, Sequence, TypeVar +from typing import Generic, Iterator, Sequence, TypeVar, Iterable _T = TypeVar('_T') class list(Generic[_T], Sequence[_T]): def __contains__(self, item: object) -> bool: pass def __getitem__(self, key: int) -> _T: pass def __iter__(self) -> Iterator[_T]: pass -class dict: pass +# class tuple(Generic[_T], Sequence[_T]): +# def __contains__(self, item: object) -> bool: pass +# def __getitem__(self, key: int) -> _T: pass +# def __iter__(self) -> Iterator[_T]: pass +class dict: pass # Definition of None is implicit From fac1e07267c98421fbcc8978d2031c0ff59001ae Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 15 Aug 2025 17:41:04 +0200 Subject: [PATCH 07/12] address review --- mypy/argmap.py | 9 ++++++--- test-data/unit/lib-stub/builtins.pyi | 8 ++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index 89f33e684bee..acedcfd24468 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -283,7 +283,7 @@ def is_iterable(self, typ: Type) -> bool: return is_subtype(typ, self.context.iterable_type) def is_iterable_instance_type(self, typ: Type) -> TypeGuard[IterableType]: - """Check if the type is an Iterable[T] or a subtype of it.""" + """Check if the type is an Iterable[T].""" p_t = get_proper_type(typ) return isinstance(p_t, Instance) and p_t.type == self.context.iterable_type.type @@ -363,7 +363,10 @@ def as_iterable_type(self, typ: Type) -> IterableType | AnyType: if self.is_iterable_instance_type(r): args.append(r.args[0]) else: - # this *should* never happen + # this *should* never happen, since UnpackType should + # only contain TypeVarTuple or a variable length tuple. + # However, we could get an `AnyType(TypeOfAny.from_error)` + # if for some reason the solver was triggered and failed. args.append(r) else: args.append(p_e) @@ -416,7 +419,7 @@ def parse_star_args_type( else: converted_types = [self.as_iterable_type(p_i) for p_i in proper_items] if all(self.is_iterable_instance_type(it) for it in converted_types): - # all items are iterable, return Iterable[T₁ | T₂ | ... | Tₙ] + # all items are iterable, return Iterable[T1 | T2 | ... | Tn] iterables = cast(list[IterableType], converted_types) arg = make_simplified_union([it.args[0] for it in iterables]) return self._make_iterable_instance_type(arg) diff --git a/test-data/unit/lib-stub/builtins.pyi b/test-data/unit/lib-stub/builtins.pyi index f2955d96c480..17d519cc8eea 100644 --- a/test-data/unit/lib-stub/builtins.pyi +++ b/test-data/unit/lib-stub/builtins.pyi @@ -23,17 +23,13 @@ class function: __name__: str class ellipsis: pass -from typing import Generic, Iterator, Sequence, TypeVar, Iterable +from typing import Generic, Iterator, Sequence, TypeVar _T = TypeVar('_T') class list(Generic[_T], Sequence[_T]): def __contains__(self, item: object) -> bool: pass def __getitem__(self, key: int) -> _T: pass def __iter__(self) -> Iterator[_T]: pass -# class tuple(Generic[_T], Sequence[_T]): -# def __contains__(self, item: object) -> bool: pass -# def __getitem__(self, key: int) -> _T: pass -# def __iter__(self) -> Iterator[_T]: pass - class dict: pass + # Definition of None is implicit From e8dcf880b18b98facdeb6dd330930743b82ec1e5 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 15 Aug 2025 18:46:57 +0200 Subject: [PATCH 08/12] check paramspec flavor --- mypy/argmap.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index acedcfd24468..79773ab07acc 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -13,6 +13,7 @@ AnyType, CallableType, Instance, + ParamSpecFlavor, ParamSpecType, ProperType, TupleType, @@ -245,6 +246,9 @@ def expand_actual_type( return item elif isinstance(star_args_type, ParamSpecType): # ParamSpec is valid in *args but it can't be unpacked. + assert ( + star_args_type.flavor == ParamSpecFlavor.ARGS + ), f"ParamSpecType for *args should have ARGS flavor, got {star_args_type.flavor}" return star_args_type else: return AnyType(TypeOfAny.from_error) @@ -386,12 +390,17 @@ def parse_star_args_type( ) -> TupleType | IterableType | ParamSpecType | AnyType: """Parse the type of a ``*args`` argument. - Returns one of TupleType, TupleInstance or AnyType. + Returns one of TupleType, IterableType, ParamSpecType (ARGS flavor), + or AnyType(TypeOfAny.from_error) if the type cannot be parsed or is invalid. """ p_t = get_proper_type(typ) - if isinstance(p_t, (TupleType, ParamSpecType, AnyType)): + if isinstance(p_t, (TupleType, AnyType)): # just return the type as-is return p_t + elif isinstance(p_t, ParamSpecType): + if p_t.flavor == ParamSpecFlavor.ARGS: + return p_t + return AnyType(TypeOfAny.from_error) elif isinstance(p_t, TypeVarTupleType): return self.parse_star_args_type(p_t.upper_bound) elif isinstance(p_t, UnionType): From fe4289a8f5039b58339685622608b9b7ed9708f3 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 15 Aug 2025 19:24:34 +0200 Subject: [PATCH 09/12] Revert "check paramspec flavor" This reverts commit e8dcf880b18b98facdeb6dd330930743b82ec1e5. --- mypy/argmap.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index 79773ab07acc..acedcfd24468 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -13,7 +13,6 @@ AnyType, CallableType, Instance, - ParamSpecFlavor, ParamSpecType, ProperType, TupleType, @@ -246,9 +245,6 @@ def expand_actual_type( return item elif isinstance(star_args_type, ParamSpecType): # ParamSpec is valid in *args but it can't be unpacked. - assert ( - star_args_type.flavor == ParamSpecFlavor.ARGS - ), f"ParamSpecType for *args should have ARGS flavor, got {star_args_type.flavor}" return star_args_type else: return AnyType(TypeOfAny.from_error) @@ -390,17 +386,12 @@ def parse_star_args_type( ) -> TupleType | IterableType | ParamSpecType | AnyType: """Parse the type of a ``*args`` argument. - Returns one of TupleType, IterableType, ParamSpecType (ARGS flavor), - or AnyType(TypeOfAny.from_error) if the type cannot be parsed or is invalid. + Returns one of TupleType, TupleInstance or AnyType. """ p_t = get_proper_type(typ) - if isinstance(p_t, (TupleType, AnyType)): + if isinstance(p_t, (TupleType, ParamSpecType, AnyType)): # just return the type as-is return p_t - elif isinstance(p_t, ParamSpecType): - if p_t.flavor == ParamSpecFlavor.ARGS: - return p_t - return AnyType(TypeOfAny.from_error) elif isinstance(p_t, TypeVarTupleType): return self.parse_star_args_type(p_t.upper_bound) elif isinstance(p_t, UnionType): From 0139c12a1b366edd708856897a4a036987e5573c Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 15 Aug 2025 19:25:24 +0200 Subject: [PATCH 10/12] update parse_star_args_type docstring --- mypy/argmap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index acedcfd24468..d0e3665d145b 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -386,7 +386,8 @@ def parse_star_args_type( ) -> TupleType | IterableType | ParamSpecType | AnyType: """Parse the type of a ``*args`` argument. - Returns one of TupleType, TupleInstance or AnyType. + Returns one of TupleType, IterableType, ParamSpecType (ARGS flavor), + or AnyType(TypeOfAny.from_error) if the type cannot be parsed or is invalid. """ p_t = get_proper_type(typ) if isinstance(p_t, (TupleType, ParamSpecType, AnyType)): From 109b4f3b1d7aab22709337ec258805821dc86bb6 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 15 Aug 2025 19:25:49 +0200 Subject: [PATCH 11/12] update parse_star_args_type docstring --- mypy/argmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index d0e3665d145b..01cfc61fdaa2 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -386,7 +386,7 @@ def parse_star_args_type( ) -> TupleType | IterableType | ParamSpecType | AnyType: """Parse the type of a ``*args`` argument. - Returns one of TupleType, IterableType, ParamSpecType (ARGS flavor), + Returns one of TupleType, IterableType, ParamSpecType, or AnyType(TypeOfAny.from_error) if the type cannot be parsed or is invalid. """ p_t = get_proper_type(typ) From e169d3e33ac75a463c257aaf32d00ac0e0ee0cc8 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Sat, 16 Aug 2025 21:26:22 +0200 Subject: [PATCH 12/12] address review --- mypy/argmap.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/mypy/argmap.py b/mypy/argmap.py index 01cfc61fdaa2..7a016a45a529 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -4,7 +4,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Callable, cast -from typing_extensions import NewType, TypeGuard +from typing_extensions import NewType, TypeGuard, TypeIs from mypy import nodes from mypy.maptype import map_instance_to_supertype @@ -278,11 +278,12 @@ def expand_actual_type( return original_actual def is_iterable(self, typ: Type) -> bool: + """Check if the type is an iterable, i.e. implements the Iterable Protocol.""" from mypy.subtypes import is_subtype return is_subtype(typ, self.context.iterable_type) - def is_iterable_instance_type(self, typ: Type) -> TypeGuard[IterableType]: + def is_iterable_instance_type(self, typ: Type) -> TypeIs[IterableType]: """Check if the type is an Iterable[T].""" p_t = get_proper_type(typ) return isinstance(p_t, Instance) and p_t.type == self.context.iterable_type.type @@ -300,8 +301,6 @@ def _solve_as_iterable(self, typ: Type) -> IterableType | AnyType: from mypy.nodes import ARG_POS from mypy.solve import solve_constraints - iterable_kind = self.context.iterable_type.type - # We first create an upcast function: # def [T] (Iterable[T]) -> Iterable[T]: ... # and then solve for T, given the input type as the argument. @@ -310,21 +309,20 @@ def _solve_as_iterable(self, typ: Type) -> IterableType | AnyType: "T", TypeVarId(-1), values=[], - upper_bound=AnyType(TypeOfAny.special_form), - default=AnyType(TypeOfAny.special_form), + upper_bound=AnyType(TypeOfAny.from_omitted_generics), + default=AnyType(TypeOfAny.from_omitted_generics), ) - target = Instance(iterable_kind, [T]) - + target = self._make_iterable_instance_type(T) upcast_callable = CallableType( variables=[T], arg_types=[target], arg_kinds=[ARG_POS], arg_names=[None], - ret_type=T, + ret_type=target, fallback=self.context.function_type, ) constraints = infer_constraints_for_callable( - upcast_callable, [typ], [ARG_POS], [None], [[0]], context=self.context + upcast_callable, [typ], [ARG_POS], [None], [[0]], self.context ) (sol,), _ = solve_constraints([T], constraints) @@ -334,7 +332,11 @@ def _solve_as_iterable(self, typ: Type) -> IterableType | AnyType: return self._make_iterable_instance_type(sol) def as_iterable_type(self, typ: Type) -> IterableType | AnyType: - """Reinterpret a type as Iterable[T], or return AnyType if not possible.""" + """Reinterpret a type as Iterable[T], or return AnyType if not possible. + + This function specially handles certain types like UnionType, TupleType, and UnpackType. + Otherwise, the upcasting is performed using the solver. + """ p_t = get_proper_type(typ) if self.is_iterable_instance_type(p_t) or isinstance(p_t, AnyType): return p_t @@ -386,8 +388,8 @@ def parse_star_args_type( ) -> TupleType | IterableType | ParamSpecType | AnyType: """Parse the type of a ``*args`` argument. - Returns one of TupleType, IterableType, ParamSpecType, - or AnyType(TypeOfAny.from_error) if the type cannot be parsed or is invalid. + Returns one of TupleType, IterableType, ParamSpecType or AnyType. + Returns AnyType(TypeOfAny.from_error) if the type cannot be parsed or is invalid. """ p_t = get_proper_type(typ) if isinstance(p_t, (TupleType, ParamSpecType, AnyType)):