Skip to content

Commit bbcd7e0

Browse files
authored
[ty] Synthetic function-like callables (#18242)
## Summary We create `Callable` types for synthesized functions like the `__init__` method of a dataclass. These generated functions are real functions though, with descriptor-like behavior. That is, they can bind `self` when accessed on an instance. This was modeled incorrectly so far. ## Test Plan Updated tests
1 parent 48c425c commit bbcd7e0

File tree

4 files changed

+167
-32
lines changed

4 files changed

+167
-32
lines changed

crates/ty_python_semantic/resources/mdtest/dataclasses.md

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ Person(20, "Eve")
5656

5757
## Signature of `__init__`
5858

59-
TODO: All of the following tests are missing the `self` argument in the `__init__` signature.
60-
6159
Declarations in the class body are used to generate the signature of the `__init__` method. If the
6260
attributes are not just declarations, but also bindings, the type inferred from bindings is used as
6361
the default value.
@@ -71,7 +69,7 @@ class D:
7169
y: str = "default"
7270
z: int | None = 1 + 2
7371

74-
reveal_type(D.__init__) # revealed: (x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
72+
reveal_type(D.__init__) # revealed: (self: D, x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
7573
```
7674

7775
This also works if the declaration and binding are split:
@@ -82,7 +80,7 @@ class D:
8280
x: int | None
8381
x = None
8482

85-
reveal_type(D.__init__) # revealed: (x: int | None = None) -> None
83+
reveal_type(D.__init__) # revealed: (self: D, x: int | None = None) -> None
8684
```
8785

8886
Non-fully static types are handled correctly:
@@ -96,7 +94,7 @@ class C:
9694
y: int | Any
9795
z: tuple[int, Any]
9896

99-
reveal_type(C.__init__) # revealed: (x: Any, y: int | Any, z: tuple[int, Any]) -> None
97+
reveal_type(C.__init__) # revealed: (self: C, x: Any, y: int | Any, z: tuple[int, Any]) -> None
10098
```
10199

102100
Variables without annotations are ignored:
@@ -107,7 +105,7 @@ class D:
107105
x: int
108106
y = 1
109107

110-
reveal_type(D.__init__) # revealed: (x: int) -> None
108+
reveal_type(D.__init__) # revealed: (self: D, x: int) -> None
111109
```
112110

113111
If attributes without default values are declared after attributes with default values, a
@@ -132,7 +130,7 @@ class D:
132130
y: ClassVar[str] = "default"
133131
z: bool
134132

135-
reveal_type(D.__init__) # revealed: (x: int, z: bool) -> None
133+
reveal_type(D.__init__) # revealed: (self: D, x: int, z: bool) -> None
136134

137135
d = D(1, True)
138136
reveal_type(d.x) # revealed: int
@@ -150,7 +148,7 @@ class D:
150148
def y(self) -> str:
151149
return ""
152150

153-
reveal_type(D.__init__) # revealed: (x: int) -> None
151+
reveal_type(D.__init__) # revealed: (self: D, x: int) -> None
154152
```
155153

156154
And neither do nested class declarations:
@@ -163,7 +161,7 @@ class D:
163161
class Nested:
164162
y: str
165163

166-
reveal_type(D.__init__) # revealed: (x: int) -> None
164+
reveal_type(D.__init__) # revealed: (self: D, x: int) -> None
167165
```
168166

169167
But if there is a variable annotation with a function or class literal type, the signature of
@@ -181,7 +179,7 @@ class D:
181179
class_literal: TypeOf[SomeClass]
182180
class_subtype_of: type[SomeClass]
183181

184-
# revealed: (function_literal: def some_function() -> None, class_literal: <class 'SomeClass'>, class_subtype_of: type[SomeClass]) -> None
182+
# revealed: (self: D, function_literal: def some_function() -> None, class_literal: <class 'SomeClass'>, class_subtype_of: type[SomeClass]) -> None
185183
reveal_type(D.__init__)
186184
```
187185

@@ -194,7 +192,7 @@ from typing import Callable
194192
class D:
195193
c: Callable[[int], str]
196194

197-
reveal_type(D.__init__) # revealed: (c: (int, /) -> str) -> None
195+
reveal_type(D.__init__) # revealed: (self: D, c: (int, /) -> str) -> None
198196
```
199197

200198
Implicit instance attributes do not affect the signature of `__init__`:
@@ -209,7 +207,7 @@ class D:
209207

210208
reveal_type(D(1).y) # revealed: str
211209

212-
reveal_type(D.__init__) # revealed: (x: int) -> None
210+
reveal_type(D.__init__) # revealed: (self: D, x: int) -> None
213211
```
214212

215213
Annotating expressions does not lead to an entry in `__annotations__` at runtime, and so it wouldn't
@@ -222,7 +220,7 @@ class D:
222220
(x): int = 1
223221

224222
# TODO: should ideally not include a `x` parameter
225-
reveal_type(D.__init__) # revealed: (x: int = Literal[1]) -> None
223+
reveal_type(D.__init__) # revealed: (self: D, x: int = Literal[1]) -> None
226224
```
227225

228226
## `@dataclass` calls with arguments
@@ -529,7 +527,7 @@ class C(Base):
529527
z: int = 10
530528
x: int = 15
531529

532-
reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
530+
reveal_type(C.__init__) # revealed: (self: C, x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
533531
```
534532

535533
## Generic dataclasses
@@ -582,7 +580,7 @@ class UppercaseString:
582580
class C:
583581
upper: UppercaseString = UppercaseString()
584582

585-
reveal_type(C.__init__) # revealed: (upper: str = str) -> None
583+
reveal_type(C.__init__) # revealed: (self: C, upper: str = str) -> None
586584

587585
c = C("abc")
588586
reveal_type(c.upper) # revealed: str
@@ -628,7 +626,7 @@ class ConvertToLength:
628626
class C:
629627
converter: ConvertToLength = ConvertToLength()
630628

631-
reveal_type(C.__init__) # revealed: (converter: str = Literal[""]) -> None
629+
reveal_type(C.__init__) # revealed: (self: C, converter: str = Literal[""]) -> None
632630

633631
c = C("abc")
634632
reveal_type(c.converter) # revealed: int
@@ -667,7 +665,7 @@ class AcceptsStrAndInt:
667665
class C:
668666
field: AcceptsStrAndInt = AcceptsStrAndInt()
669667

670-
reveal_type(C.__init__) # revealed: (field: str | int = int) -> None
668+
reveal_type(C.__init__) # revealed: (self: C, field: str | int = int) -> None
671669
```
672670

673671
## `dataclasses.field`
@@ -728,7 +726,7 @@ import dataclasses
728726
class C:
729727
x: str
730728

731-
reveal_type(C.__init__) # revealed: (x: str) -> None
729+
reveal_type(C.__init__) # revealed: (self: C, x: str) -> None
732730
```
733731

734732
### Dataclass with custom `__init__` method
@@ -821,10 +819,57 @@ reveal_type(Person.__mro__) # revealed: tuple[<class 'Person'>, <class 'object'
821819
The generated methods have the following signatures:
822820

823821
```py
824-
# TODO: `self` is missing here
825-
reveal_type(Person.__init__) # revealed: (name: str, age: int | None = None) -> None
822+
reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int | None = None) -> None
826823

