Skip to content

Commit 2c6c1df

Browse files
committed
Initial version
1 parent aa6c7c4 commit 2c6c1df

File tree

3 files changed

+135
-20
lines changed

3 files changed

+135
-20
lines changed

crates/red_knot_python_semantic/resources/mdtest/dataclasses.md

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,79 @@ The signature of the `__init__` method is generated based on the classes attribu
4040
calls are not valid:
4141

4242
```py
43-
# TODO: should be an error: too few arguments
43+
# error: [missing-argument]
4444
Person()
4545

46-
# TODO: should be an error: too many arguments
46+
# error: [too-many-positional-arguments]
4747
Person("Eve", 20, "too many arguments")
4848

49-
# TODO: should be an error: wrong argument type
49+
# error: [invalid-argument-type]
5050
Person("Eve", "string instead of int")
5151

52-
# TODO: should be an error: wrong argument types
52+
# error: [invalid-argument-type]
53+
# error: [invalid-argument-type]
5354
Person(20, "Eve")
5455
```
5556

57+
## Signature of `__init__`
58+
59+
TODO: All of the following tests are missing the `self` argument in the `__init__` signature.
60+
61+
Declarations in the class body are used to generate the signature of the `__init__` method. If the
62+
attributes are not just declarations, but also bindings, the type inferred from bindings is used as
63+
the default value.
64+
65+
```py
66+
from dataclasses import dataclass
67+
68+
@dataclass
69+
class D1:
70+
x: int
71+
y: str = "default"
72+
z: int | None = 1 + 2
73+
74+
reveal_type(D1.__init__) # revealed: (x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
75+
```
76+
77+
This also works if the declaration and binding are split:
78+
79+
```py
80+
@dataclass
81+
class D2:
82+
x: int | None
83+
x = None
84+
85+
reveal_type(D2.__init__) # revealed: (x: int | None = None) -> None
86+
```
87+
88+
Function declarations do not affect the signature of `__init__`:
89+
90+
```py
91+
@dataclass
92+
class D3:
93+
x: int
94+
95+
def y(self) -> str:
96+
return ""
97+
98+
reveal_type(D3.__init__) # revealed: (x: int) -> None
99+
```
100+
101+
Implicit instance attributes also do not affect the signature of `__init__`:
102+
103+
```py
104+
@dataclass
105+
class D4:
106+
x: int
107+
108+
def f(self, y: str) -> None:
109+
self.y: str = y
110+
111+
reveal_type(D4(1).y) # revealed: str
112+
113+
reveal_type(D4.__init__) # revealed: (x: int) -> None
114+
```
115+
56116
## `@dataclass` calls with arguments
57117

58118
The `@dataclass` decorator can take several arguments to customize the existence of the generated
@@ -228,7 +288,8 @@ class Derived(Base):
228288

229289
d = Derived("a")
230290

231-
# TODO: should be an error:
291+
# error: [too-many-positional-arguments]
292+
# error: [invalid-argument-type]
232293
Derived(1, "a")
233294
```
234295

@@ -245,6 +306,9 @@ class Base:
245306
class Derived(Base):
246307
y: str
247308

309+
# TODO: no errors
310+
# error: [too-many-positional-arguments]
311+
# error: [invalid-argument-type]
248312
d = Derived(1, "a") # OK
249313

250314
reveal_type(d.x) # revealed: int
@@ -292,10 +356,12 @@ class Descriptor:
292356
class C:
293357
d: Descriptor = Descriptor()
294358

359+
# TODO: no error
360+
# error: [invalid-argument-type]
295361
c = C(1)
296362
reveal_type(c.d) # revealed: str
297363

298-
# TODO: should be an error
364+
# error: [invalid-argument-type]
299365
C("a")
300366
```
301367

@@ -316,8 +382,7 @@ import dataclasses
316382
class C:
317383
x: str
318384

