Skip to content

Commit d0e8f67

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

File tree

3 files changed

+79
-6
lines changed

3 files changed

+79
-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: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3468,18 +3468,40 @@ 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+
println!("{:?}", get_setattr_dunder_attr_ty());
3474+
34713475
let check_setattr_return_type = |result: Bindings<'db>| -> bool {
34723476
match result.return_type(db) {
34733477
Type::Never => {
34743478
if emit_diagnostics {
34753479
if let Some(builder) =
34763480
self.context.report_lint(&INVALID_ASSIGNMENT, target)
34773481
{
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-
));
3482+
let is_setattr_synthesized = match get_setattr_dunder_attr_ty()
3483+
{
3484+
PlaceAndQualifiers {
3485+
place: Place::Type(attr_ty, _),
3486+
qualifiers: _,
3487+
} => attr_ty.is_callable_type(),
3488+
_ => false,
3489+
};
3490+
3491+
let msg = if is_setattr_synthesized {
3492+
format!(
3493+
"Property `{attribute}` defined in `{}` is read-only",
3494+
object_ty.display(db)
3495+
)
3496+
} else {
3497+
format!(
3498+
"Cannot assign to attribute `{attribute}` on type `{}` \
3499+
whose `__setattr__` method returns `Never`/`NoReturn`",
3500+
object_ty.display(db)
3501+
)
3502+
};
3503+
3504+
builder.into_diagnostic(msg);
34833505
}
34843506
}
34853507
false

0 commit comments

Comments
 (0)