[ruff] Add exception for ctypes.Structure._fields_ (RUF012) (#22559)

Closes #22166 
## Summary
Adds an exception for `ctypes.Structure._fields_` to the rule RUF012 as
it has it's own way of enforcing immutability:

> The fields class variable can only be set once. Later assignments will
raise an
[AttributeError](https://docs.python.org/3/library/exceptions.html#AttributeError).

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
This commit is contained in:
Caíque Porfirio
2026-01-15 17:49:59 -03:00
committed by GitHub
parent 78d1343583
commit 0ce5ce4de1
4 changed files with 53 additions and 2 deletions

View File

@@ -138,3 +138,15 @@ class AWithQuotes:
class P:
class_variable: ClassVar[list] = [10, 20, 30, 40, 50]
class_variable = [*class_variable[0::1], *class_variable[2::3]]
import ctypes
# Lint should trigger RUF012 only for the `test` field and not the `_fields_`
class S(ctypes.Structure):
test = [""]
_fields_ = [
("attr_set", ctypes.c_uint64),
("attr_clr", ctypes.c_uint64),
("propagation", ctypes.c_uint64),
("userns_fd", ctypes.c_uint64),
]

View File

@@ -242,3 +242,23 @@ pub(super) fn is_descriptor_class(func: &Expr, semantic: &SemanticModel) -> bool
})
})
}
/// Returns `true` if the class has `ctypes.Structure` as a base
/// and the first target is the `_fields_` attribute.
pub(super) fn is_ctypes_structure_fields(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
targets: &[Expr],
) -> bool {
let is_ctypes_structure =
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
matches!(qualified_name.segments(), ["ctypes", "Structure"])
});
let is_fields = matches!(
targets.first(),
Some(Expr::Name(ast::ExprName { id, .. })) if id == "_fields_"
);
is_ctypes_structure && is_fields
}

View File

@@ -8,8 +8,8 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::ruff::helpers::{
dataclass_kind, has_default_copy_semantics, is_class_var_annotation, is_final_annotation,
is_special_attribute,
dataclass_kind, has_default_copy_semantics, is_class_var_annotation,
is_ctypes_structure_fields, is_final_annotation, is_special_attribute,
};
/// ## What it does
@@ -140,6 +140,14 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas
.is_some_and(|name| class_var_targets.contains(&name.id))
}) && is_mutable_expr(value, checker.semantic())
{
// The `_fields_` property of a `ctypes.Structure` base class has its
// immutability enforced by the base class itself which will throw an error if
// it's set a second time
// See: https://docs.python.org/3/library/ctypes.html#ctypes.Structure._fields_
if is_ctypes_structure_fields(class_def, checker.semantic(), targets) {
return;
}
// Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation.
if has_default_copy_semantics(class_def, checker.semantic()) {
return;

View File

@@ -117,3 +117,14 @@ RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
134 | final_variable_without_subscript: 'Final' = []
| ^^
|
RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
--> RUF012.py:146:12
|
144 | # Lint should trigger RUF012 only for the `test` field and not the `_fields_`
145 | class S(ctypes.Structure):
146 | test = [""]
| ^^^^
147 | _fields_ = [
148 | ("attr_set", ctypes.c_uint64),
|