diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyDataclasses.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyDataclasses.kt index 0d90a7edb36f..9fa92027cb0b 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyDataclasses.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PyDataclasses.kt @@ -31,6 +31,7 @@ object PyDataclassNames { const val DATACLASSES_ASDICT = "dataclasses.asdict" const val DATACLASSES_FIELD = "dataclasses.field" const val DATACLASSES_REPLACE = "dataclasses.replace" + const val DATACLASSES_KW_ONLY = "dataclasses.KW_ONLY" const val DUNDER_POST_INIT = "__post_init__" val DECORATOR_PARAMETERS = listOf("init", "repr", "eq", "order", "unsafe_hash", "frozen", "kw_only") val HELPER_FUNCTIONS = setOf(DATACLASSES_FIELDS, DATACLASSES_ASDICT, "dataclasses.astuple", DATACLASSES_REPLACE) 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 bfebc845d37b..02a212d40458 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 @@ -140,24 +140,31 @@ class PyDataclassTypeProvider : PyTypeProviderBase() { seenKeywordOnlyClass = seenKeywordOnlyClass || parameters.kwOnly if (seenInit) { - current + val fieldsInfo = current .classAttributes .asReversed() .asSequence() .filterNot { PyTypingTypeProvider.isClassVar(it, context) } .mapNotNull { fieldToParameter(current, it, parameters.type, ellipsis, context) } .filterNot { it.first in seenNames } - .forEach { (name, kwOnly, parameter) -> - // note: attributes are visited from inheritors to ancestors, in reversed order for every of them + .toList() - if ((seenKeywordOnlyClass || kwOnly) && name !in collected) { - keywordOnly += name - } + val indexOfKeywordOnlyAttribute = fieldsInfo.indexOfLast { (_, _, parameter) -> + parameter != null && isKwOnlyMarkerField(parameter, context) + } - if (parameter == null) { - seenNames.add(name) - } - else if (parameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD) { + fieldsInfo.forEachIndexed { index, (name, kwOnly, parameter) -> + // note: attributes are visited from inheritors to ancestors, in reversed order for every of them + + if ((seenKeywordOnlyClass || index < indexOfKeywordOnlyAttribute || kwOnly) && name !in collected) { + keywordOnly += name + } + + if (parameter == null) { + seenNames.add(name) + } + else if (!isKwOnlyMarkerField(parameter, context)) { + if (parameters.type.asPredefinedType == PyDataclassParameters.PredefinedType.STD) { // std: attribute that overrides ancestor's attribute does not change the order but updates type collected[name] = collected.remove(name) ?: parameter } @@ -166,12 +173,20 @@ class PyDataclassTypeProvider : PyTypeProviderBase() { collected[name] = parameter } } + } } } - return if (seenInit) PyCallableTypeImpl(buildParameters(elementGenerator, collected, keywordOnly), clsType.toInstance()) else null } + private fun isKwOnlyMarkerField(parameter: PyCallableParameter, context: TypeEvalContext): Boolean { + val psi = parameter.declarationElement + if (psi !is PyTargetExpression) return false + val typeHint = PyTypingTypeProvider.getAnnotationValue(psi, context) as? PyReferenceExpression ?: return false + val type = Ref.deref(PyTypingTypeProvider.getType(typeHint, context)) + return type is PyClassType && type.classQName == Dataclasses.DATACLASSES_KW_ONLY + } + private fun buildParameters(elementGenerator: PyElementGenerator, fields: Map, keywordOnly: Set): List { diff --git a/python/testData/paramInfo/InitializingDataclassKwOnlyAttribute.py b/python/testData/paramInfo/InitializingDataclassKwOnlyAttribute.py new file mode 100644 index 000000000000..cf9446657eb5 --- /dev/null +++ b/python/testData/paramInfo/InitializingDataclassKwOnlyAttribute.py @@ -0,0 +1,46 @@ +from dataclasses import dataclass, KW_ONLY + +@dataclass +class Base: + a: int + qq: KW_ONLY + b: int + +Base() + +@dataclass +class Derived1(Base): + b: int + +Derived1() + +@dataclass +class Derived2(Base): + qq: int + +Derived2() + +@dataclass +class Derived3(Base): + c: int + ww: KW_ONLY + d: int + +Derived3() + +@dataclass +class Derived4(Base): + ww: KW_ONLY + qq: int + +Derived4() + +@dataclass +class Base2: + a: str + +@dataclass +class Derived5(Base2): + a: KW_ONLY + +Derived5() diff --git a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java index 2cf4f01b0c11..a381ef75a015 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -1205,6 +1205,18 @@ public class PyParameterInfoTest extends LightMarkedTestCase { feignCtrlP(marks.get("").getTextOffset()).check("*, a: int", new String[]{"*, a: int"}); } + // PY-53693 + public void testInitializingDataclassKwOnlyAttribute() { + final Map marks = loadTest(6); + + feignCtrlP(marks.get("").getTextOffset()).check("a: int, *, b: int", new String[]{"a: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("a: int, b: int", new String[]{"a: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("a: int, qq: int, *, b: int", new String[]{"a: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("a: int, c: int, *, b: int, d: int", new String[]{"a: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("a: int, *, b: int, qq: int", new String[]{"a: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("a: str", new String[]{"a: str"}); + } + // PY-49946 public void testInitializingDataclassKwOnlyOnClassOverridingHierarchy() { final Map marks = loadTest(3); diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java index 5b4898b7d2e6..fbe17da2fae4 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java @@ -224,4 +224,91 @@ public class Py3ArgumentListInspectionTest extends PyInspectionTestCase { foo(title='Blade Runner', year=1982) """); } + + // PY-53693 + public void testInitializingDataclassWithKwOnlyAttribute() { + doTestByText(""" + from dataclasses import dataclass, KW_ONLY + + @dataclass + class MyClass: + a: int + qq: KW_ONLY + b: int + + MyClass(0, b=0) + MyClass(0, 0) + """); + } + + // PY-53693 + public void testInitializingDerivedDataclassWithKwOnlyAttribute() { + doTestByText(""" + from dataclasses import dataclass, KW_ONLY + + @dataclass + class Base: + a: int + qq: KW_ONLY + b: int + + @dataclass + class Derived(Base): + c: int + ww: KW_ONLY + d: int + + Derived(0, 0, b=0, d=0) + Derived(0, 0, 0, b=0) + Derived(0, 0, 0, d=0) + """); + } + + // PY-53693 + public void testInitializingDerivedDataclassWithOverridenAttribute() { + doTestByText(""" + from dataclasses import dataclass, KW_ONLY + + @dataclass + class Base: + a: int + qq: KW_ONLY + b: int + + @dataclass + class Derived(Base): + b: int + + Derived(0, 0) + """); + } + + // PY-53693 + public void testInitializingDerivedDataclassWithOverridenKwOnlyAttribute() { + doTestByText(""" + from dataclasses import dataclass, KW_ONLY + + @dataclass + class Base: + a: int + qq: KW_ONLY + b: int + + @dataclass + class Derived1(Base): + qq: int + + Derived1(0, 0, b=0) + Derived1(0, 0, 0) + + @dataclass + class Derived2(Base): + ww: KW_ONLY + qq: int + + Derived2(0, b=0, qq=0) + Derived2(0, 0, qq=0) + Derived2(0, 0, b=0) + """); + } }