From 351371d20c0b9e014528238761a6eeedf8dfb926 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 24 Aug 2023 20:10:47 +0100 Subject: [PATCH] Fix type arguments validation for variadic instances (#15944) Fixes https://github.com/python/mypy/issues/15410 Fixes https://github.com/python/mypy/issues/15411 --- mypy/expandtype.py | 8 +-- mypy/semanal_typeargs.py | 23 +++++++- mypy/test/testtypes.py | 2 +- mypy/typeanal.py | 8 ++- mypy/types.py | 1 + test-data/unit/check-typevar-tuple.test | 78 +++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 8 deletions(-) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index e71f6429d9c0..dc3dae670c1f 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -409,10 +409,10 @@ def visit_tuple_type(self, t: TupleType) -> Type: # Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...] item = items[0] if isinstance(item, UnpackType): - assert isinstance(item.type, ProperType) - if isinstance(item.type, Instance): - assert item.type.type.fullname == "builtins.tuple" - return item.type + unpacked = get_proper_type(item.type) + if isinstance(unpacked, Instance): + assert unpacked.type.fullname == "builtins.tuple" + return unpacked fallback = t.partial_fallback.accept(self) assert isinstance(fallback, ProperType) and isinstance(fallback, Instance) return t.copy_modified(items=items, fallback=fallback) diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index 8d8ef66b5c69..1a37ac57be30 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -18,7 +18,7 @@ from mypy.options import Options from mypy.scope import Scope from mypy.subtypes import is_same_type, is_subtype -from mypy.typeanal import set_any_tvars +from mypy.typeanal import fix_type_var_tuple_argument, set_any_tvars from mypy.types import ( AnyType, CallableType, @@ -143,7 +143,26 @@ def visit_instance(self, t: Instance) -> None: if isinstance(info, FakeInfo): return # https://github.com/python/mypy/issues/11079 t.args = tuple(flatten_nested_tuples(t.args)) - # TODO: fix #15410 and #15411. + if t.type.has_type_var_tuple_type: + # Regular Instances are already validated in typeanal.py. + # TODO: do something with partial overlap (probably just reject). + # also in other places where split_with_prefix_and_suffix() is used. + correct = len(t.args) >= len(t.type.type_vars) - 1 + if any( + isinstance(a, UnpackType) and isinstance(get_proper_type(a.type), Instance) + for a in t.args + ): + correct = True + if not correct: + exp_len = f"at least {len(t.type.type_vars) - 1}" + self.fail( + f"Bad number of arguments, expected: {exp_len}, given: {len(t.args)}", + t, + code=codes.TYPE_ARG, + ) + any_type = AnyType(TypeOfAny.from_error) + t.args = (any_type,) * len(t.type.type_vars) + fix_type_var_tuple_argument(any_type, t) self.validate_args(info.name, t.args, info.defn.type_vars, t) super().visit_instance(t) diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 56ac86058ce4..12e7b207b00a 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -1464,7 +1464,7 @@ def make_call(*items: tuple[str, str | None]) -> CallExpr: class TestExpandTypeLimitGetProperType(TestCase): # WARNING: do not increase this number unless absolutely necessary, # and you understand what you are doing. - ALLOWED_GET_PROPER_TYPES = 6 + ALLOWED_GET_PROPER_TYPES = 7 @skipUnless(mypy.expandtype.__file__.endswith(".py"), "Skip for compiled mypy") def test_count_get_proper_type(self) -> None: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 14b37539afea..806b9967039e 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1795,6 +1795,13 @@ def fix_instance( fix_type_var_tuple_argument(any_type, t) return + + if t.type.has_type_var_tuple_type: + # This can be only correctly analyzed when all arguments are fully + # analyzed, because there may be a variadic item among them, so we + # do this in semanal_typeargs.py. + return + # Invalid number of type parameters. fail( wrong_type_arg_count(len(t.type.type_vars), str(len(t.args)), t.type.name), @@ -1805,7 +1812,6 @@ def fix_instance( # otherwise the type checker may crash as it expects # things to be right. t.args = tuple(AnyType(TypeOfAny.from_error) for _ in t.type.type_vars) - fix_type_var_tuple_argument(AnyType(TypeOfAny.from_error), t) t.invalid = True diff --git a/mypy/types.py b/mypy/types.py index c71412f4ea58..214978eab774 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -322,6 +322,7 @@ def _expand_once(self) -> Type: assert isinstance(self.alias.target, Instance) # type: ignore[misc] return self.alias.target.copy_modified(args=self.args) + # TODO: this logic duplicates the one in expand_type_by_instance(). if self.alias.tvar_tuple_index is None: mapping = {v.id: s for (v, s) in zip(self.alias.alias_tvars, self.args)} else: diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 58fc1265ae99..ee81597edadf 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -922,3 +922,81 @@ def pipeline(*xs: Unpack[Tuple[int, Unpack[Tuple[float, ...]], bool]]) -> None: for x in xs: reveal_type(x) # N: Revealed type is "builtins.float" [builtins fixtures/tuple.pyi] + +[case testFixedUnpackItemInInstanceArguments] +from typing import TypeVar, Callable, Tuple, Generic +from typing_extensions import TypeVarTuple, Unpack + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") + +class C(Generic[T, Unpack[Ts], S]): + prefix: T + suffix: S + middle: Tuple[Unpack[Ts]] + +Ints = Tuple[int, int] +c: C[Unpack[Ints]] +reveal_type(c.prefix) # N: Revealed type is "builtins.int" +reveal_type(c.suffix) # N: Revealed type is "builtins.int" +reveal_type(c.middle) # N: Revealed type is "Tuple[()]" +[builtins fixtures/tuple.pyi] + +[case testVariadicUnpackItemInInstanceArguments] +from typing import TypeVar, Callable, Tuple, Generic +from typing_extensions import TypeVarTuple, Unpack + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") + +class Other(Generic[Unpack[Ts]]): ... +class C(Generic[T, Unpack[Ts], S]): + prefix: T + suffix: S + x: Tuple[Unpack[Ts]] + y: Callable[[Unpack[Ts]], None] + z: Other[Unpack[Ts]] + +Ints = Tuple[int, ...] +c: C[Unpack[Ints]] +reveal_type(c.prefix) # N: Revealed type is "builtins.int" +reveal_type(c.suffix) # N: Revealed type is "builtins.int" +reveal_type(c.x) # N: Revealed type is "builtins.tuple[builtins.int, ...]" +reveal_type(c.y) # N: Revealed type is "def (*builtins.int)" +reveal_type(c.z) # N: Revealed type is "__main__.Other[Unpack[builtins.tuple[builtins.int, ...]]]" +[builtins fixtures/tuple.pyi] + +[case testTooFewItemsInInstanceArguments] +from typing import Generic, TypeVar +from typing_extensions import TypeVarTuple, Unpack + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") +class C(Generic[T, Unpack[Ts], S]): ... + +c: C[int] # E: Bad number of arguments, expected: at least 2, given: 1 +reveal_type(c) # N: Revealed type is "__main__.C[Any, Unpack[builtins.tuple[Any, ...]], Any]" +[builtins fixtures/tuple.pyi] + +[case testVariadicClassUpperBoundCheck] +from typing import Tuple, TypeVar, Generic +from typing_extensions import Unpack, TypeVarTuple + +class A: ... +class B: ... +class C: ... +class D: ... + +T = TypeVar("T", bound=int) +S = TypeVar("S", bound=str) +Ts = TypeVarTuple("Ts") + +class G(Generic[T, Unpack[Ts], S]): ... +First = Tuple[A, B] +Second = Tuple[C, D] +x: G[Unpack[First], Unpack[Second]] # E: Type argument "A" of "G" must be a subtype of "int" \ + # E: Type argument "D" of "G" must be a subtype of "str" +[builtins fixtures/tuple.pyi]