Update parsing of attr.s for all the changes introduced in 19.2.0 and before (PY-36113, PY-38400)

GitOrigin-RevId: 11db701c16986bc8704f139f60a85f5b9c32a509
This commit is contained in:
Semyon Proshev
2019-12-04 12:33:00 +00:00
committed by intellij-monorepo-bot
parent ef89b40dd5
commit 9ef2a6f638
7 changed files with 104 additions and 50 deletions
@@ -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<Triple<PyKnownDecoratorUtil.KnownDecorator, PyDataclassParameters.Type, List<PyCallableParameter>>> {
private fun decoratorAndTypeAndMarkedCallee(project: Project): List<Triple<KnownDecorator, Type, List<PyCallableParameter>>> {
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<String, PyExpression>()
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)
}
@@ -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)
}
}
@@ -46,7 +46,7 @@ print(hash(Derived1()))
@attr.s(frozen=True)
class A3:
def <warning descr="'__hash__' is ignored if the class already defines 'cmp' and 'frozen' parameters">__hash__</warning>(self):
def <warning descr="'__hash__' is ignored if the class already defines 'cmp/order' and 'frozen' parameters">__hash__</warning>(self):
pass
print(hash(A3()))
@@ -2,7 +2,7 @@ import attr
@attr.dataclass(repr=True, cmp=True, str=True)
class A:
class A1:
a: int = 1
def <warning descr="'__repr__' is ignored if the class already defines 'repr' parameter">__repr__</warning>(self):
@@ -11,7 +11,15 @@ class A:
def <warning descr="'__str__' is ignored if the class already defines 'str' parameter">__str__</warning>(self):
return "str1"
def <warning descr="'__eq__' is ignored if the class already defines 'cmp' parameter">__eq__</warning>(self, other):
def <warning descr="'__eq__' is ignored if the class already defines 'cmp/order' parameter">__eq__</warning>(self, other):
return "eq1"
@attr.dataclass(eq=True)
class A2:
a: int = 1
def <warning descr="'__eq__' is ignored if the class already defines 'cmp/order' parameter">__eq__</warning>(self, other):
return "eq1"
@@ -51,12 +59,13 @@ class B2:
class C1:
c: int = 1
def <warning descr="'__eq__' is ignored if the class already defines 'cmp' parameter">__eq__</warning>(self, other):
def <warning descr="'__eq__' is ignored if the class already defines 'cmp/order' parameter">__eq__</warning>(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()))
@@ -5,7 +5,7 @@ import attr
class A1:
a: int = 1
def <warning descr="'__le__' is ignored if the class already defines 'cmp' parameter">__le__</warning>(self, other):
def <warning descr="'__le__' is ignored if the class already defines 'cmp/order' parameter">__le__</warning>(self, other):
return "le1"
print(A1(1) <= A1(2))
@@ -27,7 +27,7 @@ print(Derived1(1) <= Derived1(2))
class A2:
a: int = 1
def <warning descr="'__lt__' is ignored if the class already defines 'cmp' parameter">__lt__</warning>(self, other):
def <warning descr="'__lt__' is ignored if the class already defines 'cmp/order' parameter">__lt__</warning>(self, other):
return "lt1"
print(A2(1) < A2(2))
@@ -49,7 +49,7 @@ print(Derived2(1) < Derived2(2))
class A3:
a: int = 1
def <warning descr="'__gt__' is ignored if the class already defines 'cmp' parameter">__gt__</warning>(self, other):
def <warning descr="'__gt__' is ignored if the class already defines 'cmp/order' parameter">__gt__</warning>(self, other):
return "gt1"
print(A3(1) > A3(2))
@@ -71,7 +71,7 @@ print(Derived3(1) > Derived3(2))
class A4:
a: int = 1
def <warning descr="'__ge__' is ignored if the class already defines 'cmp' parameter">__ge__</warning>(self, other):
def <warning descr="'__ge__' is ignored if the class already defines 'cmp/order' parameter">__ge__</warning>(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))
print(Derived4(1) >= Derived4(2))
@attr.dataclass(eq=True)
class A5:
a: int = 1
def <warning descr="'__le__' is ignored if the class already defines 'cmp/order' parameter">__le__</warning>(self, other):
return "le5"
print(A5(1) <= A5(2))
@attr.dataclass(eq=True, order=None)
class A6:
a: int = 1
def <warning descr="'__le__' is ignored if the class already defines 'cmp/order' parameter">__le__</warning>(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))
@@ -70,4 +70,11 @@ class F1:
foo = "bar" # <- has no type annotation, so doesn't count.
baz: str
F1(<arg8>)
F1(<arg8>)
@attr.dataclass(unknown=True)
class G1:
bar: str
G1(<arg9>)
@@ -795,7 +795,7 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
runWithLanguageLevel(
LanguageLevel.PYTHON36,
() -> {
final Map<String, PsiElement> marks = loadTest(8);
final Map<String, PsiElement> marks = loadTest(9);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("x: int, y: str, z: float=0.0", new String[]{"x: int, "});
feignCtrlP(marks.get("<arg2>").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("<arg6>").getTextOffset()).check("x: int, y: str=\"0\"", new String[]{"x: int, "});
feignCtrlP(marks.get("<arg7>").getTextOffset()).check("x: int", new String[]{"x: int"});
feignCtrlP(marks.get("<arg8>").getTextOffset()).check("baz: str", new String[]{"baz: str"});
feignCtrlP(marks.get("<arg9>").getTextOffset()).check("bar: str", new String[]{"bar: str"});
}
);
}