diff --git a/mypy/erasetype.py b/mypy/erasetype.py index b41eefcd4821..5d95b221af15 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -34,6 +34,7 @@ get_proper_type, get_proper_types, ) +from mypy.typevartuples import erased_vars def erase_type(typ: Type) -> ProperType: @@ -77,17 +78,7 @@ def visit_deleted_type(self, t: DeletedType) -> ProperType: return t def visit_instance(self, t: Instance) -> ProperType: - args: list[Type] = [] - for tv in t.type.defn.type_vars: - # Valid erasure for *Ts is *tuple[Any, ...], not just Any. - if isinstance(tv, TypeVarTupleType): - args.append( - UnpackType( - tv.tuple_fallback.copy_modified(args=[AnyType(TypeOfAny.special_form)]) - ) - ) - else: - args.append(AnyType(TypeOfAny.special_form)) + args = erased_vars(t.type.defn.type_vars, TypeOfAny.special_form) return Instance(t.type, args, t.line) def visit_type_var(self, t: TypeVarType) -> ProperType: diff --git a/mypy/nodes.py b/mypy/nodes.py index d215bcfce098..2eb39d4baaf6 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3165,9 +3165,6 @@ def add_type_vars(self) -> None: self.type_var_tuple_prefix = i self.type_var_tuple_suffix = len(self.defn.type_vars) - i - 1 self.type_vars.append(vd.name) - assert not ( - self.has_param_spec_type and self.has_type_var_tuple_type - ), "Mixing type var tuples and param specs not supported yet" @property def name(self) -> str: diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index dbf5136afa1b..646bb28a3b6e 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -39,6 +39,7 @@ get_proper_types, split_with_prefix_and_suffix, ) +from mypy.typevartuples import erased_vars class TypeArgumentAnalyzer(MixedTraverserVisitor): @@ -89,7 +90,14 @@ def visit_type_alias_type(self, t: TypeAliasType) -> None: return self.seen_aliases.add(t) assert t.alias is not None, f"Unfixed type alias {t.type_ref}" - is_error = self.validate_args(t.alias.name, tuple(t.args), t.alias.alias_tvars, t) + is_error, is_invalid = self.validate_args( + t.alias.name, tuple(t.args), t.alias.alias_tvars, t + ) + if is_invalid: + # If there is an arity error (e.g. non-Parameters used for ParamSpec etc.), + # then it is safer to erase the arguments completely, to avoid crashes later. + # TODO: can we move this logic to typeanal.py? + t.args = erased_vars(t.alias.alias_tvars, TypeOfAny.from_error) if not is_error: # If there was already an error for the alias itself, there is no point in checking # the expansion, most likely it will result in the same kind of error. @@ -113,7 +121,9 @@ def visit_instance(self, t: Instance) -> None: info = t.type if isinstance(info, FakeInfo): return # https://github.com/python/mypy/issues/11079 - self.validate_args(info.name, t.args, info.defn.type_vars, t) + _, is_invalid = self.validate_args(info.name, t.args, info.defn.type_vars, t) + if is_invalid: + t.args = tuple(erased_vars(info.defn.type_vars, TypeOfAny.from_error)) if t.type.fullname == "builtins.tuple" and len(t.args) == 1: # Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...] arg = t.args[0] @@ -125,7 +135,7 @@ def visit_instance(self, t: Instance) -> None: def validate_args( self, name: str, args: tuple[Type, ...], type_vars: list[TypeVarLikeType], ctx: Context - ) -> bool: + ) -> tuple[bool, bool]: if any(isinstance(v, TypeVarTupleType) for v in type_vars): prefix = next(i for (i, v) in enumerate(type_vars) if isinstance(v, TypeVarTupleType)) tvt = type_vars[prefix] @@ -136,10 +146,11 @@ def validate_args( args = start + (TupleType(list(middle), tvt.tuple_fallback),) + end is_error = False + is_invalid = False for (i, arg), tvar in zip(enumerate(args), type_vars): if isinstance(tvar, TypeVarType): if isinstance(arg, ParamSpecType): - is_error = True + is_invalid = True self.fail( INVALID_PARAM_SPEC_LOCATION.format(format_type(arg, self.options)), ctx, @@ -152,7 +163,7 @@ def validate_args( ) continue if isinstance(arg, Parameters): - is_error = True + is_invalid = True self.fail( f"Cannot use {format_type(arg, self.options)} for regular type variable," " only for ParamSpec", @@ -205,13 +216,16 @@ def validate_args( if not isinstance( get_proper_type(arg), (ParamSpecType, Parameters, AnyType, UnboundType) ): + is_invalid = True self.fail( "Can only replace ParamSpec with a parameter types list or" f" another ParamSpec, got {format_type(arg, self.options)}", ctx, code=codes.VALID_TYPE, ) - return is_error + if is_invalid: + is_error = True + return is_error, is_invalid def visit_unpack_type(self, typ: UnpackType) -> None: super().visit_unpack_type(typ) diff --git a/mypy/typevars.py b/mypy/typevars.py index 3d74a40c303f..e871973104a2 100644 --- a/mypy/typevars.py +++ b/mypy/typevars.py @@ -3,7 +3,6 @@ from mypy.erasetype import erase_typevars from mypy.nodes import TypeInfo from mypy.types import ( - AnyType, Instance, ParamSpecType, ProperType, @@ -15,6 +14,7 @@ TypeVarType, UnpackType, ) +from mypy.typevartuples import erased_vars def fill_typevars(typ: TypeInfo) -> Instance | TupleType: @@ -64,16 +64,7 @@ def fill_typevars(typ: TypeInfo) -> Instance | TupleType: def fill_typevars_with_any(typ: TypeInfo) -> Instance | TupleType: """Apply a correct number of Any's as type arguments to a type.""" - args: list[Type] = [] - for tv in typ.defn.type_vars: - # Valid erasure for *Ts is *tuple[Any, ...], not just Any. - if isinstance(tv, TypeVarTupleType): - args.append( - UnpackType(tv.tuple_fallback.copy_modified(args=[AnyType(TypeOfAny.special_form)])) - ) - else: - args.append(AnyType(TypeOfAny.special_form)) - inst = Instance(typ, args) + inst = Instance(typ, erased_vars(typ.defn.type_vars, TypeOfAny.special_form)) if typ.tuple_type is None: return inst erased_tuple_type = erase_typevars(typ.tuple_type, {tv.id for tv in typ.defn.type_vars}) diff --git a/mypy/typevartuples.py b/mypy/typevartuples.py index af2effbd4035..2a9998c10746 100644 --- a/mypy/typevartuples.py +++ b/mypy/typevartuples.py @@ -5,9 +5,12 @@ from typing import Sequence from mypy.types import ( + AnyType, Instance, ProperType, Type, + TypeVarLikeType, + TypeVarTupleType, UnpackType, get_proper_type, split_with_prefix_and_suffix, @@ -30,3 +33,14 @@ def extract_unpack(types: Sequence[Type]) -> ProperType | None: if isinstance(types[0], UnpackType): return get_proper_type(types[0].type) return None + + +def erased_vars(type_vars: Sequence[TypeVarLikeType], type_of_any: int) -> list[Type]: + args: list[Type] = [] + for tv in type_vars: + # Valid erasure for *Ts is *tuple[Any, ...], not just Any. + if isinstance(tv, TypeVarTupleType): + args.append(UnpackType(tv.tuple_fallback.copy_modified(args=[AnyType(type_of_any)]))) + else: + args.append(AnyType(type_of_any)) + return args diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index ea692244597c..f49e1b3c6613 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2460,3 +2460,30 @@ def test(x: T, *args: Unpack[Ts]) -> Tuple[T, Unpack[Ts]]: ... reveal_type(test) # N: Revealed type is "def [T, Ts] (builtins.list[T`2], *args: Unpack[Ts`-2]) -> __main__.CM[Tuple[T`2, Unpack[Ts`-2]]]" [builtins fixtures/tuple.pyi] + +[case testMixingTypeVarTupleAndParamSpec] +from typing import Generic, ParamSpec, TypeVarTuple, Unpack, Callable, TypeVar + +P = ParamSpec("P") +Ts = TypeVarTuple("Ts") + +class A(Generic[P, Unpack[Ts]]): ... +class B(Generic[Unpack[Ts], P]): ... + +a: A[[int, str], int, str] +reveal_type(a) # N: Revealed type is "__main__.A[[builtins.int, builtins.str], builtins.int, builtins.str]" +b: B[int, str, [int, str]] +reveal_type(b) # N: Revealed type is "__main__.B[builtins.int, builtins.str, [builtins.int, builtins.str]]" + +x: A[int, str, [int, str]] # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "int" +reveal_type(x) # N: Revealed type is "__main__.A[Any, Unpack[builtins.tuple[Any, ...]]]" +y: B[[int, str], int, str] # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "str" +reveal_type(y) # N: Revealed type is "__main__.B[Unpack[builtins.tuple[Any, ...]], Any]" + +R = TypeVar("R") +class C(Generic[P, R]): + fn: Callable[P, None] + +c: C[int, str] # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "int" +reveal_type(c.fn) # N: Revealed type is "def (*Any, **Any)" +[builtins fixtures/tuple.pyi]