From 327f9cbffdf2a0031f4c4cd158de151be298208d Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Thu, 11 Jun 2020 23:24:58 +0300 Subject: [PATCH] Support kw_only in attr.ib (PY-33189) GitOrigin-RevId: 7354162dc40909842f95f9eb5c9e6b507c544ac8 --- .../stdlib/PyDataclassTypeProvider.kt | 18 ++++--- .../inspections/PyDataclassInspection.kt | 2 +- .../python/psi/PyFileElementType.java | 2 +- .../impl/stubs/PyDataclassFieldStubImpl.kt | 47 +++++++++------- .../psi/stubs/PyDataclassFieldStub.java | 5 ++ ...eldsOrderInInheritanceKwOnlyDefaultBase.py | 39 ++++++++++++-- ...sOrderInInheritanceKwOnlyDefaultDerived.py | 26 +++++++-- ...dsOrderInInheritanceKwOnlyNoDefaultBase.py | 39 ++++++++++++-- ...rderInInheritanceKwOnlyNoDefaultDerived.py | 26 +++++++-- .../InitializingAttrsKwOnlyOnFields.py | 53 +++++++++++++++++++ python/testData/stubs/AttrsKwOnlyOnField.py | 6 +++ .../jetbrains/python/PyParameterInfoTest.java | 11 ++++ .../com/jetbrains/python/PyStubsTest.java | 13 ++++- .../PyDataclassInspectionTest.java | 8 +-- 14 files changed, 242 insertions(+), 53 deletions(-) create mode 100644 python/testData/paramInfo/InitializingAttrsKwOnlyOnFields.py create mode 100644 python/testData/stubs/AttrsKwOnlyOnField.py diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyDataclassTypeProvider.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyDataclassTypeProvider.kt index 4c94757169b8..93c2c6f5d246 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyDataclassTypeProvider.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyDataclassTypeProvider.kt @@ -168,10 +168,10 @@ class PyDataclassTypeProvider : PyTypeProviderBase() { .filterNot { PyTypingTypeProvider.isClassVar(it, context) } .mapNotNull { fieldToParameter(current, it, parameters.type, ellipsis, context) } .filterNot { it.first in seenNames } - .forEach { (name, parameter) -> + .forEach { (name, kwOnly, parameter) -> // note: attributes are visited from inheritors to ancestors, in reversed order for every of them - if (seenKeywordOnlyClass && name !in collected) { + if ((seenKeywordOnlyClass || kwOnly) && name !in collected) { keywordOnly += name } @@ -218,12 +218,12 @@ class PyDataclassTypeProvider : PyTypeProviderBase() { field: PyTargetExpression, dataclassType: PyDataclassParameters.Type, ellipsis: PyNoneLiteralExpression, - context: TypeEvalContext): Pair? { + context: TypeEvalContext): Triple? { val fieldName = field.name ?: return null val stub = field.stub val fieldStub = if (stub == null) PyDataclassFieldStubImpl.create(field) else stub.getCustomStub(PyDataclassFieldStub::class.java) - if (fieldStub != null && !fieldStub.initValue()) return fieldName to null + if (fieldStub != null && !fieldStub.initValue()) return Triple(fieldName, false, null) if (fieldStub == null && field.annotationValue == null) return null // skip fields that are not annotated val parameterName = @@ -234,11 +234,13 @@ class PyDataclassTypeProvider : PyTypeProviderBase() { else it } - val parameter = PyCallableParameterImpl.nonPsi(parameterName, - getTypeForParameter(cls, field, dataclassType, context), - getDefaultValueForParameter(cls, field, fieldStub, dataclassType, ellipsis, context)) + val parameter = PyCallableParameterImpl.nonPsi( + parameterName, + getTypeForParameter(cls, field, dataclassType, context), + getDefaultValueForParameter(cls, field, fieldStub, dataclassType, ellipsis, context) + ) - return parameterName to parameter + return Triple(parameterName, fieldStub?.kwOnly() == true, parameter) } private fun getTypeForParameter(cls: PyClass, diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt index e51bd5a4ab72..1729166e8263 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDataclassInspection.kt @@ -122,7 +122,7 @@ class PyDataclassInspection : PyInspection() { val fieldStub = if (stub == null) PyDataclassFieldStubImpl.create(it) else stub.getCustomStub(PyDataclassFieldStub::class.java) - (fieldStub == null || fieldStub.initValue()) && + (fieldStub == null || fieldStub.initValue() && !fieldStub.kwOnly()) && !(fieldStub == null && it.annotationValue == null) && // skip fields that are not annotated !PyTypingTypeProvider.isClassVar(it, myTypeEvalContext) // skip classvars }, diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/PyFileElementType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/PyFileElementType.java index a584a3c518b4..e8e3fb309c79 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/PyFileElementType.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/PyFileElementType.java @@ -63,7 +63,7 @@ public class PyFileElementType extends IStubFileElementType { @Override public int getStubVersion() { // Don't forget to update versions of indexes that use the updated stub-based elements - return 80; + return 81; } @Nullable diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyDataclassFieldStubImpl.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyDataclassFieldStubImpl.kt index b63645227876..f8a6bf75b75e 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyDataclassFieldStubImpl.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyDataclassFieldStubImpl.kt @@ -18,18 +18,16 @@ import com.jetbrains.python.psi.stubs.PyDataclassFieldStub import java.io.IOException class PyDataclassFieldStubImpl private constructor(private val calleeName: QualifiedName, - private val hasDefault: Boolean, - private val hasDefaultFactory: Boolean, - private val initValue: Boolean) : PyDataclassFieldStub { + private val parameters: FieldParameters) : PyDataclassFieldStub { companion object { fun create(expression: PyTargetExpression): PyDataclassFieldStub? { val value = expression.findAssignedValue() as? PyCallExpression ?: return null val callee = value.callee as? PyReferenceExpression ?: return null val calleeNameAndType = calculateCalleeNameAndType(callee) ?: return null - val arguments = analyzeArguments(value, calleeNameAndType.second) ?: return null + val parameters = analyzeArguments(value, calleeNameAndType.second) ?: return null - return PyDataclassFieldStubImpl(calleeNameAndType.first, arguments.first, arguments.second, arguments.third) + return PyDataclassFieldStubImpl(calleeNameAndType.first, parameters) } @Throws(IOException::class) @@ -38,8 +36,12 @@ class PyDataclassFieldStubImpl private constructor(private val calleeName: Quali val hasDefault = stream.readBoolean() val hasDefaultFactory = stream.readBoolean() val initValue = stream.readBoolean() + val kwOnly = stream.readBoolean() - return PyDataclassFieldStubImpl(QualifiedName.fromDottedString(calleeName), hasDefault, hasDefaultFactory, initValue) + return PyDataclassFieldStubImpl( + QualifiedName.fromDottedString(calleeName), + FieldParameters(hasDefault, hasDefaultFactory, initValue, kwOnly) + ) } private fun calculateCalleeNameAndType(callee: PyReferenceExpression): Pair? { @@ -60,20 +62,22 @@ class PyDataclassFieldStubImpl private constructor(private val calleeName: Quali return null } - private fun analyzeArguments(call: PyCallExpression, type: PyDataclassParameters.PredefinedType): Triple? { + private fun analyzeArguments(call: PyCallExpression, type: PyDataclassParameters.PredefinedType): FieldParameters? { val initValue = PyEvaluator.evaluateAsBooleanNoResolve(call.getKeywordArgument("init"), true) if (type == PyDataclassParameters.PredefinedType.STD) { val default = call.getKeywordArgument("default") val defaultFactory = call.getKeywordArgument("default_factory") - return Triple(default != null && !resolvesToOmittedDefault(default, type), - defaultFactory != null && !resolvesToOmittedDefault(defaultFactory, type), - initValue) + return FieldParameters(default != null && !resolvesToOmittedDefault(default, type), + defaultFactory != null && !resolvesToOmittedDefault(defaultFactory, type), + initValue, + false) } else if (type == PyDataclassParameters.PredefinedType.ATTRS) { val default = call.getKeywordArgument("default") val hasFactory = call.getKeywordArgument("factory").let { it != null && it.text != PyNames.NONE } + val kwOnly = PyEvaluator.evaluateAsBooleanNoResolve(call.getKeywordArgument("kw_only"), false) if (default != null && !resolvesToOmittedDefault(default, type)) { val callee = (default as? PyCallExpression)?.callee as? PyReferenceExpression @@ -81,10 +85,10 @@ class PyDataclassFieldStubImpl private constructor(private val calleeName: Quali callee != null && QualifiedName.fromComponents("attr", "Factory") in PyResolveUtil.resolveImportedElementQNameLocally(callee) - return Triple(!hasFactoryInDefault, hasFactory || hasFactoryInDefault, initValue) + return FieldParameters(!hasFactoryInDefault, hasFactory || hasFactoryInDefault, initValue, kwOnly) } - return Triple(false, hasFactory, initValue) + return FieldParameters(false, hasFactory, initValue, kwOnly) } return null @@ -97,13 +101,20 @@ class PyDataclassFieldStubImpl private constructor(private val calleeName: Quali override fun serialize(stream: StubOutputStream) { stream.writeName(calleeName.toString()) - stream.writeBoolean(hasDefault) - stream.writeBoolean(hasDefaultFactory) - stream.writeBoolean(initValue) + stream.writeBoolean(parameters.hasDefault) + stream.writeBoolean(parameters.hasDefaultFactory) + stream.writeBoolean(parameters.initValue) + stream.writeBoolean(parameters.kwOnly) } override fun getCalleeName(): QualifiedName = calleeName - override fun hasDefault(): Boolean = hasDefault - override fun hasDefaultFactory(): Boolean = hasDefaultFactory - override fun initValue(): Boolean = initValue + override fun hasDefault(): Boolean = parameters.hasDefault + override fun hasDefaultFactory(): Boolean = parameters.hasDefaultFactory + override fun initValue(): Boolean = parameters.initValue + override fun kwOnly(): Boolean = parameters.kwOnly + + private data class FieldParameters(val hasDefault: Boolean, + val hasDefaultFactory: Boolean, + val initValue: Boolean, + val kwOnly: Boolean) } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/stubs/PyDataclassFieldStub.java b/python/python-psi-impl/src/com/jetbrains/python/psi/stubs/PyDataclassFieldStub.java index 950bf855751a..559bb7eb19d2 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/stubs/PyDataclassFieldStub.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/stubs/PyDataclassFieldStub.java @@ -21,4 +21,9 @@ public interface PyDataclassFieldStub extends CustomTargetExpressionStub { * @return true if field is used in `__init__`. */ boolean initValue(); + + /** + * @return true if field is used in `__init__` as a keyword-only parameter (attrs). + */ + boolean kwOnly(); } diff --git a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultBase.py b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultBase.py index 98ee0d485101..b0a8af7c0f0f 100644 --- a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultBase.py +++ b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultBase.py @@ -1,29 +1,58 @@ import attr @attr.dataclass(kw_only=True) -class Base: +class Base1: a: int = 1 # no kw_only, no default @attr.dataclass -class Derived1(Base): +class Derived11(Base1): b: str # kw_only, no default @attr.dataclass(kw_only=True) -class Derived2(Base): +class Derived12(Base1): b: str # no kw_only, default @attr.dataclass -class Derived3(Base): +class Derived13(Base1): b: str = "b" # kw_only, default @attr.dataclass(kw_only=True) -class Derived4(Base): +class Derived14(Base1): + b: str = "b" + + +@attr.s +class Base2: + a = attr.ib(type=int, kw_only=True) + + +# no kw_only, no default +@attr.dataclass +class Derived21(Base2): + b: str + + +# kw_only, no default +@attr.dataclass(kw_only=True) +class Derived22(Base2): + b: str + + +# no kw_only, default +@attr.dataclass +class Derived23(Base2): + b: str = "b" + + +# kw_only, default +@attr.dataclass(kw_only=True) +class Derived24(Base2): b: str = "b" \ No newline at end of file diff --git a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultDerived.py b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultDerived.py index 20fca92c9a21..37c03b89b62c 100644 --- a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultDerived.py +++ b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultDerived.py @@ -6,9 +6,13 @@ class Base1: a: int @attr.dataclass(kw_only=True) -class Derived1(Base1): +class Derived11(Base1): b: str = "b" +@attr.s +class Derived12(Base1): + b = attr.ib(type=str, default="b", kw_only=True) + # kw_only, no default @attr.dataclass(kw_only=True) @@ -16,9 +20,13 @@ class Base2: a: int @attr.dataclass(kw_only=True) -class Derived2(Base2): +class Derived21(Base2): b: str = "b" +@attr.s +class Derived22(Base2): + b = attr.ib(type=str, default="b", kw_only=True) + # no kw_only, default @attr.dataclass @@ -26,9 +34,13 @@ class Base3: a: int = 1 @attr.dataclass(kw_only=True) -class Derived3(Base3): +class Derived31(Base3): b: str = "b" +@attr.s +class Derived32(Base3): + b = attr.ib(type=str, default="b", kw_only=True) + # kw_only, default @attr.dataclass(kw_only=True) @@ -36,5 +48,9 @@ class Base4: a: int = 1 @attr.dataclass(kw_only=True) -class Derived4(Base4): - b: str = "b" \ No newline at end of file +class Derived41(Base4): + b: str = "b" + +@attr.s +class Derived42(Base4): + b = attr.ib(type=str, default="b", kw_only=True) \ No newline at end of file diff --git a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultBase.py b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultBase.py index 312633e5b038..e10a29eb3da8 100644 --- a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultBase.py +++ b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultBase.py @@ -1,29 +1,58 @@ import attr @attr.dataclass(kw_only=True) -class Base: +class Base1: a: int # no kw_only, no default @attr.dataclass -class Derived1(Base): +class Derived11(Base1): b: str # kw_only, no default @attr.dataclass(kw_only=True) -class Derived2(Base): +class Derived12(Base1): b: str # no kw_only, default @attr.dataclass -class Derived3(Base): +class Derived13(Base1): b: str = "b" # kw_only, default @attr.dataclass(kw_only=True) -class Derived4(Base): +class Derived14(Base1): + b: str = "b" + + +@attr.s +class Base2: + a = attr.ib(type=int, kw_only=True) + + +# no kw_only, no default +@attr.dataclass +class Derived21(Base2): + b: str + + +# kw_only, no default +@attr.dataclass(kw_only=True) +class Derived22(Base2): + b: str + + +# no kw_only, default +@attr.dataclass +class Derived23(Base2): + b: str = "b" + + +# kw_only, default +@attr.dataclass(kw_only=True) +class Derived24(Base2): b: str = "b" \ No newline at end of file diff --git a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultDerived.py b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultDerived.py index bf6b6953c407..6bc742f34e78 100644 --- a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultDerived.py +++ b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultDerived.py @@ -6,9 +6,13 @@ class Base1: a: int @attr.dataclass(kw_only=True) -class Derived1(Base1): +class Derived11(Base1): b: str +@attr.s +class Derived12(Base1): + b = attr.ib(type=str, kw_only=True) + # kw_only, no default @attr.dataclass(kw_only=True) @@ -16,9 +20,13 @@ class Base2: a: int @attr.dataclass(kw_only=True) -class Derived2(Base2): +class Derived21(Base2): b: str +@attr.s +class Derived22(Base2): + b = attr.ib(type=str, kw_only=True) + # no kw_only, default @attr.dataclass @@ -26,9 +34,13 @@ class Base3: a: int = 1 @attr.dataclass(kw_only=True) -class Derived3(Base3): +class Derived31(Base3): b: str +@attr.s +class Derived32(Base3): + b = attr.ib(type=str, kw_only=True) + # kw_only, default @attr.dataclass(kw_only=True) @@ -36,5 +48,9 @@ class Base4: a: int = 1 @attr.dataclass(kw_only=True) -class Derived4(Base4): - b: str \ No newline at end of file +class Derived41(Base4): + b: str + +@attr.s +class Derived42(Base4): + b = attr.ib(type=str, kw_only=True) \ No newline at end of file diff --git a/python/testData/paramInfo/InitializingAttrsKwOnlyOnFields.py b/python/testData/paramInfo/InitializingAttrsKwOnlyOnFields.py new file mode 100644 index 000000000000..1ac157cefb5d --- /dev/null +++ b/python/testData/paramInfo/InitializingAttrsKwOnlyOnFields.py @@ -0,0 +1,53 @@ +import attr + +@attr.s +class A: + a = attr.ib(kw_only=True) + b = attr.ib(type=int) + +A() + + +@attr.s +class B1: + a = attr.ib(kw_only=True) + +@attr.s +class D1(B1): + b = attr.ib(type=int) + +D1() + + +@attr.s +class B2: + a = attr.ib(type=int) + +@attr.s +class D2(B2): + b = attr.ib(kw_only=True) + +D2() + + +@attr.s +class B3: + a = attr.ib(type=int) + +@attr.s +class D3(B3): + a = attr.ib(kw_only=True) + +D3() + + +@attr.s +class B4: + a = attr.ib(kw_only=True) + +@attr.s +class D4(B4): + a = attr.ib(type=int) + +D4() + diff --git a/python/testData/stubs/AttrsKwOnlyOnField.py b/python/testData/stubs/AttrsKwOnlyOnField.py new file mode 100644 index 000000000000..34aa5078e632 --- /dev/null +++ b/python/testData/stubs/AttrsKwOnlyOnField.py @@ -0,0 +1,6 @@ +import attr +@attr.s +class Foo: + bar1 = attr.ib(type=str) + bar2 = attr.ib(type=str, kw_only=True) + bar3 = attr.ib(type=str, kw_only=False) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java index d023f4499f26..2dc9060e6765 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -909,6 +909,17 @@ public class PyParameterInfoTest extends LightMarkedTestCase { feignCtrlP(marks.get("").getTextOffset()).check("*, a: int", new String[]{"*, a: int"}); } + // PY-33189 + public void testInitializingAttrsKwOnlyOnFields() { + final var marks = loadTest(5); + + feignCtrlP(marks.get("").getTextOffset()).check("b: int, *, a", new String[]{"b: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("b: int, *, a", new String[]{"b: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("a: int, *, b", new String[]{"a: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("*, a", ArrayUtil.EMPTY_STRING_ARRAY); + feignCtrlP(marks.get("").getTextOffset()).check("a: int", new String[]{"a: int"}); + } + // PY-28957 public void testDataclassesReplace() { runWithLanguageLevel( diff --git a/python/testSrc/com/jetbrains/python/PyStubsTest.java b/python/testSrc/com/jetbrains/python/PyStubsTest.java index 080d01b6bb8a..1912b329960c 100644 --- a/python/testSrc/com/jetbrains/python/PyStubsTest.java +++ b/python/testSrc/com/jetbrains/python/PyStubsTest.java @@ -343,7 +343,6 @@ public class PyStubsTest extends PyTestCase { private PyFile getTestFile(final String fileName) { VirtualFile sourceFile = myFixture.copyFileToProject(fileName); - assert sourceFile != null; PsiFile psiFile = myFixture.getPsiManager().findFile(sourceFile); return (PyFile)psiFile; } @@ -977,6 +976,18 @@ public class PyStubsTest extends PyTestCase { assertNotParsed(file); } + // PY-33189 + public void testAttrsKwOnlyOnField() { + final PyFile file = getTestFile(); + final PyClass cls = file.findTopLevelClass("Foo"); + + assertFalse(cls.findClassAttribute("bar1", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly()); + assertTrue(cls.findClassAttribute("bar2", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly()); + assertFalse(cls.findClassAttribute("bar3", false, null).getStub().getCustomStub(PyDataclassFieldStub.class).kwOnly()); + + assertNotParsed(file); + } + // PY-27398 public void testTypingNewType() { runWithLanguageLevel( diff --git a/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java index c542dc1864a3..b55412204068 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java @@ -66,22 +66,22 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase { doTest(); } - // PY-34374 + // PY-34374, PY-33189 public void testFieldsOrderInInheritanceKwOnlyNoDefaultBase() { doTest(); } - // PY-34374 + // PY-34374, PY-33189 public void testFieldsOrderInInheritanceKwOnlyDefaultBase() { doTest(); } - // PY-34374 + // PY-34374, PY-33189 public void testFieldsOrderInInheritanceKwOnlyNoDefaultDerived() { doTest(); } - // PY-34374 + // PY-34374, PY-33189 public void testFieldsOrderInInheritanceKwOnlyDefaultDerived() { doTest(); }