Skip to content

Commit e77fefe

Browse files
Glyphacksharkdp
andcommitted
[ty] Assume type of self is typing.Self in method calls (#18007)
Part of astral-sh/ty#159 This PR only adjusts the signature of a method so if it has a `self` argument then that argument will have type of `Typing.Self` even if it's not specified. If user provides an explicit annotation then Ty will not override that annotation. - astral-sh/ty#1131 - astral-sh/ty#1157 - astral-sh/ty#1156 - astral-sh/ty#1173 - #20328 - astral-sh/ty#1163 - astral-sh/ty#1196 Added mdtests. Also some tests need #18473 to work completely. So I added a todo for those new cases that I added. --------- Co-authored-by: David Peter <mail@david-peter.de>
1 parent 188ad30 commit e77fefe

File tree

18 files changed

+383
-103
lines changed

18 files changed

+383
-103
lines changed

crates/ruff_python_ast/src/nodes.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3030,6 +3030,12 @@ impl Parameters {
30303030
.find(|arg| arg.parameter.name.as_str() == name)
30313031
}
30323032

3033+
/// Returns the index of the parameter with the given name
3034+
pub fn index(&self, name: &str) -> Option<usize> {
3035+
self.iter_non_variadic_params()
3036+
.position(|arg| arg.parameter.name.as_str() == name)
3037+
}
3038+
30333039
/// Returns an iterator over all parameters included in this [`Parameters`] node.
30343040
pub fn iter(&self) -> ParametersIterator<'_> {
30353041
ParametersIterator::new(self)

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

Lines changed: 145 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,6 @@ class Shape:
3333
reveal_type(x) # revealed: Self@nested_func_without_enclosing_binding
3434
inner(self)
3535

36-
def implicit_self(self) -> Self:
37-
# TODO: first argument in a method should be considered as "typing.Self"
38-
reveal_type(self) # revealed: Unknown
39-
return self
40-
4136
reveal_type(Shape().nested_type()) # revealed: list[Shape]
4237
reveal_type(Shape().nested_func()) # revealed: Shape
4338

@@ -53,6 +48,104 @@ class Outer:
5348
return self
5449
```
5550

51+
## Detection of implicit Self
52+
53+
In instance methods, the first parameter (regardless of its name) is assumed to have type
54+
`typing.Self` unless it has an explicit annotation. This does not apply to `@classmethod` and
55+
`@staticmethod`.
56+
57+
```toml
58+
[environment]
59+
python-version = "3.11"
60+
```
61+
62+
```py
63+
from typing import Self
64+
65+
class A:
66+
def implicit_self(self) -> Self:
67+
# TODO: first argument in a method should be considered as "typing.Self"
68+
reveal_type(self) # revealed: Unknown
69+
return self
70+
71+
def foo(self) -> int:
72+
def first_arg_is_not_self(a: int) -> int:
73+
return a
74+
return first_arg_is_not_self(1)
75+
76+
@classmethod
77+
def bar(cls): ...
78+
@staticmethod
79+
def static(x): ...
80+
81+
a = A()
82+
# TODO: Should reveal Self@implicit_self. Requires implicit self in method body(https://github.com/astral-sh/ruff/pull/18473)
83+
reveal_type(a.implicit_self()) # revealed: A
84+
reveal_type(a.implicit_self) # revealed: bound method A.implicit_self() -> A
85+
```
86+
87+
If the method is a class or static method then first argument is not self:
88+
89+
```py
90+
A.bar()
91+
a.static(1)
92+
```
93+
94+
"self" name is not special; any first parameter name is treated as Self.
95+
96+
```py
97+
from typing import Self, Generic, TypeVar
98+
99+
T = TypeVar("T")
100+
101+
class B:
102+
def implicit_this(this) -> Self:
103+
# TODO: Should reveal Self@implicit_this
104+
reveal_type(this) # revealed: Unknown
105+
return this
106+
107+
def ponly(self, /, x: int) -> None:
108+
# TODO: Should reveal Self@ponly
109+
reveal_type(self) # revealed: Unknown
110+
111+
def kwonly(self, *, x: int) -> None:
112+
# TODO: Should reveal Self@kwonly
113+
reveal_type(self) # revealed: Unknown
114+
115+
@property
116+
def name(self) -> str:
117+
# TODO: Should reveal Self@name
118+
reveal_type(self) # revealed: Unknown
119+
return "b"
120+
121+
B.ponly(B(), 1)
122+
B.name
123+
B.kwonly(B(), x=1)
124+
125+
class G(Generic[T]):
126+
def id(self) -> Self:
127+
# TODO: Should reveal Self@id
128+
reveal_type(self) # revealed: Unknown
129+
return self
130+
131+
g = G[int]()
132+
133+
# TODO: Should reveal Self@id Requires implicit self in method body(https://github.com/astral-sh/ruff/pull/18473)
134+
reveal_type(G[int].id(g)) # revealed: G[int]
135+
```
136+
137+
Free functions and nested functions do not use implicit `Self`:
138+
139+
```py
140+
def not_a_method(self):
141+
reveal_type(self) # revealed: Unknown
142+
143+
class C:
144+
def outer(self) -> None:
145+
def inner(self):
146+
reveal_type(self) # revealed: Unknown
147+
```
148+
56149
## typing_extensions
57150

58151
```toml
@@ -208,6 +301,53 @@ class MyMetaclass(type):
208301
return super().__new__(cls)
209302
```
210303

304+
## Explicit Annotation Overrides Implicit `Self`
305+
306+
If the first parameter is explicitly annotated, that annotation takes precedence over the implicit
307+
`Self` treatment.
308+
309+
```toml
310+
[environment]
311+
python-version = "3.11"
312+
```
313+
314+
```py
315+
class Explicit:
316+
# TODO: Should warn the user if self is overriden with a type that is not subtype of the class
317+
def bad(self: int) -> None:
318+
reveal_type(self) # revealed: int
319+
320+
def forward(self: "Explicit") -> None:
321+
reveal_type(self) # revealed: Explicit
322+
323+
e = Explicit()
324+
# error: [invalid-argument-type] "Argument to bound method `bad` is incorrect: Expected `int`, found `Explicit`"
325+
e.bad()
326+
```
327+
328+
## Type of Implicit Self
329+
330+
The assigned type to self argument depends on the method signature. When the method is defined in a
331+
non-generic class and has no other mention of `typing.Self` (for example in return type) then type
332+
of `self` is instance of the class.
333+
334+
```py
335+
from typing import Self
336+
337+
class C:
338+
def f(self) -> Self:
339+
return self
340+
341+
def z(self) -> None: ...
342+
343+
C.z(1) # error: [invalid-argument-type] "Argument to function `z` is incorrect: Expected `C`, found `Literal[1]`"
344+
```
345+
346+
```py
347+
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Argument type `Literal[1]` does not satisfy upper bound `C` of type variable `Self`"
348+
C.f(1)
349+
```
350+
211351
## Binding a method fixes `Self`
212352

213353
When a method is bound, any instances of `Self` in its signature are "fixed", since we now know the

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ reveal_type(bound_method(1)) # revealed: str
6969
When we call the function object itself, we need to pass the `instance` explicitly:
7070

7171
```py
72-
C.f(1) # error: [missing-argument]
72+
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `C`, found `Literal[1]`"
73+
# error: [missing-argument]
74+
C.f(1)
7375

7476
reveal_type(C.f(C(), 1)) # revealed: str
7577
```

crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ def _(flag: bool):
431431
reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2]
432432

433433
C7.union_of_metaclass_attributes = 2 if flag else 1
434+
# TODO: https://github.com/astral-sh/ty/issues/1163
435+
# error: [invalid-assignment]
434436
C7.union_of_metaclass_data_descriptor_and_attribute = 2 if flag else 100
435437
C7.union_of_class_attributes = 2 if flag else 1
436438
C7.union_of_class_data_descriptor_and_attribute = 2 if flag else DataDescriptor()

crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -562,17 +562,17 @@ class C(Generic[T]):
562562
return u
563563

564564
reveal_type(generic_context(C)) # revealed: tuple[T@C]
565-
reveal_type(generic_context(C.method)) # revealed: None
566-
reveal_type(generic_context(C.generic_method)) # revealed: tuple[U@generic_method]
565+
reveal_type(generic_context(C.method)) # revealed: tuple[Self@method]
566+
reveal_type(generic_context(C.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
567567
reveal_type(generic_context(C[int])) # revealed: None
568-
reveal_type(generic_context(C[int].method)) # revealed: None
569-
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[U@generic_method]
568+
reveal_type(generic_context(C[int].method)) # revealed: tuple[Self@method]
569+
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
570570

571571
c: C[int] = C[int]()
572572
reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"]
573573
reveal_type(generic_context(c)) # revealed: None
574-
reveal_type(generic_context(c.method)) # revealed: None
575-
reveal_type(generic_context(c.generic_method)) # revealed: tuple[U@generic_method]
574+
reveal_type(generic_context(c.method)) # revealed: tuple[Self@method]
575+
reveal_type(generic_context(c.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
576576
```
577577

578578
## Specializations propagate

crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -504,17 +504,17 @@ class C[T]:
504504
def cannot_shadow_class_typevar[T](self, t: T): ...
505505

506506
reveal_type(generic_context(C)) # revealed: tuple[T@C]
507-
reveal_type(generic_context(C.method)) # revealed: None
508-
reveal_type(generic_context(C.generic_method)) # revealed: tuple[U@generic_method]
507+
reveal_type(generic_context(C.method)) # revealed: tuple[Self@method]
508+
reveal_type(generic_context(C.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
509509
reveal_type(generic_context(C[int])) # revealed: None
510-
reveal_type(generic_context(C[int].method)) # revealed: None
511-
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[U@generic_method]
510+
reveal_type(generic_context(C[int].method)) # revealed: tuple[Self@method]
511+
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
512512

513513
c: C[int] = C[int]()
514514
reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"]
515515
reveal_type(generic_context(c)) # revealed: None
516-
reveal_type(generic_context(c.method)) # revealed: None
517-
reveal_type(generic_context(c.generic_method)) # revealed: tuple[U@generic_method]
516+
reveal_type(generic_context(c.method)) # revealed: tuple[Self@method]
517+
reveal_type(generic_context(c.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
518518
```
519519

520520
## Specializations propagate

crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,5 @@ class C:
534534
def _(x: int):
535535
reveal_type(C().explicit_self(x)) # revealed: tuple[C, int]
536536

537-
# TODO: this should be `tuple[C, int]` as well, once we support implicit `self`
538-
reveal_type(C().implicit_self(x)) # revealed: tuple[Unknown, int]
537+
reveal_type(C().implicit_self(x)) # revealed: tuple[C, int]
539538
```

crates/ty_python_semantic/resources/mdtest/generics/scoping.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str
117117
reveal_type(C[int]().f(1)) # revealed: str
118118
reveal_type(bound_method(1)) # revealed: str
119119

120+
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Argument type `Literal[1]` does not satisfy upper bound `C[Unknown]` of type variable `Self`"
120121
C[int].f(1) # error: [missing-argument]
121122
reveal_type(C[int].f(C[int](), 1)) # revealed: str
122123

@@ -154,7 +155,7 @@ from ty_extensions import generic_context
154155
legacy.m("string", None) # error: [invalid-argument-type]
155156
reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S@m) -> S@m
156157
reveal_type(generic_context(Legacy)) # revealed: tuple[T@Legacy]
157-
reveal_type(generic_context(legacy.m)) # revealed: tuple[S@m]
158+
reveal_type(generic_context(legacy.m)) # revealed: tuple[Self@m, S@m]
158159
```
159160

160161
With PEP 695 syntax, it is clearer that the method uses a separate typevar:

crates/ty_python_semantic/resources/mdtest/named_tuple.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,9 @@ reveal_type(Person._make(("Alice", 42))) # revealed: Unknown
277277

278278
person = Person("Alice", 42)
279279

280+
# error: [invalid-argument-type] "Argument to bound method `_asdict` is incorrect: Expected `NamedTupleFallback`, found `Person`"
280281
reveal_type(person._asdict()) # revealed: dict[str, Any]
281-
# TODO: should be `Person` once we support implicit type of `self`
282-
reveal_type(person._replace(name="Bob")) # revealed: Unknown
282+
reveal_type(person._replace(name="Bob")) # revealed: Person
283283
```
284284

285285
When accessing them on child classes of generic `NamedTuple`s, the return type is specialized
@@ -296,8 +296,7 @@ class Box(NamedTuple, Generic[T]):
296296
class IntBox(Box[int]):
297297
pass
298298

299-
# TODO: should be `IntBox` once we support the implicit type of `self`
300-
reveal_type(IntBox(1)._replace(content=42)) # revealed: Unknown
299+
reveal_type(IntBox(1)._replace(content=42)) # revealed: IntBox
301300
```
302301

303302
## `collections.namedtuple`

crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,12 @@ class Invariant[T]:
363363
def _(x: object):
364364
if isinstance(x, Invariant):
365365
reveal_type(x) # revealed: Top[Invariant[Unknown]]
366+
# error: [invalid-argument-type] "Argument to bound method `get` is incorrect: Expected `Self@get`, found `Top[Invariant[Unknown]]`"
367+
# error: [invalid-argument-type] "Argument to bound method `get` is incorrect: Argument type `Top[Invariant[Unknown]]` does not satisfy upper bound `Bottom[Invariant[Unknown]]` of type variable `Self`"
366368
reveal_type(x.get()) # revealed: object
367369
# error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Never`, found `Literal[42]`"
370+
# error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Self@push`, found `Top[Invariant[Unknown]]`"
371+
# error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Argument type `Top[Invariant[Unknown]]` does not satisfy upper bound `Bottom[Invariant[Unknown]]` of type variable `Self`"
368372
x.push(42)
369373
```
370374

0 commit comments

Comments
 (0)