Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/dataclasses.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,19 @@ frozen_instance = MyFrozenClass(1)
frozen_instance.x = 2 # error: [invalid-assignment]
```

Deleting fields will also generate a diagnostic.

```py
from dataclasses import dataclass

@dataclass(frozen=True)
class MyFrozenClass:
x: int = 1

frozen_instance = MyFrozenClass()
del frozen_instance.x # TODO: error: [invalid-assignment]
```

If `__setattr__()` or `__delattr__()` is defined in the class, we should emit a diagnostic.

```py
Expand Down Expand Up @@ -427,6 +440,22 @@ frozen = MyFrozenClass()
frozen.x = 2 # error: [unresolved-attribute]
```

A diagnostic is also emitted if a frozen dataclass is inherited, and an attempt is made to mutate an
attribute in the child class:

```py
from dataclasses import dataclass

@dataclass(frozen=True)
class MyFrozenClass:
x: int = 1

class MyFrozenChildClass(MyFrozenClass): ...

frozen = MyFrozenChildClass()
frozen.x = 2 # error: [invalid-assignment]
```

### `match_args`

To do
Expand Down
20 changes: 20 additions & 0 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,26 @@ impl<'db> ClassLiteral<'db> {
.with_annotated_type(KnownClass::Type.to_instance(db));
signature_from_fields(vec![cls_parameter])
}
(CodeGeneratorKind::DataclassLike, "__setattr__" | "__delattr__") => {
if !has_dataclass_param(DataclassParams::FROZEN) {
return None;
}

let signature = Signature::new(
Parameters::new([
Parameter::positional_or_keyword(Name::new_static("self"))
.with_annotated_type(Type::instance(
db,
self.apply_optional_specialization(db, specialization),
)),
Parameter::positional_or_keyword(Name::new_static("name")),
Parameter::positional_or_keyword(Name::new_static("value")),
]),
Some(Type::Never),
);

Some(CallableType::function_like(db, signature))
}
(CodeGeneratorKind::DataclassLike, "__lt__" | "__le__" | "__gt__" | "__ge__") => {
if !has_dataclass_param(DataclassParams::ORDER) {
return None;
Expand Down
261 changes: 118 additions & 143 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3099,101 +3099,147 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::TypeVar(..)
| Type::AlwaysTruthy
| Type::AlwaysFalsy => {
let is_read_only = || {
let dataclass_params = match object_ty {
Type::NominalInstance(instance) => match instance.class {
ClassType::NonGeneric(cls) => cls.dataclass_params(self.db()),
ClassType::Generic(cls) => {
cls.origin(self.db()).dataclass_params(self.db())
}
},
_ => None,
};

dataclass_params.is_some_and(|params| params.contains(DataclassParams::FROZEN))
};
let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy(
db,
"__setattr__",
&mut CallArgumentTypes::positional([
Type::StringLiteral(StringLiteralType::new(db, Box::from(attribute))),
value_ty,
]),
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
);

match object_ty.class_member(db, attribute.into()) {
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target)
{
builder.into_diagnostic(format_args!(
"Cannot assign to ClassVar `{attribute}` \
from an instance of type `{ty}`",
ty = object_ty.display(self.db()),
));
}
}
false
}
SymbolAndQualifiers {
symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness),
qualifiers: _,
} => {
if is_read_only() {
// First, try to call the `__setattr__` dunder method. If this is present/defined, overrides
// assigning the attributed by the normal mechanism.
match setattr_dunder_call_result {
Ok(result) => match result.return_type(db) {
Type::Never => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
builder.into_diagnostic(format_args!(
"Property `{attribute}` defined in `{ty}` is read-only",
ty = object_ty.display(self.db()),
"Attribute `{attribute}` on type `{}` is read-only",
object_ty.display(db)
));
}
}
false
} else {
let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) =
meta_attr_ty.class_member(db, "__set__".into()).symbol
}
_ => true,
},
Err(CallDunderError::CallError(..)) => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
{
let successful_call = meta_dunder_set
.try_call(
db,
&CallArgumentTypes::positional([
meta_attr_ty,
object_ty,
value_ty,
]),
)
.is_ok();

if !successful_call && emit_diagnostics {
builder.into_diagnostic(format_args!(
"Can not assign object of `{}` to attribute \
`{attribute}` on type `{}` with \
custom `__setattr__` method.",
value_ty.display(db),
object_ty.display(db)
));
}
}
false
}
Err(CallDunderError::PossiblyUnbound(_)) => true,
Err(CallDunderError::MethodNotAvailable) => {
match object_ty.class_member(db, attribute.into()) {
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target)
{
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
builder.into_diagnostic(format_args!(
"Cannot assign to ClassVar `{attribute}` \
from an instance of type `{ty}`",
ty = object_ty.display(self.db()),
));
}
}
false
}
SymbolAndQualifiers {
symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness),
qualifiers: _,
} => {
let assignable_to_meta_attr =
if let Symbol::Type(meta_dunder_set, _) =
meta_attr_ty.class_member(db, "__set__".into()).symbol
{
let successful_call = meta_dunder_set
.try_call(
db,
&CallArgumentTypes::positional([
meta_attr_ty,
object_ty,
value_ty,
]),
)
.is_ok();

if !successful_call && emit_diagnostics {
if let Some(builder) = self
.context
.report_lint(&INVALID_ASSIGNMENT, target)
{
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
builder.into_diagnostic(format_args!(
"Invalid assignment to data descriptor attribute \
`{attribute}` on type `{}` with custom `__set__` method",
object_ty.display(db)
));
}
}
}
}

successful_call
} else {
ensure_assignable_to(meta_attr_ty)
};
successful_call
} else {
ensure_assignable_to(meta_attr_ty)
};

let assignable_to_instance_attribute =
if meta_attr_boundness == Boundness::PossiblyUnbound {
let (assignable, boundness) = if let Symbol::Type(
instance_attr_ty,
instance_attr_boundness,
) =
object_ty.instance_member(db, attribute).symbol
{
(
ensure_assignable_to(instance_attr_ty),
let assignable_to_instance_attribute =
if meta_attr_boundness == Boundness::PossiblyUnbound {
let (assignable, boundness) = if let Symbol::Type(
instance_attr_ty,
instance_attr_boundness,
)
) =
object_ty.instance_member(db, attribute).symbol
{
(
ensure_assignable_to(instance_attr_ty),
instance_attr_boundness,
)
} else {
(true, Boundness::PossiblyUnbound)
};

if boundness == Boundness::PossiblyUnbound {
report_possibly_unbound_attribute(
&self.context,
target,
attribute,
object_ty,
);
}

assignable
} else {
(true, Boundness::PossiblyUnbound)
true
};

if boundness == Boundness::PossiblyUnbound {
assignable_to_meta_attr && assignable_to_instance_attribute
}

SymbolAndQualifiers {
symbol: Symbol::Unbound,
..
} => {
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
object_ty.instance_member(db, attribute).symbol
{
if instance_attr_boundness == Boundness::PossiblyUnbound {
report_possibly_unbound_attribute(
&self.context,
target,
Expand All @@ -3202,79 +3248,8 @@ impl<'db> TypeInferenceBuilder<'db> {
);
}

assignable
ensure_assignable_to(instance_attr_ty)
} else {
true
};

assignable_to_meta_attr && assignable_to_instance_attribute
}
}

SymbolAndQualifiers {
symbol: Symbol::Unbound,
..
} => {
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
object_ty.instance_member(db, attribute).symbol
{
if instance_attr_boundness == Boundness::PossiblyUnbound {
report_possibly_unbound_attribute(
&self.context,
target,
attribute,
object_ty,
);
}

if is_read_only() {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
builder.into_diagnostic(format_args!(
"Property `{attribute}` defined in `{ty}` is read-only",
ty = object_ty.display(self.db()),
));
}
}
false
} else {
ensure_assignable_to(instance_attr_ty)
}
} else {
let result = object_ty.try_call_dunder_with_policy(
db,
"__setattr__",
&mut CallArgumentTypes::positional([
Type::StringLiteral(StringLiteralType::new(
db,
Box::from(attribute),
)),
value_ty,
]),
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
);

match result {
Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true,
Err(CallDunderError::CallError(..)) => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
{
builder.into_diagnostic(format_args!(
"Can not assign object of `{}` to attribute \
`{attribute}` on type `{}` with \
custom `__setattr__` method.",
value_ty.display(db),
object_ty.display(db)
));
}
}
false
}
Err(CallDunderError::MethodNotAvailable) => {
if emit_diagnostics {
if let Some(builder) =
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
Expand Down
Loading