319-
# TODO: should show the proper signature
320-
reveal_type(C.__init__) # revealed: (*args: Any, **kwargs: Any) -> None
385+
reveal_type(C.__init__) # revealed: (x: str) -> None
321386
```
322387

323388
### Dataclass with custom `__init__` method
@@ -334,6 +399,8 @@ class C:
334399
def __init__(self, x: int) -> None:
335400
self.x = str(x)
336401

402+
# TODO: no error
403+
# error: [invalid-argument-type]
337404
C(1) # OK
338405

339406
# TODO: should be an error
@@ -399,8 +466,8 @@ reveal_type(Person.__mro__) # revealed: tuple[Literal[Person], Literal[object]]
399466
The generated methods have the following signatures:
400467

401468
```py
402-
# TODO: proper signature
403-
reveal_type(Person.__init__) # revealed: (*args: Any, **kwargs: Any) -> None
469+
# TODO: `self` is missing here
470+
reveal_type(Person.__init__) # revealed: (name: str, age: int | None = None) -> None
404471

405472
reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str
406473

crates/red_knot_python_semantic/src/semantic_index/use_def.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,14 @@ impl<'db> UseDefMap<'db> {
429429
self.declarations_iterator(declarations)
430430
}
431431

432+
pub(crate) fn all_public_declarations<'map>(
433+
&'map self,
434+
) -> impl Iterator<Item = (ScopedSymbolId, DeclarationsIterator<'map, 'db>)> + 'map {
435+
(0..self.public_symbols.len())
436+
.map(ScopedSymbolId::from_usize)
437+
.map(|symbol_id| (symbol_id, self.public_declarations(symbol_id)))
438+
}
439+
432440
/// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`.
433441
pub(crate) fn can_implicit_return(&self, db: &dyn crate::Db) -> bool {
434442
!self

crates/red_knot_python_semantic/src/types/class.rs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -804,16 +804,22 @@ impl<'db> ClassLiteralType<'db> {
804804
) -> SymbolAndQualifiers<'db> {
805805
if let Some(metadata) = self.dataclass_metadata(db) {
806806
if name == "__init__" && metadata.contains(DataclassMetadata::INIT) {
807-
// TODO: Generate the signature from the attributes on the class
808-
let init_signature = Signature::new(
809-
Parameters::new([
810-
Parameter::variadic(Name::new_static("args"))
811-
.with_annotated_type(Type::any()),
812-
Parameter::keyword_variadic(Name::new_static("kwargs"))
813-
.with_annotated_type(Type::any()),
814-
]),
815-
Some(Type::none(db)),
816-
);
807+
// dbg!(&attributes);
808+
let mut parameters = vec![];
809+
810+
for (name, attr_ty, default_ty) in self.own_class_body_instance_attributes(db) {
811+
let mut parameter =
812+
Parameter::positional_or_keyword(name).with_annotated_type(attr_ty);
813+
814+
if let Some(default_ty) = default_ty {
815+
parameter = parameter.with_default_type(default_ty);
816+
}
817+
818+
parameters.push(parameter);
819+
}
820+
821+
let init_signature =
822+
Signature::new(Parameters::new(parameters), Some(Type::none(db)));
817823

818824
return Symbol::bound(Type::Callable(CallableType::new(db, init_signature))).into();
819825
} else if matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") {
@@ -836,6 +842,40 @@ impl<'db> ClassLiteralType<'db> {
836842
class_symbol(db, body_scope, name)
837843
}
838844

845+
fn own_class_body_instance_attributes(
846+
self,
847+
db: &'db dyn Db,
848+
) -> Vec<(Name, Type<'db>, Option<Type<'db>>)> {
849+
let mut attributes = vec![]; // TODO: iterator
850+
851+
let class_body_scope = self.body_scope(db);
852+
let table = symbol_table(db, class_body_scope);
853+
854+
let use_def = use_def_map(db, class_body_scope);
855+
for (symbol_id, declarations) in use_def.all_public_declarations() {
856+
let symbol = table.symbol(symbol_id);
857+
858+
if let Ok(attr) = symbol_from_declarations(db, declarations) {
859+
if attr.is_class_var() {
860+
continue;
861+
}
862+
863+
if let Some(attr_ty) = attr.symbol.ignore_possibly_unbound() {
864+
if attr_ty.is_function_literal() {
865+
continue;
866+
}
867+
868+
let bindings = use_def.public_bindings(symbol_id);
869+
let default_ty = symbol_from_bindings(db, bindings).ignore_possibly_unbound();
870+
871+
attributes.push((symbol.name().clone(), attr_ty, default_ty));
872+
}
873+
}
874+
}
875+
876+
attributes
877+
}
878+
839879
/// Returns the `name` attribute of an instance of this class.
840880
///
841881
/// The attribute could be defined in the class body, but it could also be an implicitly

0 commit comments

Comments
 (0)