Skip to content

Commit 4d39967

Browse files
[ty] Bind typing.Self in class attributes and assignment (#23108)
## Summary Closes astral-sh/ty#1124.
1 parent 8aba480 commit 4d39967

8 files changed

Lines changed: 388 additions & 54 deletions

File tree

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

Lines changed: 207 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,12 @@ class Bar:
323323
def bar(self: Self, x: Foo[Self]):
324324
# revealed: bound method Foo[Self@bar].foo() -> Self@bar
325325
reveal_type(x.foo)
326+
reveal_type(x.foo()) # revealed: Self@bar
326327

327328
def f[U: Bar](x: Foo[U]):
328329
# revealed: bound method Foo[U@f].foo() -> U@f
329330
reveal_type(x.foo)
331+
reveal_type(x.foo()) # revealed: U@f
330332
```
331333

332334
## typing_extensions
@@ -473,9 +475,6 @@ reveal_type(Child.create()) # revealed: Child
473475

474476
## Attributes
475477

476-
TODO: The use of `Self` to annotate the `next_node` attribute should be
477-
[modeled as a property][self attribute], using `Self` in its parameter and return type.
478-
479478
```py
480479
from typing import Self
481480

@@ -485,13 +484,89 @@ class LinkedList:
485484

486485
def next(self: Self) -> Self:
487486
reveal_type(self.value) # revealed: int
488-
# TODO: no error
489-
# error: [invalid-return-type]
490487
return self.next_node
491488

492489
reveal_type(LinkedList().next()) # revealed: LinkedList
493490
```
494491

492+
Dataclass fields can also use `Self` in their annotations:
493+
494+
```py
495+
from dataclasses import dataclass
496+
from typing import Self
497+
498+
@dataclass
499+
class Node:
500+
parent: Self | None = None
501+
502+
Node(Node())
503+
```
504+
505+
Attributes annotated with `Self` can be assigned on instances:
506+
507+
```py
508+
from typing import Self
509+
510+
class MyClass:
511+
field: Self | None = None
512+
513+
def _(c: MyClass):
514+
c.field = c
515+
```
516+
517+
Self from class body annotations and method signatures represent the same logical type variable.
518+
When a method returns an attribute annotated with `Self` in the class body, the class-body `Self`
519+
and the method's `Self` should be considered the same type, even though they have different binding
520+
contexts internally:
521+
522+
```py
523+
from typing import Self
524+
525+
class Chain:
526+
next: Self
527+
value: int
528+
529+
def advance(self: Self) -> Self:
530+
return self.next
531+
532+
def advance_twice(self: Self) -> Self:
533+
return self.advance().advance()
534+
535+
class SubChain(Chain):
536+
extra: str
537+
538+
reveal_type(SubChain().advance()) # revealed: SubChain
539+
reveal_type(SubChain().advance_twice()) # revealed: SubChain
540+
```
541+
542+
Self-typed attributes that flow through generic containers should also work:
543+
544+
```py
545+
from typing import Self
546+
547+
class TreeNode:
548+
children: list[Self]
549+
parent: Self | None
550+
551+
def first_child(self) -> Self | None:
552+
if self.children:
553+
return self.children[0]
554+
return None
555+
556+
def all_descendants(self) -> list[Self]:
557+
result: list[Self] = []
558+
for child in self.children:
559+
result.append(child)
560+
result.extend(child.all_descendants())
561+
return result
562+
563+
def root(self) -> Self:
564+
node = self
565+
while node.parent is not None:
566+
node = node.parent
567+
return node
568+
```
569+
495570
Attributes can also refer to a generic parameter:
496571

497572
```py
@@ -506,6 +581,26 @@ class C(Generic[T]):
506581
reveal_type(self.foo) # revealed: T@C
507582
```
508583

584+
## Callable attributes that return `Self`
585+
586+
Attributes annotated as callables returning `Self` should bind to the concrete class.
587+
588+
```py
589+
from typing import Callable, Self
590+
591+
class Factory:
592+
maker: Callable[[], Self]
593+
594+
def __init__(self) -> None:
595+
self.maker = lambda: self
596+
597+
class Sub(Factory):
598+
pass
599+
600+
def _(s: Sub):
601+
reveal_type(s.maker()) # revealed: Sub
602+
```
603+
509604
## Generic Classes
510605

511606
```py
@@ -559,7 +654,39 @@ D[K]().h()
559654

560655
## Protocols
561656

562-
TODO: <https://typing.python.org/en/latest/spec/generics.html#use-in-protocols>
657+
See also: <https://typing.python.org/en/latest/spec/generics.html#use-in-protocols>
658+
659+
```py
660+
from typing import Self, Protocol
661+
662+
class Copyable(Protocol):
663+
def copy(self) -> Self: ...
664+
665+
class Linkable(Protocol):
666+
next_node: Self
667+
668+
def advance(self) -> Self:
669+
return self.next_node
670+
671+
def _(l: Linkable) -> None:
672+
# TODO: Should be `Linkable`
673+
reveal_type(l.next_node) # revealed: @Todo(type[T] for protocols)
674+
675+
class CopyableImpl:
676+
def copy(self) -> Self:
677+
return self
678+
679+
class SubCopyable(CopyableImpl): ...
680+
681+
def copy_it(x: Copyable) -> None:
682+
reveal_type(x.copy()) # revealed: Copyable
683+
684+
def copy_concrete(x: CopyableImpl) -> None:
685+
reveal_type(x.copy()) # revealed: CopyableImpl
686+
687+
def copy_sub(x: SubCopyable) -> None:
688+
reveal_type(x.copy()) # revealed: SubCopyable
689+
```
563690

564691
## Annotations
565692

@@ -776,4 +903,77 @@ def _(c: CallableTypeOf[C().method]):
776903
reveal_type(c) # revealed: (...) -> None
777904
```
778905

779-
[self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations
906+
## Bound methods stored as instance attributes
907+
908+
Bound methods from other objects stored as instance attributes should not have their signatures
909+
affected by `Self` type binding. This is a regression test for false positives in projects like
910+
jinja's `LRUCache`.
911+
912+
```py
913+
from collections import deque
914+
915+
class MyClass:
916+
def __init__(self) -> None:
917+
self._queue: deque[int] = deque()
918+
self._append = self._queue.append
919+
920+
def add(self, value: int) -> None:
921+
self._append(value)
922+
```
923+
924+
## Self in class attributes with generic classes
925+
926+
Django-like patterns where a class attribute uses `Self` as a type argument to a generic class. Both
927+
class access (`Confirmation.objects`) and instance access (`instance.objects`) should properly bind
928+
`Self` to the concrete class.
929+
930+
```py
931+
from typing import Self, Generic, TypeVar
932+
933+
T = TypeVar("T")
934+
935+
class Manager(Generic[T]):
936+
def get(self) -> T:
937+
raise NotImplementedError
938+
939+
class Model:
940+
objects: Manager[Self]
941+
942+
class Confirmation(Model):
943+
expiry_date: int
944+
945+
def test() -> None:
946+
# Class access: Self is bound to Confirmation
947+
confirmation = Confirmation.objects.get()
948+
reveal_type(confirmation) # revealed: Confirmation
949+
x = confirmation.expiry_date # Should work - Confirmation has expiry_date
950+
951+
# Instance access: Self should also be bound to Confirmation
952+
instance = Confirmation()
953+
reveal_type(instance.objects) # revealed: Manager[Confirmation]
954+
instance_result = instance.objects.get()
955+
reveal_type(instance_result) # revealed: Confirmation
956+
```
957+
958+
## Self in class attributes with descriptors
959+
960+
`Self` binding should also work when the attribute type involves a descriptor.
961+
962+
```py
963+
from typing import Self, Generic, TypeVar
964+
965+
T = TypeVar("T")
966+
967+
class Descriptor(Generic[T]):
968+
def __get__(self, instance, owner) -> T:
969+
raise NotImplementedError
970+
971+
class Base:
972+
attr: Descriptor[Self] = Descriptor()
973+
974+
class Child(Base):
975+
pass
976+
977+
reveal_type(Child.attr) # revealed: Child
978+
reveal_type(Child().attr) # revealed: Child
979+
```

crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ X.aaaaooooooo # error: [unresolved-attribute]
2121
Foo.X.startswith # error: [unresolved-attribute]
2222
Foo.Bar().y.startswith # error: [unresolved-attribute]
2323

24-
# TODO: false positive (just testing the diagnostic in the meantime)
25-
Foo().b.a # error: [unresolved-attribute]
24+
# `Foo().b` resolves `Self` to `Foo`, so `.a` is valid.
25+
Foo().b.a
2626
```

crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu…_-_Diagnostics_for_inva…_(249d635e74a41c9e).snap

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/special_form
3131
16 | Foo.X.startswith # error: [unresolved-attribute]
3232
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
3333
18 |
34-
19 | # TODO: false positive (just testing the diagnostic in the meantime)
35-
20 | Foo().b.a # error: [unresolved-attribute]
34+
19 | # `Foo().b` resolves `Self` to `Foo`, so `.a` is valid.
35+
20 | Foo().b.a
3636
```
3737

3838
# Diagnostics
@@ -95,21 +95,9 @@ error[unresolved-attribute]: Special form `typing.LiteralString` has no attribut
9595
17 | Foo.Bar().y.startswith # error: [unresolved-attribute]
9696
| ^^^^^^^^^^^^^^^^^^^^^^
9797
18 |
98-
19 | # TODO: false positive (just testing the diagnostic in the meantime)
98+
19 | # `Foo().b` resolves `Self` to `Foo`, so `.a` is valid.
9999
|
100100
help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString`
101101
info: rule `unresolved-attribute` is enabled by default
102102
103103
```
104-
105-
```
106-
error[unresolved-attribute]: Special form `typing.Self` has no attribute `a`
107-
--> src/mdtest_snippet.py:20:1
108-
|
109-
19 | # TODO: false positive (just testing the diagnostic in the meantime)
110-
20 | Foo().b.a # error: [unresolved-attribute]
111-
| ^^^^^^^^^
112-
|
113-
info: rule `unresolved-attribute` is enabled by default
114-
115-
```

0 commit comments

Comments
 (0)