Skip to content

Commit

Permalink
Display Union of Literals as a Literal (#14993)
Browse files Browse the repository at this point in the history
## Summary

Resolves #14988

Display union of Literals like other type checkers do.

With this change we lose the sorting behavior. And we show the types as
they appeared. So it's deterministic and tests should not be flaky.
This is similar to how Mypy [reveals the
type](https://mypy-play.net/?mypy=latest&python=3.12&gist=51ad03b153bfca3b940d5084345e230f).

In some cases this makes it harder to know what is the order in revealed
type when writing tests but since it's consistent after the test fails
we know the order.

## Test Plan

I adjusted mdtests for this change. Basically merged the int and string
types of the unions.

In cases where we have types other than numbers and strings like this
[one](https://github.com/astral-sh/ruff/pull/14993/files#diff-ac50bce02b9f0ad4dc7d6b8e1046d60dad919ac52d0aeb253e5884f89ea42bfeL51).
We only group the strings and numbers as the issue suggsted.

```
def _(flag: bool, flag2: bool):
    if flag:
        f = 1
    elif flag2:
        f = "foo"
    else:
        def f() -> int:
            return 1
    # error: "Object of type `Literal[1, "foo", f]` is not callable (due to union elements Literal[1], Literal["foo"])"
    # revealed: Unknown | int
    reveal_type(f())
```

[pyright
example](https://pyright-play.net/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMoAySMApiAIYA2AUNQCYnBQD6AFMJeWgFxQBGYMJQA0UDlwBMvAUICU3alCWYm4nouWamAXigBGDUpKUkqzmimHNYqLoBEwQXavGAziQXXlDVa1lQAWgA%2BTBQYTy9rEBIYAFcQFH0rAGIoMnAQXjsAeT4AKxIAY3wwJngEEigAAyJSCkoAbT1RBydRYABdKsxXKBQwfEKqTj5KStY6WMqYMChYlCQwROMSCBIw3tqyKiaO0S36htawOw7ZZ01U6IA3EioSOl4AVRQAa36Ad0SAH1CYKxud0ozHKJHYflk1CAA)

[mypy
example](https://mypy-play.net/?mypy=latest&python=3.12&gist=31c8bdaa5521860cfeca4b92841cb3b7)

---------

Co-authored-by: Carl Meyer <[email protected]>
  • Loading branch information
Glyphack and carljm authored Jan 8, 2025
1 parent fdca2b4 commit 03ff883
Show file tree
Hide file tree
Showing 16 changed files with 107 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ from typing import Literal
from enum import Enum

mode: Literal["w", "r"]
mode2: Literal["w"] | Literal["r"]
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
a1: Literal[26]
a2: Literal[0x1A]
a3: Literal[-4]
Expand All @@ -19,7 +17,6 @@ a5: Literal[b"hello world"]
a6: Literal[True]
a7: Literal[None]
a8: Literal[Literal[1]]
a9: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]]

class Color(Enum):
RED = 0
Expand All @@ -30,9 +27,6 @@ b1: Literal[Color.RED]

def f():
reveal_type(mode) # revealed: Literal["w", "r"]
reveal_type(mode2) # revealed: Literal["w", "r"]
# TODO: should be revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(union_var) # revealed: Literal[1, 2, 3, 5] | Literal["foo"] | None
reveal_type(a1) # revealed: Literal[26]
reveal_type(a2) # revealed: Literal[26]
reveal_type(a3) # revealed: Literal[-4]
Expand All @@ -41,7 +35,6 @@ def f():
reveal_type(a6) # revealed: Literal[True]
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
reveal_type(a9) # revealed: Literal["w", "r", "w+"]
# TODO: This should be Color.RED
reveal_type(b1) # revealed: Literal[0]

Expand All @@ -61,6 +54,63 @@ invalid4: Literal[
]
```

## Shortening unions of literals

When a Literal is parameterized with more than one value, it’s treated as exactly to equivalent to
the union of those types.

```py
from typing import Literal

def x(
a1: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None],
a2: Literal["w"] | Literal["r"],
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
):
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(a2) # revealed: Literal["w", "r"]
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
```

## Display of heterogeneous unions of literals

```py
from typing import Literal, Union

def foo(x: int) -> int:
return x + 1

def bar(s: str) -> str:
return s

class A: ...
class B: ...

def union_example(
x: Union[
# unknown type
# error: [unresolved-reference]
y,
Literal[-1],
Literal["A"],
Literal[b"A"],
Literal[b"\x00"],
Literal[b"\x07"],
Literal[0],
Literal[1],
Literal["B"],
Literal["foo"],
Literal["bar"],
Literal["B"],
Literal[True],
None,
]
):
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
```

## Detecting Literal outside typing and typing_extensions

Only Literal that is defined in typing and typing_extension modules is detected as the special
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def _(flag: bool):
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]

baz_3 = "foo" if flag else 1
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
reveal_type(baz_3) # revealed: Literal["foo", 1]
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def f1(
from typing import Literal

def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
```

## Class variables
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def _(flag: bool, flag2: bool):
else:
def f() -> int:
return 1
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
reveal_type(f())
```
Expand All @@ -72,6 +72,6 @@ def _(flag: bool):
else:
f = "foo"

x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
reveal_type(x) # revealed: Unknown
```
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
reveal_type(d) # revealed: bool

int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def _(flag: bool):

reveal_type(A.always_bound) # revealed: Literal[1]

reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
reveal_type(A.union) # revealed: Literal[1, "abc"]

# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ The test inside an if expression should not affect code outside of the expressio
def _(flag: bool):
x: Literal[42, "hello"] = 42 if flag else "hello"

reveal_type(x) # revealed: Literal[42] | Literal["hello"]
reveal_type(x) # revealed: Literal[42, "hello"]

_ = ... if isinstance(x, str) else ...

reveal_type(x) # revealed: Literal[42] | Literal["hello"]
reveal_type(x) # revealed: Literal[42, "hello"]
```
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class ZeroOrStr:
reveal_type(len(Zero())) # revealed: Literal[0]
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
reveal_type(len(OneOrFalse())) # revealed: Literal[1, 0]

# TODO: Emit a diagnostic
reveal_type(len(OneOrFoo())) # revealed: int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ reveal_type(x)
for x in (1, "a", b"foo"):
pass

# revealed: Literal[1] | Literal["a"] | Literal[b"foo"]
# revealed: Literal[1, "a", b"foo"]
# error: [possibly-unresolved-reference]
reveal_type(x)
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def _(flag: bool, flag2: bool):
x = 3

reveal_type(x) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[1, 2, 4]
reveal_type(y) # revealed: Literal[4, 1, 2]
```

## Nested `while` loops
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def _(x_flag: bool, y_flag: bool):
def _(flag1: bool, flag2: bool):
x = None if flag1 else (1 if flag2 else True)

reveal_type(x) # revealed: None | Literal[1] | Literal[True]
reveal_type(x) # revealed: None | Literal[1, True]
if x is None:
reveal_type(x) # revealed: None
elif x is True:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def _(flag: bool):
reveal_type(x) # revealed: Never

if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
```

## `classinfo` is a tuple of types
Expand All @@ -30,7 +30,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
x = 1 if flag else "a"

if isinstance(x, (int, str)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
else:
reveal_type(x) # revealed: Never

Expand All @@ -43,19 +43,19 @@ def _(flag: bool, flag1: bool, flag2: bool):
# No narrowing should occur if a larger type is also
# one of the possibilities:
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
else:
reveal_type(x) # revealed: Never

y = 1 if flag1 else "a" if flag2 else b"b"
if isinstance(y, (int, str)):
reveal_type(y) # revealed: Literal[1] | Literal["a"]
reveal_type(y) # revealed: Literal[1, "a"]

if isinstance(y, (int, bytes)):
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
reveal_type(y) # revealed: Literal[1, b"b"]

if isinstance(y, (str, bytes)):
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
reveal_type(y) # revealed: Literal["a", b"b"]
```

## `classinfo` is a nested tuple of types
Expand Down Expand Up @@ -107,7 +107,7 @@ def _(flag: bool):
x = 1 if flag else "foo"

if isinstance(x, t):
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
reveal_type(x) # revealed: Literal[1, "foo"]
```

## Do not use custom `isinstance` for narrowing
Expand All @@ -119,7 +119,7 @@ def _(flag: bool):
x = 1 if flag else "a"

if isinstance(x, int):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
```

## Do support narrowing if `isinstance` is aliased
Expand Down Expand Up @@ -155,12 +155,12 @@ def _(flag: bool):
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]

# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
```

## Do not narrow if there are keyword arguments
Expand All @@ -171,7 +171,7 @@ def _(flag: bool):

# error: [unknown-argument]
if isinstance(x, int, foo="bar"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
reveal_type(x) # revealed: Literal[1, "a"]
```

## `type[]` types are narrowed as well as class-literal types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,39 @@ def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[(
x = foo()

if x:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
else:
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]

