ruff icon indicating copy to clipboard operation
ruff copied to clipboard

Pydantic model not detected by RUF012

Open cbornet opened this issue 1 year ago • 3 comments

  • List of keywords you searched for before creating this issue: pydantic ruf012
  • The current Ruff version : 0.5.0

RUF012 normally ignores pydantic models but not when the class inherits from an imported model. Eg: file b.py

from pydantic import BaseModel

class B(BaseModel):
    pass

file a.py

from pydantic import BaseModel
from b import B

class Foo(B):
    foo: list[str] = []

class C(BaseModel):
    pass

class Bar(C):
    bar: list[str] = []

Then run ruff

ruff check --select RUF012
a.py:5:22: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
  |
4 | class Foo(B):
5 |     foo: list[str] = []
  |                      ^^ RUF012
6 |
7 | class C(BaseModel):
  |

Found 1 error.

I would expect ruff to not error.

cbornet avatar Oct 04 '24 16:10 cbornet

Ruff doesn't support multi-file analysis yet, we can't inspect the type across the import boundary

xref https://github.com/astral-sh/ruff/issues/7447

zanieb avatar Oct 04 '24 17:10 zanieb

Thanks for the info. I'll follow #7447 with interest then.

cbornet avatar Oct 04 '24 18:10 cbornet

We're actively working on it, but it's a ways out.

zanieb avatar Oct 04 '24 21:10 zanieb

I nice workaround would be to make it possible to extend has_default_copy_semantics() list:

https://github.com/astral-sh/ruff/blob/10d3e64ccdf69d90c3252a15b3408a4238427792/crates/ruff_linter/src/rules/ruff/rules/helpers.rs#L166C1-L183C2

/// Returns `true` if the given class has "default copy" semantics.
///
/// For example, Pydantic `BaseModel` and `BaseSettings` subclassses copy attribute defaults on
/// instance creation. As such, the use of mutable default values is safe for such classes.
pub(super) fn has_default_copy_semantics(
    class_def: &ast::StmtClassDef,
    semantic: &SemanticModel,
) -> bool {
    analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
        matches!(
            qualified_name.segments(),
            ["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"]
                | ["pydantic_settings", "BaseSettings"]
                | ["msgspec", "Struct"]
                | ["sqlmodel", "SQLModel"]
        )
    })
}

Would you be open to take such a PR?

(such a solution would pobably also "close" https://github.com/astral-sh/ruff/issues/5639)

jankatins avatar Feb 07 '25 16:02 jankatins

Making that configurable was discussed a bit here https://github.com/astral-sh/ruff/issues/14892#issuecomment-2536960816, and it sounded like the preference is to hold off in favor of using type inference, once ruff supports that.

sjdemartini avatar Feb 07 '25 17:02 sjdemartini

Similar to #5639. Maybe this issue can be closed and followed in the other one for multiple files ?

cbornet avatar Sep 10 '25 08:09 cbornet

That makes sense to me, they do sound like the same root issue. Thanks for following up!

ntBre avatar Sep 10 '25 14:09 ntBre