Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] Method calls and the descriptor protocol #16121

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ qux = (foo, bar)
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]

# TODO: Infer "LiteralString"
reveal_type(foo.join(qux)) # revealed: @Todo(Attribute access on `StringLiteral` types)
reveal_type(foo.join(qux)) # revealed: @Todo(decorated method)

template: LiteralString = "{}, {}"
reveal_type(template) # revealed: Literal["{}, {}"]
# TODO: Infer `LiteralString`
reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types)
reveal_type(template.format(foo, bar)) # revealed: @Todo(decorated method)
```

### Assignability
Expand Down
12 changes: 6 additions & 6 deletions crates/red_knot_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,7 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
Some attributes are special-cased, however:

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

Expand All @@ -951,7 +951,7 @@ Most attribute accesses on int-literal types are delegated to `builtins.int`, si
integers are instances of that class:

```py
reveal_type((2).bit_length) # revealed: @Todo(bound method)
reveal_type((2).bit_length) # revealed: <bound method: `bit_length` of `Literal[2]`>
reveal_type((2).denominator) # revealed: @Todo(@property)
```

Expand All @@ -968,8 +968,8 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
bols are instances of that class:

```py
reveal_type(True.__and__) # revealed: @Todo(bound method)
reveal_type(False.__or__) # revealed: @Todo(bound method)
reveal_type(True.__and__) # revealed: @Todo(decorated method)
reveal_type(False.__or__) # revealed: @Todo(decorated method)
```

Some attributes are special-cased, however:
Expand All @@ -984,8 +984,8 @@ reveal_type(False.real) # revealed: Literal[0]
All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`:

```py
reveal_type(b"foo".join) # revealed: @Todo(bound method)
reveal_type(b"foo".endswith) # revealed: @Todo(bound method)
reveal_type(b"foo".join) # revealed: <bound method: `join` of `Literal[b"foo"]`>
reveal_type(b"foo".endswith) # revealed: <bound method: `endswith` of `Literal[b"foo"]`>
```

## Instance attribute edge cases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int
reveal_type(2**largest_u32) # revealed: int

def variable(x: int):
reveal_type(x**2) # revealed: @Todo(return type)
# TODO
reveal_type(x**2) # revealed: Any
reveal_type(2**x) # revealed: @Todo(return type)
reveal_type(x**x) # revealed: @Todo(return type)
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,19 @@ class C:

c = C()

# TODO: this should be `Literal[10]`
reveal_type(c.ten) # revealed: Unknown | Ten
reveal_type(c.ten) # revealed: Literal[10]

# TODO: This should `Literal[10]`
reveal_type(C.ten) # revealed: Unknown | Ten
reveal_type(C.ten) # revealed: Literal[10]

# These are fine:
c.ten = 10
C.ten = 10

# TODO: Both of these should be errors
# TODO: This should be an error
c.ten = 11

# TODO: This is not the correct error message
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
C.ten = 11
```

Expand All @@ -61,20 +62,68 @@ class C:

c = C()

# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
reveal_type(c.flexible_int) # revealed: int | None

c.flexible_int = 42 # okay
c.flexible_int = "42" # also okay!

# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
reveal_type(c.flexible_int) # revealed: int | None

# TODO: should be an error
c.flexible_int = None # not okay

# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
reveal_type(c.flexible_int) # revealed: int | None
```

## Data and non-data descriptors

Descriptors that define `__set__` or `__delete__` are called data descriptors (e.g. properties),
while those that only define `__get__` are called non-data descriptors (e.g. `classmethod` or
`staticmethod`).

The precedence chain for attribute access is:

- Data descriptors
- Instance attributes
- Non-data descriptors

```py
from typing import Literal

class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["data descriptor"]:
return "data descriptor"

def __set__(self, instance: object, value) -> None:
pass

class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data descriptor"]:
return "non-data descriptor"

class C:
data_descriptor = DataDescriptor()
non_data_descriptor = NonDataDescriptor()

def __init__(self):
self.data_descriptor = "instance attribute"
self.non_data_descriptor = "instance attribute"

c = C()

# TODO: Should be `Unknown | Literal["data descriptor"]`
reveal_type(c.data_descriptor) # revealed: Literal["data descriptor"]

# TODO: Should be `Unknown | Literal["instance attribute"]`
reveal_type(c.non_data_descriptor) # revealed: Literal["non-data descriptor"]
```

