Update `attrs` brain to exclude `ClassVar` annotated attributes (#2811)

diff --git a/ChangeLog b/ChangeLog
index 379a2a7..f207eb7 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,10 @@
 ============================
 Release date: TBA
 
+* Fix false positive `invalid-name` on `attrs` classes with `ClassVar` annotated variables.
+
+  Closes pylint-dev/pylint#10525
+
 * Prevent crash when parsing deeply nested parentheses causing MemoryError in python's built-in ast.
 
   Closes #2643
diff --git a/astroid/brain/brain_attrs.py b/astroid/brain/brain_attrs.py
index 23ec9f6..727fb90 100644
--- a/astroid/brain/brain_attrs.py
+++ b/astroid/brain/brain_attrs.py
@@ -8,6 +8,7 @@
 Without this hook pylint reports unsupported-assignment-operation
 for attrs classes
 """
+from astroid.brain.helpers import is_class_var
 from astroid.manager import AstroidManager
 from astroid.nodes.node_classes import AnnAssign, Assign, AssignName, Call, Unknown
 from astroid.nodes.scoped_nodes import ClassDef
@@ -78,6 +79,13 @@
                 continue
         elif not use_bare_annotations:
             continue
+
+        # Skip attributes that are explicitly annotated as class variables
+        if isinstance(cdef_body_node, AnnAssign) and is_class_var(
+            cdef_body_node.annotation
+        ):
+            continue
+
         targets = (
             cdef_body_node.targets
             if hasattr(cdef_body_node, "targets")
diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py
index 16e992b..e6794be 100644
--- a/astroid/brain/brain_dataclasses.py
+++ b/astroid/brain/brain_dataclasses.py
@@ -18,6 +18,7 @@
 from typing import Literal
 
 from astroid import bases, context, nodes
+from astroid.brain.helpers import is_class_var
 from astroid.builder import parse
 from astroid.const import PY313_PLUS
 from astroid.exceptions import AstroidSyntaxError, InferenceError, UseInferenceDefault
@@ -117,7 +118,7 @@
             continue
 
         # Annotation is never None
-        if _is_class_var(assign_node.annotation):  # type: ignore[arg-type]
+        if is_class_var(assign_node.annotation):  # type: ignore[arg-type]
             continue
 
         if _is_keyword_only_sentinel(assign_node.annotation):
@@ -550,16 +551,6 @@
     return None
 
 
-def _is_class_var(node: nodes.NodeNG) -> bool:
-    """Return True if node is a ClassVar, with or without subscripting."""
-    try:
-        inferred = next(node.infer())
-    except (InferenceError, StopIteration):
-        return False
-
-    return getattr(inferred, "name", "") == "ClassVar"
-
-
 def _is_keyword_only_sentinel(node: nodes.NodeNG) -> bool:
     """Return True if node is the KW_ONLY sentinel."""
     inferred = safe_infer(node)
diff --git a/astroid/brain/helpers.py b/astroid/brain/helpers.py
index 250d438..ccf6727 100644
--- a/astroid/brain/helpers.py
+++ b/astroid/brain/helpers.py
@@ -5,10 +5,15 @@
 from __future__ import annotations
 
 from collections.abc import Callable
+from typing import TYPE_CHECKING
 
+from astroid.exceptions import InferenceError
 from astroid.manager import AstroidManager
 from astroid.nodes.scoped_nodes import Module
 
+if TYPE_CHECKING:
+    from astroid.nodes.node_ng import NodeNG
+
 
 def register_module_extender(
     manager: AstroidManager, module_name: str, get_extension_mod: Callable[[], Module]
@@ -127,3 +132,13 @@
     brain_typing.register(manager)
     brain_unittest.register(manager)
     brain_uuid.register(manager)
+
+
+def is_class_var(node: NodeNG) -> bool:
+    """Return True if node is a ClassVar, with or without subscripting."""
+    try:
+        inferred = next(node.infer())
+    except (InferenceError, StopIteration):
+        return False
+
+    return getattr(inferred, "name", "") == "ClassVar"
diff --git a/tests/brain/test_attr.py b/tests/brain/test_attr.py
index ef48873..667561b 100644
--- a/tests/brain/test_attr.py
+++ b/tests/brain/test_attr.py
@@ -229,3 +229,59 @@
                 attr_name
             )[0]
             self.assertIsInstance(should_be_unknown, astroid.Unknown)
+
+    def test_attrs_with_class_var_annotation(self) -> None:
+        cases = {
+            "with-subscript": """
+                import attrs
+                from typing import ClassVar
+
+                @attrs.define
+                class Foo:
+                    bar: ClassVar[int] = 1
+                Foo()
+            """,
+            "no-subscript": """
+                import attrs
+                from typing import ClassVar
+
+                @attrs.define
+                class Foo:
+                    bar: ClassVar = 1
+                Foo()
+            """,
+        }
+
+        for name, code in cases.items():
+            with self.subTest(case=name):
+                instance = next(astroid.extract_node(code).infer())
+                self.assertIsInstance(instance.getattr("bar")[0], nodes.AssignName)
+                self.assertNotIn("bar", instance.instance_attrs)
+
+    def test_attrs_without_class_var_annotation(self) -> None:
+        cases = {
+            "wrong-name": """
+                import attrs
+                from typing import Final
+
+                @attrs.define
+                class Foo:
+                    bar: Final[int] = 1
+                Foo()
+            """,
+            "classvar-not-outermost": """
+                import attrs
+                from typing import ClassVar
+
+                @attrs.define
+                class Foo:
+                    bar: list[ClassVar[int]] = []
+                Foo()
+            """,
+        }
+
+        for name, code in cases.items():
+            with self.subTest(case=name):
+                instance = next(astroid.extract_node(code).infer())
+                self.assertIsInstance(instance.getattr("bar")[0], nodes.Unknown)
+                self.assertIn("bar", instance.instance_attrs)
diff --git a/tests/brain/test_helpers.py b/tests/brain/test_helpers.py
new file mode 100644
index 0000000..d30fd1d
--- /dev/null
+++ b/tests/brain/test_helpers.py
@@ -0,0 +1,85 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
+
+import pytest
+
+from astroid import extract_node, nodes
+from astroid.brain.helpers import is_class_var
+
+
+@pytest.mark.parametrize(
+    "code",
+    [
+        pytest.param(
+            """
+            from typing import ClassVar
+
+            foo: ClassVar[int]
+            """,
+            id="from-import",
+        ),
+        pytest.param(
+            """
+            from typing import ClassVar
+
+            foo: ClassVar
+            """,
+            id="bare-classvar",
+        ),
+        pytest.param(
+            """
+            import typing
+
+            foo: typing.ClassVar[int]
+            """,
+            id="module-import",
+        ),
+    ],
+)
+def test_is_class_var_returns_true(code):
+    node = extract_node(code)
+    assert isinstance(node, nodes.AnnAssign)
+    assert is_class_var(node.annotation)
+
+
+@pytest.mark.parametrize(
+    "code",
+    [
+        pytest.param(
+            """
+            from typing import Final
+
+            foo: Final[int]
+            """,
+            id="wrong-name",
+        ),
+        pytest.param(
+            """
+            from typing import ClassVar
+
+            foo: list[ClassVar[int]]
+            """,
+            id="classvar-not-outermost",
+        ),
+        pytest.param(
+            """
+            from typing import ClassVar
+            ClassVar = int
+
+            foo: ClassVar
+            """,
+            id="shadowed-name",
+        ),
+        pytest.param(
+            """
+            foo: ClassVar[int]
+            """,
+            id="missing-import",
+        ),
+    ],
+)
+def test_is_class_var_returns_false(code):
+    node = extract_node(code)
+    assert isinstance(node, nodes.AnnAssign)
+    assert not is_class_var(node.annotation)