Skip to content

Commit 26f95f3

Browse files
committed
Implement basic semantic analysis for variadic classes.
1 parent fa7bdd1 commit 26f95f3

13 files changed

+410
-54
lines changed

mypy/checkmember.py

+5-2
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

+62-28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Type inference constraints."""
22

3-
from typing import TYPE_CHECKING, Iterable, List, Optional, Sequence
3+
from typing import TYPE_CHECKING, Iterable, List, Optional, Sequence, Tuple, TypeVar
44
from typing_extensions import Final
55

66
from mypy.types import (
@@ -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,57 @@ 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+
assert NotImplementedError
528+
529+
mapped_args = mapped_prefix + mapped_suffix
530+
template_args = template_prefix + template_suffix
531+
532+
assert template.type.type_var_tuple_prefix is not None
533+
assert template.type.type_var_tuple_suffix is not None
534+
tvars_prefix, _, tvars_suffix = split_with_prefix_and_suffix(
535+
tuple(tvars),
536+
template.type.type_var_tuple_prefix,
537+
template.type.type_var_tuple_suffix,
538+
)
539+
tvars = list(tvars_prefix + tvars_suffix)
540+
else:
541+
mapped_args = mapped.args
542+
template_args = template.args
495543
# N.B: We use zip instead of indexing because the lengths might have
496544
# mismatches during daemon reprocessing.
497-
for tvar, mapped_arg, template_arg in zip(tvars, mapped.args, template.args):
545+
for tvar, mapped_arg, template_arg in zip(tvars, mapped_args, template_args):
546+
assert not isinstance(tvar, TypeVarTupleType)
498547
if isinstance(tvar, TypeVarType):
499548
# The constraints for generic type parameters depend on variance.
500549
# Include constraints from both directions if invariant.
@@ -573,6 +622,8 @@ def visit_instance(self, template: Instance) -> List[Constraint]:
573622
return []
574623
elif isinstance(actual, ParamSpecType):
575624
return infer_constraints(template, actual.upper_bound, self.direction)
625+
elif isinstance(actual, TypeVarTupleType):
626+
raise NotImplementedError
576627
else:
577628
return []
578629

@@ -702,7 +753,7 @@ def visit_tuple_type(self, template: TupleType) -> List[Constraint]:
702753
isinstance(actual, Instance)
703754
and actual.type.fullname == "builtins.tuple"
704755
)
705-
unpack_index = find_unpack_in_tuple(template)
756+
unpack_index = find_unpack_in_list(template.items)
706757

707758
if unpack_index is not None:
708759
unpack_item = get_proper_type(template.items[unpack_index])
@@ -727,16 +778,14 @@ def visit_tuple_type(self, template: TupleType) -> List[Constraint]:
727778
modified_actual = actual
728779
if isinstance(actual, TupleType):
729780
# 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-
)
781+
_, actual_items, _ = split_with_prefix_and_suffix(
782+
tuple(actual.items),
783+
unpack_index,
784+
len(template.items) - unpack_index - 1,
785+
)
786+
modified_actual = actual.copy_modified(
787+
items=list(actual_items)
788+
)
740789
return [Constraint(
741790
type_var=unpacked_type.id,
742791
op=self.direction,
@@ -854,18 +903,3 @@ def find_matching_overload_items(overloaded: Overloaded,
854903
# it maintains backward compatibility.
855904
res = items[:]
856905
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

+5
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

+52-12
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,27 @@ 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+
import mypy.constraints
32+
assert instance.type.type_var_tuple_prefix is not None
33+
assert instance.type.type_var_tuple_suffix is not None
34+
35+
args_prefix, args_middle, args_suffix = split_with_instance(instance)
36+
tvars_prefix, tvars_middle, tvars_suffix = split_with_prefix_and_suffix(
37+
tuple(instance.type.defn.type_vars),
38+
instance.type.type_var_tuple_prefix,
39+
instance.type.type_var_tuple_suffix,
40+
)
41+
variables = {tvars_middle[0].id: TypeList(list(args_middle))}
42+
instance_args = args_prefix + args_suffix
43+
tvars = tvars_prefix + tvars_suffix
44+
else:
45+
tvars = tuple(instance.type.defn.type_vars)
46+
instance_args = instance.args
47+
48+
for binder, arg in zip(tvars, instance_args):
3049
variables[binder.id] = arg
50+
3151
return expand_type(typ, variables)
3252

3353

@@ -46,6 +66,7 @@ def freshen_function_type_vars(callee: F) -> F:
4666
if isinstance(v, TypeVarType):
4767
tv: TypeVarLikeType = TypeVarType.new_unification_variable(v)
4868
elif isinstance(v, TypeVarTupleType):
69+
assert isinstance(v, TypeVarTupleType)
4970
tv = TypeVarTupleType.new_unification_variable(v)
5071
else:
5172
assert isinstance(v, ParamSpecType)
@@ -89,8 +110,11 @@ def visit_erased_type(self, t: ErasedType) -> Type:
89110
raise RuntimeError()
90111

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

95119
def visit_type_var(self, t: TypeVarType) -> Type:
96120
repl = get_proper_type(self.variables.get(t.id, t))
@@ -153,6 +177,8 @@ def expand_unpack(self, t: UnpackType) -> Optional[Union[List[Type], Instance, A
153177
repl = get_proper_type(self.variables.get(proper_typ.id, t))
154178
if isinstance(repl, TupleType):
155179
return repl.items
180+
if isinstance(repl, TypeList):
181+
return repl.items
156182
elif isinstance(repl, Instance) and repl.type.fullname == "builtins.tuple":
157183
return repl
158184
elif isinstance(repl, AnyType):
@@ -166,9 +192,9 @@ def expand_unpack(self, t: UnpackType) -> Optional[Union[List[Type], Instance, A
166192
elif isinstance(repl, UninhabitedType):
167193
return None
168194
else:
169-
raise NotImplementedError(f"Invalid type to expand: {repl}")
195+
raise NotImplementedError(f"Invalid type replacement to expand: {repl}")
170196
else:
171-
raise NotImplementedError
197+
raise NotImplementedError(f"Invalid type to expand: {proper_typ}")
172198

173199
def visit_parameters(self, t: Parameters) -> Type:
174200
return t.copy_modified(arg_types=self.expand_types(t.arg_types))
@@ -211,17 +237,25 @@ def visit_overloaded(self, t: Overloaded) -> Type:
211237
items.append(new_item)
212238
return Overloaded(items)
213239

214-
def visit_tuple_type(self, t: TupleType) -> Type:
215-
items = []
216-
for item in t.items:
240+
def expand_types_with_unpack(
241+
self, typs: Sequence[Type]
242+
) -> Union[List[Type], AnyType, UninhabitedType, Instance]:
243+
"""Expands a list of types that has an unpack.
244+
245+
In corner cases, this can return a type rather than a list, in which case this
246+
indicates use of Any or some error occurred earlier. In this case callers should
247+
simply propagate the resulting type.
248+
"""
249+
items: List[Type] = []
250+
for item in typs:
217251
proper_item = get_proper_type(item)
218252
if isinstance(proper_item, UnpackType):
219253
unpacked_items = self.expand_unpack(proper_item)
220254
if unpacked_items is None:
221255
# TODO: better error, something like tuple of unknown?
222256
return UninhabitedType()
223257
elif isinstance(unpacked_items, Instance):
224-
if len(t.items) == 1:
258+
if len(typs) == 1:
225259
return unpacked_items
226260
else:
227261
assert False, "Invalid unpack of variable length tuple"
@@ -231,8 +265,14 @@ def visit_tuple_type(self, t: TupleType) -> Type:
231265
items.extend(unpacked_items)
232266
else:
233267
items.append(proper_item.accept(self))
268+
return items
234269

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

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

mypy/nodes.py

+13-1
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

+5
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)