Skip to content

Commit 9941b61

Browse files
ddfisherilevkivskyi
authored andcommitted
Allow nonliteral tuple indexing (#3514)
* Allow nonliteral tuple indexing * fix indent
1 parent 43851c2 commit 9941b61

File tree

5 files changed

+44
-33
lines changed

5 files changed

+44
-33
lines changed

mypy/checkexpr.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,31 +1570,23 @@ def visit_index_expr_helper(self, e: IndexExpr) -> Type:
15701570
return self.accept(e.analyzed)
15711571
left_type = self.accept(e.base)
15721572
if isinstance(left_type, TupleType) and self.chk.in_checked_function():
1573-
# Special case for tuples. They support indexing only by integer
1574-
# literals.
1573+
# Special case for tuples. They return a more specific type when
1574+
# indexed by an integer literal.
15751575
index = e.index
15761576
if isinstance(index, SliceExpr):
15771577
return self.visit_tuple_slice_helper(left_type, index)
15781578

1579-
ok = False
1580-
if isinstance(index, IntExpr):
1581-
n = index.value
1582-
ok = True
1583-
elif isinstance(index, UnaryExpr):
1584-
if index.op == '-':
1585-
operand = index.expr
1586-
if isinstance(operand, IntExpr):
1587-
n = len(left_type.items) - operand.value
1588-
ok = True
1589-
if ok:
1579+
n = self._get_value(index)
1580+
if n is not None:
1581+
if n < 0:
1582+
n += len(left_type.items)
15901583
if n >= 0 and n < len(left_type.items):
15911584
return left_type.items[n]
15921585
else:
15931586
self.chk.fail(messages.TUPLE_INDEX_OUT_OF_RANGE, e)
15941587
return AnyType()
15951588
else:
1596-
self.chk.fail(messages.TUPLE_INDEX_MUST_BE_AN_INT_LITERAL, e)
1597-
return AnyType()
1589+
return self.nonliteral_tuple_index_helper(left_type, index)
15981590
elif isinstance(left_type, TypedDictType):
15991591
return self.visit_typeddict_index_expr(left_type, e.index)
16001592
elif (isinstance(left_type, CallableType)
@@ -1613,29 +1605,31 @@ def visit_tuple_slice_helper(self, left_type: TupleType, slic: SliceExpr) -> Typ
16131605
if slic.begin_index:
16141606
begin = self._get_value(slic.begin_index)
16151607
if begin is None:
1616-
self.chk.fail(
1617-
messages.TUPLE_SLICE_MUST_BE_AN_INT_LITERAL,
1618-
slic.begin_index)
1619-
return AnyType()
1608+
return self.nonliteral_tuple_index_helper(left_type, slic)
16201609

16211610
if slic.end_index:
16221611
end = self._get_value(slic.end_index)
16231612
if end is None:
1624-
self.chk.fail(
1625-
messages.TUPLE_SLICE_MUST_BE_AN_INT_LITERAL,
1626-
slic.end_index)
1627-
return AnyType()
1613+
return self.nonliteral_tuple_index_helper(left_type, slic)
16281614

16291615
if slic.stride:
16301616
stride = self._get_value(slic.stride)
16311617
if stride is None:
1632-
self.chk.fail(
1633-
messages.TUPLE_SLICE_MUST_BE_AN_INT_LITERAL,
1634-
slic.stride)
1635-
return AnyType()
1618+
return self.nonliteral_tuple_index_helper(left_type, slic)
16361619

16371620
return left_type.slice(begin, stride, end)
16381621

1622+
def nonliteral_tuple_index_helper(self, left_type: TupleType, index: Expression) -> Type:
1623+
index_type = self.accept(index)
1624+
expected_type = UnionType.make_union([self.named_type('builtins.int'),
1625+
self.named_type('builtins.slice')])
1626+
if not self.chk.check_subtype(index_type, expected_type, index,
1627+
messages.INVALID_TUPLE_INDEX_TYPE,
1628+
'actual type', 'expected type'):
1629+
return AnyType()
1630+
else:
1631+
return UnionType.make_simplified_union(left_type.items)
1632+
16391633
def _get_value(self, index: Expression) -> Optional[int]:
16401634
if isinstance(index, IntExpr):
16411635
return index.value

mypy/messages.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@
5454
INCOMPATIBLE_TYPES_IN_YIELD_FROM = 'Incompatible types in "yield from"'
5555
INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION = 'Incompatible types in string interpolation'
5656
MUST_HAVE_NONE_RETURN_TYPE = 'The return type of "{}" must be None'
57-
TUPLE_INDEX_MUST_BE_AN_INT_LITERAL = 'Tuple index must be an integer literal'
58-
TUPLE_SLICE_MUST_BE_AN_INT_LITERAL = 'Tuple slice must be an integer literal'
57+
INVALID_TUPLE_INDEX_TYPE = 'Invalid tuple index type'
5958
TUPLE_INDEX_OUT_OF_RANGE = 'Tuple index out of range'
6059
NEED_ANNOTATION_FOR_VAR = 'Need type annotation for variable'
6160
ITERABLE_EXPECTED = 'Iterable expected'

test-data/unit/check-class-namedtuple.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ class Base(NamedTuple):
542542
reveal_type(self.x) # E: Revealed type is 'builtins.int'
543543
self.x = 3 # E: Property "x" defined in "Base" is read-only
544544
self[1] # E: Tuple index out of range
545-
self[T] # E: Tuple index must be an integer literal
545+
reveal_type(self[T]) # E: Revealed type is 'builtins.int'
546546
return self.x
547547
def bad_override(self) -> int:
548548
return self.x
@@ -571,6 +571,7 @@ reveal_type(Child(1).good_override()) # E: Revealed type is 'builtins.int'
571571
reveal_type(Base(1).bad_override()) # E: Revealed type is 'builtins.int'
572572
reveal_type(takes_base(Base(1))) # E: Revealed type is 'builtins.int'
573573
reveal_type(takes_base(Child(1))) # E: Revealed type is 'builtins.int'
574+
[builtins fixtures/tuple.pyi]
574575

575576
[case testNewNamedTupleIllegalNames]
576577
from typing import Callable, NamedTuple

test-data/unit/check-tuples.test

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ b = t1[0] # E: Incompatible types in assignment (expression has type "A", variab
163163
t1[2] # E: Tuple index out of range
164164
t1[3] # E: Tuple index out of range
165165
t2[1] # E: Tuple index out of range
166-
t1[n] # E: Tuple index must be an integer literal
167-
t3[n:] # E: Tuple slice must be an integer literal
166+
reveal_type(t1[n]) # E: Revealed type is 'Union[__main__.A, __main__.B]'
167+
reveal_type(t3[n:]) # E: Revealed type is 'Union[__main__.A, __main__.B, __main__.C, __main__.D, __main__.E]'
168168
b = t1[(0)] # E: Incompatible types in assignment (expression has type "A", variable has type "B")
169169

170170
a = t1[0]
@@ -925,3 +925,19 @@ f((1,)) # E: Argument 1 to "f" has incompatible type "Tuple[int]"; expected "Tu
925925
f(('', '')) # E: Argument 1 to "f" has incompatible type "Tuple[str, str]"; expected "Tuple[]"
926926
f(0) # E: Argument 1 to "f" has incompatible type "int"; expected "Tuple[]"
927927
[builtins fixtures/tuple.pyi]
928+
929+
[case testNonliteralTupleIndex]
930+
t = (0, "")
931+
x = 0
932+
y = ""
933+
reveal_type(t[x]) # E: Revealed type is 'Union[builtins.int, builtins.str]'
934+
t[y] # E: Invalid tuple index type (actual type "str", expected type "Union[int, slice]")
935+
[builtins fixtures/tuple.pyi]
936+
937+
[case testNonliteralTupleSlice]
938+
t = (0, "")
939+
x = 0
940+
y = ""
941+
reveal_type(t[x:]) # E: Revealed type is 'Union[builtins.int, builtins.str]'
942+
t[y:] # E: Slice index must be an integer or None
943+
[builtins fixtures/tuple.pyi]

test-data/unit/fixtures/tuple.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ class tuple(Sequence[Tco], Generic[Tco]):
1515
def __getitem__(self, x: int) -> Tco: pass
1616
class function: pass
1717

18-
# We need int for indexing tuples.
18+
# We need int and slice for indexing tuples.
1919
class int: pass
20+
class slice: pass
2021
class bool: pass
2122
class str: pass # For convenience
2223
class unicode: pass

0 commit comments

Comments
 (0)