Files
ruff/crates/red_knot_python_semantic/resources/mdtest/expression/len.md
Shaygan Hooshyari 03ff883626 Display Union of Literals as a Literal (#14993)
## 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 <carl@oddbird.net>
2025-01-08 00:58:38 +00:00

4.7 KiB

Length (len())

Literal and constructed iterables

Strings and bytes literals

reveal_type(len("no\rmal"))  # revealed: Literal[6]
reveal_type(len(r"aw stri\ng"))  # revealed: Literal[10]
reveal_type(len(r"conca\t" "ena\tion"))  # revealed: Literal[14]
reveal_type(len(b"ytes lite" rb"al"))  # revealed: Literal[11]
reveal_type(len("𝒰𝕹🄸©🕲𝕕ℇ"))  # revealed: Literal[7]

reveal_type(  # revealed: Literal[7]
    len(
        """foo
bar"""
    )
)
reveal_type(  # revealed: Literal[9]
    len(
        r"""foo\r
bar"""
    )
)
reveal_type(  # revealed: Literal[7]
    len(
        b"""foo
bar"""
    )
)
reveal_type(  # revealed: Literal[9]
    len(
        rb"""foo\r
bar"""
    )
)

Tuples

reveal_type(len(()))  # revealed: Literal[0]
reveal_type(len((1,)))  # revealed: Literal[1]
reveal_type(len((1, 2)))  # revealed: Literal[2]

# TODO: Handle constructor calls
reveal_type(len(tuple()))  # revealed: int

# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[],)))  # revealed: Literal[1]

# TODO: Handle star unpacks; Should be: Literal[1]
reveal_type(  # revealed: Literal[2]
    len(
        (
            *[],
            1,
        )
    )
)

# TODO: Handle star unpacks; Should be: Literal[2]
reveal_type(len((*[], 1, 2)))  # revealed: Literal[3]

# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[], *{})))  # revealed: Literal[2]

Lists, sets and dictionaries

reveal_type(len([]))  # revealed: int
reveal_type(len([1]))  # revealed: int
reveal_type(len([1, 2]))  # revealed: int
reveal_type(len([*{}, *dict()]))  # revealed: int

reveal_type(len({}))  # revealed: int
reveal_type(len({**{}}))  # revealed: int
reveal_type(len({**{}, **{}}))  # revealed: int

reveal_type(len({1}))  # revealed: int
reveal_type(len({1, 2}))  # revealed: int
reveal_type(len({*[], 2}))  # revealed: int

reveal_type(len(list()))  # revealed: int
reveal_type(len(set()))  # revealed: int
reveal_type(len(dict()))  # revealed: int
reveal_type(len(frozenset()))  # revealed: int

__len__

The returned value of __len__ is implicitly and recursively converted to int.

Literal integers

from typing import Literal

class Zero:
    def __len__(self) -> Literal[0]: ...

class ZeroOrOne:
    def __len__(self) -> Literal[0, 1]: ...

class ZeroOrTrue:
    def __len__(self) -> Literal[0, True]: ...

class OneOrFalse:
    def __len__(self) -> Literal[1] | Literal[False]: ...

class OneOrFoo:
    def __len__(self) -> Literal[1, "foo"]: ...

class ZeroOrStr:
    def __len__(self) -> Literal[0] | str: ...

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[1, 0]

# TODO: Emit a diagnostic
reveal_type(len(OneOrFoo()))  # revealed: int

# TODO: Emit a diagnostic
reveal_type(len(ZeroOrStr()))  # revealed: int

Literal booleans

from typing import Literal

class LiteralTrue:
    def __len__(self) -> Literal[True]: ...

class LiteralFalse:
    def __len__(self) -> Literal[False]: ...

reveal_type(len(LiteralTrue()))  # revealed: Literal[1]
reveal_type(len(LiteralFalse()))  # revealed: Literal[0]

Enums

from enum import Enum, auto
from typing import Literal

class SomeEnum(Enum):
    AUTO = auto()
    INT = 2
    STR = "4"
    TUPLE = (8, "16")
    INT_2 = 3_2

class Auto:
    def __len__(self) -> Literal[SomeEnum.AUTO]: ...

class Int:
    def __len__(self) -> Literal[SomeEnum.INT]: ...

class Str:
    def __len__(self) -> Literal[SomeEnum.STR]: ...

class Tuple:
    def __len__(self) -> Literal[SomeEnum.TUPLE]: ...

class IntUnion:
    def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...

reveal_type(len(Auto()))  # revealed: int
reveal_type(len(Int()))  # revealed: Literal[2]
reveal_type(len(Str()))  # revealed: int
reveal_type(len(Tuple()))  # revealed: int
reveal_type(len(IntUnion()))  # revealed: Literal[2, 32]

Negative integers

from typing import Literal

class Negative:
    def __len__(self) -> Literal[-1]: ...

# TODO: Emit a diagnostic
reveal_type(len(Negative()))  # revealed: int

Wrong signature

from typing import Literal

class SecondOptionalArgument:
    def __len__(self, v: int = 0) -> Literal[0]: ...

class SecondRequiredArgument:
    def __len__(self, v: int) -> Literal[1]: ...

# TODO: Emit a diagnostic
reveal_type(len(SecondOptionalArgument()))  # revealed: Literal[0]

# TODO: Emit a diagnostic
reveal_type(len(SecondRequiredArgument()))  # revealed: Literal[1]

No __len__

class NoDunderLen: ...

# TODO: Emit a diagnostic
reveal_type(len(NoDunderLen()))  # revealed: int