ruff/crates/red_knot_python_semantic/resources/mdtest/expression/len.md
Alex Waygood e87fee4b3b
[red-knot] Add initial support for * imports (#16923)
## Summary

This PR adds initial support for `*` imports to red-knot. The approach
is to implement a standalone query, called from semantic indexing, that
visits the module referenced by the `*` import and collects all
global-scope public names that will be imported by the `*` import. The
`SemanticIndexBuilder` then adds separate definitions for each of these
names, all keyed to the same `ast::Alias` node that represents the `*`
import.

There are many pieces of `*`-import semantics that are still yet to be
done, even with this PR:
- This PR does not attempt to implement any of the semantics to do with
`__all__`. (If a module defines `__all__`, then only the symbols
included in `__all__` are imported, _not_ all public global-scope
symbols.
- With the logic implemented in this PR as it currently stands, we
sometimes incorrectly consider a symbol bound even though it is defined
in a branch that is statically known to be dead code, e.g. (assuming the
target Python version is set to 3.11):

  ```py
  # a.py

  import sys

  if sys.version_info < (3, 10):
      class Foo: ...

  ```

  ```py
  # b.py

  from a import *

  print(Foo)  # this is unbound at runtime on 3.11,
# but we currently consider it bound with the logic in this PR
  ```

Implementing these features is important, but is for now deferred to
followup PRs.

Many thanks to @ntBre, who contributed to this PR in a pairing session
on Friday!

## Test Plan

Assertions in existing mdtests are adjusted, and several new ones are
added.
2025-03-24 17:15:58 +00:00

4.9 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]:
        return 0

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

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

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

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

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

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]:
        return True

class LiteralFalse:
    def __len__(self) -> Literal[False]:
        return 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]:
        return SomeEnum.AUTO

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

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

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

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

reveal_type(len(Auto()))  # revealed: int
reveal_type(len(Int()))  # revealed: int
reveal_type(len(Str()))  # revealed: int
reveal_type(len(Tuple()))  # revealed: int
reveal_type(len(IntUnion()))  # revealed: int

Negative integers

from typing import Literal

class Negative:
    def __len__(self) -> Literal[-1]:
        return -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]:
        return 0

class SecondRequiredArgument:
    def __len__(self, v: int) -> Literal[1]:
        return 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: ...

# error: [invalid-argument-type]
reveal_type(len(NoDunderLen()))  # revealed: int