Skip to content

Commit da0576e

Browse files
authored
Support star expressions in lists, tuples, sets (#1953)
This fixes #1722 and addresses most of #704 (but doesn't close it, because of {**x}).
1 parent f6ad7bf commit da0576e

File tree

8 files changed

+112
-24
lines changed

8 files changed

+112
-24
lines changed

mypy/checkexpr.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
TupleExpr, DictExpr, FuncExpr, SuperExpr, SliceExpr, Context,
1515
ListComprehension, GeneratorExpr, SetExpr, MypyFile, Decorator,
1616
ConditionalExpr, ComparisonExpr, TempNode, SetComprehension,
17-
DictionaryComprehension, ComplexExpr, EllipsisExpr,
17+
DictionaryComprehension, ComplexExpr, EllipsisExpr, StarExpr,
1818
TypeAliasExpr, BackquoteExpr, ARG_POS, ARG_NAMED, ARG_STAR2
1919
)
2020
from mypy.nodes import function_type
@@ -1286,15 +1286,17 @@ def visit_type_alias_expr(self, alias: TypeAliasExpr) -> Type:
12861286

12871287
def visit_list_expr(self, e: ListExpr) -> Type:
12881288
"""Type check a list expression [...]."""
1289-
return self.check_list_or_set_expr(e.items, 'builtins.list', '<list>',
1290-
e)
1289+
return self.check_lst_expr(e.items, 'builtins.list', '<list>', e)
12911290

12921291
def visit_set_expr(self, e: SetExpr) -> Type:
1293-
return self.check_list_or_set_expr(e.items, 'builtins.set', '<set>', e)
1292+
return self.check_lst_expr(e.items, 'builtins.set', '<set>', e)
12941293

1295-
def check_list_or_set_expr(self, items: List[Node], fullname: str,
1296-
tag: str, context: Context) -> Type:
1294+
def check_lst_expr(self, items: List[Node], fullname: str,
1295+
tag: str, context: Context) -> Type:
12971296
# Translate into type checking a generic function call.
1297+
# Used for list and set expressions, as well as for tuples
1298+
# containing star expressions that don't refer to a
1299+
# Tuple. (Note: "lst" stands for list-set-tuple. :-)
12981300
tvdef = TypeVarDef('T', -1, [], self.chk.object_type())
12991301
tv = TypeVarType(tvdef)
13001302
constructor = CallableType(
@@ -1306,28 +1308,54 @@ def check_list_or_set_expr(self, items: List[Node], fullname: str,
13061308
name=tag,
13071309
variables=[tvdef])
13081310
return self.check_call(constructor,
1309-
items,
1310-
[nodes.ARG_POS] * len(items), context)[0]
1311+
[(i.expr if isinstance(i, StarExpr) else i)
1312+
for i in items],
1313+
[(nodes.ARG_STAR if isinstance(i, StarExpr) else nodes.ARG_POS)
1314+
for i in items],
1315+
context)[0]
13111316

