From 9ef2a6f638b11f47bc935468e759c33abe1b5f9b Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Thu, 14 Nov 2019 19:49:32 +0300 Subject: [PATCH] Update parsing of `attr.s` for all the changes introduced in 19.2.0 and before (PY-36113, PY-38400) GitOrigin-RevId: 11db701c16986bc8704f139f60a85f5b9c32a509 --- .../python/codeInsight/PyDataclasses.kt | 75 ++++++++++--------- .../inspections/PyDataclassInspection.kt | 4 +- .../PyDataclassInspection/attrsUselessHash.py | 2 +- .../attrsUselessInitReprStrEq.py | 21 ++++-- .../attrsUselessOrder.py | 40 ++++++++-- .../paramInfo/InitializingAttrsUsingPep526.py | 9 ++- .../jetbrains/python/PyParameterInfoTest.java | 3 +- 7 files changed, 104 insertions(+), 50 deletions(-) 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 26e9ea4a2896..15414a4f8a93 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 @@ -26,7 +26,8 @@ const val DUNDER_POST_INIT: String = "__post_init__" const val DUNDER_ATTRS_POST_INIT: String = "__attrs_post_init__" private val STD_PARAMETERS = listOf("init", "repr", "eq", "order", "unsafe_hash", "frozen") -private val ATTRS_PARAMETERS = listOf("these", "repr_ns", "repr", "cmp", "hash", "init", "slots", "frozen", "str", "auto_attribs") +private val ATTRS_PARAMETERS = listOf("these", "repr_ns", "repr", "cmp", "hash", "init", "slots", "frozen", "weakref_slot", "str", + "auto_attribs", "kw_only", "cache_hash", "auto_exc", "eq", "order") /** * It should be used only to map arguments to parameters and @@ -65,16 +66,15 @@ fun parseDataclassParameters(cls: PyClass, context: TypeEvalContext): PyDataclas * @see parseStdDataclassParameters * @see parseDataclassParameters */ -fun parseDataclassParametersForStub(cls: PyClass): PyDataclassParameters? = parseDataclassParametersFromAST( - cls, null) +fun parseDataclassParametersForStub(cls: PyClass): PyDataclassParameters? = parseDataclassParametersFromAST(cls, null) -fun resolvesToOmittedDefault(expression: PyExpression, type: PyDataclassParameters.Type): Boolean { +fun resolvesToOmittedDefault(expression: PyExpression, type: Type): Boolean { if (expression is PyReferenceExpression) { val qNames = PyResolveUtil.resolveImportedElementQNameLocally(expression) return when (type) { - PyDataclassParameters.Type.STD -> QualifiedName.fromComponents("dataclasses", "MISSING") in qNames - PyDataclassParameters.Type.ATTRS -> QualifiedName.fromComponents("attr", "NOTHING") in qNames + Type.STD -> QualifiedName.fromComponents("dataclasses", "MISSING") in qNames + Type.ATTRS -> QualifiedName.fromComponents("attr", "NOTHING") in qNames } } @@ -85,7 +85,7 @@ fun resolvesToOmittedDefault(expression: PyExpression, type: PyDataclassParamete * It should be used only to map arguments to parameters and * determine what settings dataclass has. */ -private fun decoratorAndTypeAndMarkedCallee(project: Project): List>> { +private fun decoratorAndTypeAndMarkedCallee(project: Project): List>> { val generator = PyElementGenerator.getInstance(project) val ellipsis = generator.createEllipsis() @@ -118,19 +118,16 @@ private fun parseDataclassParametersFromAST(cls: PyClass, context: TypeEvalConte context ?: TypeEvalContext.codeInsightFallback(cls.project) ) - if (mapping.unmappedArguments.isEmpty() && mapping.unmappedParameters.isEmpty()) { - val builder = PyDataclassParametersBuilder(decoratorAndTypeAndMarkedCallee.second, - decoratorAndTypeAndMarkedCallee.first, cls) + val builder = PyDataclassParametersBuilder(decoratorAndTypeAndMarkedCallee.second, decoratorAndTypeAndMarkedCallee.first, cls) - mapping - .mappedParameters - .entries - .forEach { - builder.update(it.value.name, it.key) - } + mapping + .mappedParameters + .entries + .forEach { + builder.update(it.value.name, it.key) + } - return builder.build() - } + return builder.build() } } @@ -181,7 +178,7 @@ data class PyDataclassParameters(val init: Boolean, } } -private class PyDataclassParametersBuilder(private val type: PyDataclassParameters.Type, +private class PyDataclassParametersBuilder(private val type: Type, decorator: KnownDecorator, anchor: PsiElement) { @@ -197,7 +194,7 @@ private class PyDataclassParametersBuilder(private val type: PyDataclassParamete private var init = DEFAULT_INIT private var repr = DEFAULT_REPR private var eq = DEFAULT_EQ - private var order = if (type == PyDataclassParameters.Type.ATTRS) DEFAULT_EQ else DEFAULT_ORDER + private var order = if (type == Type.ATTRS) DEFAULT_EQ else DEFAULT_ORDER private var unsafeHash = DEFAULT_UNSAFE_HASH private var frozen = DEFAULT_FROZEN @@ -211,7 +208,7 @@ private class PyDataclassParametersBuilder(private val type: PyDataclassParamete private val others = mutableMapOf() init { - if (type == PyDataclassParameters.Type.ATTRS && decorator == KnownDecorator.ATTR_DATACLASS) { + if (type == Type.ATTRS && decorator == KnownDecorator.ATTR_DATACLASS) { PyElementGenerator.getInstance(anchor.project) .createExpressionFromText(LanguageLevel.forElement(anchor), PyNames.TRUE) .also { others["auto_attribs"] = it } @@ -233,14 +230,13 @@ private class PyDataclassParametersBuilder(private val type: PyDataclassParamete return } "frozen" -> { - frozen = PyEvaluator.evaluateAsBoolean(value, - DEFAULT_FROZEN) + frozen = PyEvaluator.evaluateAsBoolean(value, DEFAULT_FROZEN) frozenArgument = argument return } } - if (type == PyDataclassParameters.Type.STD) { + if (type == Type.STD) { when (name) { "eq" -> { eq = PyEvaluator.evaluateAsBoolean(value, DEFAULT_EQ) @@ -248,21 +244,34 @@ private class PyDataclassParametersBuilder(private val type: PyDataclassParamete return } "order" -> { - order = PyEvaluator.evaluateAsBoolean(value, - DEFAULT_ORDER) + order = PyEvaluator.evaluateAsBoolean(value, DEFAULT_ORDER) orderArgument = argument return } "unsafe_hash" -> { - unsafeHash = PyEvaluator.evaluateAsBoolean(value, - DEFAULT_UNSAFE_HASH) + unsafeHash = PyEvaluator.evaluateAsBoolean(value, DEFAULT_UNSAFE_HASH) unsafeHashArgument = argument return } } } - else if (type == PyDataclassParameters.Type.ATTRS) { + else if (type == Type.ATTRS) { when (name) { + "eq" -> { + eq = PyEvaluator.evaluateAsBoolean(value, DEFAULT_EQ) + eqArgument = argument + + if (orderArgument == null) { + order = eq + orderArgument = eqArgument + } + } + "order" -> { + if (argument !is PyNoneLiteralExpression) { + order = PyEvaluator.evaluateAsBoolean(value, DEFAULT_EQ) + orderArgument = argument + } + } "cmp" -> { eq = PyEvaluator.evaluateAsBoolean(value, DEFAULT_EQ) eqArgument = argument @@ -272,8 +281,7 @@ private class PyDataclassParametersBuilder(private val type: PyDataclassParamete return } "hash" -> { - unsafeHash = PyEvaluator.evaluateAsBoolean(value, - DEFAULT_UNSAFE_HASH) + unsafeHash = PyEvaluator.evaluateAsBoolean(value, DEFAULT_UNSAFE_HASH) unsafeHashArgument = argument return } @@ -286,7 +294,6 @@ private class PyDataclassParametersBuilder(private val type: PyDataclassParamete } fun build() = PyDataclassParameters(init, repr, eq, order, unsafeHash, frozen, - initArgument, reprArgument, eqArgument, orderArgument, - unsafeHashArgument, frozenArgument, - type, others) + initArgument, reprArgument, eqArgument, orderArgument, unsafeHashArgument, frozenArgument, + type, others) } diff --git a/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt b/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt index 50c2e9d9ce9e..19900ae64f2e 100644 --- a/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyDataclassInspection.kt @@ -386,7 +386,7 @@ class PyDataclassInspection : PyInspection() { } if (dataclassParameters.order && cmpMethods.isNotEmpty()) { - cmpMethods.forEach { problems.add(it to "cmp") } + cmpMethods.forEach { problems.add(it to "cmp/order") } } if (dataclassParameters.frozen && mutatingMethods.isNotEmpty()) { @@ -407,7 +407,7 @@ class PyDataclassInspection : PyInspection() { if (dataclassParameters.order && dataclassParameters.frozen && hashMethod != null) { registerProblem(hashMethod?.nameIdentifier, - "'${PyNames.HASH}' is ignored if the class already defines 'cmp' and 'frozen' parameters", + "'${PyNames.HASH}' is ignored if the class already defines 'cmp/order' and 'frozen' parameters", ProblemHighlightType.GENERIC_ERROR_OR_WARNING) } } diff --git a/python/testData/inspections/PyDataclassInspection/attrsUselessHash.py b/python/testData/inspections/PyDataclassInspection/attrsUselessHash.py index b5c2776856e6..1428f8b6db2c 100644 --- a/python/testData/inspections/PyDataclassInspection/attrsUselessHash.py +++ b/python/testData/inspections/PyDataclassInspection/attrsUselessHash.py @@ -46,7 +46,7 @@ print(hash(Derived1())) @attr.s(frozen=True) class A3: - def __hash__(self): + def __hash__(self): pass print(hash(A3())) diff --git a/python/testData/inspections/PyDataclassInspection/attrsUselessInitReprStrEq.py b/python/testData/inspections/PyDataclassInspection/attrsUselessInitReprStrEq.py index 74e6a002b681..f405d1a3a1ff 100644 --- a/python/testData/inspections/PyDataclassInspection/attrsUselessInitReprStrEq.py +++ b/python/testData/inspections/PyDataclassInspection/attrsUselessInitReprStrEq.py @@ -2,7 +2,7 @@ import attr @attr.dataclass(repr=True, cmp=True, str=True) -class A: +class A1: a: int = 1 def __repr__(self): @@ -11,7 +11,15 @@ class A: def __str__(self): return "str1" - def __eq__(self, other): + def __eq__(self, other): + return "eq1" + + +@attr.dataclass(eq=True) +class A2: + a: int = 1 + + def __eq__(self, other): return "eq1" @@ -51,12 +59,13 @@ class B2: class C1: c: int = 1 - def __eq__(self, other): + def __eq__(self, other): return "eq1" -print(repr(A())) -print(str(A())) -print(A() == A()) +print(repr(A1())) +print(str(A1())) +print(A1() == A1()) +print(A2() == A2()) print(repr(Derived())) print(str(Derived())) diff --git a/python/testData/inspections/PyDataclassInspection/attrsUselessOrder.py b/python/testData/inspections/PyDataclassInspection/attrsUselessOrder.py index b507e003ed2e..7b9d89e026d9 100644 --- a/python/testData/inspections/PyDataclassInspection/attrsUselessOrder.py +++ b/python/testData/inspections/PyDataclassInspection/attrsUselessOrder.py @@ -5,7 +5,7 @@ import attr class A1: a: int = 1 - def __le__(self, other): + def __le__(self, other): return "le1" print(A1(1) <= A1(2)) @@ -27,7 +27,7 @@ print(Derived1(1) <= Derived1(2)) class A2: a: int = 1 - def __lt__(self, other): + def __lt__(self, other): return "lt1" print(A2(1) < A2(2)) @@ -49,7 +49,7 @@ print(Derived2(1) < Derived2(2)) class A3: a: int = 1 - def __gt__(self, other): + def __gt__(self, other): return "gt1" print(A3(1) > A3(2)) @@ -71,7 +71,7 @@ print(Derived3(1) > Derived3(2)) class A4: a: int = 1 - def __ge__(self, other): + def __ge__(self, other): return "ge1" print(A4(1) >= A4(2)) @@ -86,4 +86,34 @@ class Base4: class Derived4(Base4): d: int = 1 -print(Derived4(1) >= Derived4(2)) \ No newline at end of file +print(Derived4(1) >= Derived4(2)) + + +@attr.dataclass(eq=True) +class A5: + a: int = 1 + + def __le__(self, other): + return "le5" + +print(A5(1) <= A5(2)) + + +@attr.dataclass(eq=True, order=None) +class A6: + a: int = 1 + + def __le__(self, other): + return "le6" + +print(A6(1) <= A6(2)) + + +@attr.dataclass(eq=True, order=False) +class A7: + a: int = 1 + + def __le__(self, other): + return "le7" + +print(A7(1) <= A7(2)) \ No newline at end of file diff --git a/python/testData/paramInfo/InitializingAttrsUsingPep526.py b/python/testData/paramInfo/InitializingAttrsUsingPep526.py index c82f6105e103..ed0338e555a7 100644 --- a/python/testData/paramInfo/InitializingAttrsUsingPep526.py +++ b/python/testData/paramInfo/InitializingAttrsUsingPep526.py @@ -70,4 +70,11 @@ class F1: foo = "bar" # <- has no type annotation, so doesn't count. baz: str -F1() \ No newline at end of file +F1() + + +@attr.dataclass(unknown=True) +class G1: + bar: str + +G1() \ 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 970357fc0c80..7659abf7f7a5 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -795,7 +795,7 @@ public class PyParameterInfoTest extends LightMarkedTestCase { runWithLanguageLevel( LanguageLevel.PYTHON36, () -> { - final Map marks = loadTest(8); + final Map marks = loadTest(9); feignCtrlP(marks.get("").getTextOffset()).check("x: int, y: str, z: float=0.0", new String[]{"x: int, "}); feignCtrlP(marks.get("").getTextOffset()).check("x: int, y: str, z: float=0.0", new String[]{"x: int, "}); @@ -811,6 +811,7 @@ public class PyParameterInfoTest extends LightMarkedTestCase { feignCtrlP(marks.get("").getTextOffset()).check("x: int, y: str=\"0\"", new String[]{"x: int, "}); feignCtrlP(marks.get("").getTextOffset()).check("x: int", new String[]{"x: int"}); feignCtrlP(marks.get("").getTextOffset()).check("baz: str", new String[]{"baz: str"}); + feignCtrlP(marks.get("").getTextOffset()).check("bar: str", new String[]{"bar: str"}); } ); }