PY-24729 Initial support of instance attributes annotated on class level

This commit is contained in:
Mikhail Golubev
2017-12-14 19:00:46 +03:00
parent 8711c99028
commit ae58f4e52a
6 changed files with 135 additions and 18 deletions
@@ -19,6 +19,7 @@ import com.intellij.util.containers.HashMap;
import com.intellij.util.containers.HashSet;
import com.jetbrains.python.PyCustomType;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyFunctionTypeAnnotation;
import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyFunctionTypeAnnotationFile;
import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyParameterTypeList;
@@ -31,6 +32,7 @@ import com.jetbrains.python.psi.impl.stubs.PyTypingAliasStubType;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.PyResolveImportUtil;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.RatedResolveResult;
import com.jetbrains.python.psi.stubs.PyClassStub;
import com.jetbrains.python.psi.types.*;
import one.util.streamex.StreamEx;
@@ -325,20 +327,71 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
if (PROTOCOL.equals(target.getQualifiedName())) {
return createTypingProtocolType();
}
final PyExpression annotation = getAnnotationValue(target, context);
if (annotation != null) {
return Ref.deref(getType(annotation, new Context(context)));
final Ref<PyType> annotatedType = getTypeFromTargetExpressionAnnotation(target, context);
if (annotatedType != null) {
return annotatedType.get();
}
final String comment = target.getTypeCommentAnnotation();
if (comment != null) {
final PyType type = Ref.deref(getVariableTypeCommentType(comment, referenceTarget, new Context(context)));
final PyClass pyClass = target.getContainingClass();
// an assignment inside a method
if (pyClass != null && target.isQualified() && ScopeUtil.getScopeOwner(target) instanceof PyFunction) {
final String name = target.getReferencedName();
final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context);
final StreamEx<PyType> typeStream;
if (context.maySwitchToAST(target)) {
final PyType qualifierType = context.getType(target.getQualifier());
if (qualifierType instanceof PyUnionType) {
typeStream = StreamEx.of(((PyUnionType)qualifierType).getMembers());
}
else {
typeStream = StreamEx.of(qualifierType);
}
}
else if (PyUtil.isInstanceAttribute(target)) {
typeStream = StreamEx.of(new PyClassTypeImpl(pyClass, false));
}
else {
return null;
}
return typeStream
.select(PyClassLikeType.class)
.map(PyClassLikeType::toClass) // force search on the class level right away
.flatMap(x -> {
final List<? extends RatedResolveResult> resolved = x.resolveMember(name, target, AccessDirection.READ, resolveContext, true);
return resolved == null ? StreamEx.empty() : StreamEx.of(resolved);
})
.map(RatedResolveResult::getElement)
.select(PyTargetExpression.class)
.filter(x -> ScopeUtil.getScopeOwner(x) instanceof PyClass)
.map(x -> getTypeFromTargetExpressionAnnotation(x, context))
.nonNull()
.map(Ref::get)
.foldLeft(PyUnionType::union)
.orElse(null);
}
}
return null;
}
@Nullable
private static Ref<PyType> getTypeFromTargetExpressionAnnotation(@NotNull PyTargetExpression target, @NotNull TypeEvalContext context) {
final PyExpression annotation = getAnnotationValue(target, context);
if (annotation != null) {
return getType(annotation, new Context(context));
}
final String comment = target.getTypeCommentAnnotation();
if (comment != null) {
final Ref<PyType> fromTypeComment = getVariableTypeCommentType(comment, target, new Context(context));
if (fromTypeComment != null) {
final PyType type = Ref.deref(fromTypeComment);
if (type instanceof PyTupleType) {
final PyTupleExpression tupleExpr = PsiTreeUtil.getParentOfType(target, PyTupleExpression.class);
if (tupleExpr != null) {
return PyTypeChecker.getTargetTypeFromTupleAssignment(target, tupleExpr, (PyTupleType)type);
return Ref.create(PyTypeChecker.getTargetTypeFromTupleAssignment(target, tupleExpr, (PyTupleType)type));
}
}
return type;
return fromTypeComment;
}
}
return null;
@@ -0,0 +1,5 @@
class C:
def __init__(self):
self.attr = None
C().at<caret>tr
@@ -0,0 +1,2 @@
class C:
attr: int
@@ -0,0 +1,5 @@
class C:
attr: int
def __init__(self):
self.attr = 'foo'
@@ -312,7 +312,7 @@ public class PyTypingTest extends PyTestCase {
// PY-19220
public void testMultiLineAssignmentComment() {
doTest("List[str]",
doTest("List[str]",
"from typing import List\n" +
"\n" +
"expr = [\n" +
@@ -486,7 +486,7 @@ public class PyTypingTest extends PyTestCase {
// PY-18726
public void testFunctionTypeCommentCallableParameter() {
doTest("(bool, str) -> int",
doTest("(bool, str) -> int",
"from typing import Callable\n" +
"\n" +
"def f(cb):\n" +
@@ -536,12 +536,12 @@ public class PyTypingTest extends PyTestCase {
// PY-18598
public void testFunctionTypeCommentEllipsisParameters() {
doTest("(x: Any, y: Any, z: Any) -> int",
doTest("(x: Any, y: Any, z: Any) -> int",
"def f(x, y=42, z='foo'):\n" +
" # type: (...) -> int \n" +
" pass\n" +
"\n" +
"expr = f");
"expr = f");
}
// PY-20421
@@ -558,7 +558,7 @@ public class PyTypingTest extends PyTestCase {
// PY-18762
public void testHomogeneousTuple() {
doTest("Tuple[int, ...]",
doTest("Tuple[int, ...]",
"from typing import Tuple\n" +
"\n" +
"def f(xs: Tuple[int, ...]):\n" +
@@ -578,7 +578,7 @@ public class PyTypingTest extends PyTestCase {
// PY-18762
public void testHomogeneousTupleUnpackingTarget() {
doTest("int",
doTest("int",
"from typing import Tuple\n" +
"\n" +
"xs = unknown() # type: Tuple[int, ...]\n" +
@@ -618,7 +618,7 @@ public class PyTypingTest extends PyTestCase {
// PY-18877
public void testFunctionTypeCommentOnTheSameLine() {
doTest("(x: int, y: int) -> None",
doTest("(x: int, y: int) -> None",
"def f(x,\n" +
" y): # type: (int, int) -> None\n" +
" pass\n" +
@@ -729,7 +729,7 @@ public class PyTypingTest extends PyTestCase {
}
public void testIllegalAnnotationTargets() {
doTest("Tuple[Any, int, Any, Any]",
doTest("Tuple[Any, int, Any, Any]",
"(w, _): Tuple[int, Any]\n" +
"((x)): int\n" +
"y: bool = z = undefined()\n" +
@@ -929,7 +929,7 @@ public class PyTypingTest extends PyTestCase {
// PY-23053
public void testListContainingClasses() {
doTest("Type[str]",
doTest("Type[str]",
"xs = [str]\n" +
"expr = xs.pop()");
}
@@ -985,6 +985,39 @@ public class PyTypingTest extends PyTestCase {
" expr = x\n");
}
// PY-24729
public void testAnnotatedInstanceAttributeReferenceOutsideClass() {
doTest("int",
"class C:\n" +
" attr: int\n" +
"\n" +
" def __init__(self):\n" +
" self.attr = 'foo'\n" +
"\n" +
"expr = C().attr\n");
}
// PY-24729
public void testAnnotatedInstanceAttributeReferenceInsideClass() {
doTest("int",
"class C:\n" +
" attr: int\n" +
"\n" +
" def __init__(self):\n" +
" self.attr = 'foo'\n" +
" \n" +
" def m(self):\n" +
" expr = self.attr\n");
}
// PY-24729
public void testAnnotatedInstanceAttributeInOtherFile() {
doMultiFileStubAwareTest("int",
"from other import C\n" +
"\n" +
"expr = C().attr");
}
private void doTestNoInjectedText(@NotNull String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());
@@ -1008,9 +1041,23 @@ public class PyTypingTest extends PyTestCase {
myFixture.copyDirectoryToProject("typing", "");
myFixture.configureByText(PythonFileType.INSTANCE, text);
final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class);
final TypeEvalContext codeAnalysis = TypeEvalContext.codeAnalysis(expr.getProject(),expr.getContainingFile());
final TypeEvalContext codeAnalysis = TypeEvalContext.codeAnalysis(expr.getProject(), expr.getContainingFile());
final TypeEvalContext userInitiated = TypeEvalContext.userInitiated(expr.getProject(), expr.getContainingFile()).withTracing();
assertType("Failed in code analysis context", expectedType, expr, codeAnalysis);
assertType("Failed in user initiated context", expectedType, expr, userInitiated);
}
private void doMultiFileStubAwareTest(@NotNull final String expectedType, @NotNull final String text) {
myFixture.copyDirectoryToProject("types/" + getTestName(false), "");
myFixture.copyDirectoryToProject("typing", "");
myFixture.configureByText(PythonFileType.INSTANCE, text);
final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class);
final TypeEvalContext codeAnalysis = TypeEvalContext.codeAnalysis(expr.getProject(), expr.getContainingFile());
assertType("Failed in code analysis context", expectedType, expr, codeAnalysis);
assertProjectFilesNotParsed(expr.getContainingFile());
final TypeEvalContext userInitiated = TypeEvalContext.userInitiated(expr.getProject(), expr.getContainingFile()).withTracing();
assertType("Failed in user initiated context", expectedType, expr, userInitiated);
}
}
@@ -146,4 +146,9 @@ public class PyiTypeTest extends PyTestCase {
public void testComparisonOperatorOverloads() {
doTest("int");
}
// PY-24929
public void testInstanceAttributeAnnotation() {
doTest("int");
}
}