[ruff] Allow more field calls from attrs (RUF009) (#19021)

Summary
--

Closes #19014 by identifying more `field` functions from `attrs`. We
already detected these when imported from `attrs` but not the `attr`
module from the same package. These functions are identical to the
`attrs` versions:

```pycon
>>> import attrs, attr
>>> attrs.field is attr.field
True
>>> attrs.Factory is attr.Factory
True
>>>
```

Test Plan
--

Regression tests based on the issue
This commit is contained in:
Brent Westbrook 2025-07-03 10:29:55 -04:00 committed by GitHub
parent 710c60f713
commit b00f68a23c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 31 additions and 3 deletions

View file

@ -125,3 +125,20 @@ class J:
class K:
f: F = F()
g: G = G()
# Regression test for https://github.com/astral-sh/ruff/issues/19014
# These are all valid field calls and should not cause diagnostics.
@attr.define
class TestAttrField:
attr_field_factory: list[int] = attr.field(factory=list)
attr_field_default: list[int] = attr.field(default=attr.Factory(list))
attr_factory: list[int] = attr.Factory(list)
attr_ib: list[int] = attr.ib(factory=list)
attr_attr: list[int] = attr.attr(factory=list)
attr_attrib: list[int] = attr.attrib(factory=list)
@attr.attributes
class TestAttrAttributes:
x: list[int] = list() # RUF009

View file

@ -25,14 +25,16 @@ fn is_stdlib_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool {
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["dataclasses", "field"]))
}
/// Returns `true` if the given [`Expr`] is a call to `attr.ib()` or `attrs.field()`.
/// Returns `true` if the given [`Expr`] is a call to an `attrs` field function.
fn is_attrs_field(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["attrs", "field" | "Factory"] | ["attr", "ib"]
["attrs", "field" | "Factory"]
// See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L33
| ["attr", "ib" | "attr" | "attrib" | "field" | "Factory"]
)
})
}
@ -120,7 +122,8 @@ pub(super) fn dataclass_kind<'a>(
match qualified_name.segments() {
["attrs" | "attr", func @ ("define" | "frozen" | "mutable")]
| ["attr", func @ ("s" | "attrs")] => {
// See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L32
| ["attr", func @ ("s" | "attributes" | "attrs")] => {
// `.define`, `.frozen` and `.mutable` all default `auto_attribs` to `None`,
// whereas `@attr.s` implicitly sets `auto_attribs=False`.
// https://www.attrs.org/en/stable/api.html#attrs.define

View file

@ -98,3 +98,11 @@ RUF009_attrs.py:127:12: RUF009 Do not perform function call `G` in dataclass def
127 | g: G = G()
| ^^^ RUF009
|
RUF009_attrs.py:144:20: RUF009 Do not perform function call `list` in dataclass defaults
|
142 | @attr.attributes
143 | class TestAttrAttributes:
144 | x: list[int] = list() # RUF009
| ^^^^^^ RUF009
|