Skip to content

Commit 195669f

Browse files
Glyphacksharkdp
andcommitted
[ty] Infer type of self as typing.Self in method body (#18473)
Part of astral-sh/ty#159 Add support for adding a synthetic `typing.Self` type for `self` arguments in methods. `typing.Self` is assigned as the type if there are no annotations. - Updated md tests. --------- Co-authored-by: David Peter <mail@david-peter.de>
1 parent 596ee17 commit 195669f

File tree

15 files changed

+288
-203
lines changed

15 files changed

+288
-203
lines changed

crates/ruff_benchmark/benches/ty.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) {
667667
max_dep_date: "2025-06-17",
668668
python_version: PythonVersion::PY313,
669669
},
670-
100,
670+
110,
671671
);
672672

673673
bench_project(&benchmark, criterion);
@@ -684,7 +684,7 @@ fn anyio(criterion: &mut Criterion) {
684684
max_dep_date: "2025-06-17",
685685
python_version: PythonVersion::PY313,
686686
},
687-
100,
687+
150,
688688
);
689689

690690
bench_project(&benchmark, criterion);

crates/ruff_benchmark/benches/ty_walltime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ static TANJUN: Benchmark = Benchmark::new(
210210
max_dep_date: "2025-06-17",
211211
python_version: PythonVersion::PY312,
212212
},
213-
100,
213+
320,
214214
);
215215

216216
static STATIC_FRAME: Benchmark = Benchmark::new(

crates/ty_ide/src/completion.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1957,14 +1957,31 @@ class Quux:
19571957
",
19581958
);
19591959

1960-
// FIXME: This should list completions on `self`, which should
1961-
// include, at least, `foo` and `bar`. At time of writing
1962-
// (2025-06-04), the type of `self` is inferred as `Unknown` in
1963-
// this context. This in turn prevents us from getting a list
1964-
// of available attributes.
1965-
//
1966-
// See: https://github.com/astral-sh/ty/issues/159
1967-
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
1960+
assert_snapshot!(test.completions_without_builtins(), @r"
1961+
__annotations__
1962+
__class__
1963+
__delattr__
1964+
__dict__
1965+
__dir__
1966+
__doc__
1967+
__eq__
1968+
__format__
1969+
__getattribute__
1970+
__getstate__
1971+
__hash__
1972+
__init__
1973+
__init_subclass__
1974+
__module__
1975+
__ne__
1976+
__new__
1977+
__reduce__
1978+
__reduce_ex__
1979+
__repr__
1980+
__setattr__
1981+
__sizeof__
1982+
__str__
1983+
__subclasshook__
1984+
");
19681985
}
19691986

19701987
#[test]

crates/ty_ide/src/semantic_tokens.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,12 +1809,12 @@ class BoundedContainer[T: int, U = str]:
18091809
"get_first" @ 642..651: Method [definition]
18101810
"self" @ 652..656: SelfParameter
18111811
"T" @ 661..662: TypeParameter
1812-
"self" @ 679..683: Variable
1812+
"self" @ 679..683: TypeParameter
18131813
"value1" @ 684..690: Variable
18141814
"get_second" @ 700..710: Method [definition]
18151815
"self" @ 711..715: SelfParameter
18161816
"U" @ 720..721: TypeParameter
1817-
"self" @ 738..742: Variable
1817+
"self" @ 738..742: TypeParameter
18181818
"value2" @ 743..749: Variable
18191819
"BoundedContainer" @ 798..814: Class [definition]
18201820
"T" @ 815..816: TypeParameter [definition]

crates/ty_python_semantic/resources/mdtest/annotations/self.md

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ from typing import Self
6464

6565
class A:
6666
def implicit_self(self) -> Self:
67-
# TODO: This should be Self@implicit_self
68-
reveal_type(self) # revealed: Unknown
67+
reveal_type(self) # revealed: Self@implicit_self
6968

7069
return self
7170

@@ -127,19 +126,16 @@ The name `self` is not special in any way.
127126
```py
128127
class B:
129128
def name_does_not_matter(this) -> Self:
130-
# TODO: Should reveal Self@name_does_not_matter
131-
reveal_type(this) # revealed: Unknown
129+
reveal_type(this) # revealed: Self@name_does_not_matter
132130

133131
return this
134132

135133
def positional_only(self, /, x: int) -> Self:
136-
# TODO: Should reveal Self@positional_only
137-
reveal_type(self) # revealed: Unknown
134+
reveal_type(self) # revealed: Self@positional_only
138135
return self
139136

140137
def keyword_only(self, *, x: int) -> Self:
141-
# TODO: Should reveal Self@keyword_only
142-
reveal_type(self) # revealed: Unknown
138+
reveal_type(self) # revealed: Self@keyword_only
143139
return self
144140

145141
@property
@@ -165,8 +161,7 @@ T = TypeVar("T")
165161

166162
class G(Generic[T]):
167163
def id(self) -> Self:
168-
# TODO: Should reveal Self@id
169-
reveal_type(self) # revealed: Unknown
164+
reveal_type(self) # revealed: Self@id
170165

171166
return self
172167

@@ -252,6 +247,20 @@ class LinkedList:
252247
reveal_type(LinkedList().next()) # revealed: LinkedList
253248
```
254249

