Skip to content

Commit 8d84d36

Browse files
committed
Synthesized __new__ method
1 parent 2e021de commit 8d84d36

File tree

5 files changed

+79
-88
lines changed

5 files changed

+79
-88
lines changed

crates/red_knot_python_semantic/resources/mdtest/named_tuple.md

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,36 @@ class Person(NamedTuple):
1313
name: str
1414
age: int | None = None
1515

16-
alice = Person(1, "Alice", 42) # error: [no-matching-overload]
17-
alice = Person(id=1, name="Alice", age=42) # error: [no-matching-overload]
18-
bob = Person(2, "Bob") # error: [no-matching-overload]
19-
bob = Person(id=2, name="Bob") # error: [no-matching-overload]
20-
21-
reveal_type(alice.id) # revealed: @Todo(GenericAlias instance)
22-
reveal_type(alice.name) # revealed: @Todo(GenericAlias instance)
23-
reveal_type(alice.age) # revealed: int | None | @Todo(instance attribute on class with dynamic base)
16+
alice = Person(1, "Alice", 42)
17+
alice = Person(id=1, name="Alice", age=42)
18+
bob = Person(2, "Bob")
19+
bob = Person(id=2, name="Bob")
20+
21+
reveal_type(alice.id) # revealed: int
22+
reveal_type(alice.name) # revealed: str
23+
reveal_type(alice.age) # revealed: int | None
24+
25+
# error: [missing-argument]
26+
Person(3)
27+
28+
# error: [too-many-positional-arguments]
29+
Person(3, "Eve", 99, "extra")
30+
31+
# error: [invalid-argument-type]
32+
Person(id="3", name="Eve")
33+
```
34+
35+
Alternative functional syntax:
36+
37+
```py
38+
Person2 = NamedTuple("Person", [("id", int), ("name", str)])
39+
alice2 = Person2(1, "Alice")
40+
41+
# TODO: should be an error
42+
Person2(1)
43+
44+
reveal_type(alice2.id) # revealed: @Todo(GenericAlias instance)
45+
reveal_type(alice2.name) # revealed: @Todo(GenericAlias instance)
2446
```
2547

2648
## `collections.namedtuple`

crates/red_knot_python_semantic/resources/mdtest/namedtuple.md

Lines changed: 0 additions & 17 deletions
This file was deleted.

crates/red_knot_python_semantic/src/types/class.rs

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -108,23 +108,18 @@ enum DataclassFieldsPolicy {
108108
}
109109

110110
impl DataclassFieldsPolicy {
111-
fn matches<'db>(
112-
self,
113-
db: &'db dyn Db,
114-
class: ClassLiteral<'db>,
115-
specialization: Option<Specialization<'db>>,
116-
) -> bool {
111+
fn matches<'db>(self, db: &'db dyn Db, class: ClassLiteral<'db>) -> bool {
117112
match self {
118113
Self::Dataclass => {
119114
class.dataclass_params(db).is_some()
120115
|| class
121116
.try_metaclass(db)
122117
.is_ok_and(|(_, transformer_params)| transformer_params.is_some())
123118
}
124-
Self::NamedTuple => KnownClass::NamedTuple
125-
.to_class_literal(db)
126-
.to_class_type(db)
127-
.is_some_and(|named_tuple| class.is_subclass_of(db, specialization, named_tuple)),
119+
Self::NamedTuple => class.explicit_bases(db).iter().any(|base| {
120+
base.into_class_literal()
121+
.is_some_and(|c| c.is_known(db, KnownClass::NamedTuple))
122+
}),
128123
}
129124
}
130125
}
@@ -1016,12 +1011,6 @@ impl<'db> ClassLiteral<'db> {
10161011

10171012
if symbol.symbol.is_unbound() {
10181013
if let Some(dataclass_member) = self.own_dataclass_member(db, specialization, name) {
1019-
eprintln!(
1020-
"==> dataclass {} {} {}",
1021-
Type::from(self).display(db),
1022-
name,
1023-
dataclass_member.display(db)
1024-
);
10251014
return Symbol::bound(dataclass_member).into();
10261015
}
10271016
}
@@ -1038,21 +1027,21 @@ impl<'db> ClassLiteral<'db> {
10381027
) -> Option<Type<'db>> {
10391028
let params = self.dataclass_params(db);
10401029
let has_dataclass_param = |param| params.is_some_and(|params| params.contains(param));
1030+
let field_policy = if has_dataclass_param(DataclassParams::INIT)
1031+
|| self
1032+
.try_metaclass(db)
1033+
.is_ok_and(|(_, transformer_params)| transformer_params.is_some())
1034+
{
1035+
DataclassFieldsPolicy::Dataclass
1036+
} else if DataclassFieldsPolicy::NamedTuple.matches(db, self) {
1037+
DataclassFieldsPolicy::NamedTuple
1038+
} else {
1039+
return None;
1040+
};
10411041

1042-
match name {
1043-
"__init__" => {
1044-
let field_policy = if has_dataclass_param(DataclassParams::INIT)
1045-
|| self
1046-
.try_metaclass(db)
1047-
.is_ok_and(|(_, transformer_params)| transformer_params.is_some())
1048-
{
1049-
DataclassFieldsPolicy::Dataclass
1050-
} else if DataclassFieldsPolicy::NamedTuple.matches(db, self, specialization) {
1051-
DataclassFieldsPolicy::NamedTuple
1052-
} else {
1053-
return None;
1054-
};
1055-
1042+
match (name, field_policy) {
1043+
("__init__", DataclassFieldsPolicy::Dataclass)
1044+
| ("__new__", DataclassFieldsPolicy::NamedTuple) => {
10561045
let mut parameters = vec![];
10571046

10581047
for (name, (mut attr_ty, mut default_ty)) in
@@ -1118,12 +1107,20 @@ impl<'db> ClassLiteral<'db> {
11181107
parameters.push(parameter);
11191108
}
11201109

1121-
let init_signature =
1122-
Signature::new(Parameters::new(parameters), Some(Type::none(db)));
1110+
if name == "__init__" {
1111+
let init_signature =
1112+
Signature::new(Parameters::new(parameters), Some(Type::none(db)));
11231113

1124-
Some(Type::Callable(CallableType::single(db, init_signature)))
1114+
Some(Type::Callable(CallableType::single(db, init_signature)))
1115+
} else {
1116+
parameters.insert(0, Parameter::positional_or_keyword(Name::new_static("cls")));
1117+
let new_signature =
1118+
Signature::new(Parameters::new(parameters), Some(Type::none(db)));
1119+
1120+
Some(Type::Callable(CallableType::single(db, new_signature)))
1121+
}
11251122
}
1126-
"__lt__" | "__le__" | "__gt__" | "__ge__" => {
1123+
("__lt__" | "__le__" | "__gt__" | "__ge__", DataclassFieldsPolicy::Dataclass) => {
11271124
if !has_dataclass_param(DataclassParams::ORDER) {
11281125
return None;
11291126
}
@@ -1144,13 +1141,6 @@ impl<'db> ClassLiteral<'db> {
11441141
}
11451142
}
11461143

1147-
fn is_dataclass(self, db: &'db dyn Db) -> bool {
1148-
self.dataclass_params(db).is_some()
1149-
|| self
1150-
.try_metaclass(db)
1151-
.is_ok_and(|(_, transformer_params)| transformer_params.is_some())
1152-
}
1153-
11541144
/// Returns a list of all annotated attributes defined in this class, or any of its superclasses.
11551145
///
11561146
/// See [`ClassLiteral::own_dataclass_fields`] for more details.
@@ -1165,7 +1155,7 @@ impl<'db> ClassLiteral<'db> {
11651155
.filter_map(|superclass| {
11661156
if let Some(class) = superclass.into_class() {
11671157
let class_literal = class.class_literal(db).0;
1168-
if field_policy.matches(db, class_literal, specialization) {
1158+
if field_policy.matches(db, class_literal) {
11691159
Some(class_literal)
11701160
} else {
11711161
None
@@ -2113,6 +2103,7 @@ impl<'db> KnownClass {
21132103
| Self::TypeVarTuple
21142104
| Self::TypeAliasType
21152105
| Self::NoDefaultType
2106+
| Self::NamedTuple
21162107
| Self::NewType
21172108
| Self::ChainMap
21182109
| Self::Counter

crates/red_knot_python_semantic/src/types/class_base.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,15 @@ impl<'db> ClassBase<'db> {
7272
pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
7373
match ty {
7474
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
75-
Type::ClassLiteral(literal) => Some(if literal.is_known(db, KnownClass::Any) {
76-
Self::Dynamic(DynamicType::Any)
77-
} else {
78-
Self::Class(literal.default_specialization(db))
79-
}),
75+
Type::ClassLiteral(literal) => {
76+
if literal.is_known(db, KnownClass::Any) {
77+
Some(Self::Dynamic(DynamicType::Any))
78+
} else if literal.is_known(db, KnownClass::NamedTuple) {
79+
Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db))
80+
} else {
81+
Some(Self::Class(literal.default_specialization(db)))
82+
}
83+
}
8084
Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))),
8185
Type::NominalInstance(instance)
8286
if instance.class().is_known(db, KnownClass::GenericAlias) =>

crates/ruff_benchmark/benches/red_knot.rs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,22 +59,13 @@ type KeyDiagnosticFields = (
5959
Severity,
6060
);
6161

62-
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[
63-
(
64-
DiagnosticId::lint("no-matching-overload"),
65-
Some("/src/tomllib/_parser.py"),
66-
Some(2329..2358),
67-
"No overload of bound method `__init__` matches arguments",
68-
Severity::Error,
69-
),
70-
(
71-
DiagnosticId::lint("unused-ignore-comment"),
72-
Some("/src/tomllib/_parser.py"),
73-
Some(22299..22333),
74-
"Unused blanket `type: ignore` directive",
75-
Severity::Warning,
76-
),
77-
];
62+
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[(
63+
DiagnosticId::lint("unused-ignore-comment"),
64+
Some("/src/tomllib/_parser.py"),
65+
Some(22299..22333),
66+
"Unused blanket `type: ignore` directive",
67+
Severity::Warning,
68+
)];
7869

7970
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
8071
SystemPathBuf::from("src").join(file.name())

0 commit comments

Comments
 (0)