if not x:
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
else:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]

if x and not x:
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, "", b"", -1, "foo", b"bar"] | bool | None | tuple[()]

if not (x and not x):
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
reveal_type(x) # revealed: Literal[0, "", b"", -1, "foo", b"bar"] | bool | None | tuple[()]
else:
reveal_type(x) # revealed: Never

if x or not x:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[-1, "foo", b"bar", 0, "", b""] | bool | None | tuple[()]
else:
reveal_type(x) # revealed: Never

if not (x or not x):
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[-1, "foo", b"bar", 0, "", b""] | bool | None | tuple[()]

if (isinstance(x, int) or isinstance(x, str)) and x:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"]
reveal_type(x) # revealed: Literal[-1, True, "foo"]
else:
reveal_type(x) # revealed: Literal[b"", b"bar"] | None | tuple[()] | Literal[0] | Literal[False] | Literal[""]
reveal_type(x) # revealed: Literal[b"", b"bar", 0, False, ""] | None | tuple[()]
```

## Function Literals
Expand Down Expand Up @@ -166,16 +166,16 @@ y = literals()

if isinstance(x, str) and not isinstance(x, B):
reveal_type(x) # revealed: A & str & ~B
reveal_type(y) # revealed: Literal[0, 42] | Literal["", "hello"]
reveal_type(y) # revealed: Literal[0, 42, "", "hello"]

z = x if flag() else y

reveal_type(z) # revealed: A & str & ~B | Literal[0, 42] | Literal["", "hello"]
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]

if z:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42] | Literal["hello"]
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
else:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0] | Literal[""]
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
```

## Narrowing Multiple Variables
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class C:
# error: [possibly-unresolved-reference]
y = x

reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
reveal_type(C.y) # revealed: Literal[1, "abc"]
```

## Unbound function local
Expand Down
8 changes: 4 additions & 4 deletions crates/red_knot_python_semantic/resources/mdtest/unpacking.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,8 +426,8 @@ def _(flag: bool):
value = ("a", "b")

a, b = value
reveal_type(a) # revealed: Literal[1] | Literal["a"]
reveal_type(b) # revealed: Literal[2] | Literal["b"]
reveal_type(a) # revealed: Literal[1, "a"]
reveal_type(b) # revealed: Literal[2, "b"]
```

### Typing literal
Expand Down Expand Up @@ -528,8 +528,8 @@ for a, b in ((1, 2), (3, 4)):

```py
for a, b in ((1, 2), ("a", "b")):
reveal_type(a) # revealed: Literal[1] | Literal["a"]
reveal_type(b) # revealed: Literal[2] | Literal["b"]
reveal_type(a) # revealed: Literal[1, "a"]
reveal_type(b) # revealed: Literal[2, "b"]
```

### Mixed literals values (2)
Expand Down
Loading

0 comments on commit 03ff883

Please sign in to comment.