Skip to content

Commit 351371d

Browse files
authored
Fix type arguments validation for variadic instances (python#15944)
Fixes python#15410 Fixes python#15411
1 parent dc73445 commit 351371d

File tree

6 files changed

+112
-8
lines changed

6 files changed

+112
-8
lines changed

mypy/expandtype.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -409,10 +409,10 @@ def visit_tuple_type(self, t: TupleType) -> Type:
409409
# Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...]
410410
item = items[0]
411411
if isinstance(item, UnpackType):
412-
assert isinstance(item.type, ProperType)
413-
if isinstance(item.type, Instance):
414-
assert item.type.type.fullname == "builtins.tuple"
415-
return item.type
412+
unpacked = get_proper_type(item.type)
413+
if isinstance(unpacked, Instance):
414+
assert unpacked.type.fullname == "builtins.tuple"
415+
return unpacked
416416
fallback = t.partial_fallback.accept(self)
417417
assert isinstance(fallback, ProperType) and isinstance(fallback, Instance)
418418
return t.copy_modified(items=items, fallback=fallback)

mypy/semanal_typeargs.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from mypy.options import Options
1919
from mypy.scope import Scope
2020
from mypy.subtypes import is_same_type, is_subtype
21-
from mypy.typeanal import set_any_tvars
21+
from mypy.typeanal import fix_type_var_tuple_argument, set_any_tvars
2222
from mypy.types import (
2323
AnyType,
2424
CallableType,
@@ -143,7 +143,26 @@ def visit_instance(self, t: Instance) -> None:
143143
if isinstance(info, FakeInfo):
144144
return # https://github.com/python/mypy/issues/11079
145145
t.args = tuple(flatten_nested_tuples(t.args))
146-
# TODO: fix #15410 and #15411.
146+
if t.type.has_type_var_tuple_type:
147+
# Regular Instances are already validated in typeanal.py.
148+
# TODO: do something with partial overlap (probably just reject).
149+
# also in other places where split_with_prefix_and_suffix() is used.
150+
correct = len(t.args) >= len(t.type.type_vars) - 1
151+
if any(
152+
isinstance(a, UnpackType) and isinstance(get_proper_type(a.type), Instance)
153+
for a in t.args
154+
):
155+
correct = True
156+
if not correct:
157+
exp_len = f"at least {len(t.type.type_vars) - 1}"
158+
self.fail(
159+
f"Bad number of arguments, expected: {exp_len}, given: {len(t.args)}",
160+
t,
161+
code=codes.TYPE_ARG,
162+
)
163+
any_type = AnyType(TypeOfAny.from_error)
164+
t.args = (any_type,) * len(t.type.type_vars)
165+
fix_type_var_tuple_argument(any_type, t)
147166
self.validate_args(info.name, t.args, info.defn.type_vars, t)
148167
super().visit_instance(t)
149168

mypy/test/testtypes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1464,7 +1464,7 @@ def make_call(*items: tuple[str, str | None]) -> CallExpr:
14641464
class TestExpandTypeLimitGetProperType(TestCase):
14651465
# WARNING: do not increase this number unless absolutely necessary,
14661466
# and you understand what you are doing.
1467-
ALLOWED_GET_PROPER_TYPES = 6
1467+
ALLOWED_GET_PROPER_TYPES = 7
14681468

14691469
@skipUnless(mypy.expandtype.__file__.endswith(".py"), "Skip for compiled mypy")
14701470
def test_count_get_proper_type(self) -> None:

mypy/typeanal.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1795,6 +1795,13 @@ def fix_instance(
17951795
fix_type_var_tuple_argument(any_type, t)
17961796

17971797
return
1798+
1799+
if t.type.has_type_var_tuple_type:
1800+
# This can be only correctly analyzed when all arguments are fully
1801+
# analyzed, because there may be a variadic item among them, so we
1802+
# do this in semanal_typeargs.py.
1803+
return
1804+
17981805
# Invalid number of type parameters.
17991806
fail(
18001807
wrong_type_arg_count(len(t.type.type_vars), str(len(t.args)), t.type.name),
@@ -1805,7 +1812,6 @@ def fix_instance(
18051812
# otherwise the type checker may crash as it expects
18061813
# things to be right.
18071814
t.args = tuple(AnyType(TypeOfAny.from_error) for _ in t.type.type_vars)
1808-
fix_type_var_tuple_argument(AnyType(TypeOfAny.from_error), t)
18091815
t.invalid = True
18101816

18111817

mypy/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ def _expand_once(self) -> Type:
322322
assert isinstance(self.alias.target, Instance) # type: ignore[misc]
323323
return self.alias.target.copy_modified(args=self.args)
324324

325+
# TODO: this logic duplicates the one in expand_type_by_instance().
325326
if self.alias.tvar_tuple_index is None:
326327
mapping = {v.id: s for (v, s) in zip(self.alias.alias_tvars, self.args)}
327328
else:

test-data/unit/check-typevar-tuple.test

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,3 +922,81 @@ def pipeline(*xs: Unpack[Tuple[int, Unpack[Tuple[float, ...]], bool]]) -> None:
922922
for x in xs:
923923
reveal_type(x) # N: Revealed type is "builtins.float"
924924
[builtins fixtures/tuple.pyi]
925+
926+
[case testFixedUnpackItemInInstanceArguments]
927+
from typing import TypeVar, Callable, Tuple, Generic
928+
from typing_extensions import TypeVarTuple, Unpack
929+
930+
T = TypeVar("T")
931+
S = TypeVar("S")
932+
Ts = TypeVarTuple("Ts")
933+
934+
class C(Generic[T, Unpack[Ts], S]):
935+
prefix: T
936+
suffix: S
937+
middle: Tuple[Unpack[Ts]]
938+
939+
Ints = Tuple[int, int]
940+
c: C[Unpack[Ints]]
941+
reveal_type(c.prefix) # N: Revealed type is "builtins.int"
942+
reveal_type(c.suffix) # N: Revealed type is "builtins.int"
943+
reveal_type(c.middle) # N: Revealed type is "Tuple[()]"
944+
[builtins fixtures/tuple.pyi]
945+
946+
[case testVariadicUnpackItemInInstanceArguments]
947+
from typing import TypeVar, Callable, Tuple, Generic
948+
from typing_extensions import TypeVarTuple, Unpack
949+
950+
T = TypeVar("T")
951+
S = TypeVar("S")
952+
Ts = TypeVarTuple("Ts")
953+
954+
class Other(Generic[Unpack[Ts]]): ...
955+
class C(Generic[T, Unpack[Ts], S]):
956+
prefix: T
957+
suffix: S
958+
x: Tuple[Unpack[Ts]]
959+
y: Callable[[Unpack[Ts]], None]
960+
z: Other[Unpack[Ts]]
961+
962+
Ints = Tuple[int, ...]
963+
c: C[Unpack[Ints]]
964+
reveal_type(c.prefix) # N: Revealed type is "builtins.int"
965+
reveal_type(c.suffix) # N: Revealed type is "builtins.int"
966+
reveal_type(c.x) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
967+
reveal_type(c.y) # N: Revealed type is "def (*builtins.int)"
968+
reveal_type(c.z) # N: Revealed type is "__main__.Other[Unpack[builtins.tuple[builtins.int, ...]]]"
969+
[builtins fixtures/tuple.pyi]
970+
971+
[case testTooFewItemsInInstanceArguments]
972+
from typing import Generic, TypeVar
973+
from typing_extensions import TypeVarTuple, Unpack
974+
975+
T = TypeVar("T")
976+
S = TypeVar("S")
977+
Ts = TypeVarTuple("Ts")
978+
class C(Generic[T, Unpack[Ts], S]): ...
979+
980+
c: C[int] # E: Bad number of arguments, expected: at least 2, given: 1
981+
reveal_type(c) # N: Revealed type is "__main__.C[Any, Unpack[builtins.tuple[Any, ...]], Any]"
982+
[builtins fixtures/tuple.pyi]
983+
984+
[case testVariadicClassUpperBoundCheck]
985+
from typing import Tuple, TypeVar, Generic
986+
from typing_extensions import Unpack, TypeVarTuple
987+
988+
class A: ...
989+
class B: ...
990+
class C: ...
991+
class D: ...
992+
993+
T = TypeVar("T", bound=int)
994+
S = TypeVar("S", bound=str)
995+
Ts = TypeVarTuple("Ts")
996+
997+
class G(Generic[T, Unpack[Ts], S]): ...
998+
First = Tuple[A, B]
999+
Second = Tuple[C, D]
1000+
x: G[Unpack[First], Unpack[Second]] # E: Type argument "A" of "G" must be a subtype of "int" \
1001+
# E: Type argument "D" of "G" must be a subtype of "str"
1002+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)