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:
Charlie Marsh 2023-07-05 15:19:24 -04:00 committed by GitHub
parent 9e1039f823
commit c5bfd1e877
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 72 additions and 28 deletions

View File

@ -6,6 +6,7 @@ from fractions import Fraction
from pathlib import Path
from typing import ClassVar, NamedTuple
def default_function() -> list[int]:
return []
@ -25,12 +26,13 @@ class A:
fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7)
fine_tuple: tuple[int] = tuple([1])
fine_regex: re.Pattern = re.compile(r".*")
fine_float: float = float('-inf')
fine_float: float = float("-inf")
fine_int: int = int(12)
fine_complex: complex = complex(1, 2)
fine_str: str = str("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_A_FOR_ALL_DATACLASSES = A([1, 2, 3])
@ -45,3 +47,25 @@ class B:
okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
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)

View File

@ -8,7 +8,7 @@ use ruff_python_semantic::analyze::typing::is_immutable_func;
use crate::checkers::ast::Checker;
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
@ -98,6 +98,7 @@ pub(crate) fn function_call_in_dataclass_default(
if !is_class_var_annotation(annotation, checker.semantic())
&& !is_immutable_func(func, checker.semantic(), &extend_immutable_calls)
&& !is_dataclass_field(func, checker.semantic())
&& !is_descriptor_class(func, checker.semantic())
{
checker.diagnostics.push(Diagnostic::new(
FunctionCallInDataclassDefaultArgument {

View File

@ -1,7 +1,7 @@
use rustpython_parser::ast::{self, Expr};
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__`.
///
@ -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()
})
})
})
}

View File

@ -1,44 +1,44 @@
---
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 | class A:
19 | hidden_mutable_default: list[int] = default_function()
18 | @dataclass()
19 | class A:
20 | hidden_mutable_default: list[int] = default_function()
| ^^^^^^^^^^^^^^^^^^ RUF009
20 | class_variable: typing.ClassVar[list[int]] = default_function()
21 | another_class_var: ClassVar[list[int]] = default_function()
21 | class_variable: typing.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
40 | class B:
41 | hidden_mutable_default: list[int] = default_function()
41 | @dataclass
42 | class B:
43 | hidden_mutable_default: list[int] = default_function()
| ^^^^^^^^^^^^^^^^^^ RUF009
42 | another_dataclass: A = A()
43 | not_optimal: ImmutableType = ImmutableType(20)
44 | another_dataclass: A = A()
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:
41 | hidden_mutable_default: list[int] = default_function()
42 | another_dataclass: A = A()
42 | class B:
43 | hidden_mutable_default: list[int] = default_function()
44 | another_dataclass: A = A()
| ^^^ RUF009
43 | not_optimal: ImmutableType = ImmutableType(20)
44 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
45 | not_optimal: ImmutableType = ImmutableType(20)
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()
42 | another_dataclass: A = A()
43 | not_optimal: ImmutableType = ImmutableType(20)
43 | hidden_mutable_default: list[int] = default_function()
44 | another_dataclass: A = A()
45 | not_optimal: ImmutableType = ImmutableType(20)
| ^^^^^^^^^^^^^^^^^ RUF009
44 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
45 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
47 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|