ruff/crates/red_knot_python_semantic/resources/mdtest/attributes.md

5.2 KiB

Attributes

Tests for attribute access on various kinds of types.

Union of attributes

def _(flag: bool):
    if flag:
        class C1:
            x = 1

    else:
        class C1:
            x = 2

    class C2:
        if flag:
            x = 3
        else:
            x = 4

    reveal_type(C1.x)  # revealed: Literal[1, 2]
    reveal_type(C2.x)  # revealed: Literal[3, 4]

Inherited attributes

class A:
    X = "foo"

class B(A): ...
class C(B): ...

reveal_type(C.X)  # revealed: Literal["foo"]

Inherited attributes (multiple inheritance)

class O: ...

class F(O):
    X = 56

class E(O):
    X = 42

class D(O): ...
class C(D, F): ...
class B(E, D): ...
class A(B, C): ...

# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(A.__mro__)

# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
reveal_type(A.X)  # revealed: Literal[42]

Unions with possibly unbound paths

Definite boundness within a class

In this example, the x attribute is not defined in the C2 element of the union:

def _(flag1: bool, flag2: bool):
    class C1:
        x = 1

    class C2: ...

    class C3:
        x = 3

    C = C1 if flag1 else C2 if flag2 else C3

    # error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
    reveal_type(C.x)  # revealed: Literal[1, 3]

Possibly-unbound within a class

We raise the same diagnostic if the attribute is possibly-unbound in at least one element of the union:

def _(flag: bool, flag1: bool, flag2: bool):
    class C1:
        x = 1

    class C2:
        if flag:
            x = 2

    class C3:
        x = 3

    C = C1 if flag1 else C2 if flag2 else C3

    # error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
    reveal_type(C.x)  # revealed: Literal[1, 2, 3]

Unions with all paths unbound

If the symbol is unbound in all elements of the union, we detect that:

def _(flag: bool):
    class C1: ...
    class C2: ...
    C = C1 if flag else C2

    # error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
    reveal_type(C.x)  # revealed: Unknown

Objects of all types have a __class__ method

import typing_extensions

reveal_type(typing_extensions.__class__)  # revealed: Literal[ModuleType]

a = 42
reveal_type(a.__class__)  # revealed: Literal[int]

b = "42"
reveal_type(b.__class__)  # revealed: Literal[str]

c = b"42"
reveal_type(c.__class__)  # revealed: Literal[bytes]

d = True
reveal_type(d.__class__)  # revealed: Literal[bool]

e = (42, 42)
reveal_type(e.__class__)  # revealed: Literal[tuple]

def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
    reveal_type(a.__class__)  # revealed: type[int]
    reveal_type(b.__class__)  # revealed: Literal[str]
    reveal_type(c.__class__)  # revealed: type[int] | type[str]

    # `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
    # It would be incorrect to infer `Literal[type]` here,
    # as `c` could be some subclass of `str` with a custom metaclass.
    # All we know is that the metaclass must be a (non-strict) subclass of `type`.
    reveal_type(d.__class__)  # revealed: type[type]

reveal_type(f.__class__)  # revealed: Literal[FunctionType]

class Foo: ...

reveal_type(Foo.__class__)  # revealed: Literal[type]

Function-literal attributes

Most attribute accesses on function-literal types are delegated to types.FunctionType, since all functions are instances of that class:

def f(): ...

reveal_type(f.__defaults__)  # revealed: @Todo(instance attributes)
reveal_type(f.__kwdefaults__)  # revealed: @Todo(instance attributes)

Some attributes are special-cased, however:

def f(): ...

reveal_type(f.__get__)  # revealed: @Todo(`__get__` method on functions)
reveal_type(f.__call__)  # revealed: @Todo(`__call__` method on functions)

Int-literal attributes

Most attribute accesses on int-literal types are delegated to builtins.int, since all literal integers are instances of that class:

reveal_type((2).bit_length)  # revealed: @Todo(instance attributes)
reveal_type((2).denominator)  # revealed: @Todo(instance attributes)

Some attributes are special-cased, however:

reveal_type((2).numerator)  # revealed: Literal[2]
reveal_type((2).real)  # revealed: Literal[2]

Literal bool attributes

Most attribute accesses on bool-literal types are delegated to builtins.bool, since all literal bols are instances of that class:

reveal_type(True.__and__)  # revealed: @Todo(instance attributes)
reveal_type(False.__or__)  # revealed: @Todo(instance attributes)

Some attributes are special-cased, however:

reveal_type(True.numerator)  # revealed: Literal[1]
reveal_type(False.real)  # revealed: Literal[0]

Bytes-literal attributes

All attribute access on literal bytes types is currently delegated to buitins.bytes:

reveal_type(b"foo".join)  # revealed: @Todo(instance attributes)
reveal_type(b"foo".endswith)  # revealed: @Todo(instance attributes)