Skip to content

Commit c6fda6f

Browse files
committed
[ty] synthesize __setattr__ and __delattr__ for frozen dataclasses
1 parent dca594f commit c6fda6f

File tree

3 files changed

+78
-6
lines changed

3 files changed

+78
-6
lines changed

crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,39 @@ from dataclasses import dataclass
425425
class MyFrozenClass: ...
426426

427427
frozen = MyFrozenClass()
428-
frozen.x = 2 # error: [unresolved-attribute]
428+
frozen.x = 2 # error: [invalid-assignment] "Property `x` defined in `MyFrozenClass` is read-only"
429+
```
430+
431+
diagnostic is also emitted if a frozen dataclass is inherited, and an attempt is made to mutate an
432+
attribute in the child class:
433+
434+
```py
435+
from dataclasses import dataclass
436+
437+
@dataclass(frozen=True)
438+
class MyFrozenClass:
439+
x: int = 1
440+
441+
class MyFrozenChildClass(MyFrozenClass): ...
442+
443+
frozen = MyFrozenChildClass()
444+
frozen.x = 2 # error: [invalid-assignment]
445+
```
446+
447+
The same diagnostic is emitted if a frozen dataclass is inherited, and an attempt is made to delete
448+
an attribute:
449+
450+
```py
451+
from dataclasses import dataclass
452+
453+
@dataclass(frozen=True)
454+
class MyFrozenClass:
455+
x: int = 1
456+
457+
class MyFrozenChildClass(MyFrozenClass): ...
458+
459+
frozen = MyFrozenChildClass()
460+
del frozen.x # TODO this should emit an [invalid-assignment]
429461
```
430462

431463
### `match_args`

crates/ty_python_semantic/src/types/class.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,6 +1589,25 @@ impl<'db> ClassLiteral<'db> {
15891589
.place
15901590
.ignore_possibly_unbound()
15911591
}
1592+
(CodeGeneratorKind::DataclassLike, "__setattr__") => {
1593+
if has_dataclass_param(DataclassParams::FROZEN) {
1594+
let signature = Signature::new(
1595+
Parameters::new([
1596+
Parameter::positional_or_keyword(Name::new_static("self"))
1597+
.with_annotated_type(Type::instance(
1598+
db,
1599+
self.apply_optional_specialization(db, specialization),
1600+
)),
1601+
Parameter::positional_or_keyword(Name::new_static("name")),
1602+
Parameter::positional_or_keyword(Name::new_static("value")),
1603+
]),
1604+
Some(Type::Never),
1605+
);
1606+
1607+
return Some(CallableType::function_like(db, signature));
1608+
}
1609+
None
1610+
}
15921611
_ => None,
15931612
}
15941613
}

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3468,18 +3468,39 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
34683468
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
34693469
);
34703470

3471+
let get_setattr_dunder_attr_ty =
3472+
|| object_ty.class_member(db, "__setattr__".into());
3473+
34713474
let check_setattr_return_type = |result: Bindings<'db>| -> bool {
34723475
match result.return_type(db) {
34733476
Type::Never => {
34743477
if emit_diagnostics {
34753478
if let Some(builder) =
34763479
self.context.report_lint(&INVALID_ASSIGNMENT, target)
34773480
{
3478-
builder.into_diagnostic(format_args!(
3479-
"Cannot assign to attribute `{attribute}` on type `{}` \
3480-
whose `__setattr__` method returns `Never`/`NoReturn`",
3481-
object_ty.display(db)
3482-
));
3481+
let is_setattr_synthesized = match get_setattr_dunder_attr_ty()
3482+
{
3483+
PlaceAndQualifiers {
3484+
place: Place::Type(attr_ty, _),
3485+
qualifiers: _,
3486+
} => attr_ty.is_callable_type(),
3487+
_ => false,
3488+
};
3489+
3490+
let msg = if is_setattr_synthesized {
3491+
format!(
3492+
"Property `{attribute}` defined in `{}` is read-only",
3493+
object_ty.display(db)
3494+
)
3495+
} else {
3496+
format!(
3497+
"Cannot assign to attribute `{attribute}` on type `{}` \
3498+
whose `__setattr__` method returns `Never`/`NoReturn`",
3499+
object_ty.display(db)
3500+
)
3501+
};
3502+
3503+
builder.into_diagnostic(msg);
34833504
}
34843505
}
34853506
false

0 commit comments

Comments
 (0)