Skip to content

Commit

Permalink
Allow mixing ParamSpec and TypeVarTuple in Generic (#17450)
Browse files Browse the repository at this point in the history
Fixes #16696
Fixes #16695

I think there are no good reasons to not allow this anymore. Also I am
using this opportunity to tighten a bit invalid instances/aliases where
a regular type variable is replaced with parameters and vice versa.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
ilevkivskyi and pre-commit-ci[bot] committed Jun 29, 2024
1 parent b88fdbd commit 69042d3
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 31 deletions.
13 changes: 2 additions & 11 deletions mypy/erasetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
get_proper_type,
get_proper_types,
)
from mypy.typevartuples import erased_vars


def erase_type(typ: Type) -> ProperType:
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 0 additions & 3 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 20 additions & 6 deletions mypy/semanal_typeargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
get_proper_types,
split_with_prefix_and_suffix,
)
from mypy.typevartuples import erased_vars


class TypeArgumentAnalyzer(MixedTraverserVisitor):
Expand Down Expand Up @@ -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.
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 2 additions & 11 deletions mypy/typevars.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from mypy.erasetype import erase_typevars
from mypy.nodes import TypeInfo
from mypy.types import (
AnyType,
Instance,
ParamSpecType,
ProperType,
Expand All @@ -15,6 +14,7 @@
TypeVarType,
UnpackType,
)
from mypy.typevartuples import erased_vars


def fill_typevars(typ: TypeInfo) -> Instance | TupleType:
Expand Down Expand Up @@ -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})
Expand Down
14 changes: 14 additions & 0 deletions mypy/typevartuples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
27 changes: 27 additions & 0 deletions test-data/unit/check-typevar-tuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit 69042d3

Please sign in to comment.