astral-sh / ruff

An extremely fast Python linter and code formatter, written in Rust.
https://docs.astral.sh/ruff
MIT License
32.83k stars 1.1k forks source link

`object.__setattr__` triggering PLC2801 outside of dunder methods #11636

Open my1e5 opened 5 months ago

my1e5 commented 5 months ago

Related to false positive on PLC2801 "unnecessary dunder... for setattr" #9584

object.__setattr__ is the recommended way to set attributes on frozen dataclasses. This was addressed in #9584 but PLC2801 is triggering when object.__setattr__ is in non-dunder methods.

See - https://play.ruff.rs/1a9602da-78ce-44c9-bbfe-b17b147da04b

from dataclasses import dataclass, field

@dataclass(frozen=True)
class Foo:
    a: int
    b: int
    c: int = field(init=False)

    def __post_init__(self):
        object.__setattr__(self, "c", self.a + self.b)

    def foo(self):
        object.__setattr__(self, "c", self.a - self.b)    # <-------

    def __str__(self):
        object.__setattr__(self, "c", self.a * self.b) 
14:9: PLC2801 Unnecessary dunder call to `__setattr__`. Mutate attribute directly or use setattr built-in function.

ruff v0.4.6

AlexWaygood commented 5 months ago

Thanks for the report!

I'm curious, what's the purpose of mutating attributes on a frozen dataclass from arbitrary non-dunder methods? Wouldn't the idiomatic thing here be to make it a regular (not frozen) dataclass?

AlexWaygood commented 5 months ago

Having said that, we may want to consider relaxing the rules here anyway. object.__setattr__ can be useful in various other situations (though it's quite niche) where you want to make sure that you're avoiding triggering custom logic in descriptor or __setattr__ methods

my1e5 commented 5 months ago

I'm curious, what's the purpose of mutating attributes on a frozen dataclass from arbitrary non-dunder methods?

So my code looks something like this

def __post_init__(self):
    self._init_foo()
    self._init_bar()
    self._init_baz()

Basically just breaking up the __post_init__ step for readability.