mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[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:
parent
21ac16db85
commit
585ce12ace
9 changed files with 216 additions and 69 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue