[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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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

View file

@ -27,7 +27,7 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
class Foo:
def method(self, x: Self):
reveal_type(x) # revealed: Self@Foo
reveal_type(x) # revealed: Self@method
```
## Type expressions

View file

@ -365,3 +365,33 @@ def g(x: T) -> T | None:
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
```
## Opaque decorators don't affect typevar binding
Inside the body of a generic function, we should be able to see that the typevars bound by that
function are in fact bound by that function. This requires being able to see the enclosing
function's _undecorated_ type and signature, especially in the case where a gradually typed
decorator "hides" the function type from outside callers.
```py
from typing import cast, Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
T = TypeVar("T")
def opaque_decorator(f: Any) -> Any:
return f
def transparent_decorator(f: F) -> F:
return f
@opaque_decorator
def decorated(t: T) -> None:
# error: [redundant-cast]
reveal_type(cast(T, t)) # revealed: T@decorated
@transparent_decorator
def decorated(t: T) -> None:
# error: [redundant-cast]
reveal_type(cast(T, t)) # revealed: T@decorated
```

View file

@ -377,3 +377,30 @@ def f(
# error: [invalid-argument-type] "does not satisfy upper bound"
reveal_type(close_and_return(g)) # revealed: Unknown
```
## Opaque decorators don't affect typevar binding
Inside the body of a generic function, we should be able to see that the typevars bound by that
function are in fact bound by that function. This requires being able to see the enclosing
function's _undecorated_ type and signature, especially in the case where a gradually typed
decorator "hides" the function type from outside callers.
```py
from typing import cast, Any, Callable
def opaque_decorator(f: Any) -> Any:
return f
def transparent_decorator[F: Callable[..., Any]](f: F) -> F:
return f
@opaque_decorator
def decorated[T](t: T) -> None:
# error: [redundant-cast]
reveal_type(cast(T, t)) # revealed: T@decorated
@transparent_decorator
def decorated[T](t: T) -> None:
# error: [redundant-cast]
reveal_type(cast(T, t)) # revealed: T@decorated
```

View file

@ -150,9 +150,9 @@ class Person(NamedTuple):
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._fields) # revealed: tuple[str, ...]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Self@NamedTupleFallback
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Self@_make
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@NamedTupleFallback
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace
# TODO: should be `Person` once we support `Self`
reveal_type(Person._make(("Alice", 42))) # revealed: Unknown