Rename Red Knot (#17820)

This commit is contained in:
Micha Reiser 2025-05-03 19:49:15 +02:00 committed by GitHub
parent e6a798b962
commit b51c4f82ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1564 changed files with 1598 additions and 1578 deletions

View file

@ -0,0 +1,237 @@
# Narrowing for conditionals with boolean expressions
## Narrowing in `and` conditional
```py
class A: ...
class B: ...
def _(x: A | B):
if isinstance(x, A) and isinstance(x, B):
reveal_type(x) # revealed: A & B
else:
reveal_type(x) # revealed: (B & ~A) | (A & ~B)
```
## Arms might not add narrowing constraints
```py
class A: ...
class B: ...
def _(flag: bool, x: A | B):
if isinstance(x, A) and flag:
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
if flag and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
reveal_type(x) # revealed: A | B
```
## Statically known arms
```py
class A: ...
class B: ...
def _(x: A | B):
if isinstance(x, A) and True:
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if True and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if False and isinstance(x, A):
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
if False or isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if True or isinstance(x, A):
reveal_type(x) # revealed: A | B
else:
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: B & ~A
reveal_type(x) # revealed: A | B
```
## The type of multiple symbols can be narrowed down
```py
class A: ...
class B: ...
def _(x: A | B, y: A | B):
if isinstance(x, A) and isinstance(y, B):
reveal_type(x) # revealed: A
reveal_type(y) # revealed: B
else:
# No narrowing: Only-one or both checks might have failed
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
```
## Narrowing in `or` conditional
```py
class A: ...
class B: ...
class C: ...
def _(x: A | B | C):
if isinstance(x, A) or isinstance(x, B):
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: C & ~A & ~B
```
## In `or`, all arms should add constraint in order to narrow
```py
class A: ...
class B: ...
class C: ...
def _(flag: bool, x: A | B | C):
if isinstance(x, A) or isinstance(x, B) or flag:
reveal_type(x) # revealed: A | B | C
else:
reveal_type(x) # revealed: C & ~A & ~B
```
## in `or`, all arms should narrow the same set of symbols
```py
class A: ...
class B: ...
class C: ...
def _(x: A | B | C, y: A | B | C):
if isinstance(x, A) or isinstance(y, A):
# The predicate might be satisfied by the right side, so the type of `x` cant be narrowed down here.
reveal_type(x) # revealed: A | B | C
# The same for `y`
reveal_type(y) # revealed: A | B | C
else:
reveal_type(x) # revealed: (B & ~A) | (C & ~A)
reveal_type(y) # revealed: (B & ~A) | (C & ~A)
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
else:
reveal_type(x) # revealed: A | B | C
reveal_type(y) # revealed: A | B | C
```
## mixing `and` and `not`
```py
class A: ...
class B: ...
class C: ...
def _(x: A | B | C):
if isinstance(x, B) and not isinstance(x, C):
reveal_type(x) # revealed: B & ~C
else:
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
reveal_type(x) # revealed: (A & ~B) | C
```
## mixing `or` and `not`
```py
class A: ...
class B: ...
class C: ...
def _(x: A | B | C):
if isinstance(x, B) or not isinstance(x, C):
reveal_type(x) # revealed: B | (A & ~C)
else:
reveal_type(x) # revealed: C & ~B
```
## `or` with nested `and`
```py
class A: ...
class B: ...
class C: ...
def _(x: A | B | C):
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
reveal_type(x) # revealed: A | (B & ~C)
else:
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
reveal_type(x) # revealed: C & ~A
```
## `and` with nested `or`
```py
class A: ...
class B: ...
class C: ...
def _(x: A | B | C):
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
# A & (B | ~C) -> (A & B) | (A & ~C)
reveal_type(x) # revealed: (A & B) | (A & ~C)
else:
# ~((A & B) | (A & ~C)) ->
# ~(A & B) & ~(A & ~C) ->
# (~A | ~B) & (~A | C) ->
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
# ~A | (~A & C) | (~B & C) ->
# ~A | (C & ~B) ->
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
reveal_type(x) # revealed: (B & ~A) | (C & ~A) | (C & ~B)
```
## Boolean expression internal narrowing
```py
def _(x: str | None, y: str | None):
if x is None and y is not x:
reveal_type(y) # revealed: str
# Neither of the conditions alone is sufficient for narrowing y's type:
if x is None:
reveal_type(y) # revealed: str | None
if y is not x:
reveal_type(y) # revealed: str | None
```
## Assignment expressions
```py
def f() -> bool:
return True
if x := f():
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
```

View file

@ -0,0 +1,57 @@
# Narrowing for conditionals with elif and else
## Positive contributions become negative in elif-else blocks
```py
def _(x: int):
if x == 1:
# cannot narrow; could be a subclass of `int`
reveal_type(x) # revealed: int
elif x == 2:
reveal_type(x) # revealed: int & ~Literal[1]
elif x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
```
## Positive contributions become negative in elif-else blocks, with simplification
```py
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
if x == 1:
reveal_type(x) # revealed: Literal[1]
elif x == 2:
reveal_type(x) # revealed: Literal[2]
else:
reveal_type(x) # revealed: Literal[3]
```
## Multiple negative contributions using elif, with simplification
```py
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
elif x != 2:
reveal_type(x) # revealed: Literal[1]
elif x == 3:
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Never
```
## Assignment expressions
```py
def f() -> int | str | None: ...
if isinstance(x := f(), int):
reveal_type(x) # revealed: int
elif isinstance(x, str):
reveal_type(x) # revealed: str & ~int
else:
reveal_type(x) # revealed: None
```

View file

@ -0,0 +1,157 @@
# Narrowing for `!=` conditionals
## `x != None`
```py
def _(flag: bool):
x = None if flag else 1
if x != None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
```
## `!=` for other singleton types
```py
def _(flag: bool):
x = True if flag else False
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
```
## `x != y` where `y` is of literal type
```py
def _(flag: bool):
x = 1 if flag else 2
if x != 1:
reveal_type(x) # revealed: Literal[2]
```
## `x != y` where `y` is a single-valued type
```py
def _(flag: bool):
class A: ...
class B: ...
C = A if flag else B
if C != A:
reveal_type(C) # revealed: Literal[B]
else:
reveal_type(C) # revealed: Literal[A]
```
## `x != y` where `y` has multiple single-valued options
```py
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2
y = 2 if flag2 else 3
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[2]
```
## `!=` for non-single-valued types
Only single-valued types should narrow the type:
```py
def _(flag: bool, a: int, y: int):
x = a if flag else None
if x != y:
reveal_type(x) # revealed: int | None
```
## Mix of single-valued and non-single-valued types
```py
def _(flag1: bool, flag2: bool, a: int):
x = 1 if flag1 else 2
y = 2 if flag2 else a
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[1, 2]
```
## Assignment expressions
```py
from typing import Literal
def f() -> Literal[1, 2, 3]:
return 1
if (x := f()) != 1:
reveal_type(x) # revealed: Literal[2, 3]
else:
reveal_type(x) # revealed: Literal[1]
```
## Union with `Any`
```py
from typing import Any
def _(x: Any | None, y: Any | None):
if x != 1:
reveal_type(x) # revealed: (Any & ~Literal[1]) | None
if y == 1:
reveal_type(y) # revealed: Any & ~None
```
## Booleans and integers
```py
from typing import Literal
def _(b: bool, i: Literal[1, 2]):
if b == 1:
reveal_type(b) # revealed: Literal[True]
else:
reveal_type(b) # revealed: Literal[False]
if b == 6:
reveal_type(b) # revealed: Never
else:
reveal_type(b) # revealed: bool
if b == 0:
reveal_type(b) # revealed: Literal[False]
else:
reveal_type(b) # revealed: Literal[True]
if i == True:
reveal_type(i) # revealed: Literal[1]
else:
reveal_type(i) # revealed: Literal[2]
```
## Narrowing `LiteralString` in union
```py
from typing_extensions import Literal, LiteralString, Any
def _(s: LiteralString | None, t: LiteralString | Any):
if s == "foo":
reveal_type(s) # revealed: Literal["foo"]
if s == 1:
reveal_type(s) # revealed: Never
if t == "foo":
# TODO could be `Literal["foo"] | Any`
reveal_type(t) # revealed: LiteralString | Any
```

View file

@ -0,0 +1,94 @@
# Narrowing for `in` conditionals
## `in` for tuples
```py
def _(x: int):
if x in (1, 2, 3):
reveal_type(x) # revealed: int
else:
reveal_type(x) # revealed: int
```
```py
def _(x: str):
if x in ("a", "b", "c"):
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: str
```
```py
from typing import Literal
def _(x: Literal[1, 2, "a", "b", False, b"abc"]):
if x in (1,):
reveal_type(x) # revealed: Literal[1]
elif x in (2, "a"):
reveal_type(x) # revealed: Literal[2, "a"]
elif x in (b"abc",):
reveal_type(x) # revealed: Literal[b"abc"]
elif x not in (3,):
reveal_type(x) # revealed: Literal["b", False]
else:
reveal_type(x) # revealed: Never
```
```py
def _(x: Literal["a", "b", "c", 1]):
if x in ("a", "b", "c", 2):
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: Literal[1]
```
## `in` for `str` and literal strings
```py
def _(x: str):
if x in "abc":
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: str
```
```py
from typing import Literal
def _(x: Literal["a", "b", "c", "d"]):
if x in "abc":
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: Literal["d"]
```
```py
def _(x: Literal["a", "b", "c", "e"]):
if x in "abcd":
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: Literal["e"]
```
```py
def _(x: Literal[1, "a", "b", "c", "d"]):
# error: [unsupported-operator]
if x in "abc":
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: Literal[1, "d"]
```
## Assignment expressions
```py
from typing import Literal
def f() -> Literal[1, 2, 3]:
return 1
if (x := f()) in (1,):
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Literal[2, 3]
```

View file

@ -0,0 +1,115 @@
# Narrowing for `is` conditionals
## `is None`
```py
def _(flag: bool):
x = None if flag else 1
if x is None:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1]
reveal_type(x) # revealed: None | Literal[1]
```
## `is` for other types
```py
def _(flag: bool):
class A: ...
x = A()
y = x if flag else None
if y is x:
reveal_type(y) # revealed: A
else:
reveal_type(y) # revealed: A | None
reveal_type(y) # revealed: A | None
```
## `is` in chained comparisons
```py
def _(x_flag: bool, y_flag: bool):
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is x is False: # Interpreted as `(y is x) and (x is False)`
reveal_type(x) # revealed: Literal[False]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is not x) or (x is not False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
```
## `is` in elif clause
```py
def _(flag1: bool, flag2: bool):
x = None if flag1 else (1 if flag2 else True)
reveal_type(x) # revealed: None | Literal[1, True]
if x is None:
reveal_type(x) # revealed: None
elif x is True:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[1]
```
## `is` for `EllipsisType` (Python 3.10+)
```toml
[environment]
python-version = "3.10"
```
```py
from types import EllipsisType
def _(x: int | EllipsisType):
if x is ...:
reveal_type(x) # revealed: EllipsisType
else:
reveal_type(x) # revealed: int
```
## `is` for `EllipsisType` (Python 3.9 and below)
```toml
[environment]
python-version = "3.9"
```
```py
def _(flag: bool):
x = ... if flag else 42
reveal_type(x) # revealed: ellipsis | Literal[42]
if x is ...:
reveal_type(x) # revealed: ellipsis
else:
reveal_type(x) # revealed: Literal[42]
```
## Assignment expressions
```py
from typing import Literal
def f() -> Literal[1, 2] | None: ...
if (x := f()) is None:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1, 2]
```

View file

@ -0,0 +1,95 @@
# Narrowing for `is not` conditionals
## `is not None`
The type guard removes `None` from the union type:
```py
def _(flag: bool):
x = None if flag else 1
if x is not None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
reveal_type(x) # revealed: None | Literal[1]
```
## `is not` for other singleton types
```py
def _(flag: bool):
x = True if flag else False
reveal_type(x) # revealed: bool
if x is not False:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
```
## `is not` for non-singleton types
Non-singleton types should *not* narrow the type: two instances of a non-singleton class may occupy
different addresses in memory even if they compare equal.
```py
x = 345
y = 345
if x is not y:
reveal_type(x) # revealed: Literal[345]
else:
reveal_type(x) # revealed: Literal[345]
```
## `is not` for other types
```py
def _(flag: bool):
class A: ...
x = A()
y = x if flag else None
if y is not x:
reveal_type(y) # revealed: A | None
else:
reveal_type(y) # revealed: A
reveal_type(y) # revealed: A | None
```
## `is not` in chained comparisons
The type guard removes `False` from the union type of the tested value only.
```py
def _(x_flag: bool, y_flag: bool):
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
reveal_type(x) # revealed: Literal[True]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is x) or (x is False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
```
## Assignment expressions
```py
def f() -> int | str | None: ...
if (x := f()) is not None:
reveal_type(x) # revealed: int | str
else:
reveal_type(x) # revealed: None
```

View file

@ -0,0 +1,44 @@
# Narrowing for nested conditionals
## Multiple negative contributions
```py
def _(x: int):
if x != 1:
if x != 2:
if x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
```
## Multiple negative contributions with simplification
```py
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x != 2:
reveal_type(x) # revealed: Literal[3]
```
## elif-else blocks
```py
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x == 2:
reveal_type(x) # revealed: Literal[2]
elif x == 3:
reveal_type(x) # revealed: Literal[3]
else:
reveal_type(x) # revealed: Never
elif x != 2:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Never
```

View file

@ -0,0 +1,29 @@
# Narrowing for `not` conditionals
The `not` operator negates a constraint.
## `not is None`
```py
def _(flag: bool):
x = None if flag else 1
if not x is None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
reveal_type(x) # revealed: None | Literal[1]
```
## `not isinstance`
```py
def _(flag: bool):
x = 1 if flag else "a"
if not isinstance(x, (int)):
reveal_type(x) # revealed: Literal["a"]
else:
reveal_type(x) # revealed: Literal[1]
```