Skip to content

Commit 127e77e

Browse files
[ty] Keep unresolved TypeIs targets provisional (#29066)
## Summary We now terminate inference for recursive `TypeIs` calls like this one: ```python from typing import reveal_type from typing_extensions import TypeIs class Container[T]: ... def is_container[T](value: object, other: T) -> TypeIs[Container[T]]: return True while True: if is_container(value, type(value)): value = value.__str__ else: value = {value} reveal_type(value) ``` Previously, an unresolved `TypeIs` target could make the `else` branch appear unreachable. We now defer that narrowing until the target is known, preserving the recursive type and diagnostics in both branches. Addresses astral-sh/ty#3837.
1 parent cbc71c2 commit 127e77e

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

‎crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,3 +1297,56 @@ def overwritten_target(value: object):
12971297
else:
12981298
reveal_type(value) # revealed: TypeGuard[list[int] @ value] & ~AlwaysTruthy
12991299
```
1300+
1301+
## Recursive TypeIs targets
1302+
1303+
An unresolved recursive `TypeIs` target does not make the negative branch unreachable. We retain the
1304+
recursive input while inferring assignments in both branches.
1305+
1306+
```py
1307+
from typing import Generic, TypeVar
1308+
from typing_extensions import TypeIs
1309+
1310+
T = TypeVar("T")
1311+
1312+
class Container(Generic[T]): ...
1313+
1314+
def is_container(value: object, other: T) -> TypeIs[Container[T]]:
1315+
return True
1316+
1317+
while True:
1318+
# error: [possibly-unresolved-reference]
1319+
# error: [possibly-unresolved-reference]
1320+
if is_container(value, type(value)):
1321+
value = value.__str__ # error: [possibly-unresolved-reference]
1322+
bad: str = 42 # error: [invalid-assignment]
1323+
else:
1324+
value = {value} # error: [possibly-unresolved-reference]
1325+
bad: str = 42 # error: [invalid-assignment]
1326+
reveal_type(value) # revealed: Divergent | set[Divergent]
1327+
```
1328+
1329+
## Recursive TypeIs targets with PEP 695 generics
1330+
1331+
```toml
1332+
[environment]
1333+
python-version = "3.12"
1334+
```
1335+
1336+
```py
1337+
from typing_extensions import TypeIs
1338+
1339+
class Container[T]: ...
1340+
1341+
def is_container[T](value: object, other: T) -> TypeIs[Container[T]]:
1342+
return True
1343+
1344+
while True:
1345+
# error: [possibly-unresolved-reference]
1346+
# error: [possibly-unresolved-reference]
1347+
if is_container(value, type(value)):
1348+
value = value.__str__ # error: [possibly-unresolved-reference]
1349+
else:
1350+
value = {value} # error: [possibly-unresolved-reference]
1351+
reveal_type(value) # revealed: Divergent | set[Divergent]
1352+
```

‎crates/ty_python_semantic/src/types/narrow.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4682,6 +4682,14 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> {
46824682
let (_, place) = type_is.place_info(db)?;
46834683
let mut target = type_is.return_type(db);
46844684

4685+
// An unresolved target does not establish that either branch is unreachable.
4686+
// Materializing Divergent to object here would discard the recursive marker
4687+
// from the negative branch before its target type becomes available.
4688+
if target.is_divergent() {
4689+
self.is_provisional = true;
4690+
return None;
4691+
}
4692+
46854693
if let Some(kind) = type_is.materialization_kind(db) {
46864694
let kind = if is_positive { kind } else { kind.flip() };
46874695
target = target.materialization(db, &self.env, kind);

0 commit comments

Comments
 (0)