diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF012.py b/crates/ruff/resources/test/fixtures/ruff/RUF012.py index 1a51f179cd..83cb33045b 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF012.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF012.py @@ -31,3 +31,15 @@ class C: correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] + + +from pydantic import BaseModel + + +class D(BaseModel): + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + perfectly_fine: list[int] = field(default_factory=list) + class_variable: ClassVar[list[int]] = [] diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs index b5d09012e5..83cf0db919 100644 --- a/crates/ruff/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -28,3 +28,12 @@ pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticMod }) }) } + +/// Returns `true` if the given class is a Pydantic `BaseModel`. +pub(super) fn is_pydantic_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { + class_def.bases.iter().any(|expr| { + semantic.resolve_call_path(expr).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["pydantic", "BaseModel"]) + }) + }) +} diff --git a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs index d2c263ec21..83b02fe3c2 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs @@ -5,7 +5,9 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; use crate::checkers::ast::Checker; -use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass}; +use crate::rules::ruff::rules::helpers::{ + is_class_var_annotation, is_dataclass, is_pydantic_model, +}; /// ## What it does /// Checks for mutable default values in class attributes. @@ -57,6 +59,11 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt && !is_immutable_annotation(annotation, checker.semantic()) && !is_dataclass(class_def, checker.semantic()) { + // Avoid Pydantic models, which end up copying defaults on instance creation. + if is_pydantic_model(class_def, checker.semantic()) { + return; + } + checker .diagnostics .push(Diagnostic::new(MutableClassDefault, value.range())); @@ -64,6 +71,11 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt } Stmt::Assign(ast::StmtAssign { value, .. }) => { if is_mutable_expr(value, checker.semantic()) { + // Avoid Pydantic models, which end up copying defaults on instance creation. + if is_pydantic_model(class_def, checker.semantic()) { + return; + } + checker .diagnostics .push(Diagnostic::new(MutableClassDefault, value.range()));