827824
reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str
828825

829826
reveal_type(Person.__eq__) # revealed: def __eq__(self, value: object, /) -> bool
830827
```
828+
829+
## Function-like behavior of synthesized methods
830+
831+
Here, we make sure that the synthesized methods of dataclasses behave like proper functions.
832+
833+
```toml
834+
[environment]
835+
python-version = "3.12"
836+
```
837+
838+
```py
839+
from dataclasses import dataclass
840+
from typing import Callable
841+
from types import FunctionType
842+
from ty_extensions import CallableTypeOf, TypeOf, static_assert, is_subtype_of, is_assignable_to
843+
844+
@dataclass
845+
class C:
846+
x: int
847+
848+
reveal_type(C.__init__) # revealed: (self: C, x: int) -> None
849+
reveal_type(type(C.__init__)) # revealed: <class 'FunctionType'>
850+
851+
# We can access attributes that are defined on functions:
852+
reveal_type(type(C.__init__).__code__) # revealed: CodeType
853+
reveal_type(C.__init__.__code__) # revealed: CodeType
854+
855+
def equivalent_signature(self: C, x: int) -> None:
856+
pass
857+
858+
type DunderInitType = TypeOf[C.__init__]
859+
type EquivalentPureCallableType = Callable[[C, int], None]
860+
type EquivalentFunctionLikeCallableType = CallableTypeOf[equivalent_signature]
861+
862+
static_assert(is_subtype_of(DunderInitType, EquivalentPureCallableType))
863+
static_assert(is_assignable_to(DunderInitType, EquivalentPureCallableType))
864+
865+
static_assert(not is_subtype_of(EquivalentPureCallableType, DunderInitType))
866+
static_assert(not is_assignable_to(EquivalentPureCallableType, DunderInitType))
867+
868+
static_assert(is_subtype_of(DunderInitType, EquivalentFunctionLikeCallableType))
869+
static_assert(is_assignable_to(DunderInitType, EquivalentFunctionLikeCallableType))
870+
871+
static_assert(not is_subtype_of(EquivalentFunctionLikeCallableType, DunderInitType))
872+
static_assert(not is_assignable_to(EquivalentFunctionLikeCallableType, DunderInitType))
873+
874+
static_assert(is_subtype_of(DunderInitType, FunctionType))
875+
```

0 commit comments

Comments
 (0)