13121317
def visit_tuple_expr(self, e: TupleExpr) -> Type:
13131318
"""Type check a tuple expression."""
13141319
ctx = None # type: TupleType
13151320
# Try to determine type context for type inference.
13161321
if isinstance(self.chk.type_context[-1], TupleType):
13171322
t = self.chk.type_context[-1]
1318-
if len(t.items) == len(e.items):
1319-
ctx = t
1320-
# Infer item types.
1323+
ctx = t
1324+
# NOTE: it's possible for the context to have a different
1325+
# number of items than e. In that case we use those context
1326+
# items that match a position in e, and we'll worry about type
1327+
# mismatches later.
1328+
1329+
# Infer item types. Give up if there's a star expression
1330+
# that's not a Tuple.
13211331
items = [] # type: List[Type]
1332+
j = 0 # Index into ctx.items; irrelevant if ctx is None.
13221333
for i in range(len(e.items)):
13231334
item = e.items[i]
13241335
tt = None # type: Type
1325-
if not ctx:
1326-
tt = self.accept(item)
1336+
if isinstance(item, StarExpr):
1337+
# Special handling for star expressions.
1338+
# TODO: If there's a context, and item.expr is a
1339+
# TupleExpr, flatten it, so we can benefit from the
1340+
# context? Counterargument: Why would anyone write
1341+
# (1, *(2, 3)) instead of (1, 2, 3) except in a test?
1342+
tt = self.accept(item.expr)
1343+
self.check_not_void(tt, e)
1344+
if isinstance(tt, TupleType):
1345+
items.extend(tt.items)
1346+
j += len(tt.items)
1347+
else:
1348+
# A star expression that's not a Tuple.
1349+
# Treat the whole thing as a variable-length tuple.
1350+
return self.check_lst_expr(e.items, 'builtins.tuple', '<tuple>', e)
13271351
else:
1328-
tt = self.accept(item, ctx.items[i])
1329-
self.check_not_void(tt, e)
1330-
items.append(tt)
1352+
if not ctx or j >= len(ctx.items):
1353+
tt = self.accept(item)
1354+
else:
1355+
tt = self.accept(item, ctx.items[j])
1356+
j += 1
1357+
self.check_not_void(tt, e)
1358+
items.append(tt)
13311359
fallback_item = join.join_type_list(items)
13321360
return TupleType(items, self.chk.named_generic_type('builtins.tuple', [fallback_item]))
13331361

mypy/semanal.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,14 +1919,20 @@ def visit_super_expr(self, expr: SuperExpr) -> None:
19191919

19201920
def visit_tuple_expr(self, expr: TupleExpr) -> None:
19211921
for item in expr.items:
1922+
if isinstance(item, StarExpr):
1923+
item.valid = True
19221924
item.accept(self)
19231925

19241926
def visit_list_expr(self, expr: ListExpr) -> None:
19251927
for item in expr.items:
1928+
if isinstance(item, StarExpr):
1929+
item.valid = True
19261930
item.accept(self)
19271931

19281932
def visit_set_expr(self, expr: SetExpr) -> None:
19291933
for item in expr.items:
1934+
if isinstance(item, StarExpr):
1935+
item.valid = True
19301936
item.accept(self)
19311937

19321938
def visit_dict_expr(self, expr: DictExpr) -> None:
@@ -1936,6 +1942,7 @@ def visit_dict_expr(self, expr: DictExpr) -> None:
19361942

19371943
def visit_star_expr(self, expr: StarExpr) -> None:
19381944
if not expr.valid:
1945+
# XXX TODO Change this error message
19391946
self.fail('Can use starred expression only as assignment target', expr)
19401947
else:
19411948
expr.expr.accept(self)

test-data/unit/check-inference.test

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,14 @@ s = s_i()
809809
s = s_s() # E: Incompatible types in assignment (expression has type Set[str], variable has type Set[int])
810810
[builtins fixtures/set.py]
811811

812+
[case testSetWithStarExpr]
813+
# options: fast_parser
814+
s = {1, 2, *(3, 4)}
815+
t = {1, 2, *s}
816+
reveal_type(s) # E: Revealed type is 'builtins.set[builtins.int*]'
817+
reveal_type(t) # E: Revealed type is 'builtins.set[builtins.int*]'
818+
[builtins fixtures/set.py]
819+
812820

813821
-- For statements
814822
-- --------------

test-data/unit/check-lists.test

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ class C: pass
6363
[out]
6464
main: note: In function "f":
6565

66-
[case testListContainingStarExpr]
67-
a = [1, *[2]] # E: Can use starred expression only as assignment target
66+
[case testListWithStarExpr]
67+
(x, *a) = [1, 2, 3]
68+
a = [1, *[2, 3]]
69+
reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]'
70+
b = [0, *a]
71+
reveal_type(b) # E: Revealed type is 'builtins.list[builtins.int*]'
72+
c = [*a, 0]
73+
reveal_type(c) # E: Revealed type is 'builtins.list[builtins.int*]'
6874
[builtins fixtures/list.py]

test-data/unit/check-tuples.test

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,3 +768,43 @@ a = ()
768768
from typing import Sized
769769
a = None # type: Sized
770770
a = ()
771+
772+
[case testTupleWithStarExpr1]
773+
# options: fast_parser
774+
a = (1, 2)
775+
b = (*a, '')
776+
reveal_type(b) # E: Revealed type is 'Tuple[builtins.int, builtins.int, builtins.str]'
777+
778+
[case testTupleWithStarExpr2]
779+
a = [1]
780+
b = (0, *a)
781+
reveal_type(b) # E: Revealed type is 'builtins.tuple[builtins.int*]'
782+
[builtins fixtures/tuple.py]
783+
784+
[case testTupleWithStarExpr3]
785+
a = ['']
786+
b = (0, *a)
787+
reveal_type(b) # E: Revealed type is 'builtins.tuple[builtins.object*]'
788+
c = (*a, '')
789+
reveal_type(c) # E: Revealed type is 'builtins.tuple[builtins.str*]'
790+
[builtins fixtures/tuple.py]
791+
792+
[case testTupleWithStarExpr4]
793+
a = (1, 1, 'x', 'x')
794+
b = (1, 'x')
795+
a = (0, *b, '')
796+
[builtins fixtures/tuple.py]
797+
798+
[case testTupleWithUndersizedContext]
799+
a = ([1], 'x')
800+
a = ([], 'x', 1) # E: Incompatible types in assignment (expression has type "Tuple[List[int], str, int]", variable has type "Tuple[List[int], str]")
801+
[builtins fixtures/tuple.py]
802+
803+
[case testTupleWithOversizedContext]
804+
a = (1, [1], 'x')
805+
a = (1, []) # E: Incompatible types in assignment (expression has type "Tuple[int, List[int]]", variable has type "Tuple[int, List[int], str]")
806+
[builtins fixtures/tuple.py]
807+
808+
[case testTupleWithoutContext]
809+
a = (1, []) # E: Need type annotation for variable
810+
[builtins fixtures/tuple.py]

test-data/unit/fixtures/tuple.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Builtins stub used in tuple-related test cases.
22

3-
from typing import Iterable, TypeVar, Generic, Sequence
3+
from typing import Iterable, Iterator, TypeVar, Generic, Sequence
44

55
Tco = TypeVar('Tco', covariant=True)
66

@@ -11,6 +11,7 @@ class type:
1111
def __init__(self, *a) -> None: pass
1212
def __call__(self, *a) -> object: pass
1313
class tuple(Sequence[Tco], Generic[Tco]):
14+
def __iter__(self) -> Iterator[Tco]: pass
1415
def __getitem__(self, x: int) -> Tco: pass
1516
class function: pass
1617

@@ -21,6 +22,8 @@ class str: pass # For convenience
2122

2223
T = TypeVar('T')
2324

25+
class list(Sequence[T], Generic[T]): pass
26+
2427
def sum(iterable: Iterable[T], start: T = None) -> T: pass
2528

2629
True = bool()

test-data/unit/lib-stub/typing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __aiter__(self) -> 'AsyncIterator[T]': return self
7070
@abstractmethod
7171
def __anext__(self) -> Awaitable[T]: pass
7272

73-
class Sequence(Generic[T]):
73+
class Sequence(Iterable[T], Generic[T]):
7474
@abstractmethod
7575
def __getitem__(self, n: Any) -> T: pass
7676

test-data/unit/semanal-errors.test

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -486,12 +486,8 @@ b = 1
486486
c = 1
487487
d = 1
488488
a = *b
489-
a = b, (c, *d)
490-
a = [1, *[2]]
491489
[out]
492490
main:4: error: Can use starred expression only as assignment target
493-
main:5: error: Can use starred expression only as assignment target
494-
main:6: error: Can use starred expression only as assignment target
495491

496492
[case testStarExpressionInExp]
497493
a = 1

0 commit comments

Comments
 (0)