mirror of https://github.com/astral-sh/ruff
Allow descriptor instantiations in dataclass fields (#5537)
## Summary
Per the Python documentation, dataclasses are allowed to instantiate
descriptors, like so:
```python
class IntConversionDescriptor:
def __init__(self, *, default):
self._default = default
def __set_name__(self, owner, name):
self._name = "_" + name
def __get__(self, obj, type):
if obj is None:
return self._default
return getattr(obj, self._name, self._default)
def __set__(self, obj, value):
setattr(obj, self._name, int(value))
@dataclass
class InventoryItem:
quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)
```
Closes https://github.com/astral-sh/ruff/issues/4451.
This commit is contained in:
parent
9e1039f823
commit
c5bfd1e877
|
|
@ -6,6 +6,7 @@ from fractions import Fraction
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import ClassVar, NamedTuple
|
from typing import ClassVar, NamedTuple
|
||||||
|
|
||||||
|
|
||||||
def default_function() -> list[int]:
|
def default_function() -> list[int]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -25,12 +26,13 @@ class A:
|
||||||
fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7)
|
fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7)
|
||||||
fine_tuple: tuple[int] = tuple([1])
|
fine_tuple: tuple[int] = tuple([1])
|
||||||
fine_regex: re.Pattern = re.compile(r".*")
|
fine_regex: re.Pattern = re.compile(r".*")
|
||||||
fine_float: float = float('-inf')
|
fine_float: float = float("-inf")
|
||||||
fine_int: int = int(12)
|
fine_int: int = int(12)
|
||||||
fine_complex: complex = complex(1, 2)
|
fine_complex: complex = complex(1, 2)
|
||||||
fine_str: str = str("foo")
|
fine_str: str = str("foo")
|
||||||
fine_bool: bool = bool("foo")
|
fine_bool: bool = bool("foo")
|
||||||
fine_fraction: Fraction = Fraction(1,2)
|
fine_fraction: Fraction = Fraction(1, 2)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40)
|
DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40)
|
||||||
DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3])
|
DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3])
|
||||||
|
|
@ -45,3 +47,25 @@ class B:
|
||||||
okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|
okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|
||||||
|
|
||||||
fine_dataclass_function: list[int] = field(default_factory=list)
|
fine_dataclass_function: list[int] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class IntConversionDescriptor:
|
||||||
|
def __init__(self, *, default):
|
||||||
|
self._default = default
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
self._name = "_" + name
|
||||||
|
|
||||||
|
def __get__(self, obj, type):
|
||||||
|
if obj is None:
|
||||||
|
return self._default
|
||||||
|
|
||||||
|
return getattr(obj, self._name, self._default)
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
setattr(obj, self._name, int(value))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InventoryItem:
|
||||||
|
quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use ruff_python_semantic::analyze::typing::is_immutable_func;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::rules::ruff::rules::helpers::{
|
use crate::rules::ruff::rules::helpers::{
|
||||||
is_class_var_annotation, is_dataclass, is_dataclass_field,
|
is_class_var_annotation, is_dataclass, is_dataclass_field, is_descriptor_class,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
|
|
@ -98,6 +98,7 @@ pub(crate) fn function_call_in_dataclass_default(
|
||||||
if !is_class_var_annotation(annotation, checker.semantic())
|
if !is_class_var_annotation(annotation, checker.semantic())
|
||||||
&& !is_immutable_func(func, checker.semantic(), &extend_immutable_calls)
|
&& !is_immutable_func(func, checker.semantic(), &extend_immutable_calls)
|
||||||
&& !is_dataclass_field(func, checker.semantic())
|
&& !is_dataclass_field(func, checker.semantic())
|
||||||
|
&& !is_descriptor_class(func, checker.semantic())
|
||||||
{
|
{
|
||||||
checker.diagnostics.push(Diagnostic::new(
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
FunctionCallInDataclassDefaultArgument {
|
FunctionCallInDataclassDefaultArgument {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use rustpython_parser::ast::{self, Expr};
|
use rustpython_parser::ast::{self, Expr};
|
||||||
|
|
||||||
use ruff_python_ast::helpers::map_callable;
|
use ruff_python_ast::helpers::map_callable;
|
||||||
use ruff_python_semantic::SemanticModel;
|
use ruff_python_semantic::{BindingKind, SemanticModel};
|
||||||
|
|
||||||
/// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`.
|
/// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`.
|
||||||
///
|
///
|
||||||
|
|
@ -64,3 +64,22 @@ pub(super) fn is_pydantic_model(class_def: &ast::StmtClassDef, semantic: &Semant
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the given function is an instantiation of a class that implements the
|
||||||
|
/// descriptor protocol.
|
||||||
|
///
|
||||||
|
/// See: <https://docs.python.org/3.10/reference/datamodel.html#descriptors>
|
||||||
|
pub(super) fn is_descriptor_class(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||||
|
semantic.lookup_attribute(func).map_or(false, |id| {
|
||||||
|
let BindingKind::ClassDefinition(scope_id) = semantic.binding(id).kind else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look for `__get__`, `__set__`, and `__delete__` methods.
|
||||||
|
["__get__", "__set__", "__delete__"].iter().any(|method| {
|
||||||
|
semantic.scopes[scope_id].get(method).map_or(false, |id| {
|
||||||
|
semantic.binding(id).kind.is_function_definition()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,44 @@
|
||||||
---
|
---
|
||||||
source: crates/ruff/src/rules/ruff/mod.rs
|
source: crates/ruff/src/rules/ruff/mod.rs
|
||||||
---
|
---
|
||||||
RUF009.py:19:41: RUF009 Do not perform function call `default_function` in dataclass defaults
|
RUF009.py:20:41: RUF009 Do not perform function call `default_function` in dataclass defaults
|
||||||
|
|
|
|
||||||
17 | @dataclass()
|
18 | @dataclass()
|
||||||
18 | class A:
|
19 | class A:
|
||||||
19 | hidden_mutable_default: list[int] = default_function()
|
20 | hidden_mutable_default: list[int] = default_function()
|
||||||
| ^^^^^^^^^^^^^^^^^^ RUF009
|
| ^^^^^^^^^^^^^^^^^^ RUF009
|
||||||
20 | class_variable: typing.ClassVar[list[int]] = default_function()
|
21 | class_variable: typing.ClassVar[list[int]] = default_function()
|
||||||
21 | another_class_var: ClassVar[list[int]] = default_function()
|
22 | another_class_var: ClassVar[list[int]] = default_function()
|
||||||
|
|
|
|
||||||
|
|
||||||
RUF009.py:41:41: RUF009 Do not perform function call `default_function` in dataclass defaults
|
RUF009.py:43:41: RUF009 Do not perform function call `default_function` in dataclass defaults
|
||||||
|
|
|
|
||||||
39 | @dataclass
|
41 | @dataclass
|
||||||
40 | class B:
|
42 | class B:
|
||||||
41 | hidden_mutable_default: list[int] = default_function()
|
43 | hidden_mutable_default: list[int] = default_function()
|
||||||
| ^^^^^^^^^^^^^^^^^^ RUF009
|
| ^^^^^^^^^^^^^^^^^^ RUF009
|
||||||
42 | another_dataclass: A = A()
|
44 | another_dataclass: A = A()
|
||||||
43 | not_optimal: ImmutableType = ImmutableType(20)
|
45 | not_optimal: ImmutableType = ImmutableType(20)
|
||||||
|
|
|
|
||||||
|
|
||||||
RUF009.py:42:28: RUF009 Do not perform function call `A` in dataclass defaults
|
RUF009.py:44:28: RUF009 Do not perform function call `A` in dataclass defaults
|
||||||
|
|
|
|
||||||
40 | class B:
|
42 | class B:
|
||||||
41 | hidden_mutable_default: list[int] = default_function()
|
43 | hidden_mutable_default: list[int] = default_function()
|
||||||
42 | another_dataclass: A = A()
|
44 | another_dataclass: A = A()
|
||||||
| ^^^ RUF009
|
| ^^^ RUF009
|
||||||
43 | not_optimal: ImmutableType = ImmutableType(20)
|
45 | not_optimal: ImmutableType = ImmutableType(20)
|
||||||
44 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
|
46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
|
||||||
|
|
|
|
||||||
|
|
||||||
RUF009.py:43:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults
|
RUF009.py:45:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults
|
||||||
|
|
|
|
||||||
41 | hidden_mutable_default: list[int] = default_function()
|
43 | hidden_mutable_default: list[int] = default_function()
|
||||||
42 | another_dataclass: A = A()
|
44 | another_dataclass: A = A()
|
||||||
43 | not_optimal: ImmutableType = ImmutableType(20)
|
45 | not_optimal: ImmutableType = ImmutableType(20)
|
||||||
| ^^^^^^^^^^^^^^^^^ RUF009
|
| ^^^^^^^^^^^^^^^^^ RUF009
|
||||||
44 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
|
46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
|
||||||
45 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|
47 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|
||||||
|
|
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue