Skip to content
Merged
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
16 changes: 16 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF009.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,19 @@ class ShouldMatchB008RuleOfImmutableTypeAnnotationIgnored:
# ignored
this_is_fine: int = f()


# Test for:
# https://github.com/astral-sh/ruff/issues/17424
@dataclass(frozen=True)
class C:
foo: int = 1


@dataclass
class D:
c: C = C()


@dataclass
class E:
c: C = C()
52 changes: 52 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF009_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,55 @@ def __set__(self, obj, value):
@frozen
class InventoryItem:
quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)


# Test for:
# https://github.com/astral-sh/ruff/issues/17424
@frozen
class C:
foo: int = 1


@attr.frozen
class D:
foo: int = 1


@define
class E:
c: C = C()
d: D = D()


@attr.s
class F:
foo: int = 1


@attr.mutable
class G:
foo: int = 1


@attr.attrs
class H:
f: F = F()
g: G = G()


@attr.define
class I:
f: F = F()
g: G = G()


@attr.frozen
class J:
f: F = F()
g: G = G()


@attr.mutable
class K:
f: F = F()
g: G = G()
32 changes: 31 additions & 1 deletion crates/ruff_linter/src/rules/ruff/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ pub(super) fn dataclass_kind<'a>(
};

match qualified_name.segments() {
["attrs", func @ ("define" | "frozen" | "mutable")] | ["attr", func @ "s"] => {
["attrs" | "attr", func @ ("define" | "frozen" | "mutable")]
| ["attr", func @ ("s" | "attrs")] => {
// `.define`, `.frozen` and `.mutable` all default `auto_attribs` to `None`,
// whereas `@attr.s` implicitly sets `auto_attribs=False`.
// https://www.attrs.org/en/stable/api.html#attrs.define
Expand Down Expand Up @@ -163,6 +164,35 @@ pub(super) fn dataclass_kind<'a>(
None
}

/// Return true if dataclass (stdlib or `attrs`) is frozen
pub(super) fn is_frozen_dataclass(
dataclass_decorator: &ast::Decorator,
semantic: &SemanticModel,
) -> bool {
let Some(qualified_name) =
semantic.resolve_qualified_name(map_callable(&dataclass_decorator.expression))
else {
return false;
};

match qualified_name.segments() {
["dataclasses", "dataclass"] => {
let Expr::Call(ExprCall { arguments, .. }) = &dataclass_decorator.expression else {
return false;
};

let Some(keyword) = arguments.find_keyword("frozen") else {
return false;
};
Truthiness::from_expr(&keyword.value, |id| semantic.has_builtin_binding(id))
.into_bool()
.unwrap_or_default()
}
["attrs" | "attr", "frozen"] => true,
_ => false,
}
}

/// Returns `true` if the given class has "default copy" semantics.
///
/// For example, Pydantic `BaseModel` and `BaseSettings` subclasses copy attribute defaults on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use ruff_python_ast::{self as ast, Expr, Stmt};

use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing::{
is_immutable_annotation, is_immutable_func, is_immutable_newtype_call,
};
Expand All @@ -11,7 +12,7 @@ use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::ruff::helpers::{
AttrsAutoAttribs, DataclassKind, dataclass_kind, is_class_var_annotation, is_dataclass_field,
is_descriptor_class,
is_descriptor_class, is_frozen_dataclass,
};

/// ## What it does
Expand Down Expand Up @@ -143,6 +144,7 @@ pub(crate) fn function_call_in_dataclass_default(checker: &Checker, class_def: &
|| func.as_name_expr().is_some_and(|name| {
is_immutable_newtype_call(name, checker.semantic(), &extend_immutable_calls)
})
|| is_frozen_dataclass_instantiation(func, semantic)
{
continue;
}
Expand All @@ -160,3 +162,19 @@ fn any_annotated(class_body: &[Stmt]) -> bool {
.iter()
.any(|stmt| matches!(stmt, Stmt::AnnAssign(..)))
}

/// Checks that the passed function is an instantiation of the class,
/// retrieves the ``StmtClassDef`` and verifies that it is a frozen dataclass
fn is_frozen_dataclass_instantiation(func: &Expr, semantic: &SemanticModel) -> bool {
semantic.lookup_attribute(func).is_some_and(|id| {
let binding = &semantic.binding(id);
let Some(Stmt::ClassDef(class_def)) = binding.statement(semantic) else {
return false;
};

let Some((_, dataclass_decorator)) = dataclass_kind(class_def, semantic) else {
return false;
};
is_frozen_dataclass(dataclass_decorator, semantic)
})
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
snapshot_kind: text
---
RUF009_attrs.py:46:41: RUF009 Do not perform function call `default_function` in dataclass defaults
|
Expand Down Expand Up @@ -31,3 +30,71 @@ RUF009_attrs.py:48:34: RUF009 Do not perform function call `ImmutableType` in da
49 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
50 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|

RUF009_attrs.py:108:12: RUF009 Do not perform function call `F` in dataclass defaults
|
106 | @attr.attrs
107 | class H:
108 | f: F = F()
| ^^^ RUF009
109 | g: G = G()
|

RUF009_attrs.py:109:12: RUF009 Do not perform function call `G` in dataclass defaults
|
107 | class H:
108 | f: F = F()
109 | g: G = G()
| ^^^ RUF009
|

RUF009_attrs.py:114:12: RUF009 Do not perform function call `F` in dataclass defaults
|
112 | @attr.define
113 | class I:
114 | f: F = F()
| ^^^ RUF009
115 | g: G = G()
|

RUF009_attrs.py:115:12: RUF009 Do not perform function call `G` in dataclass defaults
|
113 | class I:
114 | f: F = F()
115 | g: G = G()
| ^^^ RUF009
|

RUF009_attrs.py:120:12: RUF009 Do not perform function call `F` in dataclass defaults
|
118 | @attr.frozen
119 | class J:
120 | f: F = F()
| ^^^ RUF009
121 | g: G = G()
|

RUF009_attrs.py:121:12: RUF009 Do not perform function call `G` in dataclass defaults
|
119 | class J:
120 | f: F = F()
121 | g: G = G()
| ^^^ RUF009
|

RUF009_attrs.py:126:12: RUF009 Do not perform function call `F` in dataclass defaults
|
124 | @attr.mutable
125 | class K:
126 | f: F = F()
| ^^^ RUF009
127 | g: G = G()
|

RUF009_attrs.py:127:12: RUF009 Do not perform function call `G` in dataclass defaults
|
125 | class K:
126 | f: F = F()
127 | g: G = G()
| ^^^ RUF009
|
Loading