diff --git a/python/python-psi-impl/resources/intellij.python.psi.impl.xml b/python/python-psi-impl/resources/intellij.python.psi.impl.xml index 6d3f13a859ff..2d48452f760c 100644 --- a/python/python-psi-impl/resources/intellij.python.psi.impl.xml +++ b/python/python-psi-impl/resources/intellij.python.psi.impl.xml @@ -563,6 +563,7 @@ + diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyAttrsClassMembersProvider.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyAttrsClassMembersProvider.java new file mode 100644 index 000000000000..2f87c8d22e7b --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyAttrsClassMembersProvider.java @@ -0,0 +1,37 @@ +// Copyright 2000-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.codeInsight; + +import com.intellij.psi.PsiElement; +import com.jetbrains.python.PyNames; +import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.impl.PyBuiltinCache; +import com.jetbrains.python.psi.types.PyClassMembersProviderBase; +import com.jetbrains.python.psi.types.PyClassType; +import com.jetbrains.python.psi.types.TypeEvalContext; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static com.jetbrains.python.codeInsight.PyDataclassesKt.parseDataclassParameters; + +/** + * Adds member __attrs_attrs__ caused by decorator @attrs.define + */ +public final class PyAttrsClassMembersProvider extends PyClassMembersProviderBase { + + @Override + public @NotNull Collection getMembers(PyClassType clazz, PsiElement location, @NotNull TypeEvalContext context) { + PyClass pyClass = clazz.getPyClass(); + PyDataclassParameters dataclassParameters = parseDataclassParameters(pyClass, context); + boolean hasAttrs = dataclassParameters != null && dataclassParameters.getType() == PyDataclassParameters.PredefinedType.ATTRS; + if (hasAttrs) { + PyBuiltinCache builtinCache = PyBuiltinCache.getInstance(pyClass); + PyClass objectClass = builtinCache.getClass(PyNames.OBJECT); + return List.of(new PyCustomMember("__attrs_attrs__", objectClass)); + } + return Collections.emptyList(); + } + +} diff --git a/python/testSrc/com/jetbrains/python/Py3CompletionTest.java b/python/testSrc/com/jetbrains/python/Py3CompletionTest.java index 64cace63e508..58fa8738ebb7 100644 --- a/python/testSrc/com/jetbrains/python/Py3CompletionTest.java +++ b/python/testSrc/com/jetbrains/python/Py3CompletionTest.java @@ -389,6 +389,36 @@ public class Py3CompletionTest extends PyTestCase { ); } + // PY-55691 + public void testAttrsClassMembersProvider() { + runWithAdditionalClassEntryInSdkRoots( + "../packages", + () -> { + doTestByText( + """ + import attrs + + @attrs.define + class User: + password: str + + User().__at""" + ); + + myFixture.checkResult( + """ + import attrs + + @attrs.define + class User: + password: str + + User().__attrs_attrs__""" + ); + } + ); + } + //PY-28332 public void testImportNamespacePackageInMultipleRoots() { doMultiFileTest(Arrays.asList("root1/src", "root2/src")); diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java index 872e12199c2a..679db3161687 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java @@ -2894,4 +2894,35 @@ def foo(param: str | int) -> TypeGuard[str]: f(a=2) """); } + + // PY-55691 + public void testAttrsDataclassProtocolMatchingDefine() { + runWithAdditionalClassEntryInSdkRoots("packages", () -> + doTestByText(""" + import attrs + + @attrs.define + class User: + password: str + + attrs.fields(User) + """) + ); + } + + // PY-55691 + public void testAttrsDataclassProtocolMatchingFrozen() { + runWithAdditionalClassEntryInSdkRoots("packages", () -> + doTestByText(""" + import attrs + + @attrs.frozen + class User: + password: str + + attrs.fields(User) + """) + ); + } + } diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java index e945f89ba40c..c93fa1324969 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java @@ -447,4 +447,21 @@ public class Py3UnresolvedReferencesInspectionTest extends PyInspectionTestCase x.is_integer() # E """); } + + // PY-55691 + public void testAttrsClassMembersProviderAttrsProperty() { + runWithAdditionalClassEntryInSdkRoots("packages", () -> + doTestByText( + """ + import attrs + + @attrs.define + class User: + password: str + + User().__attrs_attrs__ # OK + """) + ); + } + }