Fix processing attributes that override ancestors' ones (PY-28506, PY-31762)

This commit is contained in:
Semyon Proshev
2018-12-28 12:41:07 +03:00
parent 92f9b2474b
commit a349fad10c
6 changed files with 86 additions and 18 deletions
@@ -15,7 +15,6 @@ import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.stubs.PyDataclassFieldStub
import com.jetbrains.python.psi.types.*
import one.util.streamex.StreamEx
import java.util.*
class PyDataclassTypeProvider : PyTypeProviderBase() {
@@ -112,8 +111,7 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
val resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context)
val ellipsis = PyElementGenerator.getInstance(cls.project).createEllipsis()
val collected = LinkedList<PyCallableParameter>()
val seen = mutableSetOf<String?>()
val collected = linkedMapOf<String, PyCallableParameter>()
var seenInit = false
for (currentType in StreamEx.of(clsType).append(cls.getAncestorTypes(context))) {
@@ -132,16 +130,27 @@ class PyDataclassTypeProvider : PyTypeProviderBase() {
current
.classAttributes
.asReversed()
.filterNot { it.name in seen || PyTypingTypeProvider.isClassVar(it, context) }
.asSequence()
.filterNot { PyTypingTypeProvider.isClassVar(it, context) }
.mapNotNull { fieldToParameter(cls, it, parameters.type, ellipsis, context) }
.forEach {
seen += it.name
collected.addFirst(it)
.forEach { parameter ->
parameter.name?.let {
// note: attributes are visited from inheritors to ancestors, in reversed order for every of them
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
}
else if (!collected.containsKey(it)) {
// attrs: attribute that overrides ancestor's attribute changes the order
collected[it] = parameter
}
}
}
}
}
return if (seenInit) PyCallableTypeImpl(collected, clsType.toInstance()) else null
return if (seenInit) PyCallableTypeImpl(collected.values.reversed(), clsType.toInstance()) else null
}
private fun fieldToParameter(cls: PyClass,
@@ -41,4 +41,17 @@ class A4:
class B4(A4):
b: str
evolve(B4(), <arg4>)
evolve(B4(), <arg4>)
@dataclass
class A5:
x: Any = 15.0
y: int = 0
@dataclass
class B5(A5):
z: int = 10
x: int = 15
evolve(B5(), <arg5>)
@@ -41,4 +41,17 @@ class A4:
class B4(A4):
b: str
replace(B4(), <arg4>)
replace(B4(), <arg4>)
@dataclass
class A5:
x: Any = 15.0
y: int = 0
@dataclass
class B5(A5):
z: int = 10
x: int = 15
replace(B5(), <arg5>)
@@ -41,4 +41,17 @@ class A4:
class B4(A4):
b: str
B4(<arg4>)
B4(<arg4>)
@dataclass
class A5:
x: Any = 15.0
y: int = 0
@dataclass
class B5(A5):
z: int = 10
x: int = 15
B5(<arg5>)
@@ -41,4 +41,17 @@ class A4:
class B4(A4):
b: str
B4(<arg4>)
B4(<arg4>)
@dataclass
class A5:
x: Any = 15.0
y: int = 0
@dataclass
class B5(A5):
z: int = 10
x: int = 15
B5(<arg5>)
@@ -766,7 +766,7 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
runWithLanguageLevel(
LanguageLevel.PYTHON37,
() -> {
final Map<String, PsiElement> marks = loadMultiFileTest(4);
final Map<String, PsiElement> marks = loadMultiFileTest(5);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("a: int, b: str", new String[]{"a: int, "});
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("a: int, b: str", new String[]{"a: int, "});
@@ -775,6 +775,8 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
feignCtrlP(marks.get("<arg4>").getTextOffset()).check(Arrays.asList("self: object", "cls: object"),
Arrays.asList(ArrayUtil.EMPTY_STRING_ARRAY, ArrayUtil.EMPTY_STRING_ARRAY),
Arrays.asList(new String[]{"self: object"}, new String[]{"cls: object"}));
feignCtrlP(marks.get("<arg5>").getTextOffset()).check("x: int=15, y: int=0, z: int=10", new String[]{"x: int=15, "});
}
);
}
@@ -842,12 +844,12 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
// PY-31762
public void testInitializingAttrsHierarchy() {
// same as for std dataclasses
// same as for std dataclasses except overridding
runWithLanguageLevel(
LanguageLevel.PYTHON37,
() -> {
final Map<String, PsiElement> marks = loadTest(4);
final Map<String, PsiElement> marks = loadTest(5);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("a: int, b: str", new String[]{"a: int, "});
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("a: int, b: str", new String[]{"a: int, "});
@@ -856,6 +858,8 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
feignCtrlP(marks.get("<arg4>").getTextOffset()).check(Arrays.asList("self: object", "cls: object"),
Arrays.asList(ArrayUtil.EMPTY_STRING_ARRAY, ArrayUtil.EMPTY_STRING_ARRAY),
Arrays.asList(new String[]{"self: object"}, new String[]{"cls: object"}));
feignCtrlP(marks.get("<arg5>").getTextOffset()).check("y: int=0, z: int=10, x: int=15", new String[]{"y: int=0, "});
}
);
}
@@ -900,12 +904,13 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
runWithLanguageLevel(
LanguageLevel.PYTHON37,
() -> {
final Map<String, PsiElement> marks = loadMultiFileTest(4);
final Map<String, PsiElement> marks = loadMultiFileTest(5);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("obj: B1, *, a: int=..., b: str=...", ArrayUtil.EMPTY_STRING_ARRAY);
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("obj: B2, *, a: int=..., b: str=...", ArrayUtil.EMPTY_STRING_ARRAY);
feignCtrlP(marks.get("<arg3>").getTextOffset()).check("obj: B3, *, a: int=...", ArrayUtil.EMPTY_STRING_ARRAY);
feignCtrlP(marks.get("<arg4>").getTextOffset()).check("obj, **changes", new String[]{"**changes"});
feignCtrlP(marks.get("<arg5>").getTextOffset()).check("obj: B5, *, x: int=15, y: int=0, z: int=10", ArrayUtil.EMPTY_STRING_ARRAY);
}
);
}
@@ -946,17 +951,19 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
// PY-31762
public void testAttrsHierarchyReplace() {
// same as for std dataclasses
// same as for std dataclasses except overridding
runWithLanguageLevel(
LanguageLevel.PYTHON37,
() -> {
final Map<String, PsiElement> marks = loadTest(4);
final Map<String, PsiElement> marks = loadTest(5);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("inst: B1, *, a: int=..., b: str=...", ArrayUtil.EMPTY_STRING_ARRAY);
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("inst: B2, *, a: int=..., b: str=...", ArrayUtil.EMPTY_STRING_ARRAY);
feignCtrlP(marks.get("<arg3>").getTextOffset()).check("inst: B3, *, a: int=...", ArrayUtil.EMPTY_STRING_ARRAY);
feignCtrlP(marks.get("<arg4>").getTextOffset()).check("inst: _T, **changes", new String[]{"**changes"});
feignCtrlP(marks.get("<arg5>").getTextOffset()).check("inst: B5, *, y: int=0, z: int=10, x: int=15", ArrayUtil.EMPTY_STRING_ARRAY);
}
);
}