Skip to content

Commit f547a69

Browse files
committed
[red-knot] Skip constructor check for dataclasses
1 parent 701aecb commit f547a69

File tree

9 files changed

+287
-5
lines changed

9 files changed

+287
-5
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Dataclasses
2+
3+
## Basic
4+
5+
Decorating a class with `@dataclass` is a convenient way to add special methods such as `__init__`,
6+
`__repr__`, and `__eq__` to a class. The following example shows the basic usage of the `@dataclass`
7+
decorator. By default, only the three mentioned methods are generated.
8+
9+
```py
10+
from dataclasses import dataclass
11+
12+
@dataclass
13+
class Person:
14+
name: str
15+
age: int | None = None
16+
17+
alice1 = Person("Alice", 30)
18+
alice2 = Person(name="Alice", age=30)
19+
alice3 = Person(age=30, name="Alice")
20+
alice4 = Person("Alice", age=30)
21+
22+
reveal_type(alice1) # revealed: Person
23+
reveal_type(type(alice1)) # revealed: type[Person]
24+
25+
reveal_type(alice1.name) # revealed: str
26+
reveal_type(alice1.age) # revealed: int | None
27+
28+
reveal_type(repr(alice1)) # revealed: str
29+
30+
reveal_type(alice1 == alice2) # revealed: bool
31+
reveal_type(alice1 == "Alice") # revealed: bool
32+
33+
bob = Person("Bob")
34+
bob2 = Person("Bob", None)
35+
bob3 = Person(name="Bob")
36+
bob4 = Person(name="Bob", age=None)
37+
```
38+
39+
The signature of the `__init__` method is generated based on the classes attributes. The following
40+
calls are not valid:
41+
42+
```py
43+
# TODO: should be an error: too few arguments
44+
Person()
45+
46+
# TODO: should be an error: too many arguments
47+
Person("Eve", 20, "too many arguments")
48+
49+
# TODO: should be an error: wrong argument type
50+
Person("Eve", "string instead of int")
51+
52+
# TODO: should be an error: wrong argument types
53+
Person(20, "Eve")
54+
```
55+
56+
## `@dataclass` calls with arguments
57+
58+
The `@dataclass` decorator can take several arguments to customize the behavior of the generated
59+
methods. This first test merely makes sure that we still treat the class as a dataclass if (the
60+
default) arguments are passed in:
61+
62+
```py
63+
from dataclasses import dataclass
64+
65+
@dataclass(init=True, repr=True, eq=True)
66+
class Person:
67+
name: str
68+
age: int | None = None
69+
70+
alice = Person("Alice", 30)
71+
reveal_type(repr(alice)) # revealed: str
72+
reveal_type(alice == alice) # revealed: bool
73+
```
74+
75+
## `dataclasses.field`
76+
77+
To do
78+
79+
## Inheritance
80+
81+
To do
82+
83+
## Generic dataclass
84+
85+
```py
86+
from dataclasses import dataclass
87+
88+
@dataclass
89+
class DataWithDescription[T]:
90+
data: T
91+
description: str
92+
93+
reveal_type(DataWithDescription[int]) # revealed: Literal[DataWithDescription[int]]
94+
95+
d_int = DataWithDescription[int](1, "description") # OK
96+
reveal_type(d_int.data) # revealed: int
97+
reveal_type(d_int.description) # revealed: str
98+
99+
# TODO: should be an error: wrong argument type
100+
DataWithDescription[int](None, "description")
101+
```
102+
103+
## Frozen instances
104+
105+
To do
106+
107+
## Descriptor-typed fields
108+
109+
To do
110+
111+
## Special cases
112+
113+
### `dataclasses.dataclass`
114+
115+
We also understand dataclasses, if they are decorated with the fully qualified name
116+
117+
```py
118+
import dataclasses
119+
120+
@dataclasses.dataclass
121+
class Person:
122+
name: str
123+
age: int | None = None
124+
125+
# TODO: should show the proper signature
126+
reveal_type(Person.__init__) # revealed: (*args: Any, **kwargs: Any) -> None
127+
```
128+
129+
### Dataclass with `init=False`
130+
131+
To do
132+
133+
### Dataclass with custom `__init__` method
134+
135+
To do
136+
137+
### Dataclass with `ClassVar`s
138+
139+
To do
140+
141+
### Using `dataclass` as a function
142+
143+
To do
144+
145+
## Internals
146+
147+
The `dataclass` decorator returns the class itself. This means that the type of `Person` is `type`,
148+
and attributes like the MRO are unchanged:
149+
150+
```py
151+
from dataclasses import dataclass
152+
153+
@dataclass
154+
class Person:
155+
name: str
156+
age: int | None = None
157+
158+
reveal_type(type(Person)) # revealed: Literal[type]
159+
reveal_type(Person.__mro__) # revealed: tuple[Literal[Person], Literal[object]]
160+
```
161+
162+
The generated methods have the following signatures:
163+
164+
```py
165+
from typing import Callable
166+
from knot_extensions import CallableTypeOf
167+
168+
def _(c: CallableTypeOf[Person.__init__]):
169+
# TODO: Proper signature
170+
reveal_type(c) # revealed: (*args: Any, **kwargs: Any) -> None
171+
172+
def _(c: CallableTypeOf[Person.__repr__]):
173+
reveal_type(c) # revealed: (self) -> str
174+
175+
def _(c: CallableTypeOf[Person.__eq__]):
176+
reveal_type(c) # revealed: (self, value: object, /) -> bool
177+
```

crates/red_knot_python_semantic/src/module_resolver/module.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ pub enum KnownModule {
116116
Sys,
117117
#[allow(dead_code)]
118118
Abc, // currently only used in tests
119+
Dataclasses,
119120
Collections,
120121
Inspect,
121122
KnotExtensions,
@@ -132,6 +133,7 @@ impl KnownModule {
132133
Self::TypingExtensions => "typing_extensions",
133134
Self::Sys => "sys",
134135
Self::Abc => "abc",
136+
Self::Dataclasses => "dataclasses",
135137
Self::Collections => "collections",
136138
Self::Inspect => "inspect",
137139
Self::KnotExtensions => "knot_extensions",

0 commit comments

Comments
 (0)