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 15414a4f8a93..7747ede2029c 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 @@ -137,8 +137,8 @@ private fun parseDataclassParametersFromAST(cls: PyClass, context: TypeEvalConte private fun parseDataclassParametersFromStub(stub: PyDataclassStub?): PyDataclassParameters? { return stub?.let { PyDataclassParameters( - it.initValue(), it.reprValue(), it.eqValue(), it.orderValue(), it.unsafeHashValue(), it.frozenValue(), - null, null, null, null, null, null, + it.initValue(), it.reprValue(), it.eqValue(), it.orderValue(), it.unsafeHashValue(), it.frozenValue(), it.kwOnly(), + null, null, null, null, null, null, null, Type.valueOf(it.type), emptyMap() ) } @@ -164,12 +164,14 @@ data class PyDataclassParameters(val init: Boolean, val order: Boolean, val unsafeHash: Boolean, val frozen: Boolean, + val kwOnly: Boolean, val initArgument: PyExpression?, val reprArgument: PyExpression?, val eqArgument: PyExpression?, val orderArgument: PyExpression?, val unsafeHashArgument: PyExpression?, val frozenArgument: PyExpression?, + val kwOnlyArgument: PyExpression?, val type: Type, val others: Map) { @@ -189,6 +191,7 @@ private class PyDataclassParametersBuilder(private val type: Type, private const val DEFAULT_ORDER = false private const val DEFAULT_UNSAFE_HASH = false private const val DEFAULT_FROZEN = false + private const val DEFAULT_KW_ONLY = false } private var init = DEFAULT_INIT @@ -197,6 +200,7 @@ private class PyDataclassParametersBuilder(private val type: Type, private var order = if (type == Type.ATTRS) DEFAULT_EQ else DEFAULT_ORDER private var unsafeHash = DEFAULT_UNSAFE_HASH private var frozen = DEFAULT_FROZEN + private var kwOnly = DEFAULT_KW_ONLY private var initArgument: PyExpression? = null private var reprArgument: PyExpression? = null @@ -204,6 +208,7 @@ private class PyDataclassParametersBuilder(private val type: Type, private var orderArgument: PyExpression? = null private var unsafeHashArgument: PyExpression? = null private var frozenArgument: PyExpression? = null + private var kwOnlyArgument: PyExpression? = null private val others = mutableMapOf() @@ -285,6 +290,11 @@ private class PyDataclassParametersBuilder(private val type: Type, unsafeHashArgument = argument return } + "kw_only" -> { + kwOnly = PyEvaluator.evaluateAsBoolean(value, DEFAULT_KW_ONLY) + kwOnlyArgument = argument + return + } } } @@ -293,7 +303,9 @@ private class PyDataclassParametersBuilder(private val type: Type, } } - fun build() = PyDataclassParameters(init, repr, eq, order, unsafeHash, frozen, - initArgument, reprArgument, eqArgument, orderArgument, unsafeHashArgument, frozenArgument, - type, others) + fun build() = PyDataclassParameters( + init, repr, eq, order, unsafeHash, frozen, kwOnly, + initArgument, reprArgument, eqArgument, orderArgument, unsafeHashArgument, frozenArgument, kwOnlyArgument, + type, others + ) } 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 33163acdfb56..9553b73535bd 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 @@ -62,7 +62,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 79; + return 80; } @Nullable diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyDataclassStubs.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyDataclassStubs.kt index 24c119a05253..2a842e867b39 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyDataclassStubs.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyDataclassStubs.kt @@ -26,14 +26,14 @@ class PyDataclassStubImpl private constructor(private val type: String, private val eq: Boolean, private val order: Boolean, private val unsafeHash: Boolean, - private val frozen: Boolean) : PyDataclassStub { + private val frozen: Boolean, + private val kwOnly: Boolean) : PyDataclassStub { companion object { fun create(cls: PyClass): PyDataclassStub? { return parseDataclassParametersForStub(cls)?.let { - PyDataclassStubImpl(it.type.toString(), it.init, it.repr, it.eq, it.order, it.unsafeHash, - it.frozen) + PyDataclassStubImpl(it.type.toString(), it.init, it.repr, it.eq, it.order, it.unsafeHash, it.frozen, it.kwOnly) } } @@ -47,8 +47,9 @@ class PyDataclassStubImpl private constructor(private val type: String, val order = stream.readBoolean() val unsafeHash = stream.readBoolean() val frozen = stream.readBoolean() + val kwOnly = stream.readBoolean() - return PyDataclassStubImpl(type, init, repr, eq, order, unsafeHash, frozen) + return PyDataclassStubImpl(type, init, repr, eq, order, unsafeHash, frozen, kwOnly) } } @@ -62,13 +63,15 @@ class PyDataclassStubImpl private constructor(private val type: String, stream.writeBoolean(order) stream.writeBoolean(unsafeHash) stream.writeBoolean(frozen) + stream.writeBoolean(kwOnly) } - override fun getType() = type - override fun initValue() = init - override fun reprValue() = repr - override fun eqValue() = eq - override fun orderValue() = order - override fun unsafeHashValue() = unsafeHash - override fun frozenValue() = frozen + override fun getType(): String = type + override fun initValue(): Boolean = init + override fun reprValue(): Boolean = repr + override fun eqValue(): Boolean = eq + override fun orderValue(): Boolean = order + override fun unsafeHashValue(): Boolean = unsafeHash + override fun frozenValue(): Boolean = frozen + override fun kwOnly(): Boolean = kwOnly } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/stubs/PyDataclassStub.java b/python/python-psi-impl/src/com/jetbrains/python/psi/stubs/PyDataclassStub.java index d01aea44db49..68ba53e9e386 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/stubs/PyDataclassStub.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/stubs/PyDataclassStub.java @@ -47,4 +47,10 @@ public interface PyDataclassStub extends PyCustomClassStub { * its default value if it is not specified or could not be evaluated. */ boolean frozenValue(); + + /** + * @return value of `kw_only` (attrs) parameter or + * its default value if it is not specified or could not be evaluated. + */ + boolean kwOnly(); } diff --git a/python/src/com/jetbrains/python/codeInsight/stdlib/PyDataclassTypeProvider.kt b/python/src/com/jetbrains/python/codeInsight/stdlib/PyDataclassTypeProvider.kt index 393069f9c816..458dbd3b5329 100644 --- a/python/src/com/jetbrains/python/codeInsight/stdlib/PyDataclassTypeProvider.kt +++ b/python/src/com/jetbrains/python/codeInsight/stdlib/PyDataclassTypeProvider.kt @@ -110,10 +110,13 @@ class PyDataclassTypeProvider : PyTypeProviderBase() { val clsType = (context.getType(cls) as? PyClassLikeType) ?: return null val resolveContext = PyResolveContext.defaultContext().withTypeEvalContext(context) - val ellipsis = PyElementGenerator.getInstance(cls.project).createEllipsis() + val elementGenerator = PyElementGenerator.getInstance(cls.project) + val ellipsis = elementGenerator.createEllipsis() val collected = linkedMapOf() var seenInit = false + val keywordOnly = linkedSetOf() + var seenKeywordOnlyClass = false for (currentType in StreamEx.of(clsType).append(cls.getAncestorTypes(context))) { if (currentType == null || @@ -128,6 +131,7 @@ class PyDataclassTypeProvider : PyTypeProviderBase() { val parameters = parseDataclassParameters(current, context) ?: continue seenInit = seenInit || parameters.init + seenKeywordOnlyClass = seenKeywordOnlyClass || parameters.kwOnly if (seenInit) { current @@ -140,6 +144,10 @@ class PyDataclassTypeProvider : PyTypeProviderBase() { parameter.name?.let { // note: attributes are visited from inheritors to ancestors, in reversed order for every of them + if (seenKeywordOnlyClass && it !in collected) { + keywordOnly += it + } + if (parameters.type == PyDataclassParameters.Type.STD) { // std: attribute that overrides ancestor's attribute does not change the order but updates type collected[it] = collected.remove(it) ?: parameter @@ -153,7 +161,28 @@ class PyDataclassTypeProvider : PyTypeProviderBase() { } } - return if (seenInit) PyCallableTypeImpl(collected.values.reversed(), clsType.toInstance()) else null + return if (seenInit) PyCallableTypeImpl(buildParameters(elementGenerator, collected, keywordOnly), clsType.toInstance()) else null + } + + private fun buildParameters(elementGenerator: PyElementGenerator, + fields: Map, + keywordOnly: Set): List { + if (keywordOnly.isEmpty()) return fields.values.reversed() + + val positionalOrKeyword = mutableListOf() + val keyword = mutableListOf() + + for ((name, value) in fields.entries.reversed()) { + if (name !in keywordOnly) { + positionalOrKeyword += value + } + else { + keyword += value + } + } + + val singleStarParameter = elementGenerator.createSingleStarParameter() + return positionalOrKeyword + listOf(PyCallableParameterImpl.psi(singleStarParameter)) + keyword } private fun fieldToParameter(cls: PyClass, diff --git a/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt b/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt index 19900ae64f2e..c397e5e171e8 100644 --- a/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt @@ -109,7 +109,10 @@ class PyDataclassInspection : PyInspection() { PyNamedTupleInspection.inspectFieldsOrder( node, - { parseDataclassParameters(it, myTypeEvalContext) != null }, + { + val parameters = parseDataclassParameters(it, myTypeEvalContext) + parameters != null && !parameters.kwOnly + }, dataclassParameters.type == PyDataclassParameters.Type.STD, myTypeEvalContext, this::registerProblem, @@ -525,8 +528,7 @@ class PyDataclassInspection : PyInspection() { val default = call.getKeywordArgument("default") val factory = call.getKeywordArgument("factory") - if (default != null && factory != null && !resolvesToOmittedDefault(default, - PyDataclassParameters.Type.ATTRS)) { + if (default != null && factory != null && !resolvesToOmittedDefault(default, PyDataclassParameters.Type.ATTRS)) { registerProblem(call.argumentList, "Cannot specify both 'default' and 'factory'", ProblemHighlightType.GENERIC_ERROR) } } diff --git a/python/src/com/jetbrains/python/inspections/PyNamedTupleInspection.kt b/python/src/com/jetbrains/python/inspections/PyNamedTupleInspection.kt index 7cddc848860e..d7112341b4c7 100644 --- a/python/src/com/jetbrains/python/inspections/PyNamedTupleInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyNamedTupleInspection.kt @@ -19,18 +19,20 @@ class PyNamedTupleInspection : PyInspection() { companion object { fun inspectFieldsOrder(cls: PyClass, - ancestorsFilter: (PyClass) -> Boolean, + classFieldsFilter: (PyClass) -> Boolean, checkInheritedOrder: Boolean, context: TypeEvalContext, callback: (PsiElement, String, ProblemHighlightType) -> Unit, fieldsFilter: (PyTargetExpression) -> Boolean = { true }, hasAssignedValue: (PyTargetExpression) -> Boolean = PyTargetExpression::hasAssignedValue) { - val fieldsProcessor = processFields(cls, fieldsFilter, hasAssignedValue) + val fieldsProcessor = if (classFieldsFilter(cls)) processFields(cls, fieldsFilter, hasAssignedValue) else null + + if (fieldsProcessor?.lastFieldWithoutDefaultValue == null && !checkInheritedOrder) return val ancestors = cls.getAncestorClasses(context) val ancestorsFields = ancestors.map { when { - !ancestorsFilter(it) -> Ancestor.FILTERED + !classFieldsFilter(it) -> Ancestor.FILTERED processFields(it, fieldsFilter, hasAssignedValue).fieldsWithDefaultValue.isNotEmpty() -> Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE else -> Ancestor.HAS_NOT_FIELD_WITH_DEFAULT_VALUE } @@ -53,7 +55,7 @@ class PyNamedTupleInspection : PyInspection() { } } - val lastFieldWithoutDefaultValue = fieldsProcessor.lastFieldWithoutDefaultValue + val lastFieldWithoutDefaultValue = fieldsProcessor?.lastFieldWithoutDefaultValue if (lastFieldWithoutDefaultValue != null) { if (ancestorsFields.contains(Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE)) { cls.nameIdentifier?.let { name -> @@ -103,7 +105,7 @@ class PyNamedTupleInspection : PyInspection() { if (node != null && LanguageLevel.forElement(node).isAtLeast(LanguageLevel.PYTHON36) && PyNamedTupleTypeProvider.isTypingNamedTupleDirectInheritor(node, myTypeEvalContext)) { - inspectFieldsOrder(node, { false }, false, myTypeEvalContext, this::registerProblem) + inspectFieldsOrder(node, { it == node }, false, myTypeEvalContext, this::registerProblem) } } } diff --git a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultBase.py b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultBase.py new file mode 100644 index 000000000000..98ee0d485101 --- /dev/null +++ b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultBase.py @@ -0,0 +1,29 @@ +import attr + +@attr.dataclass(kw_only=True) +class Base: + a: int = 1 + + +# no kw_only, no default +@attr.dataclass +class Derived1(Base): + b: str + + +# kw_only, no default +@attr.dataclass(kw_only=True) +class Derived2(Base): + b: str + + +# no kw_only, default +@attr.dataclass +class Derived3(Base): + b: str = "b" + + +# kw_only, default +@attr.dataclass(kw_only=True) +class Derived4(Base): + 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 new file mode 100644 index 000000000000..20fca92c9a21 --- /dev/null +++ b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyDefaultDerived.py @@ -0,0 +1,40 @@ +import attr + +# no kw_only, no default +@attr.dataclass +class Base1: + a: int + +@attr.dataclass(kw_only=True) +class Derived1(Base1): + b: str = "b" + + +# kw_only, no default +@attr.dataclass(kw_only=True) +class Base2: + a: int + +@attr.dataclass(kw_only=True) +class Derived2(Base2): + b: str = "b" + + +# no kw_only, default +@attr.dataclass +class Base3: + a: int = 1 + +@attr.dataclass(kw_only=True) +class Derived3(Base3): + b: str = "b" + + +# kw_only, default +@attr.dataclass(kw_only=True) +class Base4: + a: int = 1 + +@attr.dataclass(kw_only=True) +class Derived4(Base4): + b: str = "b" \ No newline at end of file diff --git a/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultBase.py b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultBase.py new file mode 100644 index 000000000000..312633e5b038 --- /dev/null +++ b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultBase.py @@ -0,0 +1,29 @@ +import attr + +@attr.dataclass(kw_only=True) +class Base: + a: int + + +# no kw_only, no default +@attr.dataclass +class Derived1(Base): + b: str + + +# kw_only, no default +@attr.dataclass(kw_only=True) +class Derived2(Base): + b: str + + +# no kw_only, default +@attr.dataclass +class Derived3(Base): + b: str = "b" + + +# kw_only, default +@attr.dataclass(kw_only=True) +class Derived4(Base): + 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 new file mode 100644 index 000000000000..bf6b6953c407 --- /dev/null +++ b/python/testData/inspections/PyDataclassInspection/fieldsOrderInInheritanceKwOnlyNoDefaultDerived.py @@ -0,0 +1,40 @@ +import attr + +# no kw_only, no default +@attr.dataclass +class Base1: + a: int + +@attr.dataclass(kw_only=True) +class Derived1(Base1): + b: str + + +# kw_only, no default +@attr.dataclass(kw_only=True) +class Base2: + a: int + +@attr.dataclass(kw_only=True) +class Derived2(Base2): + b: str + + +# no kw_only, default +@attr.dataclass +class Base3: + a: int = 1 + +@attr.dataclass(kw_only=True) +class Derived3(Base3): + b: str + + +# kw_only, default +@attr.dataclass(kw_only=True) +class Base4: + a: int = 1 + +@attr.dataclass(kw_only=True) +class Derived4(Base4): + b: str \ No newline at end of file diff --git a/python/testData/paramInfo/InitializingAttrsKwOnlyOnBaseClass.py b/python/testData/paramInfo/InitializingAttrsKwOnlyOnBaseClass.py new file mode 100644 index 000000000000..d8adcc2dcbcb --- /dev/null +++ b/python/testData/paramInfo/InitializingAttrsKwOnlyOnBaseClass.py @@ -0,0 +1,45 @@ +import attr + + +@attr.s(kw_only=True) +class Base1: + a = attr.ib(type=int) + +@attr.s +class Derived1(Base1): + b = attr.ib(type=int) + +Derived1() + + +@attr.s(kw_only=True) +class Base2: + a = attr.ib(type=int) + +@attr.s +class Derived2(Base2): + b = attr.ib(type=int, default=1) + +Derived2() + + +@attr.s(kw_only=True) +class Base3: + a = attr.ib(type=int, default=1) + +@attr.s +class Derived3(Base3): + b = attr.ib(type=int) + +Derived3() + + +@attr.s(kw_only=True) +class Base4: + a = attr.ib(type=int, default=1) + +@attr.s +class Derived4(Base4): + b = attr.ib(type=int, default=1) + +Derived4() \ No newline at end of file diff --git a/python/testData/paramInfo/InitializingAttrsKwOnlyOnClass.py b/python/testData/paramInfo/InitializingAttrsKwOnlyOnClass.py new file mode 100644 index 000000000000..2cf88294060f --- /dev/null +++ b/python/testData/paramInfo/InitializingAttrsKwOnlyOnClass.py @@ -0,0 +1,53 @@ +import attr + + +@attr.s(kw_only=True) +class A1: + a = attr.ib(type=int) +A1() + + +@attr.s(kw_only=True) +class Base1: + a = attr.ib(type=int) + +@attr.s(kw_only=True) +class Derived1(Base1): + b = attr.ib(type=int) + +Derived1() + + +@attr.s(kw_only=True) +class Base2: + a = attr.ib(type=int) + +@attr.s(kw_only=True) +class Derived2(Base2): + b = attr.ib(type=int, default=1) + +Derived2() + + +@attr.s(kw_only=True) +class Base3: + a = attr.ib(type=int, default=1) + +@attr.s(kw_only=True) +class Derived3(Base3): + b = attr.ib(type=int) + +Derived3() + + +@attr.s(kw_only=True) +class Base4: + a = attr.ib(type=int, default=1) + +@attr.s(kw_only=True) +class Derived4(Base4): + b = attr.ib(type=int, default=1) + +Derived4() + + diff --git a/python/testData/paramInfo/InitializingAttrsKwOnlyOnClassOverridingHierarchy.py b/python/testData/paramInfo/InitializingAttrsKwOnlyOnClassOverridingHierarchy.py new file mode 100644 index 000000000000..6e0935b99e83 --- /dev/null +++ b/python/testData/paramInfo/InitializingAttrsKwOnlyOnClassOverridingHierarchy.py @@ -0,0 +1,34 @@ +import attr + + +@attr.s +class Base1: + a = attr.ib(type=int) + +@attr.s(kw_only=True) +class Derived1(Base1): + a = attr.ib(type=int) + +Derived1() + + +@attr.s(kw_only=True) +class Base2: + a = attr.ib(type=int) + +@attr.s +class Derived2(Base2): + a = attr.ib(type=int) + +Derived2() + + +@attr.s(kw_only=True) +class Base3: + a = attr.ib(type=int) + +@attr.s(kw_only=True) +class Derived3(Base3): + a = attr.ib(type=int) + +Derived3() \ No newline at end of file diff --git a/python/testData/paramInfo/InitializingAttrsKwOnlyOnDerivedClass.py b/python/testData/paramInfo/InitializingAttrsKwOnlyOnDerivedClass.py new file mode 100644 index 000000000000..032823afb900 --- /dev/null +++ b/python/testData/paramInfo/InitializingAttrsKwOnlyOnDerivedClass.py @@ -0,0 +1,45 @@ +import attr + + +@attr.s +class Base1: + a = attr.ib(type=int) + +@attr.s(kw_only=True) +class Derived1(Base1): + b = attr.ib(type=int) + +Derived1() + + +@attr.s +class Base2: + a = attr.ib(type=int) + +@attr.s(kw_only=True) +class Derived2(Base2): + b = attr.ib(type=int, default=1) + +Derived2() + + +@attr.s +class Base3: + a = attr.ib(type=int, default=1) + +@attr.s(kw_only=True) +class Derived3(Base3): + b = attr.ib(type=int) + +Derived3() + + +@attr.s +class Base4: + a = attr.ib(type=int, default=1) + +@attr.s(kw_only=True) +class Derived4(Base4): + b = attr.ib(type=int, default=1) + +Derived4() \ No newline at end of file diff --git a/python/testData/stubs/AttrsKwOnlyOnClass.py b/python/testData/stubs/AttrsKwOnlyOnClass.py new file mode 100644 index 000000000000..091c210f6d15 --- /dev/null +++ b/python/testData/stubs/AttrsKwOnlyOnClass.py @@ -0,0 +1,10 @@ +import attr +@attr.s(kw_only=True) +class Foo1: + bar = attr.ib(type=str) +@attr.s(kw_only=False) +class Foo2: + bar = attr.ib(type=str) +@attr.s +class Foo3: + bar = attr.ib(type=str) \ 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 7659abf7f7a5..1df32abe8a34 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -878,6 +878,46 @@ public class PyParameterInfoTest extends LightMarkedTestCase { ); } + // PY-34374 + public void testInitializingAttrsKwOnlyOnClass() { + final Map marks = loadTest(5); + + feignCtrlP(marks.get("").getTextOffset()).check("*, a: 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, 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=..., b: int=...", new String[]{"*, a: int"}); + } + + // PY-34374 + public void testInitializingAttrsKwOnlyOnBaseClass() { + final Map marks = loadTest(4); + + feignCtrlP(marks.get("").getTextOffset()).check("b: int, *, a: int", new String[]{"b: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("b: int=..., *, a: int", new String[]{"b: int=..., "}); + feignCtrlP(marks.get("").getTextOffset()).check("b: int, *, a: int=...", new String[]{"b: int, "}); + feignCtrlP(marks.get("").getTextOffset()).check("b: int=..., *, a: int=...", new String[]{"b: int=..., "}); + } + + // PY-34374 + public void testInitializingAttrsKwOnlyOnDerivedClass() { + final Map marks = loadTest(4); + + 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=..., b: int", new String[]{"*, a: int"}); + feignCtrlP(marks.get("").getTextOffset()).check("*, a: int=..., b: int=...", new String[]{"*, a: int"}); + } + + // PY-34374 + public void testInitializingAttrsKwOnlyOnClassOverridingHierarchy() { + final Map marks = loadTest(3); + + feignCtrlP(marks.get("").getTextOffset()).check("*, a: int", new String[]{"*, a: int"}); + feignCtrlP(marks.get("").getTextOffset()).check("a: int", new String[]{"a: int"}); + 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 a64c20ba97ec..080d01b6bb8a 100644 --- a/python/testSrc/com/jetbrains/python/PyStubsTest.java +++ b/python/testSrc/com/jetbrains/python/PyStubsTest.java @@ -966,6 +966,17 @@ public class PyStubsTest extends PyTestCase { ); } + // PY-34374 + public void testAttrsKwOnlyOnClass() { + final PyFile file = getTestFile(); + + assertTrue(file.findTopLevelClass("Foo1").getStub().getCustomStub(PyDataclassStub.class).kwOnly()); + assertFalse(file.findTopLevelClass("Foo2").getStub().getCustomStub(PyDataclassStub.class).kwOnly()); + assertFalse(file.findTopLevelClass("Foo3").getStub().getCustomStub(PyDataclassStub.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 ab327971e93a..84f7f017c7f9 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyDataclassInspectionTest.java @@ -66,6 +66,26 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase { doTest(); } + // PY-34374 + public void testFieldsOrderInInheritanceKwOnlyNoDefaultBase() { + doTest(); + } + + // PY-34374 + public void testFieldsOrderInInheritanceKwOnlyDefaultBase() { + doTest(); + } + + // PY-34374 + public void testFieldsOrderInInheritanceKwOnlyNoDefaultDerived() { + doTest(); + } + + // PY-34374 + public void testFieldsOrderInInheritanceKwOnlyDefaultDerived() { + doTest(); + } + // PY-27398 public void testComparisonForOrdered() { doTest(); @@ -279,7 +299,7 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase { @Override protected void doTest() { runWithLanguageLevel( - LanguageLevel.PYTHON37, + LanguageLevel.getLatest(), () -> { myFixture.copyFileToProject(getTestCaseDirectory() + "/dataclasses.py", "dataclasses.py"); super.doTest();