Access on the class itself only sees the descriptors:

```py
reveal_type(C.data_descriptor) # revealed: Literal["data descriptor"]

reveal_type(C.non_data_descriptor) # revealed: Literal["non-data descriptor"]
```

## Built-in `property` descriptor
Expand All @@ -101,10 +150,10 @@ c = C()
reveal_type(c._name) # revealed: str | None

# Should be `str`
reveal_type(c.name) # revealed: @Todo(bound method)
reveal_type(c.name) # revealed: @Todo(decorated method)

# Should be `builtins.property`
reveal_type(C.name) # revealed: Literal[name]
reveal_type(C.name) # revealed: <function `name`>

# This is fine:
c.name = "new"
Expand Down Expand Up @@ -142,7 +191,7 @@ reveal_type(c1) # revealed: @Todo(return type)
reveal_type(C.get_name()) # revealed: @Todo(return type)

# TODO: should be `str`
reveal_type(C("42").get_name()) # revealed: @Todo(bound method)
reveal_type(C("42").get_name()) # revealed: @Todo(decorated method)
```

## Descriptors only work when used as class variables
Expand All @@ -162,7 +211,8 @@ class C:
def __init__(self):
self.ten = Ten()

reveal_type(C().ten) # revealed: Unknown | Ten
# TODO: Should be Unknown | Ten
reveal_type(C().ten) # revealed: Literal[10]
```

## Descriptors distinguishing between class and instance access
Expand All @@ -189,10 +239,10 @@ class C:
d = Descriptor()

# TODO: should be `Literal["called on class object"]
reveal_type(C.d) # revealed: Unknown | Descriptor
reveal_type(C.d) # revealed: LiteralString

# TODO: should be `Literal["called on instance"]
reveal_type(C().d) # revealed: Unknown | Descriptor
reveal_type(C().d) # revealed: LiteralString
```

[descriptors]: https://docs.python.org/3/howto/descriptor.html
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ reveal_type(secure_box) # revealed: MySecureBox
# TODO reveal int
# The @Todo(…) is misleading here. We currently treat `MyBox[T]` as a dynamic base class because we
# don't understand generics and therefore infer `Unknown` for the `MyBox[T]` base of `MySecureBox[T]`.
reveal_type(secure_box.data) # revealed: @Todo(instance attribute on class with dynamic base)
# error: [unresolved-attribute]
reveal_type(secure_box.data) # revealed: Unknown
```

## Cyclical class definition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ inside the module:
import typing

reveal_type(typing.__name__) # revealed: str
reveal_type(typing.__init__) # revealed: @Todo(bound method)
reveal_type(typing.__init__) # revealed: <bound method: `__init__` of `ModuleType`>

# These come from `builtins.object`, not `types.ModuleType`:
reveal_type(typing.__eq__) # revealed: @Todo(bound method)
reveal_type(typing.__eq__) # revealed: <bound method: `__eq__` of `ModuleType`>

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@ It is [recommended](https://docs.python.org/3/library/sys.html#sys.platform) to
```py
import sys

reveal_type(sys.platform.startswith("freebsd")) # revealed: @Todo(Attribute access on `LiteralString` types)
reveal_type(sys.platform.startswith("linux")) # revealed: @Todo(Attribute access on `LiteralString` types)
reveal_type(sys.platform.startswith("freebsd")) # revealed: bool
reveal_type(sys.platform.startswith("linux")) # revealed: bool
```
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ in strict mode.
```py
def f(x: type):
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(bound method)
reveal_type(x.__repr__) # revealed: <bound method: `__repr__` of `type`>

class A: ...

Expand All @@ -50,7 +50,7 @@ x: type = A() # error: [invalid-assignment]
```py
def f(x: type[object]):
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(bound method)
reveal_type(x.__repr__) # revealed: <bound method: `__repr__` of `type`>

class A: ...

Expand Down
Loading
Loading