Skip to content

Commit e196c2a

Browse files
authored
[ty] Consider __len__ when determining the truthiness of an instance of a tuple class or a @final class (#21049)
1 parent 4522f35 commit e196c2a

File tree

6 files changed

+345
-61
lines changed

6 files changed

+345
-61
lines changed

crates/ty_ide/src/goto_definition.rs

Lines changed: 207 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,11 +1294,11 @@ class Test:
12941294
"main.py",
12951295
"
12961296
class Test:
1297-
def __bool__(self) -> bool: ...
1297+
def __invert__(self) -> 'Test': ...
12981298
12991299
a = Test()
13001300
1301-
<CURSOR>not a
1301+
<CURSOR>~a
13021302
",
13031303
)
13041304
.build();
@@ -1308,8 +1308,8 @@ a = Test()
13081308
--> main.py:3:9
13091309
|
13101310
2 | class Test:
1311-
3 | def __bool__(self) -> bool: ...
1312-
| ^^^^^^^^
1311+
3 | def __invert__(self) -> 'Test': ...
1312+
| ^^^^^^^^^^
13131313
4 |
13141314
5 | a = Test()
13151315
|
@@ -1318,8 +1318,46 @@ a = Test()
13181318
|
13191319
5 | a = Test()
13201320
6 |
1321-
7 | not a
1322-
| ^^^
1321+
7 | ~a
1322+
| ^
1323+
|
1324+
");
1325+
}
1326+
1327+
/// We jump to the `__invert__` definition here even though its signature is incorrect.
1328+
#[test]
1329+
fn goto_definition_unary_operator_with_bad_dunder_definition() {
1330+
let test = CursorTest::builder()
1331+
.source(
1332+
"main.py",
1333+
"
1334+
class Test:
1335+
def __invert__(self, extra_arg) -> 'Test': ...
1336+
1337+
a = Test()
1338+
1339+
<CURSOR>~a
1340+
",
1341+
)
1342+
.build();
1343+
1344+
assert_snapshot!(test.goto_definition(), @r"
1345+
info[goto-definition]: Definition
1346+
--> main.py:3:9
1347+
|
1348+
2 | class Test:
1349+
3 | def __invert__(self, extra_arg) -> 'Test': ...
1350+
| ^^^^^^^^^^
1351+
4 |
1352+
5 | a = Test()
1353+
|
1354+
info: Source
1355+
--> main.py:7:1
1356+
|
1357+
5 | a = Test()
1358+
6 |
1359+
7 | ~a
1360+
| ^
13231361
|
13241362
");
13251363
}
@@ -1331,11 +1369,11 @@ a = Test()
13311369
"main.py",
13321370
"
13331371
class Test:
1334-
def __bool__(self) -> bool: ...
1372+
def __invert__(self) -> 'Test': ...
13351373
13361374
a = Test()
13371375
1338-
not<CURSOR> a
1376+
~<CURSOR> a
13391377
",
13401378
)
13411379
.build();
@@ -1345,8 +1383,8 @@ not<CURSOR> a
13451383
--> main.py:3:9
13461384
|
13471385
2 | class Test:
1348-
3 | def __bool__(self) -> bool: ...
1349-
| ^^^^^^^^
1386+
3 | def __invert__(self) -> 'Test': ...
1387+
| ^^^^^^^^^^
13501388
4 |
13511389
5 | a = Test()
13521390
|
@@ -1355,8 +1393,8 @@ not<CURSOR> a
13551393
|
13561394
5 | a = Test()
13571395
6 |
1358-
7 | not a
1359-
| ^^^
1396+
7 | ~ a
1397+
| ^
13601398
|
13611399
");
13621400
}
@@ -1368,7 +1406,7 @@ not<CURSOR> a
13681406
"main.py",
13691407
"
13701408
class Test:
1371-
def __bool__(self) -> bool: ...
1409+
def __invert__(self) -> 'Test': ...
13721410
13731411
a = Test()
13741412
@@ -1381,7 +1419,7 @@ a = Test()
13811419
info[goto-definition]: Definition
13821420
--> main.py:5:1
13831421
|
1384-
3 | def __bool__(self) -> bool: ...
1422+
3 | def __invert__(self) -> 'Test': ...
13851423
4 |
13861424
5 | a = Test()
13871425
| ^
@@ -1399,6 +1437,161 @@ a = Test()
13991437
");
14001438
}
14011439

