Skip to content

Commit 15e4de5

Browse files
authored
Implement basic subtyping & inferrence for variadic classes. (python#13105)
This makes several basic testcases for using classes with variadic generics pass. Some pieces of this are left as TODOs to flesh out various edge cases to avoid the diff growing in complexity.
1 parent da0213b commit 15e4de5

13 files changed

+415
-54
lines changed

mypy/checkmember.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Type, Instance, AnyType, TupleType, TypedDictType, CallableType, FunctionLike,
88
TypeVarLikeType, Overloaded, TypeVarType, UnionType, PartialType, TypeOfAny, LiteralType,
99
DeletedType, NoneType, TypeType, has_type_vars, get_proper_type, ProperType, ParamSpecType,
10-
ENUM_REMOVED_PROPS
10+
TypeVarTupleType, ENUM_REMOVED_PROPS
1111
)
1212
from mypy.nodes import (
1313
TypeInfo, FuncBase, Var, FuncDef, SymbolNode, SymbolTable, Context,
@@ -693,6 +693,7 @@ def f(self: S) -> T: ...
693693
new_items = []
694694
if is_classmethod:
695695
dispatched_arg_type = TypeType.make_normalized(dispatched_arg_type)
696+
696697
for item in items:
697698
if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR):
698699
# No positional first (self) argument (*args is okay).
@@ -701,12 +702,14 @@ def f(self: S) -> T: ...
701702
# there is at least one such error.
702703
return functype
703704
else:
704-
selfarg = item.arg_types[0]
705+
selfarg = get_proper_type(item.arg_types[0])
705706
if subtypes.is_subtype(dispatched_arg_type, erase_typevars(erase_to_bound(selfarg))):
706707
new_items.append(item)
707708
elif isinstance(selfarg, ParamSpecType):
708709
# TODO: This is not always right. What's the most reasonable thing to do here?
709710
new_items.append(item)
711+
elif isinstance(selfarg, TypeVarTupleType):
712+
raise NotImplementedError
710713
if not new_items:
711714
# Choose first item for the message (it may be not very helpful for overloads).
712715
msg.incompatible_self_argument(name, dispatched_arg_type, items[0],

mypy/constraints.py

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
UninhabitedType, TypeType, TypeVarId, TypeQuery, is_named_instance, TypeOfAny, LiteralType,
1010
ProperType, ParamSpecType, get_proper_type, TypeAliasType, is_union_with_any,
1111
UnpackType, callable_with_ellipsis, Parameters, TUPLE_LIKE_INSTANCE_NAMES, TypeVarTupleType,
12+
TypeList,
1213
)
1314
from mypy.maptype import map_instance_to_supertype
1415
import mypy.subtypes
@@ -18,6 +19,12 @@
1819
from mypy.nodes import COVARIANT, CONTRAVARIANT, ArgKind
1920
from mypy.argmap import ArgTypeExpander
2021
from mypy.typestate import TypeState
22+
from mypy.typevartuples import (
23+
split_with_instance,
24+
split_with_prefix_and_suffix,
25+
extract_unpack,
26+
find_unpack_in_list,
27+
)
2128

2229
if TYPE_CHECKING:
2330
from mypy.infer import ArgumentInferContext
@@ -486,15 +493,60 @@ def visit_instance(self, template: Instance) -> List[Constraint]:
486493
res.append(Constraint(mapped_arg.id, SUPERTYPE_OF, suffix))
487494
elif isinstance(suffix, ParamSpecType):
488495
res.append(Constraint(mapped_arg.id, SUPERTYPE_OF, suffix))
496+
elif isinstance(tvar, TypeVarTupleType):
497+
raise NotImplementedError
489498

