Skip to content
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
4 changes: 2 additions & 2 deletions crates/ruff_benchmark/benches/ty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) {
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
100,
110,
);

bench_project(&benchmark, criterion);
Expand All @@ -684,7 +684,7 @@ fn anyio(criterion: &mut Criterion) {
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
100,
150,
);

bench_project(&benchmark, criterion);
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_benchmark/benches/ty_walltime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ static TANJUN: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
100,
320,
);

static STATIC_FRAME: Benchmark = Benchmark::new(
Expand Down
33 changes: 25 additions & 8 deletions crates/ty_ide/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1944,14 +1944,31 @@ class Quux:
",
);

// FIXME: This should list completions on `self`, which should
// include, at least, `foo` and `bar`. At time of writing
// (2025-06-04), the type of `self` is inferred as `Unknown` in
// this context. This in turn prevents us from getting a list
// of available attributes.
//
// See: https://github.com/astral-sh/ty/issues/159
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
assert_snapshot!(test.completions_without_builtins(), @r"
__annotations__
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__format__
__getattribute__
__getstate__
__hash__
__init__
__init_subclass__
__module__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
");
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions crates/ty_ide/src/semantic_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1802,12 +1802,12 @@ class BoundedContainer[T: int, U = str]:
"get_first" @ 642..651: Method [definition]
"self" @ 652..656: SelfParameter
"T" @ 661..662: TypeParameter
"self" @ 679..683: Variable
"self" @ 679..683: TypeParameter
"value1" @ 684..690: Variable
"get_second" @ 700..710: Method [definition]
"self" @ 711..715: SelfParameter
"U" @ 720..721: TypeParameter
"self" @ 738..742: Variable
"self" @ 738..742: TypeParameter
"value2" @ 743..749: Variable
"BoundedContainer" @ 798..814: Class [definition]
"T" @ 815..816: TypeParameter [definition]
Expand Down
42 changes: 24 additions & 18 deletions crates/ty_python_semantic/resources/mdtest/annotations/self.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ from typing import Self

class A:
def implicit_self(self) -> Self:
# TODO: This should be Self@implicit_self
reveal_type(self) # revealed: Unknown
reveal_type(self) # revealed: Self@implicit_self

return self

Expand Down Expand Up @@ -127,19 +126,16 @@ The name `self` is not special in any way.
```py
class B:
def name_does_not_matter(this) -> Self:
# TODO: Should reveal Self@name_does_not_matter
reveal_type(this) # revealed: Unknown
reveal_type(this) # revealed: Self@name_does_not_matter

return this

def positional_only(self, /, x: int) -> Self:
# TODO: Should reveal Self@positional_only
reveal_type(self) # revealed: Unknown
reveal_type(self) # revealed: Self@positional_only
return self

def keyword_only(self, *, x: int) -> Self:
# TODO: Should reveal Self@keyword_only
reveal_type(self) # revealed: Unknown
reveal_type(self) # revealed: Self@keyword_only
return self

@property
Expand All @@ -165,8 +161,7 @@ T = TypeVar("T")

class G(Generic[T]):
def id(self) -> Self:
# TODO: Should reveal Self@id
reveal_type(self) # revealed: Unknown
reveal_type(self) # revealed: Self@id

return self

Expand Down Expand Up @@ -252,6 +247,20 @@ class LinkedList:
reveal_type(LinkedList().next()) # revealed: LinkedList
```

Attributes can also refer to a generic parameter:

```py
from typing import Generic, TypeVar

T = TypeVar("T")

class C(Generic[T]):
foo: T
def method(self) -> None:
reveal_type(self) # revealed: Self@method
reveal_type(self.foo) # revealed: T@C
```

## Generic Classes

```py
Expand Down Expand Up @@ -342,31 +351,28 @@ b: Self

# TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self"
class Foo:
# TODO: rejected Self because self has a different type
# TODO: This `self: T` annotation should be rejected because `T` is not `Self`
def has_existing_self_annotation(self: T) -> Self:
return self # error: [invalid-return-type]

def return_concrete_type(self) -> Self:
# TODO: tell user to use "Foo" instead of "Self"
# TODO: We could emit a hint that suggests annotating with `Foo` instead of `Self`
# error: [invalid-return-type]
return Foo()

@staticmethod
# TODO: reject because of staticmethod
# TODO: The usage of `Self` here should be rejected because this is a static method
def make() -> Self:
# error: [invalid-return-type]
return Foo()

class Bar(Generic[T]):
foo: T
def bar(self) -> T:
return self.foo
class Bar(Generic[T]): ...

# error: [invalid-type-form]
class Baz(Bar[Self]): ...

class MyMetaclass(type):
# TODO: rejected
# TODO: reject the Self usage. because self cannot be used within a metaclass.
def __new__(cls) -> Self:
return super().__new__(cls)
```
Expand Down
28 changes: 18 additions & 10 deletions crates/ty_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ class C:
c_instance = C(1)

reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]

# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"]

# There is no special handling of attributes that are (directly) assigned to a declared parameter,
# which means we union with `Unknown` here, since the attribute itself is not declared. This is
Expand Down Expand Up @@ -177,8 +175,7 @@ c_instance = C(1)

reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]

# TODO: Should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"]

reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None

Expand Down Expand Up @@ -399,9 +396,19 @@ class TupleIterable:

class C:
def __init__(self) -> None:
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
[... for self.a in IntIterable()]
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
# error: [unresolved-attribute]
[... for (self.b, self.c) in TupleIterable()]
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
# error: [unresolved-attribute]
[... for self.d in IntIterable() for self.e in IntIterable()]
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
[[... for self.f in IntIterable()] for _ in IntIterable()]
[[... for self.g in IntIterable()] for self in [D()]]

Expand Down Expand Up @@ -598,6 +605,8 @@ class C:
self.c = c
if False:
def set_e(self, e: str) -> None:
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
self.e = e

# TODO: this would ideally be `Unknown | Literal[1]`
Expand Down Expand Up @@ -685,7 +694,7 @@ class C:
pure_class_variable2: ClassVar = 1

def method(self):
# TODO: this should be an error
# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `Self@method`"
self.pure_class_variable1 = "value set through instance"

reveal_type(C.pure_class_variable1) # revealed: str
Expand Down Expand Up @@ -885,11 +894,9 @@ class Intermediate(Base):
# TODO: This should be an error (violates Liskov)
self.redeclared_in_method_with_wider_type: object = object()

# TODO: This should be an `invalid-assignment` error
self.overwritten_in_subclass_method = None
self.overwritten_in_subclass_method = None # error: [invalid-assignment]

# TODO: This should be an `invalid-assignment` error
self.pure_overwritten_in_subclass_method = None
self.pure_overwritten_in_subclass_method = None # error: [invalid-assignment]

self.pure_undeclared = "intermediate"

Expand Down Expand Up @@ -1839,6 +1846,7 @@ def external_getattribute(name) -> int:

class ThisFails:
def __init__(self):
# error: [invalid-assignment] "Implicit shadowing of function `__getattribute__`"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably something we should look at or create an issue for.

self.__getattribute__ = external_getattribute

# error: [unresolved-attribute]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ class C:
return str(key)

def f(self):
# TODO: This should emit an `invalid-assignment` diagnostic once we understand the type of `self`
# error: [invalid-assignment] "Implicit shadowing of function `__getitem__`"
self.__getitem__ = None

# This is still fine, and simply calls the `__getitem__` method on the class
Expand Down
13 changes: 6 additions & 7 deletions crates/ty_python_semantic/resources/mdtest/class/super.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,13 @@ class A:

class B(A):
def __init__(self, a: int):
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, B>`
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
reveal_type(super()) # revealed: <super: <class 'B'>, B>
reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
super().__init__(a)

@classmethod
def f(cls):
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
# TODO: Once `cls` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
super().f()

Expand Down Expand Up @@ -358,15 +357,15 @@ from __future__ import annotations

class A:
def test(self):
reveal_type(super()) # revealed: <super: <class 'A'>, Unknown>
reveal_type(super()) # revealed: <super: <class 'A'>, A>

class B:
def test(self):
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
reveal_type(super()) # revealed: <super: <class 'B'>, B>

class C(A.B):
def test(self):
reveal_type(super()) # revealed: <super: <class 'C'>, Unknown>
reveal_type(super()) # revealed: <super: <class 'C'>, C>

def inner(t: C):
reveal_type(super()) # revealed: <super: <class 'B'>, C>
Expand Down Expand Up @@ -616,7 +615,7 @@ class A:
class B(A):
def __init__(self, a: int):
super().__init__(a)
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
# error: [unresolved-attribute] "Type `<super: <class 'B'>, B>` has no attribute `a`"
super().a

# error: [unresolved-attribute] "Type `<super: <class 'B'>, B>` has no attribute `a`"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def f1(flag: bool):
attr = DataDescriptor()

def f(self):
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `attr` on type `Self@f` with custom `__set__` method"
self.attr = "normal"

reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]
Expand Down
3 changes: 1 addition & 2 deletions crates/ty_python_semantic/resources/mdtest/named_tuple.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,7 @@ class SuperUser(User):
def now_called_robert(self):
self.name = "Robert" # fine because overridden with a mutable attribute

# TODO: this should cause us to emit an error as we're assigning to a read-only property
# inherited from the `NamedTuple` superclass (requires https://github.com/astral-sh/ty/issues/159)
# error: 9 [invalid-assignment] "Cannot assign to read-only property `nickname` on object of type `Self@now_called_robert`"
self.nickname = "Bob"

james = SuperUser(0, "James", 42, "Jimmy")
Expand Down
Loading
Loading