diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 08bcdab8d0a20..56682724a3fe7 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1080,6 +1080,25 @@ def f(self: S) -> T: ... dispatched_arg_type = TypeType.make_normalized(dispatched_arg_type) p_dispatched_arg_type = get_proper_type(dispatched_arg_type) + def effective_selfarg(item: CallableType) -> ProperType: + selfarg = get_proper_type(item.arg_types[0]) + if ( + isinstance(selfarg, Instance) + and isinstance(p_dispatched_arg_type, Instance) + and selfarg.type is p_dispatched_arg_type.type + and selfarg.args + and any(isinstance(tv, TypeVarTupleType) for tv in get_all_type_vars(selfarg)) + ): + # If the explicit self-type contains an unpack of a class-level + # TypeVarTuple (e.g. A[*TS, int, int]), first substitute the class + # type variables using the receiver, so that e.g. accessing .add on + # A[1, 2, 3] checks against A[1, 2, 3, int, int] (see #21878). + # Otherwise the unpack would make the overlap/subtype checks below + # vacuously succeed, and expand_type_by_instance() would later + # splice the whole instance argument tuple into *TS. + selfarg = expand_type_by_instance(selfarg, p_dispatched_arg_type) + return selfarg + for item in items: if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR): # No positional first (self) argument (*args is okay). @@ -1087,7 +1106,7 @@ def f(self: S) -> T: ... # This is pretty bad, so just return the original signature if # there is at least one such error. return functype - selfarg = get_proper_type(item.arg_types[0]) + selfarg = effective_selfarg(item) if isinstance(selfarg, Instance) and isinstance(p_dispatched_arg_type, Instance): if selfarg.type is p_dispatched_arg_type.type and selfarg.args: if not is_overlapping_types(p_dispatched_arg_type, selfarg): @@ -1102,7 +1121,7 @@ def f(self: S) -> T: ... new_items = [] for item in items: - selfarg = get_proper_type(item.arg_types[0]) + selfarg = effective_selfarg(item) # This matches similar special-casing in bind_self(), see more details there. self_callable = name == "__call__" and isinstance(selfarg, CallableType) if self_callable or is_subtype( diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index c298e8e118d7e..ffd7d576a8302 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -3249,6 +3249,28 @@ def test(f: Callable[[object], object]) -> None: ... test(foo) # E: Argument 1 to "test" has incompatible type overloaded function; expected "Callable[[object], object]" [builtins fixtures/tuple.pyi] +[case testTypeVarTupleExplicitSelfTypeWithExtraArgs] +# https://github.com/python/mypy/issues/21878 +from typing import Generic, TypeVarTuple, Unpack +Ts = TypeVarTuple("Ts") +Us = TypeVarTuple("Us") + +class A(Generic[Unpack[Ts]]): + def __init__(self, *state: Unpack[Ts]) -> None: ... + def add(self: "A[Unpack[Ts], int, int]") -> "A[Unpack[Ts], int]": ... + def plain(self: "A[Unpack[Ts]]") -> "A[Unpack[Ts]]": ... + def meth_tv(self: "A[Unpack[Us]]", x: tuple[Unpack[Us]]) -> "A[Unpack[Us]]": ... + +# A class-level TypeVarTuple cannot take two different values in the same scope, +# so the explicit self-type below can never match: it is checked with Ts bound +# to the full instance arguments, i.e. as A[1, 2, 3, int, int]. +A(1, 2, 3).add() # E: Invalid self argument "A[int, int, int]" to attribute function "add" with type "Callable[[A[Unpack[Ts], int, int]], A[Unpack[Ts], int]]" +# Ordinary explicit self-types still work ... +reveal_type(A(1, 2, 3).plain()) # N: Revealed type is "__main__.A[Literal[1]?, Literal[2]?, Literal[3]?]" +# ... as do method-level TypeVarTuples in self-types. +reveal_type(A(1, 2, 3).meth_tv((1, 2, 3))) # N: Revealed type is "__main__.A[Literal[1]?, Literal[2]?, Literal[3]?]" +[builtins fixtures/tuple.pyi] + [case testDeferredNestedTupleAlias] from typing import Unpack