490499
return res
491500
elif (self.direction == SUPERTYPE_OF and
492501
instance.type.has_base(template.type.fullname)):
493502
mapped = map_instance_to_supertype(instance, template.type)
494503
tvars = template.type.defn.type_vars
504+
if template.type.has_type_var_tuple_type:
505+
mapped_prefix, mapped_middle, mapped_suffix = (
506+
split_with_instance(mapped)
507+
)
508+
template_prefix, template_middle, template_suffix = (
509+
split_with_instance(template)
510+
)
511+
512+
# Add a constraint for the type var tuple, and then
513+
# remove it for the case below.
514+
template_unpack = extract_unpack(template_middle)
515+
if template_unpack is not None:
516+
if isinstance(template_unpack, TypeVarTupleType):
517+
res.append(Constraint(
518+
template_unpack.id,
519+
SUPERTYPE_OF,
520+
TypeList(list(mapped_middle))
521+
))
522+
elif (
523+
isinstance(template_unpack, Instance) and
524+
template_unpack.type.fullname == "builtins.tuple"
525+
):
526+
# TODO: check homogenous tuple case
527+
raise NotImplementedError
528+
elif isinstance(template_unpack, TupleType):
529+
# TODO: check tuple case
530+
raise NotImplementedError
531+
532+
mapped_args = mapped_prefix + mapped_suffix
533+
template_args = template_prefix + template_suffix
534+
535+
assert template.type.type_var_tuple_prefix is not None
536+
assert template.type.type_var_tuple_suffix is not None
537+
tvars_prefix, _, tvars_suffix = split_with_prefix_and_suffix(
538+
tuple(tvars),
539+
template.type.type_var_tuple_prefix,
540+
template.type.type_var_tuple_suffix,
541+
)
542+
tvars = list(tvars_prefix + tvars_suffix)
543+
else:
544+
mapped_args = mapped.args
545+
template_args = template.args
495546
# N.B: We use zip instead of indexing because the lengths might have
496547
# mismatches during daemon reprocessing.
497-
for tvar, mapped_arg, template_arg in zip(tvars, mapped.args, template.args):
548+
for tvar, mapped_arg, template_arg in zip(tvars, mapped_args, template_args):
549+
assert not isinstance(tvar, TypeVarTupleType)
498550
if isinstance(tvar, TypeVarType):
499551
# The constraints for generic type parameters depend on variance.
500552
# Include constraints from both directions if invariant.
@@ -573,6 +625,8 @@ def visit_instance(self, template: Instance) -> List[Constraint]:
573625
return []
574626
elif isinstance(actual, ParamSpecType):
575627
return infer_constraints(template, actual.upper_bound, self.direction)
628+
elif isinstance(actual, TypeVarTupleType):
629+
raise NotImplementedError
576630
else:
577631
return []
578632