250+
Attributes can also refer to a generic parameter:
251+
252+
```py
253+
from typing import Generic, TypeVar
254+
255+
T = TypeVar("T")
256+
257+
class C(Generic[T]):
258+
foo: T
259+
def method(self) -> None:
260+
reveal_type(self) # revealed: Self@method
261+
reveal_type(self.foo) # revealed: T@C
262+
```
263+
255264
## Generic Classes
256265

257266
```py
@@ -342,31 +351,28 @@ b: Self
342351

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

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

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

360-
class Bar(Generic[T]):
361-
foo: T
362-
def bar(self) -> T:
363-
return self.foo
369+
class Bar(Generic[T]): ...
364370

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

368374
class MyMetaclass(type):
369-
# TODO: rejected
375+
# TODO: reject the Self usage. because self cannot be used within a metaclass.
370376
def __new__(cls) -> Self:
371377
return super().__new__(cls)
372378
```

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ class C:
2626
c_instance = C(1)
2727

2828
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
29-
30-
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
31-
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
29+
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"]
3230

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

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

180-
# TODO: Should be `Unknown | Literal[1, "a"]`
181-
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
178+
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"]
182179

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

@@ -399,9 +396,19 @@ class TupleIterable:
399396

400397
class C:
401398
def __init__(self) -> None:
399+
# TODO: Should not emit this diagnostic
400+
# error: [unresolved-attribute]
402401
[... for self.a in IntIterable()]
402+
# TODO: Should not emit this diagnostic
403+
# error: [unresolved-attribute]
404+
# error: [unresolved-attribute]
403405
[... for (self.b, self.c) in TupleIterable()]
406+
# TODO: Should not emit this diagnostic
407+
# error: [unresolved-attribute]
408+
# error: [unresolved-attribute]
404409
[... for self.d in IntIterable() for self.e in IntIterable()]
410+
# TODO: Should not emit this diagnostic
411+
# error: [unresolved-attribute]
405412
[[... for self.f in IntIterable()] for _ in IntIterable()]
406413
[[... for self.g in IntIterable()] for self in [D()]]
407414

@@ -598,6 +605,8 @@ class C:
598605
self.c = c
599606
if False:
600607
def set_e(self, e: str) -> None:
608+
# TODO: Should not emit this diagnostic
609+
# error: [unresolved-attribute]
601610
self.e = e
602611

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

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

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

888-
# TODO: This should be an `invalid-assignment` error
889-
self.overwritten_in_subclass_method = None
897+
self.overwritten_in_subclass_method = None # error: [invalid-assignment]
890898

891-
# TODO: This should be an `invalid-assignment` error
892-
self.pure_overwritten_in_subclass_method = None
899+
self.pure_overwritten_in_subclass_method = None # error: [invalid-assignment]
893900

894901
self.pure_undeclared = "intermediate"
895902

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

18401847
class ThisFails:
18411848
def __init__(self):
1849+
# error: [invalid-assignment] "Implicit shadowing of function `__getattribute__`"
18421850
self.__getattribute__ = external_getattribute
18431851

18441852
# error: [unresolved-attribute]

crates/ty_python_semantic/resources/mdtest/call/dunder.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ class C:
205205
return str(key)
206206

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

211211
# This is still fine, and simply calls the `__getitem__` method on the class

crates/ty_python_semantic/resources/mdtest/class/super.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,13 @@ class A:
163163

164164
class B(A):
165165
def __init__(self, a: int):
166-
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, B>`
167-
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
166+
reveal_type(super()) # revealed: <super: <class 'B'>, B>
168167
reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
169168
super().__init__(a)
170169

171170
@classmethod
172171
def f(cls):
173-
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
172+
# TODO: Once `cls` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
174173
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
175174
super().f()
176175

@@ -358,15 +357,15 @@ from __future__ import annotations
358357

359358
class A:
360359
def test(self):
361-
reveal_type(super()) # revealed: <super: <class 'A'>, Unknown>
360+
reveal_type(super()) # revealed: <super: <class 'A'>, A>
362361

363362
class B:
364363
def test(self):
365-
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
364+
reveal_type(super()) # revealed: <super: <class 'B'>, B>
366365

367366
class C(A.B):
368367
def test(self):
369-
reveal_type(super()) # revealed: <super: <class 'C'>, Unknown>
368+
reveal_type(super()) # revealed: <super: <class 'C'>, C>
370369

371370
def inner(t: C):
372371
reveal_type(super()) # revealed: <super: <class 'B'>, C>
@@ -616,7 +615,7 @@ class A:
616615
class B(A):
617616
def __init__(self, a: int):
618617
super().__init__(a)
619-
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
618+
# error: [unresolved-attribute] "Type `<super: <class 'B'>, B>` has no attribute `a`"
620619
super().a
621620

622621
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"

crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def f1(flag: bool):
170170
attr = DataDescriptor()
171171

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

175176
reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]

crates/ty_python_semantic/resources/mdtest/named_tuple.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,7 @@ class SuperUser(User):
208208
def now_called_robert(self):
209209
self.name = "Robert" # fine because overridden with a mutable attribute
210210

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

215214
james = SuperUser(0, "James", 42, "Jimmy")

0 commit comments

Comments
 (0)