Repository: pylint-dev/pylint

Issue:
New-style attrs decorators not recognized as data class decorators
### Bug description

Beginning with attrs 21.1.0, the recommended way to use attrs is through `import attrs` and using `attrs.define`/`attrs.frozen`, not `import attr` and `attr.s` or `attr.attrs`. Pylint does understand `attr.attrs` (#2988), but new-style uses of attrs are not understood to be data class decorators.

```python
"""Example module"""

from attrs import define


@define
class Coordinates:
    """An integer X/Y coordinate on the Cartesian coordinate plane."""

    x: int
    y: int
```


### Configuration

_No response_

### Command used

```shell
pylint example.py
```


### Pylint output

```python
************* Module example
example.py:7:0: R0903: Too few public methods (0/2) (too-few-public-methods)
```


### Expected behavior

I expect the code to pass pylint without triggering the `too-few-public-methods` check.

### Pylint version

```shell
pylint 3.0.3
astroid 3.0.2
Python 3.9.6 (default, Oct 18 2022, 12:41:40) 
[Clang 14.0.0 (clang-1400.0.29.202)]
```


### OS / Environment

_No response_

### Additional dependencies

```python
attrs==23.2.0
```



Discussion:

Patch:
diff --git a/doc/whatsnew/fragments/9345.false_positive b/doc/whatsnew/fragments/9345.false_positive
new file mode 100644
index 0000000000..af8a3866b3
--- /dev/null
+++ b/doc/whatsnew/fragments/9345.false_positive
@@ -0,0 +1,4 @@
+Treat `attrs.define` and `attrs.frozen` as dataclass decorators in
+`too-few-public-methods` check.
+
+Closes #9345
diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py
index 97623feae8..8dfe6feca3 100644
--- a/pylint/checkers/design_analysis.py
+++ b/pylint/checkers/design_analysis.py
@@ -92,6 +92,8 @@
 SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$")
 DATACLASSES_DECORATORS = frozenset({"dataclass", "attrs"})
 DATACLASS_IMPORT = "dataclasses"
+ATTRS_DECORATORS = frozenset({"define", "frozen"})
+ATTRS_IMPORT = "attrs"
 TYPING_NAMEDTUPLE = "typing.NamedTuple"
 TYPING_TYPEDDICT = "typing.TypedDict"
 TYPING_EXTENSIONS_TYPEDDICT = "typing_extensions.TypedDict"
@@ -214,6 +216,10 @@ def _is_exempt_from_public_methods(node: astroid.ClassDef) -> bool:
             or DATACLASS_IMPORT in root_locals
         ):
             return True
+        if name in ATTRS_DECORATORS and (
+            root_locals.intersection(ATTRS_DECORATORS) or ATTRS_IMPORT in root_locals
+        ):
+            return True
     return False
 
 


Test patch:
diff --git a/tests/functional/t/too/too_few_public_methods_37.py b/tests/functional/t/too/too_few_public_methods_37.py
index db9c9f171e..3d3a12517b 100644
--- a/tests/functional/t/too/too_few_public_methods_37.py
+++ b/tests/functional/t/too/too_few_public_methods_37.py
@@ -8,6 +8,9 @@
 import typing
 from dataclasses import dataclass
 
+import attrs  # pylint: disable=import-error
+from attrs import define, frozen  # pylint: disable=import-error
+
 
 @dataclasses.dataclass
 class ScheduledTxSearchModel:
@@ -40,3 +43,27 @@ class Point:
     def to_array(self):
         """Convert to a NumPy array `np.array((x, y, z))`."""
         return self.attr1
+
+
+@define
+class AttrsBarePoint:
+    x: float
+    y: float
+
+
+@frozen
+class AttrsBareFrozenPoint:
+    x: float
+    y: float
+
+
+@attrs.define
+class AttrsQualifiedPoint:
+    x: float
+    y: float
+
+
+@attrs.frozen
+class AttrsQualifiedFrozenPoint:
+    x: float
+    y: float