1440+
#[test]
1441+
fn goto_definition_unary_not_with_dunder_bool() {
1442+
let test = CursorTest::builder()
1443+
.source(
1444+
"main.py",
1445+
"
1446+
class Test:
1447+
def __bool__(self) -> bool: ...
1448+
1449+
a = Test()
1450+
1451+
<CURSOR>not a
1452+
",
1453+
)
1454+
.build();
1455+
1456+
assert_snapshot!(test.goto_definition(), @r"
1457+
info[goto-definition]: Definition
1458+
--> main.py:3:9
1459+
|
1460+
2 | class Test:
1461+
3 | def __bool__(self) -> bool: ...
1462+
| ^^^^^^^^
1463+
4 |
1464+
5 | a = Test()
1465+
|
1466+
info: Source
1467+
--> main.py:7:1
1468+
|
1469+
5 | a = Test()
1470+
6 |
1471+
7 | not a
1472+
| ^^^
1473+
|
1474+
");
1475+
}
1476+
1477+
#[test]
1478+
fn goto_definition_unary_not_with_dunder_len() {
1479+
let test = CursorTest::builder()
1480+
.source(
1481+
"main.py",
1482+
"
1483+
class Test:
1484+
def __len__(self) -> 42: ...
1485+
1486+
a = Test()
1487+
1488+
<CURSOR>not a
1489+
",
1490+
)
1491+
.build();
1492+
1493+
assert_snapshot!(test.goto_definition(), @r"
1494+
info[goto-definition]: Definition
1495+
--> main.py:3:9
1496+
|
1497+
2 | class Test:
1498+
3 | def __len__(self) -> 42: ...
1499+
| ^^^^^^^
1500+
4 |
1501+
5 | a = Test()
1502+
|
1503+
info: Source
1504+
--> main.py:7:1
1505+
|
1506+
5 | a = Test()
1507+
6 |
1508+
7 | not a
1509+
| ^^^
1510+
|
1511+
");
1512+
}
1513+
1514+
/// If `__bool__` is defined incorrectly, `not` does not fallback to `__len__`.
1515+
/// Instead, we jump to the `__bool__` definition as usual.
1516+
/// The fallback only occurs if `__bool__` is not defined at all.
1517+
#[test]
1518+
fn goto_definition_unary_not_with_bad_dunder_bool_and_dunder_len() {
1519+
let test = CursorTest::builder()
1520+
.source(
1521+
"main.py",
1522+
"
1523+
class Test:
1524+
def __bool__(self, extra_arg) -> bool: ...
1525+
def __len__(self) -> 42: ...
1526+
1527+
a = Test()
1528+
1529+
<CURSOR>not a
1530+
",
1531+
)
1532+
.build();
1533+
1534+
assert_snapshot!(test.goto_definition(), @r"
1535+
info[goto-definition]: Definition
1536+
--> main.py:3:9
1537+
|
1538+
2 | class Test:
1539+
3 | def __bool__(self, extra_arg) -> bool: ...
1540+
| ^^^^^^^^
1541+
4 | def __len__(self) -> 42: ...
1542+
|
1543+
info: Source
1544+
--> main.py:8:1
1545+
|
1546+
6 | a = Test()
1547+
7 |
1548+
8 | not a
1549+
| ^^^
1550+
|
1551+
");
1552+
}
1553+
1554+
/// Same as for unary operators that only use a single dunder,
1555+
/// we still jump to `__len__` for `not` goto-definition even if
1556+
/// the `__len__` signature is incorrect (but only if there is no
1557+
/// `__bool__` definition).
1558+
#[test]
1559+
fn goto_definition_unary_not_with_no_dunder_bool_and_bad_dunder_len() {
1560+
let test = CursorTest::builder()
1561+
.source(
1562+
"main.py",
1563+
"
1564+
class Test:
1565+
def __len__(self, extra_arg) -> 42: ...
1566+
1567+
a = Test()
1568+
1569+
<CURSOR>not a
1570+
",
1571+
)
1572+
.build();
1573+
1574+
assert_snapshot!(test.goto_definition(), @r"
1575+
info[goto-definition]: Definition
1576+
--> main.py:3:9
1577+
|
1578+
2 | class Test:
1579+
3 | def __len__(self, extra_arg) -> 42: ...
1580+
| ^^^^^^^
1581+
4 |
1582+
5 | a = Test()
1583+
|
1584+
info: Source
1585+
--> main.py:7:1
1586+
|
1587+
5 | a = Test()
1588+
6 |
1589+
7 | not a
1590+
| ^^^
1591+
|
1592+
");
1593+
}
1594+
14021595
impl CursorTest {
14031596
fn goto_definition(&self) -> String {
14041597
let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset)

crates/ty_python_semantic/resources/mdtest/expression/boolean.md

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ python-version = "3.11"
7878
```
7979

8080
```py
81-
from typing import Literal
81+
from typing import Literal, final
8282

8383
reveal_type(bool(1)) # revealed: Literal[True]
8484
reveal_type(bool((0,))) # revealed: Literal[True]
@@ -92,26 +92,18 @@ reveal_type(bool(foo)) # revealed: Literal[True]
9292
class SingleElementTupleSubclass(tuple[int]): ...
9393

9494
reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True]
95-
reveal_type(SingleElementTupleSubclass.__bool__) # revealed: (self: tuple[int], /) -> Literal[True]
96-
reveal_type(SingleElementTupleSubclass((1,)).__bool__) # revealed: () -> Literal[True]
9795

9896
# Unknown length, but we know the length is guaranteed to be >=2
9997
class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ...
10098

10199
reveal_type(bool(MixedTupleSubclass((1, b"foo")))) # revealed: Literal[True]
102-
reveal_type(MixedTupleSubclass.__bool__) # revealed: (self: tuple[int, *tuple[str, ...], bytes], /) -> Literal[True]
103-
reveal_type(MixedTupleSubclass((1, b"foo")).__bool__) # revealed: () -> Literal[True]
104100

105101
# Unknown length with an overridden `__bool__`:
106102
class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]):
107103
def __bool__(self) -> Literal[True]:
108104
return True
109105

110106
reveal_type(bool(VariadicTupleSubclassWithDunderBoolOverride((1,)))) # revealed: Literal[True]
111-
reveal_type(VariadicTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True]
112-
113-
# revealed: bound method VariadicTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
114-
reveal_type(VariadicTupleSubclassWithDunderBoolOverride().__bool__)
115107

116108
# Same again but for a subclass of a fixed-length tuple:
117109
class EmptyTupleSubclassWithDunderBoolOverride(tuple[()]):
@@ -124,11 +116,28 @@ reveal_type(EmptyTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def
124116

125117
# revealed: bound method EmptyTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
126118
reveal_type(EmptyTupleSubclassWithDunderBoolOverride().__bool__)
119+
120+
@final
121+
class FinalClassOverridingLenAndNotBool:
122+
def __len__(self) -> Literal[42]:
123+
return 42
124+
125+
reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[True]
126+
127+
@final
128+
class FinalClassWithNoLenOrBool: ...
129+
130+
reveal_type(bool(FinalClassWithNoLenOrBool())) # revealed: Literal[True]
131+
132+
def f(x: SingleElementTupleSubclass | FinalClassOverridingLenAndNotBool | FinalClassWithNoLenOrBool):
133+
reveal_type(bool(x)) # revealed: Literal[True]
127134
```
128135

129136
## Falsy values
130137

131138
```py
139+
from typing import final, Literal
140+
132141
reveal_type(bool(0)) # revealed: Literal[False]
133142
reveal_type(bool(())) # revealed: Literal[False]
134143
reveal_type(bool(None)) # revealed: Literal[False]
@@ -139,13 +148,23 @@ reveal_type(bool()) # revealed: Literal[False]
139148
class EmptyTupleSubclass(tuple[()]): ...
140149

141150
reveal_type(bool(EmptyTupleSubclass())) # revealed: Literal[False]
142-
reveal_type(EmptyTupleSubclass.__bool__) # revealed: (self: tuple[()], /) -> Literal[False]
143-
reveal_type(EmptyTupleSubclass().__bool__) # revealed: () -> Literal[False]
151+
152+
@final
153+
class FinalClassOverridingLenAndNotBool:
154+
def __len__(self) -> Literal[0]:
155+
return 0
156+
157+
reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[False]
158+
159+
def f(x: EmptyTupleSubclass | FinalClassOverridingLenAndNotBool):
160+
reveal_type(bool(x)) # revealed: Literal[False]
144161
```
145162

146163
## Ambiguous values
147164

148165
```py
166+
from typing import Literal
167+
149168
reveal_type(bool([])) # revealed: bool
150169
reveal_type(bool({})) # revealed: bool
151170
reveal_type(bool(set())) # revealed: bool
@@ -154,8 +173,15 @@ class VariadicTupleSubclass(tuple[int, ...]): ...
154173

155174
def f(x: tuple[int, ...], y: VariadicTupleSubclass):
156175
reveal_type(bool(x)) # revealed: bool
157-
reveal_type(x.__bool__) # revealed: () -> bool
158-
reveal_type(y.__bool__) # revealed: () -> bool
176+
177+
class NonFinalOverridingLenAndNotBool:
178+
def __len__(self) -> Literal[42]:
179+
return 42
180+
181+
# We cannot consider `__len__` for a non-`@final` type,
182+
# because a subclass might override `__bool__`,
183+
# and `__bool__` takes precedence over `__len__`
184+
reveal_type(bool(NonFinalOverridingLenAndNotBool())) # revealed: bool
159185
```
160186

161187
## `__bool__` returning `NoReturn`

0 commit comments

Comments
 (0)