Skip to content

Commit d64efcd

Browse files
authored
Implement basic typevartuple constraints/inference (#12688)
Implements basic inference for TypeVarTuple along with various check tests verifying uses of TypeVarTuple with functions works reasonably.
1 parent a3abd36 commit d64efcd

File tree

8 files changed

+122
-8
lines changed

8 files changed

+122
-8
lines changed

mypy/applytype.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from mypy.expandtype import expand_type
66
from mypy.types import (
77
Type, TypeVarId, TypeVarType, CallableType, AnyType, PartialType, get_proper_types,
8-
TypeVarLikeType, ProperType, ParamSpecType, Parameters, get_proper_type
8+
TypeVarLikeType, ProperType, ParamSpecType, Parameters, get_proper_type,
9+
TypeVarTupleType,
910
)
1011
from mypy.nodes import Context
1112

@@ -20,6 +21,8 @@ def get_target_type(
2021
) -> Optional[Type]:
2122
if isinstance(tvar, ParamSpecType):
2223
return type
24+
if isinstance(tvar, TypeVarTupleType):
25+
return type
2326
assert isinstance(tvar, TypeVarType)
2427
values = get_proper_types(tvar.values)
2528
if values:

mypy/constraints.py

+21
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,27 @@ def infer_against_overloaded(self, overloaded: Overloaded,
694694

695695
def visit_tuple_type(self, template: TupleType) -> List[Constraint]:
696696
actual = self.actual
697+
# TODO: Support other items in the tuple besides Unpack
698+
# TODO: Support subclasses of Tuple
699+
is_varlength_tuple = (
700+
isinstance(actual, Instance)
701+
and actual.type.fullname == "builtins.tuple"
702+
)
703+
if len(template.items) == 1:
704+
item = get_proper_type(template.items[0])
705+
if isinstance(item, UnpackType):
706+
unpacked_type = get_proper_type(item.type)
707+
if isinstance(unpacked_type, TypeVarTupleType):
708+
if (
709+
isinstance(actual, (TupleType, AnyType))
710+
or is_varlength_tuple
711+
):
712+
return [Constraint(
713+
type_var=unpacked_type.id,
714+
op=self.direction,
715+
target=actual,
716+
)]
717+
697718
if isinstance(actual, TupleType) and len(actual.items) == len(template.items):
698719
res: List[Constraint] = []
699720
for i in range(len(template.items)):

mypy/expandtype.py

+54-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, Iterable, List, TypeVar, Mapping, cast
1+
from typing import Dict, Iterable, List, TypeVar, Mapping, cast, Union, Optional
22

33
from mypy.types import (
44
Type, Instance, CallableType, TypeVisitor, UnboundType, AnyType,
@@ -45,6 +45,8 @@ def freshen_function_type_vars(callee: F) -> F:
4545
# TODO(PEP612): fix for ParamSpecType
4646
if isinstance(v, TypeVarType):
4747
tv: TypeVarLikeType = TypeVarType.new_unification_variable(v)
48+
elif isinstance(v, TypeVarTupleType):
49+
tv = TypeVarTupleType.new_unification_variable(v)
4850
else:
4951
assert isinstance(v, ParamSpecType)
5052
tv = ParamSpecType.new_unification_variable(v)
@@ -135,7 +137,36 @@ def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type:
135137
raise NotImplementedError
136138

137139
def visit_unpack_type(self, t: UnpackType) -> Type:
138-
raise NotImplementedError
140+
# It is impossible to reasonally implement visit_unpack_type, because
141+
# unpacking inherently expands to something more like a list of types.
142+
#
143+
# Relevant sections that can call unpack should call expand_unpack()
144+
# instead.
145+
assert False, "Mypy bug: unpacking must happen at a higher level"
146+
147+
def expand_unpack(self, t: UnpackType) -> Optional[Union[List[Type], Instance, AnyType]]:
148+
"""May return either a list of types to unpack to, any, or a single
149+
variable length tuple. The latter may not be valid in all contexts.
150+
"""
151+
proper_typ = get_proper_type(t.type)
152+
if isinstance(proper_typ, TypeVarTupleType):
153+
repl = get_proper_type(self.variables.get(proper_typ.id, t))
154+
if isinstance(repl, TupleType):
155+
return repl.items
156+
elif isinstance(repl, Instance) and repl.type.fullname == "builtins.tuple":
157+
return repl
158+
elif isinstance(repl, AnyType):
159+
# tuple[Any, ...] would be better, but we don't have
160+
# the type info to construct that type here.
161+
return repl
162+
elif isinstance(repl, TypeVarTupleType):
163+
return [UnpackType(typ=repl)]
164+
elif isinstance(repl, UninhabitedType):
165+
return None
166+
else:
167+
raise NotImplementedError("Invalid type to expand: {}".format(repl))
168+
else:
169+
raise NotImplementedError
139170

140171
def visit_parameters(self, t: Parameters) -> Type:
141172
return t.copy_modified(arg_types=self.expand_types(t.arg_types))
@@ -179,7 +210,27 @@ def visit_overloaded(self, t: Overloaded) -> Type:
179210
return Overloaded(items)
180211

181212
def visit_tuple_type(self, t: TupleType) -> Type:
182-
return t.copy_modified(items=self.expand_types(t.items))
213+
items = []
214+
for item in t.items:
215+
proper_item = get_proper_type(item)
216+
if isinstance(proper_item, UnpackType):
217+
unpacked_items = self.expand_unpack(proper_item)
218+
if unpacked_items is None:
219+
# TODO: better error, something like tuple of unknown?
220+
return UninhabitedType()
221+
elif isinstance(unpacked_items, Instance):
222+
if len(t.items) == 1:
223+
return unpacked_items
224+
else:
225+
assert False, "Invalid unpack of variable length tuple"
226+
elif isinstance(unpacked_items, AnyType):
227+
return unpacked_items
228+
else:
229+
items.extend(unpacked_items)
230+
else:
231+
items.append(proper_item.accept(self))
232+
233+
return t.copy_modified(items=items)
183234

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

mypy/subtypes.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,9 @@ def visit_type_var_tuple(self, left: TypeVarTupleType) -> bool:
350350
return self._is_subtype(left.upper_bound, self.right)
351351

352352
def visit_unpack_type(self, left: UnpackType) -> bool:
353-
raise NotImplementedError
353+
if isinstance(self.right, UnpackType):
354+
return self._is_subtype(left.type, self.right.type)
355+
return False
354356

355357
def visit_parameters(self, left: Parameters) -> bool:
356358
right = self.right
@@ -1482,7 +1484,9 @@ def visit_type_var_tuple(self, left: TypeVarTupleType) -> bool:
14821484
return self._is_proper_subtype(left.upper_bound, self.right)
14831485

14841486
def visit_unpack_type(self, left: UnpackType) -> bool:
1485-
raise NotImplementedError
1487+
if isinstance(self.right, UnpackType):
1488+
return self._is_proper_subtype(left.type, self.right.type)
1489+
return False
14861490

14871491
def visit_parameters(self, left: Parameters) -> bool:
14881492
right = self.right

mypy/test/testcheck.py

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
'check-errorcodes.test',
9292
'check-annotated.test',
9393
'check-parameter-specification.test',
94+
'check-typevar-tuple.test',
9495
'check-generic-alias.test',
9596
'check-typeguard.test',
9697
'check-functools.test',
@@ -164,6 +165,7 @@ def run_case_once(self, testcase: DataDrivenTestCase,
164165
# Parse options after moving files (in case mypy.ini is being moved).
165166
options = parse_options(original_program_text, testcase, incremental_step)
166167
options.use_builtins_fixtures = True
168+
options.enable_incomplete_features = True
167169
options.show_traceback = True
168170

169171
# Enable some options automatically based on test file name.

mypy/typeanal.py

-2
Original file line numberDiff line numberDiff line change
@@ -1202,8 +1202,6 @@ def anal_var_def(self, var_def: TypeVarLikeType) -> TypeVarLikeType:
12021202
var_def.variance,
12031203
var_def.line
12041204
)
1205-
elif isinstance(var_def, TypeVarTupleType):
1206-
raise NotImplementedError
12071205
else:
12081206
return var_def
12091207

mypy/types.py

+6
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,12 @@ def __eq__(self, other: object) -> bool:
696696
return NotImplemented
697697
return self.id == other.id
698698

699+
@staticmethod
700+
def new_unification_variable(old: 'TypeVarTupleType') -> 'TypeVarTupleType':
701+
new_id = TypeVarId.new(meta_level=1)
702+
return TypeVarTupleType(old.name, old.fullname, new_id, old.upper_bound,
703+
line=old.line, column=old.column)
704+
699705

700706
class UnboundType(ProperType):
701707
"""Instance type that has not been bound during semantic analysis."""
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[case testTypeVarTupleBasic]
2+
from typing import Any, Tuple
3+
from typing_extensions import Unpack, TypeVarTuple
4+
5+
Ts = TypeVarTuple("Ts")
6+
7+
def f(a: Tuple[Unpack[Ts]]) -> Tuple[Unpack[Ts]]:
8+
return a
9+
10+
any: Any
11+
args: Tuple[int, str] = (1, 'x')
12+
args2: Tuple[bool, str] = (False, 'y')
13+
args3: Tuple[int, str, bool] = (2, 'z', True)
14+
varargs: Tuple[int, ...] = (1, 2, 3)
15+
16+
reveal_type(f(args)) # N: Revealed type is "Tuple[builtins.int, builtins.str]"
17+
18+
reveal_type(f(varargs)) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
19+
20+
f(0) # E: Argument 1 to "f" has incompatible type "int"; expected <nothing>
21+
22+
def g(a: Tuple[Unpack[Ts]], b: Tuple[Unpack[Ts]]) -> Tuple[Unpack[Ts]]:
23+
return a
24+
25+
reveal_type(g(args, args)) # N: Revealed type is "Tuple[builtins.int, builtins.str]"
26+
reveal_type(g(args, args2)) # N: Revealed type is "Tuple[builtins.int, builtins.str]"
27+
reveal_type(g(args, args3)) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
28+
reveal_type(g(any, any)) # N: Revealed type is "Any"
29+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)