diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 2172361ea2f0..6f089e35e50f 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -7,7 +7,7 @@ Type, Instance, AnyType, TupleType, TypedDictType, CallableType, FunctionLike, TypeVarLikeType, Overloaded, TypeVarType, UnionType, PartialType, TypeOfAny, LiteralType, DeletedType, NoneType, TypeType, has_type_vars, get_proper_type, ProperType, ParamSpecType, - ENUM_REMOVED_PROPS + TypeVarTupleType, ENUM_REMOVED_PROPS ) from mypy.nodes import ( TypeInfo, FuncBase, Var, FuncDef, SymbolNode, SymbolTable, Context, @@ -693,6 +693,7 @@ def f(self: S) -> T: ... new_items = [] if is_classmethod: dispatched_arg_type = TypeType.make_normalized(dispatched_arg_type) + 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). @@ -701,12 +702,14 @@ def f(self: S) -> T: ... # there is at least one such error. return functype else: - selfarg = item.arg_types[0] + selfarg = get_proper_type(item.arg_types[0]) if subtypes.is_subtype(dispatched_arg_type, erase_typevars(erase_to_bound(selfarg))): new_items.append(item) elif isinstance(selfarg, ParamSpecType): # TODO: This is not always right. What's the most reasonable thing to do here? new_items.append(item) + elif isinstance(selfarg, TypeVarTupleType): + raise NotImplementedError if not new_items: # Choose first item for the message (it may be not very helpful for overloads). msg.incompatible_self_argument(name, dispatched_arg_type, items[0], diff --git a/mypy/constraints.py b/mypy/constraints.py index 01dce64520fb..9e49cc9709bf 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -9,6 +9,7 @@ UninhabitedType, TypeType, TypeVarId, TypeQuery, is_named_instance, TypeOfAny, LiteralType, ProperType, ParamSpecType, get_proper_type, TypeAliasType, is_union_with_any, UnpackType, callable_with_ellipsis, Parameters, TUPLE_LIKE_INSTANCE_NAMES, TypeVarTupleType, + TypeList, ) from mypy.maptype import map_instance_to_supertype import mypy.subtypes @@ -18,6 +19,12 @@ from mypy.nodes import COVARIANT, CONTRAVARIANT, ArgKind from mypy.argmap import ArgTypeExpander from mypy.typestate import TypeState +from mypy.typevartuples import ( + split_with_instance, + split_with_prefix_and_suffix, + extract_unpack, + find_unpack_in_list, +) if TYPE_CHECKING: from mypy.infer import ArgumentInferContext @@ -486,15 +493,60 @@ def visit_instance(self, template: Instance) -> List[Constraint]: res.append(Constraint(mapped_arg.id, SUPERTYPE_OF, suffix)) elif isinstance(suffix, ParamSpecType): res.append(Constraint(mapped_arg.id, SUPERTYPE_OF, suffix)) + elif isinstance(tvar, TypeVarTupleType): + raise NotImplementedError return res elif (self.direction == SUPERTYPE_OF and instance.type.has_base(template.type.fullname)): mapped = map_instance_to_supertype(instance, template.type) tvars = template.type.defn.type_vars + if template.type.has_type_var_tuple_type: + mapped_prefix, mapped_middle, mapped_suffix = ( + split_with_instance(mapped) + ) + template_prefix, template_middle, template_suffix = ( + split_with_instance(template) + ) + + # Add a constraint for the type var tuple, and then + # remove it for the case below. + template_unpack = extract_unpack(template_middle) + if template_unpack is not None: + if isinstance(template_unpack, TypeVarTupleType): + res.append(Constraint( + template_unpack.id, + SUPERTYPE_OF, + TypeList(list(mapped_middle)) + )) + elif ( + isinstance(template_unpack, Instance) and + template_unpack.type.fullname == "builtins.tuple" + ): + # TODO: check homogenous tuple case + raise NotImplementedError + elif isinstance(template_unpack, TupleType): + # TODO: check tuple case + raise NotImplementedError + + mapped_args = mapped_prefix + mapped_suffix + template_args = template_prefix + template_suffix + + assert template.type.type_var_tuple_prefix is not None + assert template.type.type_var_tuple_suffix is not None + tvars_prefix, _, tvars_suffix = split_with_prefix_and_suffix( + tuple(tvars), + template.type.type_var_tuple_prefix, + template.type.type_var_tuple_suffix, + ) + tvars = list(tvars_prefix + tvars_suffix) + else: + mapped_args = mapped.args + template_args = template.args # N.B: We use zip instead of indexing because the lengths might have # mismatches during daemon reprocessing. - for tvar, mapped_arg, template_arg in zip(tvars, mapped.args, template.args): + for tvar, mapped_arg, template_arg in zip(tvars, mapped_args, template_args): + assert not isinstance(tvar, TypeVarTupleType) if isinstance(tvar, TypeVarType): # The constraints for generic type parameters depend on variance. # Include constraints from both directions if invariant. @@ -573,6 +625,8 @@ def visit_instance(self, template: Instance) -> List[Constraint]: return [] elif isinstance(actual, ParamSpecType): return infer_constraints(template, actual.upper_bound, self.direction) + elif isinstance(actual, TypeVarTupleType): + raise NotImplementedError else: return [] @@ -696,13 +750,12 @@ def infer_against_overloaded(self, overloaded: Overloaded, def visit_tuple_type(self, template: TupleType) -> List[Constraint]: actual = self.actual - # TODO: Support other items in the tuple besides Unpack # TODO: Support subclasses of Tuple is_varlength_tuple = ( isinstance(actual, Instance) and actual.type.fullname == "builtins.tuple" ) - unpack_index = find_unpack_in_tuple(template) + unpack_index = find_unpack_in_list(template.items) if unpack_index is not None: unpack_item = get_proper_type(template.items[unpack_index]) @@ -727,16 +780,15 @@ def visit_tuple_type(self, template: TupleType) -> List[Constraint]: modified_actual = actual if isinstance(actual, TupleType): # Exclude the items from before and after the unpack index. - head = unpack_index - tail = len(template.items) - unpack_index - 1 - if tail: - modified_actual = actual.copy_modified( - items=actual.items[head:-tail], - ) - else: - modified_actual = actual.copy_modified( - items=actual.items[head:], - ) + # TODO: Support including constraints from the prefix/suffix. + _, actual_items, _ = split_with_prefix_and_suffix( + tuple(actual.items), + unpack_index, + len(template.items) - unpack_index - 1, + ) + modified_actual = actual.copy_modified( + items=list(actual_items) + ) return [Constraint( type_var=unpacked_type.id, op=self.direction, @@ -854,18 +906,3 @@ def find_matching_overload_items(overloaded: Overloaded, # it maintains backward compatibility. res = items[:] return res - - -def find_unpack_in_tuple(t: TupleType) -> Optional[int]: - unpack_index: Optional[int] = None - for i, item in enumerate(t.items): - proper_item = get_proper_type(item) - if isinstance(proper_item, UnpackType): - # We cannot fail here, so we must check this in an earlier - # semanal phase. - # Funky code here avoids mypyc narrowing the type of unpack_index. - old_index = unpack_index - assert old_index is None - # Don't return so that we can also sanity check there is only one. - unpack_index = i - return unpack_index diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 21ca5771b32e..ec0ad1338840 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -137,6 +137,11 @@ def visit_type_var(self, t: TypeVarType) -> Type: return self.replacement return t + def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: + if self.erase_id(t.id): + return self.replacement + return t + def visit_param_spec(self, t: ParamSpecType) -> Type: if self.erase_id(t.id): return self.replacement diff --git a/mypy/expandtype.py b/mypy/expandtype.py index ce43aeaeb6e5..630c809b46ca 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -1,4 +1,4 @@ -from typing import Dict, Iterable, List, TypeVar, Mapping, cast, Union, Optional +from typing import Dict, Iterable, List, TypeVar, Mapping, cast, Union, Optional, Sequence from mypy.types import ( Type, Instance, CallableType, TypeVisitor, UnboundType, AnyType, @@ -6,8 +6,9 @@ ErasedType, PartialType, DeletedType, UninhabitedType, TypeType, TypeVarId, FunctionLike, TypeVarType, LiteralType, get_proper_type, ProperType, TypeAliasType, ParamSpecType, TypeVarLikeType, Parameters, ParamSpecFlavor, - UnpackType, TypeVarTupleType + UnpackType, TypeVarTupleType, TypeList ) +from mypy.typevartuples import split_with_instance, split_with_prefix_and_suffix def expand_type(typ: Type, env: Mapping[TypeVarId, Type]) -> Type: @@ -26,8 +27,26 @@ def expand_type_by_instance(typ: Type, instance: Instance) -> Type: return typ else: variables: Dict[TypeVarId, Type] = {} - for binder, arg in zip(instance.type.defn.type_vars, instance.args): + if instance.type.has_type_var_tuple_type: + assert instance.type.type_var_tuple_prefix is not None + assert instance.type.type_var_tuple_suffix is not None + + args_prefix, args_middle, args_suffix = split_with_instance(instance) + tvars_prefix, tvars_middle, tvars_suffix = split_with_prefix_and_suffix( + tuple(instance.type.defn.type_vars), + instance.type.type_var_tuple_prefix, + instance.type.type_var_tuple_suffix, + ) + variables = {tvars_middle[0].id: TypeList(list(args_middle))} + instance_args = args_prefix + args_suffix + tvars = tvars_prefix + tvars_suffix + else: + tvars = tuple(instance.type.defn.type_vars) + instance_args = instance.args + + for binder, arg in zip(tvars, instance_args): variables[binder.id] = arg + return expand_type(typ, variables) @@ -46,6 +65,7 @@ def freshen_function_type_vars(callee: F) -> F: if isinstance(v, TypeVarType): tv: TypeVarLikeType = TypeVarType.new_unification_variable(v) elif isinstance(v, TypeVarTupleType): + assert isinstance(v, TypeVarTupleType) tv = TypeVarTupleType.new_unification_variable(v) else: assert isinstance(v, ParamSpecType) @@ -89,8 +109,11 @@ def visit_erased_type(self, t: ErasedType) -> Type: raise RuntimeError() def visit_instance(self, t: Instance) -> Type: - args = self.expand_types(t.args) - return Instance(t.type, args, t.line, t.column) + args = self.expand_types_with_unpack(list(t.args)) + if isinstance(args, list): + return Instance(t.type, args, t.line, t.column) + else: + return args def visit_type_var(self, t: TypeVarType) -> Type: repl = get_proper_type(self.variables.get(t.id, t)) @@ -153,6 +176,8 @@ def expand_unpack(self, t: UnpackType) -> Optional[Union[List[Type], Instance, A repl = get_proper_type(self.variables.get(proper_typ.id, t)) if isinstance(repl, TupleType): return repl.items + if isinstance(repl, TypeList): + return repl.items elif isinstance(repl, Instance) and repl.type.fullname == "builtins.tuple": return repl elif isinstance(repl, AnyType): @@ -166,9 +191,9 @@ def expand_unpack(self, t: UnpackType) -> Optional[Union[List[Type], Instance, A elif isinstance(repl, UninhabitedType): return None else: - raise NotImplementedError(f"Invalid type to expand: {repl}") + raise NotImplementedError(f"Invalid type replacement to expand: {repl}") else: - raise NotImplementedError + raise NotImplementedError(f"Invalid type to expand: {proper_typ}") def visit_parameters(self, t: Parameters) -> Type: return t.copy_modified(arg_types=self.expand_types(t.arg_types)) @@ -211,9 +236,17 @@ def visit_overloaded(self, t: Overloaded) -> Type: items.append(new_item) return Overloaded(items) - def visit_tuple_type(self, t: TupleType) -> Type: - items = [] - for item in t.items: + def expand_types_with_unpack( + self, typs: Sequence[Type] + ) -> Union[List[Type], AnyType, UninhabitedType, Instance]: + """Expands a list of types that has an unpack. + + In corner cases, this can return a type rather than a list, in which case this + indicates use of Any or some error occurred earlier. In this case callers should + simply propagate the resulting type. + """ + items: List[Type] = [] + for item in typs: proper_item = get_proper_type(item) if isinstance(proper_item, UnpackType): unpacked_items = self.expand_unpack(proper_item) @@ -221,7 +254,7 @@ def visit_tuple_type(self, t: TupleType) -> Type: # TODO: better error, something like tuple of unknown? return UninhabitedType() elif isinstance(unpacked_items, Instance): - if len(t.items) == 1: + if len(typs) == 1: return unpacked_items else: assert False, "Invalid unpack of variable length tuple" @@ -231,8 +264,14 @@ def visit_tuple_type(self, t: TupleType) -> Type: items.extend(unpacked_items) else: items.append(proper_item.accept(self)) + return items - return t.copy_modified(items=items) + def visit_tuple_type(self, t: TupleType) -> Type: + items = self.expand_types_with_unpack(t.items) + if isinstance(items, list): + return t.copy_modified(items=items) + else: + return items def visit_typeddict_type(self, t: TypedDictType) -> Type: return t.copy_modified(item_types=self.expand_types(t.items.values())) diff --git a/mypy/nodes.py b/mypy/nodes.py index b7824cdd079b..75ec06583f9b 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2577,6 +2577,7 @@ class is generic then it will be a type constructor of higher kind. 'inferring', 'is_enum', 'fallback_to_any', 'type_vars', 'has_param_spec_type', 'bases', '_promote', 'tuple_type', 'is_named_tuple', 'typeddict_type', 'is_newtype', 'is_intersection', 'metadata', 'alt_promote', + 'has_type_var_tuple_type', 'type_var_tuple_prefix', 'type_var_tuple_suffix' ) _fullname: Bogus[str] # Fully qualified name @@ -2719,6 +2720,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No self.module_name = module_name self.type_vars = [] self.has_param_spec_type = False + self.has_type_var_tuple_type = False self.bases = [] self.mro = [] self._mro_refs = None @@ -2734,6 +2736,8 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No self.inferring = [] self.is_protocol = False self.runtime_protocol = False + self.type_var_tuple_prefix: Optional[int] = None + self.type_var_tuple_suffix: Optional[int] = None self.add_type_vars() self.is_final = False self.is_enum = False @@ -2749,10 +2753,18 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No def add_type_vars(self) -> None: if self.defn.type_vars: - for vd in self.defn.type_vars: + for i, vd in enumerate(self.defn.type_vars): if isinstance(vd, mypy.types.ParamSpecType): self.has_param_spec_type = True + if isinstance(vd, mypy.types.TypeVarTupleType): + assert not self.has_type_var_tuple_type + self.has_type_var_tuple_type = True + 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.py b/mypy/semanal.py index 6bfa6e7783c4..b803de743c2f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1421,6 +1421,11 @@ def analyze_unbound_tvar(self, t: Type) -> Optional[Tuple[str, TypeVarLikeExpr]] # It's bound by our type variable scope return None return unbound.name, sym.node + if sym and isinstance(sym.node, TypeVarTupleExpr): + if sym.fullname and not self.tvar_scope.allow_binding(sym.fullname): + # It's bound by our type variable scope + return None + return unbound.name, sym.node if sym is None or not isinstance(sym.node, TypeVarExpr): return None elif sym.fullname and not self.tvar_scope.allow_binding(sym.fullname): diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a521cb1799d5..de223b5cf95c 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -8,7 +8,8 @@ Instance, TypeVarType, CallableType, TupleType, TypedDictType, UnionType, Overloaded, ErasedType, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance, FunctionLike, TypeOfAny, LiteralType, get_proper_type, TypeAliasType, ParamSpecType, - Parameters, UnpackType, TUPLE_LIKE_INSTANCE_NAMES, TYPED_NAMEDTUPLE_NAMES, TypeVarTupleType, + Parameters, UnpackType, TUPLE_LIKE_INSTANCE_NAMES, TYPED_NAMEDTUPLE_NAMES, + TypeVarTupleType, ProperType ) import mypy.applytype import mypy.constraints @@ -26,6 +27,7 @@ from mypy.typestate import TypeState, SubtypeKind from mypy.options import Options from mypy.state import state +from mypy.typevartuples import split_with_instance, extract_unpack # Flags for detected protocol members IS_SETTABLE: Final = 1 @@ -293,7 +295,72 @@ def visit_instance(self, left: Instance) -> bool: # Map left type to corresponding right instances. t = map_instance_to_supertype(left, right.type) nominal = True - for lefta, righta, tvar in zip(t.args, right.args, right.type.defn.type_vars): + if right.type.has_type_var_tuple_type: + left_prefix, left_middle, left_suffix = ( + split_with_instance(left) + ) + right_prefix, right_middle, right_suffix = ( + split_with_instance(right) + ) + + left_unpacked = extract_unpack( + left_middle + ) + right_unpacked = extract_unpack( + right_middle + ) + + # Helper for case 2 below so we can treat them the same. + def check_mixed( + unpacked_type: ProperType, + compare_to: Tuple[Type, ...] + ) -> bool: + if isinstance(unpacked_type, TypeVarTupleType): + return False + if isinstance(unpacked_type, AnyType): + return True + assert False + + # Case 1: Both are unpacks, in this case we check what is being + # unpacked is the same. + if left_unpacked is not None and right_unpacked is not None: + if not is_equivalent(left_unpacked, right_unpacked): + return False + + # Case 2: Only one of the types is an unpack. + elif left_unpacked is not None and right_unpacked is None: + if not check_mixed(left_unpacked, right_middle): + return False + elif left_unpacked is None and right_unpacked is not None: + if not check_mixed(right_unpacked, left_middle): + return False + + # Case 3: Neither type is an unpack. In this case we just compare + # the items themselves. + else: + if len(left_middle) != len(right_middle): + return False + for left_t, right_t in zip(left_middle, right_middle): + if not is_equivalent(left_t, right_t): + return False + + left_items = t.args[:right.type.type_var_tuple_prefix] + right_items = right.args[:right.type.type_var_tuple_prefix] + if right.type.type_var_tuple_suffix: + left_items += t.args[-right.type.type_var_tuple_suffix:] + right_items += right.args[-right.type.type_var_tuple_suffix:] + + unpack_index = right.type.type_var_tuple_prefix + assert unpack_index is not None + type_params = zip( + left_prefix + right_suffix, + right_prefix + right_suffix, + right.type.defn.type_vars[:unpack_index] + + right.type.defn.type_vars[unpack_index+1:] + ) + else: + type_params = zip(t.args, right.args, right.type.defn.type_vars) + for lefta, righta, tvar in type_params: if isinstance(tvar, TypeVarType): if not self.check_type_parameter(lefta, righta, tvar.variance): nominal = False diff --git a/mypy/test/testsubtypes.py b/mypy/test/testsubtypes.py index 3bfa3e174cfd..c0c49c82c637 100644 --- a/mypy/test/testsubtypes.py +++ b/mypy/test/testsubtypes.py @@ -2,7 +2,7 @@ from mypy.nodes import CONTRAVARIANT, INVARIANT, COVARIANT from mypy.subtypes import is_subtype from mypy.test.typefixture import TypeFixture, InterfaceTypeFixture -from mypy.types import Type +from mypy.types import Type, Instance, UnpackType class SubtypingSuite(Suite): @@ -177,6 +177,46 @@ def test_type_callable_subtyping(self) -> None: self.assert_strict_subtype(self.fx.callable_type(self.fx.a, self.fx.b), self.fx.callable(self.fx.a, self.fx.b)) + def test_type_var_tuple(self) -> None: + self.assert_subtype( + Instance(self.fx.gvi, []), + Instance(self.fx.gvi, []), + ) + self.assert_subtype( + Instance(self.fx.gvi, [self.fx.a, self.fx.b]), + Instance(self.fx.gvi, [self.fx.a, self.fx.b]), + ) + self.assert_not_subtype( + Instance(self.fx.gvi, [self.fx.a, self.fx.b]), + Instance(self.fx.gvi, [self.fx.b, self.fx.a]), + ) + self.assert_not_subtype( + Instance(self.fx.gvi, [self.fx.a, self.fx.b]), + Instance(self.fx.gvi, [self.fx.a]), + ) + + self.assert_subtype( + Instance(self.fx.gvi, [UnpackType(self.fx.ss)]), + Instance(self.fx.gvi, [UnpackType(self.fx.ss)]), + ) + self.assert_not_subtype( + Instance(self.fx.gvi, [UnpackType(self.fx.ss)]), + Instance(self.fx.gvi, [UnpackType(self.fx.us)]), + ) + + self.assert_subtype( + Instance(self.fx.gvi, [UnpackType(self.fx.anyt)]), + Instance(self.fx.gvi, [self.fx.anyt]), + ) + self.assert_not_subtype( + Instance(self.fx.gvi, [UnpackType(self.fx.ss)]), + Instance(self.fx.gvi, []), + ) + self.assert_not_subtype( + Instance(self.fx.gvi, [UnpackType(self.fx.ss)]), + Instance(self.fx.gvi, [self.fx.anyt]), + ) + # IDEA: Maybe add these test cases (they are tested pretty well in type # checker tests already): # * more interface subtyping test cases diff --git a/mypy/test/typefixture.py b/mypy/test/typefixture.py index c8bbf67510a6..f5c47d968ba8 100644 --- a/mypy/test/typefixture.py +++ b/mypy/test/typefixture.py @@ -9,7 +9,7 @@ from mypy.types import ( Type, AnyType, NoneType, Instance, CallableType, TypeVarType, TypeType, UninhabitedType, TypeOfAny, TypeAliasType, UnionType, LiteralType, - TypeVarLikeType + TypeVarLikeType, TypeVarTupleType ) from mypy.nodes import ( TypeInfo, ClassDef, FuncDef, Block, ARG_POS, ARG_OPT, ARG_STAR, SymbolTable, @@ -34,6 +34,9 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, variance: int) -> TypeVarType: return TypeVarType(name, name, id, values, upper_bound, variance) + def make_type_var_tuple(name: str, id: int, upper_bound: Type) -> TypeVarTupleType: + return TypeVarTupleType(name, name, id, upper_bound) + self.t = make_type_var('T', 1, [], self.o, variance) # T`1 (type variable) self.tf = make_type_var('T', -1, [], self.o, variance) # T`-1 (type variable) self.tf2 = make_type_var('T', -2, [], self.o, variance) # T`-2 (type variable) @@ -42,6 +45,10 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, self.sf = make_type_var('S', -2, [], self.o, variance) # S`-2 (type variable) self.sf1 = make_type_var('S', -1, [], self.o, variance) # S`-1 (type variable) + self.ts = make_type_var_tuple('Ts', 1, self.o) # Ts`1 (type var tuple) + self.ss = make_type_var_tuple('Ss', 2, self.o) # Ss`2 (type var tuple) + self.us = make_type_var_tuple('Us', 3, self.o) # Us`3 (type var tuple) + # Simple types self.anyt = AnyType(TypeOfAny.special_form) self.nonet = NoneType() @@ -101,6 +108,10 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, typevars=['S'], variances=[variance], bases=[Instance(self.gi, [self.s1])]) + + self.gvi = self.make_type_info('GV', mro=[self.oi], + typevars=['Ts'], + typevar_tuple_index=0) # list[T] self.std_listi = self.make_type_info('builtins.list', mro=[self.oi], typevars=['T'], @@ -233,6 +244,7 @@ def make_type_info(self, name: str, mro: Optional[List[TypeInfo]] = None, bases: Optional[List[Instance]] = None, typevars: Optional[List[str]] = None, + typevar_tuple_index: Optional[int] = None, variances: Optional[List[int]] = None) -> TypeInfo: """Make a TypeInfo suitable for use in unit tests.""" @@ -248,11 +260,14 @@ def make_type_info(self, name: str, if typevars: v: List[TypeVarLikeType] = [] for id, n in enumerate(typevars, 1): - if variances: - variance = variances[id - 1] + if typevar_tuple_index is not None and id-1 == typevar_tuple_index: + v.append(TypeVarTupleType(n, n, id, self.o)) else: - variance = COVARIANT - v.append(TypeVarType(n, n, id, [], self.o, variance=variance)) + if variances: + variance = variances[id - 1] + else: + variance = COVARIANT + v.append(TypeVarType(n, n, id, [], self.o, variance=variance)) class_def.type_vars = v info = TypeInfo(SymbolTable(), class_def, module_name) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 38970db66747..2700ff10758e 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -482,8 +482,14 @@ def analyze_type_with_type_info( args = instance.args instance.args = (Parameters(args, [ARG_POS] * len(args), [None] * len(args)),) + if info.has_type_var_tuple_type: + # - 1 to allow for the empty type var tuple case. + valid_arg_length = len(instance.args) >= len(info.type_vars) - 1 + else: + valid_arg_length = len(instance.args) == len(info.type_vars) + # Check type argument count. - if len(instance.args) != len(info.type_vars) and not self.defining_alias: + if not valid_arg_length and not self.defining_alias: fix_instance(instance, self.fail, self.note, disallow_any=self.options.disallow_any_generics and not self.is_typeshed_stub, diff --git a/mypy/typevars.py b/mypy/typevars.py index b49194f342e0..bd1c325b4c81 100644 --- a/mypy/typevars.py +++ b/mypy/typevars.py @@ -3,7 +3,10 @@ from mypy.nodes import TypeInfo from mypy.erasetype import erase_typevars -from mypy.types import Instance, TypeVarType, TupleType, Type, TypeOfAny, AnyType, ParamSpecType +from mypy.types import ( + Instance, TypeVarType, TupleType, Type, TypeOfAny, AnyType, ParamSpecType, + TypeVarTupleType, UnpackType, TypeVarLikeType +) def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]: @@ -14,13 +17,17 @@ def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]: tvs: List[Type] = [] # TODO: why do we need to keep both typ.type_vars and typ.defn.type_vars? for i in range(len(typ.defn.type_vars)): - tv = typ.defn.type_vars[i] + tv: Union[TypeVarLikeType, UnpackType] = typ.defn.type_vars[i] # Change the line number if isinstance(tv, TypeVarType): tv = TypeVarType( tv.name, tv.fullname, tv.id, tv.values, tv.upper_bound, tv.variance, line=-1, column=-1, ) + elif isinstance(tv, TypeVarTupleType): + tv = UnpackType(TypeVarTupleType( + tv.name, tv.fullname, tv.id, tv.upper_bound, line=-1, column=-1 + )) else: assert isinstance(tv, ParamSpecType) tv = ParamSpecType(tv.name, tv.fullname, tv.id, tv.flavor, tv.upper_bound, diff --git a/mypy/typevartuples.py b/mypy/typevartuples.py new file mode 100644 index 000000000000..dbbd780f9d56 --- /dev/null +++ b/mypy/typevartuples.py @@ -0,0 +1,55 @@ +"""Helpers for interacting with type var tuples.""" + +from typing import TypeVar, Optional, Tuple, Sequence + +from mypy.types import Instance, UnpackType, ProperType, get_proper_type, Type + + +def find_unpack_in_list(items: Sequence[Type]) -> Optional[int]: + unpack_index: Optional[int] = None + for i, item in enumerate(items): + proper_item = get_proper_type(item) + if isinstance(proper_item, UnpackType): + # We cannot fail here, so we must check this in an earlier + # semanal phase. + # Funky code here avoids mypyc narrowing the type of unpack_index. + old_index = unpack_index + assert old_index is None + # Don't return so that we can also sanity check there is only one. + unpack_index = i + return unpack_index + + +T = TypeVar("T") + + +def split_with_prefix_and_suffix( + types: Tuple[T, ...], + prefix: int, + suffix: int, +) -> Tuple[Tuple[T, ...], Tuple[T, ...], Tuple[T, ...]]: + if suffix: + return (types[:prefix], types[prefix:-suffix], types[-suffix:]) + else: + return (types[:prefix], types[prefix:], ()) + + +def split_with_instance( + typ: Instance +) -> Tuple[Tuple[Type, ...], Tuple[Type, ...], Tuple[Type, ...]]: + assert typ.type.type_var_tuple_prefix is not None + assert typ.type.type_var_tuple_suffix is not None + return split_with_prefix_and_suffix( + typ.args, + typ.type.type_var_tuple_prefix, + typ.type.type_var_tuple_suffix, + ) + + +def extract_unpack(types: Sequence[Type]) -> Optional[ProperType]: + """Given a list of types, extracts either a single type from an unpack, or returns None.""" + if len(types) == 1: + proper_type = get_proper_type(types[0]) + if isinstance(proper_type, UnpackType): + return get_proper_type(proper_type.type) + return None diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index ac7bca34879d..c461aecaa40c 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -93,3 +93,73 @@ args: Tuple[bool, int, str, int, str, object] reveal_type(g(args)) # N: Revealed type is "Tuple[builtins.str, builtins.str, builtins.int]" reveal_type(h(args)) # N: Revealed type is "Tuple[builtins.str, builtins.str, builtins.int]" [builtins fixtures/tuple.pyi] + +[case testTypeVarTupleGenericClassDefn] +from typing import Generic, TypeVar, Tuple +from typing_extensions import TypeVarTuple + +T = TypeVar("T") +Ts = TypeVarTuple("Ts") + +class Variadic(Generic[Ts]): + pass + +class Mixed1(Generic[T, Ts]): + pass + +class Mixed2(Generic[Ts, T]): + pass + +variadic: Variadic[int, str] +reveal_type(variadic) # N: Revealed type is "__main__.Variadic[builtins.int, builtins.str]" + +variadic_single: Variadic[int] +reveal_type(variadic_single) # N: Revealed type is "__main__.Variadic[builtins.int]" + +empty: Variadic[()] +# TODO: fix pretty printer to be better. +reveal_type(empty) # N: Revealed type is "__main__.Variadic" + +m1: Mixed1[int, str, bool] +reveal_type(m1) # N: Revealed type is "__main__.Mixed1[builtins.int, builtins.str, builtins.bool]" + +[builtins fixtures/tuple.pyi] + +[case testTypeVarTupleGenericClassWithFunctions] +from typing import Generic, Tuple, TypeVar +from typing_extensions import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +T = TypeVar("T") +S = TypeVar("S") + +class Variadic(Generic[T, Ts, S]): + pass + +def foo(t: Variadic[int, Unpack[Ts], object]) -> Tuple[int, Unpack[Ts]]: + ... + +v: Variadic[int, str, bool, object] +reveal_type(foo(v)) # N: Revealed type is "Tuple[builtins.int, builtins.str, builtins.bool]" + +[builtins fixtures/tuple.pyi] + +[case testTypeVarTupleGenericClassWithMethods] +from typing import Generic, Tuple, TypeVar +from typing_extensions import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +T = TypeVar("T") +S = TypeVar("S") + +class Variadic(Generic[T, Ts, S]): + def __init__(self, t: Tuple[Unpack[Ts]]) -> None: + ... + + def foo(self, t: int) -> Tuple[int, Unpack[Ts]]: + ... + +v: Variadic[float, str, bool, object] +reveal_type(v.foo(0)) # N: Revealed type is "Tuple[builtins.int, builtins.str, builtins.bool]" + +[builtins fixtures/tuple.pyi]