Skip to content

Commit

Permalink
Fix type arguments validation for variadic instances (#15944)
Browse files Browse the repository at this point in the history
Fixes #15410
Fixes #15411
  • Loading branch information
ilevkivskyi authored Aug 24, 2023
1 parent dc73445 commit 351371d
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 8 deletions.
8 changes: 4 additions & 4 deletions mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 21 additions & 2 deletions mypy/semanal_typeargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion mypy/test/testtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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


Expand Down
1 change: 1 addition & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
78 changes: 78 additions & 0 deletions test-data/unit/check-typevar-tuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit 351371d

Please sign in to comment.