Skip to content

Commit 2207c55

Browse files
committed
[red-knot] Dataclasses: support order=True
1 parent 03adae8 commit 2207c55

File tree

2 files changed

+164
-19
lines changed

2 files changed

+164
-19
lines changed

crates/red_knot_python_semantic/resources/mdtest/dataclasses.md

Lines changed: 142 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,98 @@ repr(C())
9191
C() == C()
9292
```
9393

94+
## Other dataclass parameters
95+
96+
### `repr`
97+
98+
A custom `__repr__` method is generated by default. It can be disabled by passing `repr=False`, but
99+
in that case `__repr__` is still available via `object.__repr__`:
100+
101+
```py
102+
from dataclasses import dataclass
103+
104+
@dataclass(repr=False)
105+
class WithoutRepr:
106+
x: int
107+
108+
reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str
109+
```
110+
111+
### `eq`
112+
113+
The same is true for `__eq__`. Setting `eq=False` disables the generated `__eq__` method, but
114+
`__eq__` is still available via `object.__eq__`:
115+
116+
```py
117+
from dataclasses import dataclass
118+
119+
@dataclass(eq=False)
120+
class WithoutEq:
121+
x: int
122+
123+
reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
124+
```
125+
126+
### `order`
127+
128+
`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__`
129+
methods will be generated:
130+
131+
```py
132+
from dataclasses import dataclass
133+
134+
@dataclass
135+
class WithoutOrder:
136+
x: int
137+
138+
WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator]
139+
WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator]
140+
WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator]
141+
WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator]
142+
143+
@dataclass(order=True)
144+
class WithOrder:
145+
x: int
146+
147+
WithOrder(1) < WithOrder(2)
148+
WithOrder(1) <= WithOrder(2)
149+
WithOrder(1) > WithOrder(2)
150+
WithOrder(1) >= WithOrder(2)
151+
```
152+
153+
Comparisons are only allowed for `WithOrder` instances:
154+
155+
```py
156+
WithOrder(1) < 2 # error: [unsupported-operator]
157+
WithOrder(1) <= 2 # error: [unsupported-operator]
158+
WithOrder(1) > 2 # error: [unsupported-operator]
159+
WithOrder(1) >= 2 # error: [unsupported-operator]
160+
```
161+
162+
### `unsafe_hash`
163+
164+
To do
165+
166+
### `frozen`
167+
168+
To do
169+
170+
### `match_args`
171+
172+
To do
173+
174+
### `kw_only`
175+
176+
To do
177+
178+
### `slots`
179+
180+
To do
181+
182+
### `weakref_slot`
183+
184+
To do
185+
94186
## Inheritance
95187

96188
### Normal class inheriting from a dataclass
@@ -174,7 +266,28 @@ To do
174266

175267
## Descriptor-typed fields
176268

177-
To do
269+
```py
270+
from dataclasses import dataclass
271+
272+
class Descriptor:
273+
_value: int = 0
274+
275+
def __get__(self, instance, owner) -> str:
276+
return str(self._value)
277+
278+
def __set__(self, instance, value: int) -> None:
279+
self._value = value
280+
281+
@dataclass
282+
class C:
283+
d: Descriptor = Descriptor()
284+
285+
c = C(1)
286+
reveal_type(c.d) # revealed: str
287+
288+
# TODO: should be an error
289+
C("a")
290+
```
178291

179292
## `dataclasses.field`
180293

@@ -197,13 +310,37 @@ class C:
197310
reveal_type(C.__init__) # revealed: (*args: Any, **kwargs: Any) -> None
198311
```
199312

200-
### Dataclass with `init=False`
313+
### Dataclass with custom `__init__` method
201314

202-
To do
315+
If a class already defines `__init__`, it is not replaced by the `dataclass` decorator.
203316

204-
### Dataclass with custom `__init__` method
317+
```py
318+
from dataclasses import dataclass
205319

206-
To do
320+
@dataclass(init=True)
321+
class C:
322+
x: str
323+
324+
def __init__(self, x: int) -> None:
325+
self.x = str(x)
326+
327+
C(1) # OK
328+
329+
# TODO: should be an error
330+
C("a")
331+
```
332+
333+
Similarly, if we set `init=False`, we still recognize the custom `__init__` method:
334+
335+
```py
336+
@dataclass(init=False)
337+
class D:
338+
def __init__(self, x: int) -> None:
339+
self.x = str(x)
340+
341+
D(1) # OK
342+
D() # error: [missing-argument]
343+
```
207344

208345
### Dataclass with `ClassVar`s
209346

crates/red_knot_python_semantic/src/types/class.rs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -792,21 +792,29 @@ impl<'db> ClassLiteralType<'db> {
792792
/// traverse through the MRO until it finds the member.
793793
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
794794
if let Some(metadata) = self.dataclass_metadata(db) {
795-
if name == "__init__" {
796-
if metadata.contains(DataclassMetadata::INIT) {
797-
// TODO: Generate the signature from the attributes on the class
798-
let init_signature = Signature::new(
799-
Parameters::new([
800-
Parameter::variadic(Name::new_static("args"))
801-
.with_annotated_type(Type::any()),
802-
Parameter::keyword_variadic(Name::new_static("kwargs"))
803-
.with_annotated_type(Type::any()),
804-
]),
805-
Some(Type::none(db)),
795+
if name == "__init__" && metadata.contains(DataclassMetadata::INIT) {
796+
// TODO: Generate the signature from the attributes on the class
797+
let init_signature = Signature::new(
798+
Parameters::new([
799+
Parameter::variadic(Name::new_static("args"))
800+
.with_annotated_type(Type::any()),
801+
Parameter::keyword_variadic(Name::new_static("kwargs"))
802+
.with_annotated_type(Type::any()),
803+
]),
804+
Some(Type::none(db)),
805+
);
806+
807+
return Symbol::bound(Type::Callable(CallableType::new(db, init_signature))).into();
808+
} else if matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") {
809+
if metadata.contains(DataclassMetadata::ORDER) {
810+
let signature = Signature::new(
811+
Parameters::new([Parameter::positional_or_keyword(Name::new_static(
812+
"other",
813+
))
814+
.with_annotated_type(Type::instance(self.default_specialization(db)))]),
815+
Some(KnownClass::Bool.to_instance(db)),
806816
);
807-
808-
return Symbol::bound(Type::Callable(CallableType::new(db, init_signature)))
809-
.into();
817+
return Symbol::bound(Type::Callable(CallableType::new(db, signature))).into();
810818
}
811819
}
812820
}

0 commit comments

Comments
 (0)