mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
[ty] Improve disjointness inference for NominalInstanceType
s and SubclassOfType
s (#18864)
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
d89f75f9cc
commit
9d8cba4e8b
23 changed files with 1255 additions and 442 deletions
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue