diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e0e10e8b890d..6fcf977d3394 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5192,19 +5192,6 @@ class C(Generic[T, Unpack[Ts]]): ... vars = list(t.variables) args = flatten_nested_tuples(args) - # TODO: this logic is duplicated with semanal_typeargs. - for tv, arg in zip(t.variables, args): - if isinstance(tv, ParamSpecType): - if not isinstance( - get_proper_type(arg), (Parameters, ParamSpecType, AnyType, UnboundType) - ): - self.chk.fail( - "Can only replace ParamSpec with a parameter types list or" - f" another ParamSpec, got {format_type(arg, self.chk.options)}", - ctx, - ) - return [AnyType(TypeOfAny.from_error)] * len(vars) - # TODO: in future we may want to support type application to variadic functions. if not vars or not t.is_type_obj() or t.type_object().fullname == "builtins.tuple": return list(args) @@ -5221,14 +5208,28 @@ class C(Generic[T, Unpack[Ts]]): ... args = list(fake.args) if not any(isinstance(v, TypeVarTupleType) for v in vars): - return args - - prefix = next(i for (i, v) in enumerate(vars) if isinstance(v, TypeVarTupleType)) - suffix = len(vars) - prefix - 1 - tvt = vars[prefix] - assert isinstance(tvt, TypeVarTupleType) - start, middle, end = split_with_prefix_and_suffix(tuple(args), prefix, suffix) - return list(start) + [TupleType(list(middle), tvt.tuple_fallback)] + list(end) + result = list(args) + else: + prefix = next(i for (i, v) in enumerate(vars) if isinstance(v, TypeVarTupleType)) + suffix = len(vars) - prefix - 1 + tvt = vars[prefix] + assert isinstance(tvt, TypeVarTupleType) + start, middle, end = split_with_prefix_and_suffix(tuple(args), prefix, suffix) + result = list(start) + [TupleType(list(middle), tvt.tuple_fallback)] + list(end) + + for tv, arg in zip(vars, result): + if isinstance(tv, ParamSpecType): + if not isinstance( + get_proper_type(arg), (Parameters, ParamSpecType, AnyType, UnboundType) + ): + self.chk.fail( + "Can only replace ParamSpec with a parameter types list or" + f" another ParamSpec, got {format_type(arg, self.chk.options)}", + ctx, + ) + return [AnyType(TypeOfAny.from_error)] * len(vars) + + return result def apply_type_arguments_to_callable( self, tp: Type, args: Sequence[Type], ctx: Context diff --git a/mypy/typeanal.py b/mypy/typeanal.py index bd6f97cc94ce..1d5771ba9387 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -118,6 +118,7 @@ ) from mypy.types_utils import get_bad_type_type_item from mypy.typevars import fill_typevars +from mypy.typevartuples import split_variadic_args_with_defaults T = TypeVar("T") @@ -2165,16 +2166,73 @@ def fix_instance( for tv in t.type.defn.type_vars ) max_tv_count = len(t.type.type_vars) - if arg_count < min_tv_count or arg_count > max_tv_count: + if arg_count < min_tv_count or ( + not t.type.has_type_var_tuple_type and arg_count > max_tv_count + ): # Don't use existing args if arg_count doesn't match - if arg_count > max_tv_count: + if not t.type.has_type_var_tuple_type and arg_count > max_tv_count: # Already wrong arg count error, don't emit missing type parameters error as well. disallow_any = False t.args = () + env: dict[TypeVarId, Type] = {} + if t.type.has_type_var_tuple_type and t.args: + prefix = t.type.type_var_tuple_prefix + assert prefix is not None + prefix_args, middle_args, suffix_args, omitted_suffix_vars = ( + split_variadic_args_with_defaults(t.type.defn.type_vars, prefix, t.args) + ) + if omitted_suffix_vars: + for tv, arg in zip(t.type.defn.type_vars[:prefix], prefix_args): + env[tv.id] = arg + tvt = t.type.defn.type_vars[prefix] + assert isinstance(tvt, TypeVarTupleType) + env[tvt.id] = TupleType(list(middle_args), tvt.tuple_fallback) + suffix_vars = t.type.defn.type_vars[prefix + 1 :] + for tv, arg in zip(suffix_vars[: len(suffix_args)], suffix_args): + env[tv.id] = arg + default_args: list[Type] = [] + for tv in omitted_suffix_vars: + if tv.has_default(): + arg = tv.default + if analyzing_tvar_def: + used_default = True + if is_typevar_default_recursive(tv.fullname, t.type): + fullname = None if use_generic_error else t.type.fullname + arg = get_omitted_any( + disallow_any, + fail, + note, + t, + options, + fullname, + unexpanded_type, + used_default, + ) + with state.strict_optional_set(options.strict_optional): + arg = expand_type(arg, env) + env[tv.id] = arg + default_args.append(arg) + used_default = True + else: + fullname = None if use_generic_error else t.type.fullname + arg = get_omitted_any( + disallow_any, + fail, + note, + t, + options, + fullname, + unexpanded_type, + used_default, + ) + env[tv.id] = arg + default_args.append(arg) + t.args = prefix_args + middle_args + suffix_args + tuple(default_args) + return used_default + args: list[Type] = list(t.args) any_type: AnyType | None = None - env: dict[TypeVarId, Type] = {} tvt_no_default = False for tv, arg in itertools.zip_longest(t.type.defn.type_vars, t.args, fillvalue=None): @@ -2340,6 +2398,15 @@ def instantiate_type_alias( # Variadic tuple is always correct. correct = True fill_typevars = not correct + if correct and node.tvar_tuple_index is not None: + prefix = node.tvar_tuple_index + suffix_vars = node.alias_tvars[prefix + 1 :] + if any(v.has_default() for v in suffix_vars): + _, _, _, omitted_suffix_vars = split_variadic_args_with_defaults( + node.alias_tvars, prefix, args + ) + if omitted_suffix_vars: + fill_typevars = True if fill_typevars: if not correct: if use_standard_error: @@ -2429,8 +2496,42 @@ def set_any_tvars( else: type_of_any = TypeOfAny.from_omitted_generics any_type = AnyType(type_of_any, line=newline, column=newcolumn) - env: dict[TypeVarId, Type] = {} + + if node.tvar_tuple_index is not None and args: + prefix = node.tvar_tuple_index + prefix_args, middle_args, suffix_args, omitted_suffix_vars = ( + split_variadic_args_with_defaults(node.alias_tvars, prefix, args) + ) + if omitted_suffix_vars: + for tv, arg in zip(node.alias_tvars[:prefix], prefix_args): + env[tv.id] = arg + tvt = node.alias_tvars[prefix] + assert isinstance(tvt, TypeVarTupleType) + env[tvt.id] = TupleType(list(middle_args), tvt.tuple_fallback) + suffix_vars = node.alias_tvars[prefix + 1 :] + for tv, arg in zip(suffix_vars[: len(suffix_args)], suffix_args): + env[tv.id] = arg + default_args: list[Type] = [] + for tv in omitted_suffix_vars: + if tv.has_default(): + arg = tv.default + if analyzing_tvar_def: + used_default = True + if is_typevar_default_recursive(tv.fullname, node): + arg = any_type + with state.strict_optional_set(options.strict_optional): + arg = expand_type(arg, env) + env[tv.id] = arg + default_args.append(arg) + used_default = True + else: + arg = any_type + env[tv.id] = arg + default_args.append(arg) + args = list(prefix_args + middle_args + suffix_args + tuple(default_args)) + return TypeAliasType(node, args, newline, newcolumn), used_default + used_any_type = False for tv, arg in itertools.zip_longest(node.alias_tvars, args, fillvalue=None): if tv is None: @@ -2704,6 +2805,15 @@ def validate_instance(t: Instance, fail: MsgCallback, indexed: bool) -> bool: if act_prefix < exp_prefix or act_suffix < exp_suffix: fail("TypeVarTuple cannot be split", t, code=codes.TYPE_ARG) return False + prefix = t.type.type_var_tuple_prefix + assert prefix is not None + suffix_vars = t.type.defn.type_vars[prefix + 1 :] + if any(v.has_default() for v in suffix_vars): + _, _, _, omitted_suffix_vars = split_variadic_args_with_defaults( + t.type.defn.type_vars, prefix, t.args + ) + if omitted_suffix_vars: + return False elif any(isinstance(a, UnpackType) for a in t.args): # A variadic unpack in fixed size instance (fixed unpacks must be flattened by the caller) fail(message_registry.INVALID_UNPACK_POSITION, t, code=codes.VALID_TYPE) diff --git a/mypy/typevartuples.py b/mypy/typevartuples.py index 1bf1a59f7d3f..1694a9d6c982 100644 --- a/mypy/typevartuples.py +++ b/mypy/typevartuples.py @@ -7,10 +7,14 @@ from mypy.types import ( AnyType, Instance, + Parameters, + ParamSpecType, Type, TypeVarLikeType, TypeVarTupleType, + UnboundType, UnpackType, + get_proper_type, split_with_prefix_and_suffix, ) @@ -34,3 +38,44 @@ def erased_vars(type_vars: Sequence[TypeVarLikeType], type_of_any: int) -> list[ else: args.append(AnyType(type_of_any)) return args + + +def is_valid_paramspec_arg(arg: Type) -> bool: + proper = get_proper_type(arg) + return isinstance(proper, (Parameters, ParamSpecType, AnyType, UnboundType)) + + +def split_variadic_args_with_defaults( + type_vars: Sequence[TypeVarLikeType], prefix: int, args: Sequence[Type] +) -> tuple[tuple[Type, ...], tuple[Type, ...], tuple[Type, ...], list[TypeVarLikeType]]: + """Partition arguments for a variadic generic with possible defaulted suffix variables. + + Returns: (prefix_args, middle_args, suffix_args, omitted_suffix_vars) + """ + suffix_vars = type_vars[prefix + 1 :] + num_req_suffix = sum(not v.has_default() for v in suffix_vars) + num_opt_suffix = len(suffix_vars) - num_req_suffix + + if len(args) < prefix + num_req_suffix: + return tuple(args[:prefix]), (), tuple(args[prefix:]), list(suffix_vars) + + opt_args_count = 0 + if num_opt_suffix > 0 and len(args) - prefix > num_req_suffix: + for _ in range(num_opt_suffix): + idx = len(args) - 1 - opt_args_count + if idx < prefix + num_req_suffix: + break + candidate = args[idx] + tv = suffix_vars[len(suffix_vars) - 1 - opt_args_count] + if isinstance(tv, ParamSpecType) and is_valid_paramspec_arg(candidate): + opt_args_count += 1 + else: + break + + prov_suffix_count = num_req_suffix + opt_args_count + omitted_suffix_vars = list(suffix_vars[prov_suffix_count:]) + + prefix_args = tuple(args[:prefix]) + middle_args = tuple(args[prefix : len(args) - prov_suffix_count]) + suffix_args = tuple(args[len(args) - prov_suffix_count :]) + return prefix_args, middle_args, suffix_args, omitted_suffix_vars diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index 7af7a55e73f3..51c6514d817f 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -503,3 +503,116 @@ def test_vec_concat(left: Vec[A], right: Vec[B]) -> None: _0: Vec[A | B] = concat_vecs(left, right) # OK _1: Vec[A | B | C] = concat_vecs(left, right) # OK _2: Vec[object] = concat_vecs(left, right) # OK + +[case testPEP695TypeVarTupleWithDefaultedParamSpec] +from typing import Callable + +class Foo[*Ts, **P = [float, bool]]: + def args(self) -> tuple[*Ts]: ... + def fn(self) -> Callable[P, None]: ... + +foo = Foo[int, str]() +reveal_type(foo.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(foo.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" + +bar = Foo[int, str, [bytes]]() +reveal_type(bar.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(bar.fn()) # N: Revealed type is "def (builtins.bytes)" + +baz = Foo[int, str, ...]() +reveal_type(baz.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(baz.fn()) # N: Revealed type is "def (*Any, **Any)" + +qux = Foo[[bytes]]() +reveal_type(qux.args()) # N: Revealed type is "tuple[()]" +reveal_type(qux.fn()) # N: Revealed type is "def (builtins.bytes)" + +single = Foo[int]() +reveal_type(single.args()) # N: Revealed type is "tuple[builtins.int]" +reveal_type(single.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" + +def test_ann( + a: Foo[int, str], + b: Foo[int, str, [bytes]], + c: Foo[[bytes]], + d: Foo[int], +) -> None: + reveal_type(a.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" + reveal_type(a.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" + reveal_type(b.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" + reveal_type(b.fn()) # N: Revealed type is "def (builtins.bytes)" + reveal_type(c.args()) # N: Revealed type is "tuple[()]" + reveal_type(c.fn()) # N: Revealed type is "def (builtins.bytes)" + reveal_type(d.args()) # N: Revealed type is "tuple[builtins.int]" + reveal_type(d.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" + +type Alias[*Ts, **P = [float, bool]] = Foo[*Ts, P] +al1: Alias[int, str] +al2: Alias[int, str, [bytes]] +reveal_type(al1.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(al1.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" +reveal_type(al2.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(al2.fn()) # N: Revealed type is "def (builtins.bytes)" + +class Sub1(Foo[int, str]): ... +class Sub2(Foo[int, str, [bytes]]): ... +s1 = Sub1() +s2 = Sub2() +reveal_type(s1.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(s1.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" +reveal_type(s2.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(s2.fn()) # N: Revealed type is "def (builtins.bytes)" +[builtins fixtures/tuple.pyi] + +[case testPEP695TypeVarTupleWithDefaultedParamSpecPrefix] +from typing import Callable + +class PrefFoo[T, *Ts, **P = [float, bool]]: + def first(self) -> T: ... + def args(self) -> tuple[*Ts]: ... + def fn(self) -> Callable[P, None]: ... + +p1 = PrefFoo[int, str, bool]() +reveal_type(p1.first()) # N: Revealed type is "builtins.int" +reveal_type(p1.args()) # N: Revealed type is "tuple[builtins.str, builtins.bool]" +reveal_type(p1.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" + +p2 = PrefFoo[int, str, [bytes]]() +reveal_type(p2.first()) # N: Revealed type is "builtins.int" +reveal_type(p2.args()) # N: Revealed type is "tuple[builtins.str]" +reveal_type(p2.fn()) # N: Revealed type is "def (builtins.bytes)" + +p3 = PrefFoo[int, [bytes]]() +reveal_type(p3.first()) # N: Revealed type is "builtins.int" +reveal_type(p3.args()) # N: Revealed type is "tuple[()]" +reveal_type(p3.fn()) # N: Revealed type is "def (builtins.bytes)" + +p4 = PrefFoo[int]() +reveal_type(p4.first()) # N: Revealed type is "builtins.int" +reveal_type(p4.args()) # N: Revealed type is "tuple[()]" +reveal_type(p4.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" +[builtins fixtures/tuple.pyi] + +[case testPEP695TypeVarTupleWithDefaultedParamSpecBothDefaults] +from typing import Callable + +class BothFoo[*Ts = *tuple[int, str], **P = [float, bool]]: + def args(self) -> tuple[*Ts]: ... + def fn(self) -> Callable[P, None]: ... + +b1 = BothFoo() +reveal_type(b1.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(b1.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" + +b2 = BothFoo[bool]() +reveal_type(b2.args()) # N: Revealed type is "tuple[builtins.bool]" +reveal_type(b2.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" + +b3 = BothFoo[[bytes]]() +reveal_type(b3.args()) # N: Revealed type is "tuple[()]" +reveal_type(b3.fn()) # N: Revealed type is "def (builtins.bytes)" + +b4 = BothFoo[bool, [bytes]]() +reveal_type(b4.args()) # N: Revealed type is "tuple[builtins.bool]" +reveal_type(b4.fn()) # N: Revealed type is "def (builtins.bytes)" +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index a5cdaea16105..11850ad37aa9 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -1063,3 +1063,42 @@ A = list[T] a: A reveal_type(a) # N: Revealed type is "builtins.list[builtins.list[Any]]" [builtins fixtures/tuple.pyi] + +[case testTypeVarTupleWithDefaultedParamSpecLegacy] +from typing import Generic, ParamSpec, Callable +from typing_extensions import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +P = ParamSpec("P", default=[float, bool]) + +class Foo(Generic[Unpack[Ts], P]): + def args(self) -> tuple[Unpack[Ts]]: ... + def fn(self) -> Callable[P, None]: ... + +foo = Foo[int, str]() +reveal_type(foo.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(foo.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" + +bar = Foo[int, str, [bytes]]() +reveal_type(bar.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(bar.fn()) # N: Revealed type is "def (builtins.bytes)" + +def test_ann(a: Foo[int, str], b: Foo[int, str, [bytes]]) -> None: + reveal_type(a.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" + reveal_type(a.fn()) # N: Revealed type is "def (builtins.float, builtins.bool)" + reveal_type(b.args()) # N: Revealed type is "tuple[builtins.int, builtins.str]" + reveal_type(b.fn()) # N: Revealed type is "def (builtins.bytes)" +[builtins fixtures/tuple.pyi] + +[case testTypeVarTupleWithParamSpecNegative] +from typing import Generic, ParamSpec +from typing_extensions import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +P = ParamSpec("P") + +class Bar(Generic[Unpack[Ts], P]): ... + +b1 = Bar[int, str]() # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "str" +def f(x: Bar[int, str]) -> None: ... # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "str" +[builtins fixtures/tuple.pyi]