@@ -696,13 +750,12 @@ def infer_against_overloaded(self, overloaded: Overloaded,
696750

697751
def visit_tuple_type(self, template: TupleType) -> List[Constraint]:
698752
actual = self.actual
699-
# TODO: Support other items in the tuple besides Unpack
700753
# TODO: Support subclasses of Tuple
701754
is_varlength_tuple = (
702755
isinstance(actual, Instance)
703756
and actual.type.fullname == "builtins.tuple"
704757
)
705-
unpack_index = find_unpack_in_tuple(template)
758+
unpack_index = find_unpack_in_list(template.items)
706759

707760
if unpack_index is not None:
708761
unpack_item = get_proper_type(template.items[unpack_index])
@@ -727,16 +780,15 @@ def visit_tuple_type(self, template: TupleType) -> List[Constraint]:
727780
modified_actual = actual
728781
if isinstance(actual, TupleType):
729782
# Exclude the items from before and after the unpack index.
730-
head = unpack_index
731-
tail = len(template.items) - unpack_index - 1
732-
if tail:
733-
modified_actual = actual.copy_modified(
734-
items=actual.items[head:-tail],
735-
)
736-
else:
737-
modified_actual = actual.copy_modified(
738-
items=actual.items[head:],
739-
)
783+
# TODO: Support including constraints from the prefix/suffix.
784+
_, actual_items, _ = split_with_prefix_and_suffix(
785+
tuple(actual.items),
786+
unpack_index,
787+
len(template.items) - unpack_index - 1,
788+
)
789+
modified_actual = actual.copy_modified(
790+
items=list(actual_items)
791+
)
740792
return [Constraint(
741793
type_var=unpacked_type.id,
742794
op=self.direction,
@@ -854,18 +906,3 @@ def find_matching_overload_items(overloaded: Overloaded,
854906
# it maintains backward compatibility.
855907
res = items[:]
856908
return res
857-
858-
859-
def find_unpack_in_tuple(t: TupleType) -> Optional[int]:
860-
unpack_index: Optional[int] = None
861-
for i, item in enumerate(t.items):
862-
proper_item = get_proper_type(item)
863-
if isinstance(proper_item, UnpackType):
864-
# We cannot fail here, so we must check this in an earlier
865-
# semanal phase.
866-
# Funky code here avoids mypyc narrowing the type of unpack_index.
867-
old_index = unpack_index
868-
assert old_index is None
869-
# Don't return so that we can also sanity check there is only one.
870-
unpack_index = i
871-
return unpack_index

mypy/erasetype.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ def visit_type_var(self, t: TypeVarType) -> Type:
137137
return self.replacement
138138
return t
139139

140+
def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type:
141+
if self.erase_id(t.id):
142+
return self.replacement
143+
return t
144+
140145
def visit_param_spec(self, t: ParamSpecType) -> Type:
141146
if self.erase_id(t.id):
142147
return self.replacement

mypy/expandtype.py

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
from typing import Dict, Iterable, List, TypeVar, Mapping, cast, Union, Optional
1+
from typing import Dict, Iterable, List, TypeVar, Mapping, cast, Union, Optional, Sequence
22

33
from mypy.types import (
44
Type, Instance, CallableType, TypeVisitor, UnboundType, AnyType,
55
NoneType, Overloaded, TupleType, TypedDictType, UnionType,
66
ErasedType, PartialType, DeletedType, UninhabitedType, TypeType, TypeVarId,
77
FunctionLike, TypeVarType, LiteralType, get_proper_type, ProperType,
88
TypeAliasType, ParamSpecType, TypeVarLikeType, Parameters, ParamSpecFlavor,
9-
UnpackType, TypeVarTupleType
9+
UnpackType, TypeVarTupleType, TypeList
1010
)
11+
from mypy.typevartuples import split_with_instance, split_with_prefix_and_suffix
1112

1213

1314
def expand_type(typ: Type, env: Mapping[TypeVarId, Type]) -> Type:
@@ -26,8 +27,26 @@ def expand_type_by_instance(typ: Type, instance: Instance) -> Type:
2627
return typ
2728
else:
2829
variables: Dict[TypeVarId, Type] = {}
29-
for binder, arg in zip(instance.type.defn.type_vars, instance.args):
30+
if instance.type.has_type_var_tuple_type:
31+
assert instance.type.type_var_tuple_prefix is not None
32+
assert instance.type.type_var_tuple_suffix is not None
33+
34+
args_prefix, args_middle, args_suffix = split_with_instance(instance)
35+
tvars_prefix, tvars_middle, tvars_suffix = split_with_prefix_and_suffix(
36+
tuple(instance.type.defn.type_vars),
37+
instance.type.type_var_tuple_prefix,
38+
instance.type.type_var_tuple_suffix,
39+
)
40+
variables = {tvars_middle[0].id: TypeList(list(args_middle))}
41+
instance_args = args_prefix + args_suffix
42+
tvars = tvars_prefix + tvars_suffix
43+
else:
44+
tvars = tuple(instance.type.defn.type_vars)
45+
instance_args = instance.args
46+
47+
for binder, arg in zip(tvars, instance_args):
3048
variables[binder.id] = arg
49+
3150
return expand_type(typ, variables)
3251

3352

@@ -46,6 +65,7 @@ def freshen_function_type_vars(callee: F) -> F:
4665
if isinstance(v, TypeVarType):
4766
tv: TypeVarLikeType = TypeVarType.new_unification_variable(v)
4867
elif isinstance(v, TypeVarTupleType):
68+
assert isinstance(v, TypeVarTupleType)
4969
tv = TypeVarTupleType.new_unification_variable(v)
5070
else:
5171
assert isinstance(v, ParamSpecType)
@@ -89,8 +109,11 @@ def visit_erased_type(self, t: ErasedType) -> Type:
89109
raise RuntimeError()
90110

91111
def visit_instance(self, t: Instance) -> Type:
92-
args = self.expand_types(t.args)
93-
return Instance(t.type, args, t.line, t.column)
112+
args = self.expand_types_with_unpack(list(t.args))
113+
if isinstance(args, list):
114+
return Instance(t.type, args, t.line, t.column)
115+
else:
116+
return args
94117

95118
def visit_type_var(self, t: TypeVarType) -> Type:
96119
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
153176
repl = get_proper_type(self.variables.get(proper_typ.id, t))
154177
if isinstance(repl, TupleType):
155178
return repl.items
179+
if isinstance(repl, TypeList):
180+
return repl.items
156181
elif isinstance(repl, Instance) and repl.type.fullname == "builtins.tuple":
157182
return repl
158183
elif isinstance(repl, AnyType):
@@ -166,9 +191,9 @@ def expand_unpack(self, t: UnpackType) -> Optional[Union[List[Type], Instance, A
166191
elif isinstance(repl, UninhabitedType):
167192
return None
168193
else:
169-
raise NotImplementedError(f"Invalid type to expand: {repl}")
194+
raise NotImplementedError(f"Invalid type replacement to expand: {repl}")
170195
else:
171-
raise NotImplementedError
196+
raise NotImplementedError(f"Invalid type to expand: {proper_typ}")
172197

173198
def visit_parameters(self, t: Parameters) -> Type:
174199
return t.copy_modified(arg_types=self.expand_types(t.arg_types))
@@ -211,17 +236,25 @@ def visit_overloaded(self, t: Overloaded) -> Type:
211236
items.append(new_item)
212237
return Overloaded(items)
213238

214-
def visit_tuple_type(self, t: TupleType) -> Type:
215-
items = []
216-
for item in t.items:
239+
def expand_types_with_unpack(
240+
self, typs: Sequence[Type]
241+
) -> Union[List[Type], AnyType, UninhabitedType, Instance]:
242+
"""Expands a list of types that has an unpack.
243+
244+
In corner cases, this can return a type rather than a list, in which case this
245+
indicates use of Any or some error occurred earlier. In this case callers should
246+
simply propagate the resulting type.
247+
"""
248+
items: List[Type] = []
249+
for item in typs:
217250
proper_item = get_proper_type(item)
218251
if isinstance(proper_item, UnpackType):
219252
unpacked_items = self.expand_unpack(proper_item)
220253
if unpacked_items is None:
221254
# TODO: better error, something like tuple of unknown?
222255
return UninhabitedType()
223256
elif isinstance(unpacked_items, Instance):
224-
if len(t.items) == 1:
257+
if len(typs) == 1:
225258
return unpacked_items
226259
else:
227260
assert False, "Invalid unpack of variable length tuple"
@@ -231,8 +264,14 @@ def visit_tuple_type(self, t: TupleType) -> Type:
231264
items.extend(unpacked_items)
232265
else:
233266
items.append(proper_item.accept(self))
267+
return items
234268

235-
return t.copy_modified(items=items)
269+
def visit_tuple_type(self, t: TupleType) -> Type:
270+
items = self.expand_types_with_unpack(t.items)
271+
if isinstance(items, list):
272+
return t.copy_modified(items=items)
273+
else:
274+
return items
236275

237276
def visit_typeddict_type(self, t: TypedDictType) -> Type:
238277
return t.copy_modified(item_types=self.expand_types(t.items.values()))

mypy/nodes.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2577,6 +2577,7 @@ class is generic then it will be a type constructor of higher kind.
25772577
'inferring', 'is_enum', 'fallback_to_any', 'type_vars', 'has_param_spec_type',
25782578
'bases', '_promote', 'tuple_type', 'is_named_tuple', 'typeddict_type',
25792579
'is_newtype', 'is_intersection', 'metadata', 'alt_promote',
2580+
'has_type_var_tuple_type', 'type_var_tuple_prefix', 'type_var_tuple_suffix'
25802581
)
25812582

25822583
_fullname: Bogus[str] # Fully qualified name
@@ -2719,6 +2720,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No
27192720
self.module_name = module_name
27202721
self.type_vars = []
27212722
self.has_param_spec_type = False
2723+
self.has_type_var_tuple_type = False
27222724
self.bases = []
27232725
self.mro = []
27242726
self._mro_refs = None
@@ -2734,6 +2736,8 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No
27342736
self.inferring = []
27352737
self.is_protocol = False
27362738
self.runtime_protocol = False
2739+
self.type_var_tuple_prefix: Optional[int] = None
2740+
self.type_var_tuple_suffix: Optional[int] = None
27372741
self.add_type_vars()
27382742
self.is_final = False
27392743
self.is_enum = False
@@ -2749,10 +2753,18 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No
27492753

27502754
def add_type_vars(self) -> None:
27512755
if self.defn.type_vars:
2752-
for vd in self.defn.type_vars:
2756+
for i, vd in enumerate(self.defn.type_vars):
27532757
if isinstance(vd, mypy.types.ParamSpecType):
27542758
self.has_param_spec_type = True
2759+
if isinstance(vd, mypy.types.TypeVarTupleType):
2760+
assert not self.has_type_var_tuple_type
2761+
self.has_type_var_tuple_type = True
2762+
self.type_var_tuple_prefix = i
2763+
self.type_var_tuple_suffix = len(self.defn.type_vars) - i - 1
27552764
self.type_vars.append(vd.name)
2765+
assert not (
2766+
self.has_param_spec_type and self.has_type_var_tuple_type
2767+
), "Mixing type var tuples and param specs not supported yet"
27562768

27572769
@property
27582770
def name(self) -> str:

mypy/semanal.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,11 @@ def analyze_unbound_tvar(self, t: Type) -> Optional[Tuple[str, TypeVarLikeExpr]]
14211421
# It's bound by our type variable scope
14221422
return None
14231423
return unbound.name, sym.node
1424+
if sym and isinstance(sym.node, TypeVarTupleExpr):
1425+
if sym.fullname and not self.tvar_scope.allow_binding(sym.fullname):
1426+
# It's bound by our type variable scope
1427+
return None
1428+
return unbound.name, sym.node
14241429
if sym is None or not isinstance(sym.node, TypeVarExpr):
14251430
return None
14261431
elif sym.fullname and not self.tvar_scope.allow_binding(sym.fullname):

0 commit comments

Comments
 (0)