mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-11 22:29:39 +00:00
[ty] Support frozen dataclasses (#17974)
## Summary https://github.com/astral-sh/ty/issues/111 This PR adds support for `frozen` dataclasses. It will emit a diagnostic with a similar message to mypy Note: This does not include emitting a diagnostic if `__setattr__` or `__delattr__` are defined on the object as per the [spec](https://docs.python.org/3/library/dataclasses.html#module-contents) ## Test Plan mdtest --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
cb04343b3b
commit
01eeb2f0d6
2 changed files with 153 additions and 52 deletions
|
@ -369,7 +369,65 @@ To do
|
||||||
|
|
||||||
### `frozen`
|
### `frozen`
|
||||||
|
|
||||||
To do
|
If true (the default is False), assigning to fields will generate a diagnostic.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MyFrozenClass:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
frozen_instance = MyFrozenClass(1)
|
||||||
|
frozen_instance.x = 2 # error: [invalid-assignment]
|
||||||
|
```
|
||||||
|
|
||||||
|
If `__setattr__()` or `__delattr__()` is defined in the class, we should emit a diagnostic.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MyFrozenClass:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
# TODO: Emit a diagnostic here
|
||||||
|
def __setattr__(self, name: str, value: object) -> None: ...
|
||||||
|
|
||||||
|
# TODO: Emit a diagnostic here
|
||||||
|
def __delattr__(self, name: str) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This also works for generic dataclasses:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MyFrozenGeneric[T]:
|
||||||
|
x: T
|
||||||
|
|
||||||
|
frozen_instance = MyFrozenGeneric[int](1)
|
||||||
|
frozen_instance.x = 2 # error: [invalid-assignment]
|
||||||
|
```
|
||||||
|
|
||||||
|
When attempting to mutate an unresolved attribute on a frozen dataclass, only `unresolved-attribute`
|
||||||
|
is emitted:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MyFrozenClass: ...
|
||||||
|
|
||||||
|
frozen = MyFrozenClass()
|
||||||
|
frozen.x = 2 # error: [unresolved-attribute]
|
||||||
|
```
|
||||||
|
|
||||||
### `match_args`
|
### `match_args`
|
||||||
|
|
||||||
|
|
|
@ -3077,6 +3077,20 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
| Type::TypeVar(..)
|
| Type::TypeVar(..)
|
||||||
| Type::AlwaysTruthy
|
| Type::AlwaysTruthy
|
||||||
| Type::AlwaysFalsy => {
|
| 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))
|
||||||
|
};
|
||||||
|
|
||||||
match object_ty.class_member(db, attribute.into()) {
|
match object_ty.class_member(db, attribute.into()) {
|
||||||
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
|
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
|
||||||
if emit_diagnostics {
|
if emit_diagnostics {
|
||||||
|
@ -3096,6 +3110,19 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness),
|
symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness),
|
||||||
qualifiers: _,
|
qualifiers: _,
|
||||||
} => {
|
} => {
|
||||||
|
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 {
|
||||||
let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) =
|
let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) =
|
||||||
meta_attr_ty.class_member(db, "__set__".into()).symbol
|
meta_attr_ty.class_member(db, "__set__".into()).symbol
|
||||||
{
|
{
|
||||||
|
@ -3128,11 +3155,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
ensure_assignable_to(meta_attr_ty)
|
ensure_assignable_to(meta_attr_ty)
|
||||||
};
|
};
|
||||||
|
|
||||||
let assignable_to_instance_attribute = if meta_attr_boundness
|
let assignable_to_instance_attribute =
|
||||||
== Boundness::PossiblyUnbound
|
if meta_attr_boundness == Boundness::PossiblyUnbound {
|
||||||
{
|
let (assignable, boundness) = if let Symbol::Type(
|
||||||
let (assignable, boundness) =
|
instance_attr_ty,
|
||||||
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
|
instance_attr_boundness,
|
||||||
|
) =
|
||||||
object_ty.instance_member(db, attribute).symbol
|
object_ty.instance_member(db, attribute).symbol
|
||||||
{
|
{
|
||||||
(
|
(
|
||||||
|
@ -3159,6 +3187,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
|
|
||||||
assignable_to_meta_attr && assignable_to_instance_attribute
|
assignable_to_meta_attr && assignable_to_instance_attribute
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SymbolAndQualifiers {
|
SymbolAndQualifiers {
|
||||||
symbol: Symbol::Unbound,
|
symbol: Symbol::Unbound,
|
||||||
|
@ -3176,7 +3205,21 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
ensure_assignable_to(instance_attr_ty)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let result = object_ty.try_call_dunder_with_policy(
|
let result = object_ty.try_call_dunder_with_policy(
|
||||||
db,
|
db,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue