[ty] Improve disjointness inference for NominalInstanceTypes and SubclassOfTypes (#18864)

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Alex Waygood 2025-06-24 21:27:37 +01:00 committed by GitHub
parent d89f75f9cc
commit 9d8cba4e8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1255 additions and 442 deletions

View file

@ -46,12 +46,15 @@ def _(flag1: bool, flag2: bool):
## Assignment expressions
```py
def f() -> int | str | None: ...
class Foo: ...
class Bar: ...
if isinstance(x := f(), int):
reveal_type(x) # revealed: int
elif isinstance(x, str):
reveal_type(x) # revealed: str & ~int
def f() -> Foo | Bar | None: ...
if isinstance(x := f(), Foo):
reveal_type(x) # revealed: Foo
elif isinstance(x, Bar):
reveal_type(x) # revealed: Bar & ~Foo
else:
reveal_type(x) # revealed: None
```

View file

@ -87,7 +87,7 @@ match x:
case 6.0:
reveal_type(x) # revealed: float
case 1j:
reveal_type(x) # revealed: complex & ~float
reveal_type(x) # revealed: complex
case b"foo":
reveal_type(x) # revealed: Literal[b"foo"]
@ -137,7 +137,7 @@ match x:
case True | False:
reveal_type(x) # revealed: bool
case 3.14 | 2.718 | 1.414:
reveal_type(x) # revealed: float & ~tuple[Unknown, ...]
reveal_type(x) # revealed: float
reveal_type(x) # revealed: object
```

View file

@ -178,25 +178,26 @@ def _(d: Any):
from typing import Any
from typing_extensions import TypeGuard, TypeIs
def guard_str(a: object) -> TypeGuard[str]:
class Foo: ...
class Bar: ...
def guard_foo(a: object) -> TypeGuard[Foo]:
return True
def is_int(a: object) -> TypeIs[int]:
def is_bar(a: object) -> TypeIs[Bar]:
return True
```
```py
def _(a: str | int):
if guard_str(a):
# TODO: Should be `str`
reveal_type(a) # revealed: str | int
def _(a: Foo | Bar):
if guard_foo(a):
# TODO: Should be `Foo`
reveal_type(a) # revealed: Foo | Bar
else:
reveal_type(a) # revealed: str | int
reveal_type(a) # revealed: Foo | Bar
if is_int(a):
reveal_type(a) # revealed: int
if is_bar(a):
reveal_type(a) # revealed: Bar
else:
reveal_type(a) # revealed: str & ~int
reveal_type(a) # revealed: Foo & ~Bar
```
Attribute and subscript narrowing is supported:
@ -209,68 +210,68 @@ T = TypeVar("T")
class C(Generic[T]):
v: T
def _(a: tuple[str, int] | tuple[int, str], c: C[Any]):
# TODO: Should be `TypeGuard[str @ a[1]]`
if reveal_type(guard_str(a[1])): # revealed: @Todo(`TypeGuard[]` special form)
# TODO: Should be `tuple[int, str]`
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
# TODO: Should be `str`
reveal_type(a[1]) # revealed: int | str
def _(a: tuple[Foo, Bar] | tuple[Bar, Foo], c: C[Any]):
# TODO: Should be `TypeGuard[Foo @ a[1]]`
if reveal_type(guard_foo(a[1])): # revealed: @Todo(`TypeGuard[]` special form)
# TODO: Should be `tuple[Bar, Foo]`
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
# TODO: Should be `Foo`
reveal_type(a[1]) # revealed: Bar | Foo
if reveal_type(is_int(a[0])): # revealed: TypeIs[int @ a[0]]
# TODO: Should be `tuple[int, str]`
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
reveal_type(a[0]) # revealed: int
if reveal_type(is_bar(a[0])): # revealed: TypeIs[Bar @ a[0]]
# TODO: Should be `tuple[Bar, Bar & Foo]`
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
reveal_type(a[0]) # revealed: Bar
# TODO: Should be `TypeGuard[str @ c.v]`
if reveal_type(guard_str(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
# TODO: Should be `TypeGuard[Foo @ c.v]`
if reveal_type(guard_foo(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
reveal_type(c) # revealed: C[Any]
# TODO: Should be `str`
# TODO: Should be `Foo`
reveal_type(c.v) # revealed: Any
if reveal_type(is_int(c.v)): # revealed: TypeIs[int @ c.v]
if reveal_type(is_bar(c.v)): # revealed: TypeIs[Bar @ c.v]
reveal_type(c) # revealed: C[Any]
reveal_type(c.v) # revealed: Any & int
reveal_type(c.v) # revealed: Any & Bar
```
Indirect usage is supported within the same scope:
```py
def _(a: str | int):
b = guard_str(a)
c = is_int(a)
def _(a: Foo | Bar):
b = guard_foo(a)
c = is_bar(a)
reveal_type(a) # revealed: str | int
# TODO: Should be `TypeGuard[str @ a]`
reveal_type(a) # revealed: Foo | Bar
# TODO: Should be `TypeGuard[Foo @ a]`
reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form)
reveal_type(c) # revealed: TypeIs[int @ a]
reveal_type(c) # revealed: TypeIs[Bar @ a]
if b:
# TODO should be `str`
reveal_type(a) # revealed: str | int
# TODO should be `Foo`
reveal_type(a) # revealed: Foo | Bar
else:
reveal_type(a) # revealed: str | int
reveal_type(a) # revealed: Foo | Bar
if c:
# TODO should be `int`
reveal_type(a) # revealed: str | int
# TODO should be `Bar`
reveal_type(a) # revealed: Foo | Bar
else:
# TODO should be `str & ~int`
reveal_type(a) # revealed: str | int
# TODO should be `Foo & ~Bar`
reveal_type(a) # revealed: Foo | Bar
```
Further writes to the narrowed place invalidate the narrowing:
```py
def _(x: str | int, flag: bool) -> None:
b = is_int(x)
reveal_type(b) # revealed: TypeIs[int @ x]
def _(x: Foo | Bar, flag: bool) -> None:
b = is_bar(x)
reveal_type(b) # revealed: TypeIs[Bar @ x]
if flag:
x = ""
x = Foo()
if b:
reveal_type(x) # revealed: str | int
reveal_type(x) # revealed: Foo | Bar
```
The `TypeIs` type remains effective across generic boundaries:
@ -280,19 +281,19 @@ from typing_extensions import TypeVar, reveal_type
T = TypeVar("T")
def f(v: object) -> TypeIs[int]:
def f(v: object) -> TypeIs[Bar]:
return True
def g(v: T) -> T:
return v
def _(a: str):
def _(a: Foo):
# `reveal_type()` has the type `[T]() -> T`
if reveal_type(f(a)): # revealed: TypeIs[int @ a]
reveal_type(a) # revealed: str & int
if reveal_type(f(a)): # revealed: TypeIs[Bar @ a]
reveal_type(a) # revealed: Foo & Bar
if g(f(a)):
reveal_type(a) # revealed: str & int
reveal_type(a) # revealed: Foo & Bar
```
## `TypeGuard` special cases
@ -301,28 +302,32 @@ def _(a: str):
from typing import Any
from typing_extensions import TypeGuard, TypeIs
def guard_int(a: object) -> TypeGuard[int]:
class Foo: ...
class Bar: ...
class Baz(Bar): ...
def guard_foo(a: object) -> TypeGuard[Foo]:
return True
def is_int(a: object) -> TypeIs[int]:
def is_bar(a: object) -> TypeIs[Bar]:
return True
def does_not_narrow_in_negative_case(a: str | int):
if not guard_int(a):
# TODO: Should be `str`
reveal_type(a) # revealed: str | int
def does_not_narrow_in_negative_case(a: Foo | Bar):
if not guard_foo(a):
# TODO: Should be `Bar`
reveal_type(a) # revealed: Foo | Bar
else:
reveal_type(a) # revealed: str | int
reveal_type(a) # revealed: Foo | Bar
def narrowed_type_must_be_exact(a: object, b: bool):
if guard_int(b):
# TODO: Should be `int`
reveal_type(b) # revealed: bool
def narrowed_type_must_be_exact(a: object, b: Baz):
if guard_foo(b):
# TODO: Should be `Foo`
reveal_type(b) # revealed: Baz
if isinstance(a, bool) and is_int(a):
reveal_type(a) # revealed: bool
if isinstance(a, Baz) and is_bar(a):
reveal_type(a) # revealed: Baz
if isinstance(a, bool) and guard_int(a):
# TODO: Should be `int`
reveal_type(a) # revealed: bool
if isinstance(a, Bar) and guard_foo(a):
# TODO: Should be `Foo`
reveal_type(a) # revealed: Bar
```