[ty] typing.Self is bound by the method, not the class (#19784)

This fixes our logic for binding a legacy typevar with its binding
context. (To recap, a legacy typevar starts out "unbound" when it is
first created, and each time it's used in a generic class or function,
we "bind" it with the corresponding `Definition`.)

We treat `typing.Self` the same as a legacy typevar, and so we apply
this binding logic to it too. Before, we were using the enclosing class
as its binding context. But that's not correct — it's the method where
`typing.Self` is used that binds the typevar. (Each invocation of the
method will find a new specialization of `Self` based on the specific
instance type containing the invoked method.)

This required plumbing through some additional state to the
`in_type_expression` method.

This also revealed that we weren't handling `Self`-typed instance
attributes correctly (but were coincidentally not getting the expected
false positive diagnostics).
This commit is contained in:
Douglas Creager
2025-08-06 17:26:17 -04:00
committed by GitHub
parent 21ac16db85
commit 585ce12ace
9 changed files with 216 additions and 69 deletions

View File

@@ -16,7 +16,7 @@ from typing import Self
class Shape:
def set_scale(self: Self, scale: float) -> Self:
reveal_type(self) # revealed: Self@Shape
reveal_type(self) # revealed: Self@set_scale
return self
def nested_type(self: Self) -> list[Self]:
@@ -24,10 +24,17 @@ class Shape:
def nested_func(self: Self) -> Self:
def inner() -> Self:
reveal_type(self) # revealed: Self@Shape
reveal_type(self) # revealed: Self@nested_func
return self
return inner()
def nested_func_without_enclosing_binding(self):
def inner(x: Self):
# TODO: revealed: Self@nested_func_without_enclosing_binding
# (The outer method binds an implicit `Self`)
reveal_type(x) # revealed: Self@inner
inner(self)
def implicit_self(self) -> Self:
# TODO: first argument in a method should be considered as "typing.Self"
reveal_type(self) # revealed: Unknown
@@ -38,13 +45,13 @@ reveal_type(Shape().nested_func()) # revealed: Shape
class Circle(Shape):
def set_scale(self: Self, scale: float) -> Self:
reveal_type(self) # revealed: Self@Circle
reveal_type(self) # revealed: Self@set_scale
return self
class Outer:
class Inner:
def foo(self: Self) -> Self:
reveal_type(self) # revealed: Self@Inner
reveal_type(self) # revealed: Self@foo
return self
```
@@ -99,6 +106,9 @@ reveal_type(Shape.bar()) # revealed: Unknown
python-version = "3.11"
```
TODO: The use of `Self` to annotate the `next_node` attribute should be
[modeled as a property][self attribute], using `Self` in its parameter and return type.
```py
from typing import Self
@@ -108,6 +118,8 @@ class LinkedList:
def next(self: Self) -> Self:
reveal_type(self.value) # revealed: int
# TODO: no error
# error: [invalid-return-type]
return self.next_node
reveal_type(LinkedList().next()) # revealed: LinkedList
@@ -151,7 +163,7 @@ from typing import Self
class Shape:
def union(self: Self, other: Self | None):
reveal_type(other) # revealed: Self@Shape | None
reveal_type(other) # revealed: Self@union | None
return self
```
@@ -205,3 +217,5 @@ class MyMetaclass(type):
def __new__(cls) -> Self:
return super().__new__(cls)
```
[self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations