From cad1138e5932ca2e3de9b21ba5d87f32f913e0c8 Mon Sep 17 00:00:00 2001 From: "andrey.matveev" Date: Wed, 20 Apr 2022 13:39:15 +0700 Subject: [PATCH] PY-53104 Support PEP 673 typing.Self type GitOrigin-RevId: 8efc65725580510dc3e9a93e1242e1a69032c8f1 --- .../resources/messages/PyPsiBundle.properties | 7 +- .../typing/PyTypingTypeProvider.java | 95 +++++- .../inspections/PyTypeHintsInspection.kt | 57 +++- .../psi/impl/PyCallExpressionHelper.java | 2 +- .../python/psi/impl/PyFunctionImpl.java | 2 +- .../psi/impl/PyReferenceExpressionImpl.java | 2 +- .../python/psi/types/PyCallableTypeImpl.java | 6 +- .../python/psi/types/PySelfType.java | 74 +++++ .../python/psi/types/PyTypeChecker.java | 131 ++++---- python/testData/completion/onListSelf.py | 9 + .../completion/onListSelfInNestedClass.py | 10 + python/testData/completion/onSelf.py | 9 + .../completion/onSelfInsideClassFunction.py | 8 + ...onSelfInsideClassFunctionNestedFunction.py | 12 + python/testData/quickdoc/Self.html | 1 + python/testData/quickdoc/Self.py | 9 + .../quickdoc/TypingExtensionsSelf.html | 1 + .../testData/quickdoc/TypingExtensionsSelf.py | 10 + .../other.py | 5 + .../jetbrains/python/Py3CompletionTest.java | 33 ++ .../com/jetbrains/python/Py3QuickDocTest.java | 10 + .../com/jetbrains/python/PyTypingTest.java | 94 ++++++ .../Py3TypeCheckerInspectionTest.java | 296 ++++++++++++++++++ .../PyTypeHintsInspectionTest.java | 95 ++++++ 24 files changed, 904 insertions(+), 74 deletions(-) create mode 100644 python/python-psi-impl/src/com/jetbrains/python/psi/types/PySelfType.java create mode 100644 python/testData/completion/onListSelf.py create mode 100644 python/testData/completion/onListSelfInNestedClass.py create mode 100644 python/testData/completion/onSelf.py create mode 100644 python/testData/completion/onSelfInsideClassFunction.py create mode 100644 python/testData/completion/onSelfInsideClassFunctionNestedFunction.py create mode 100644 python/testData/quickdoc/Self.html create mode 100644 python/testData/quickdoc/Self.py create mode 100644 python/testData/quickdoc/TypingExtensionsSelf.html create mode 100644 python/testData/quickdoc/TypingExtensionsSelf.py create mode 100644 python/testData/types/NoUnstubInCalculateSelfTypeInFunctionDefinedInImportedFile/other.py diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index f7870a0958f9..f1ef5df1ad85 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -1131,6 +1131,7 @@ INSP.typeddict.required.notrequired.must.have.exactly.one.type.argument=''{0}'' # PyTypeHintsInspection INSP.NAME.type.hints=Invalid type hints definitions and usages INSP.type.hints.builtin.cannot.be.parameterized.directly=Builtin ''{0}'' cannot be parameterized directly +INSP.type.hints.typing.self.cannot.be.parameterized='Self' cannot be parameterized INSP.type.hints.invalid.type.self=Invalid type 'self' INSP.type.hints.literal.must.have.at.least.one.parameter='Literal' must have at least one parameter INSP.type.hints.annotated.must.be.called.with.at.least.two.arguments='Annotated' must be called with at least two arguments @@ -1167,6 +1168,10 @@ INSP.type.hints.type.alias.must.be.used.as.standalone.type.hint='TypeAlias' must INSP.type.hints.type.alias.must.be.immediately.initialized=Type alias must be immediately initialized INSP.type.hints.type.alias.must.be.top.level.declaration=Type alias must be top-level declaration INSP.type.hints.type.alias.cannot.be.parameterized='TypeAlias' cannot be parameterized +INSP.type.hints.self.use.in.staticmethod=Cannot use 'Self' in staticmethod +INSP.type.hints.self.use.outside.class=Cannot use 'Self' outside class +INSP.type.hints.self.use.for.self.parameter.with.self.annotation=Cannot use 'Self' if 'self' parameter is not 'Self' annotated +INSP.type.hints.self.use.for.cls.parameter.with.self.annotation=Cannot use 'Self' if 'cls' parameter is not 'Self' annotated QFIX.remove.function.annotations=Remove function annotations QFIX.replace.with.target.name=Replace with the target name QFIX.remove.generic.parameters=Remove generic parameters @@ -1223,4 +1228,4 @@ INSP.class.var.can.not.override.class.variable=Cannot override class variable '' INSP.class.var.can.not.override.instance.variable=Cannot override instance variable ''{0}'' (previously declared on base class ''{1}'') with class variable INSP.class.var.can.not.be.used.in.annotations.for.function.parameters='ClassVar' cannot be used in annotations for function parameters INSP.class.var.can.not.be.used.in.annotation.for.function.return.value='ClassVar' cannot be used in annotation for a function return value -INSP.class.var.can.not.include.type.variables='ClassVar' parameter cannot include type variables \ No newline at end of file +INSP.class.var.can.not.include.type.variables='ClassVar' parameter cannot include type variables diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java index b3afc427b2fc..be880d90410e 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java @@ -3,6 +3,7 @@ package com.jetbrains.python.codeInsight.typing; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.TextRange; @@ -17,6 +18,7 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.QualifiedName; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.Stack; import com.jetbrains.python.PyCustomType; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; @@ -108,6 +110,8 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { public static final String NOT_REQUIRED = "typing.NotRequired"; public static final String NOT_REQUIRED_EXT = "typing_extensions.NotRequired"; + public static final String SELF = "typing.Self"; + public static final String SELF_EXT = "typing_extensions.Self"; private static final String PY2_FILE_TYPE = "typing.BinaryIO"; private static final String PY3_BINARY_FILE_TYPE = "typing.BinaryIO"; private static final String PY3_TEXT_FILE_TYPE = "typing.TextIO"; @@ -192,8 +196,11 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { .add(TYPE_ALIAS, TYPE_ALIAS_EXT) .add(REQUIRED, REQUIRED_EXT) .add(NOT_REQUIRED, NOT_REQUIRED_EXT) + .add(SELF, SELF_EXT) .build(); + private static final Key FRAGMENT_OWNER = Key.create("PY_FRAGMENT_OWNER"); + @Nullable @Override public PyType getReferenceExpressionType(@NotNull PyReferenceExpression referenceExpression, @NotNull TypeEvalContext context) { @@ -680,6 +687,12 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { final Iterable allBaseClassesQNames; final List subscriptedBaseClasses = PyClassElementType.getSubscriptedSuperClassesStubLike(pyClass); + for (PySubscriptionExpression subscrExpr : subscriptedBaseClasses) { + PsiFile containingFile = subscrExpr.getContainingFile(); + if (containingFile instanceof PyExpressionCodeFragment) { + containingFile.putUserData(FRAGMENT_OWNER, pyClass); + } + } final Map baseClassQNameToExpr = new HashMap<>(); if (classStub == null) { allBaseClassesQNames = PyClassElementType.getSuperClassQNames(pyClass).keySet(); @@ -753,20 +766,26 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { @Nullable static Ref getType(@NotNull PyExpression expression, @NotNull Context context) { - final List members = new ArrayList<>(); - boolean foundAny = false; - for (Pair pair : tryResolvingWithAliases(expression, context.getTypeContext())) { - final Ref typeRef = getTypeForResolvedElement(pair.getFirst(), pair.getSecond(), context); - if (typeRef != null) { - final PyType type = typeRef.get(); - if (type == null) { - foundAny = true; + context.getExpressionStack().push(expression); + try { + final List members = new ArrayList<>(); + boolean foundAny = false; + for (Pair pair : tryResolvingWithAliases(expression, context.getTypeContext())) { + final Ref typeRef = getTypeForResolvedElement(pair.getFirst(), pair.getSecond(), context); + if (typeRef != null) { + final PyType type = typeRef.get(); + if (type == null) { + foundAny = true; + } + members.add(type); } - members.add(type); } + final PyType union = PyUnionType.union(members); + return union != null || foundAny ? Ref.create(union) : null; + } + finally { + context.getExpressionStack().pop(); } - final PyType union = PyUnionType.union(members); - return union != null || foundAny ? Ref.create(union) : null; } @Nullable @@ -830,6 +849,7 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { } context.getExpressionCache().add(alias); } + context.getExpressionStack().push(resolved); try { final Ref typeFromParenthesizedExpression = getTypeFromParenthesizedExpression(resolved, context); if (typeFromParenthesizedExpression != null) { @@ -924,15 +944,39 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { if (unionTypeFromBinaryOr != null) { return unionTypeFromBinaryOr; } + final Ref selfType = getSelfType(resolved, context); + if (selfType != null) { + return selfType; + } return null; } finally { + context.getExpressionStack().pop(); if (alias != null) { context.getExpressionCache().remove(alias); } } } + private static Ref getSelfType(@NotNull PsiElement resolved, @NotNull Context context) { + if (resolved instanceof PyQualifiedNameOwner && + (SELF.equals(((PyQualifiedNameOwner)resolved).getQualifiedName()) || + SELF_EXT.equals(((PyQualifiedNameOwner)resolved).getQualifiedName()))) { + PsiElement lastTypeHintExpr = context.getExpressionStack().get(0); + PsiElement typeHintContext = getStubRetainedTypeHintContext(lastTypeHintExpr); + + PyClass containingClass = typeHintContext instanceof PyClass ? (PyClass)typeHintContext + : PsiTreeUtil.getStubOrPsiParentOfType(typeHintContext, PyClass.class); + if (containingClass == null) return null; + + PyClassLikeType scopeClassType = containingClass.getType(context.getTypeContext()); + if (scopeClassType == null) return null; + + return Ref.create(new PySelfType(scopeClassType)); + } + return null; + } + @Nullable private static Ref getTypeFromBinaryExpression(@NotNull PsiElement resolved, @NotNull Context context) { if (resolved instanceof PyBinaryExpression) { @@ -1227,7 +1271,11 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { private static PyExpression toExpression(@NotNull String contents, @NotNull PsiElement anchor) { final PsiFile file = FileContextUtil.getContextFile(anchor); if (file == null) return null; - return PyUtil.createExpressionFromFragment(contents, file); + PyExpression fragment = PyUtil.createExpressionFromFragment(contents, file); + if (fragment != null) { + fragment.getContainingFile().putUserData(FRAGMENT_OWNER, anchor); + } + return fragment; } @Nullable @@ -1498,6 +1546,22 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { return null; } + @NotNull + private static PsiElement getStubRetainedTypeHintContext(@NotNull PsiElement typeHintExpression) { + // Values from PSI stubs and regular type comments + PsiElement fragmentOwner = typeHintExpression.getContainingFile().getUserData(FRAGMENT_OWNER); + if (fragmentOwner != null) { + return fragmentOwner; + } + // Values from function type comments + else if (typeHintExpression.getContainingFile() instanceof PyFunctionTypeAnnotationFile) { + return PyPsiUtils.getRealContext(typeHintExpression); + } + else { + return typeHintExpression; + } + } + @Nullable private static PyParamSpecType getParamSpecType(@NotNull PsiElement element, @NotNull Context context) { if (!(element instanceof PyCallExpression)) return null; @@ -1830,6 +1894,8 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { @NotNull private final TypeEvalContext myContext; @NotNull private final Set myCache = new HashSet<>(); + @NotNull private final Stack myExpressionStack = new Stack<>(); + Context(@NotNull TypeEvalContext context) { myContext = context; } @@ -1843,5 +1909,10 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { public Set getExpressionCache() { return myCache; } + + @NotNull + Stack getExpressionStack() { + return myExpressionStack; + } } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt index 32ac010aec93..e97fabeb5363 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt @@ -167,6 +167,51 @@ class PyTypeHintsInspection : PyInspection() { } } + override fun visitPyAnnotation(node: PyAnnotation) { + fun PyAnnotation.findSelvesInAnnotation(context: TypeEvalContext): List = + PsiTreeUtil.findChildrenOfAnyType(this.value, false, PyReferenceExpression::class.java).filter { refExpr -> + PyTypingTypeProvider.resolveToQualifiedNames(refExpr, context).any { + PyTypingTypeProvider.SELF == it || PyTypingTypeProvider.SELF_EXT == it + } + } + + val selves = node.findSelvesInAnnotation(myTypeEvalContext) + if (selves.isEmpty()) { + return + } + + fun registerProblemForSelves(message: String) { + selves.forEach { + registerProblem(it, message) + } + } + + val classParent = PsiTreeUtil.getParentOfType(node, PyClass::class.java) + if (classParent == null) { + registerProblemForSelves(PyPsiBundle.message("INSP.type.hints.self.use.outside.class")) + } + + val functionParent = PsiTreeUtil.getParentOfType(node, PyFunction::class.java) + if (functionParent != null) { + if (PyFunction.Modifier.STATICMETHOD == functionParent.modifier) { + registerProblemForSelves(PyPsiBundle.message("INSP.type.hints.self.use.in.staticmethod")) + } + + val parameters = functionParent.parameterList.parameters + if (parameters.isNotEmpty()) { + val firstParameter = parameters[0] + val annotation = (firstParameter as? PyNamedParameter)?.annotation + if (annotation != null && firstParameter.isSelf && annotation.findSelvesInAnnotation(myTypeEvalContext).isEmpty()) { + val message = if (PyFunction.Modifier.CLASSMETHOD == functionParent.modifier) + PyPsiBundle.message("INSP.type.hints.self.use.for.cls.parameter.with.self.annotation") + else + PyPsiBundle.message("INSP.type.hints.self.use.for.self.parameter.with.self.annotation") + registerProblemForSelves(message) + } + } + } + } + override fun visitPyFunction(node: PyFunction) { super.visitPyFunction(node) @@ -365,7 +410,9 @@ class PyTypeHintsInspection : PyInspection() { PyTypingTypeProvider.ANNOTATED, PyTypingTypeProvider.ANNOTATED_EXT, PyTypingTypeProvider.TYPE_ALIAS, - PyTypingTypeProvider.TYPE_ALIAS_EXT -> { + PyTypingTypeProvider.TYPE_ALIAS_EXT, + PyTypingTypeProvider.SELF, + PyTypingTypeProvider.SELF_EXT -> { val shortName = it.substringAfterLast('.') registerProblem(base, PyPsiBundle.message("INSP.type.hints.type.cannot.be.used.with.instance.class.checks", shortName), ProblemHighlightType.GENERIC_ERROR) @@ -593,6 +640,8 @@ class PyTypeHintsInspection : PyInspection() { val annotatedExtQName = QualifiedName.fromDottedString(PyTypingTypeProvider.ANNOTATED_EXT) val typeAliasQName = QualifiedName.fromDottedString(PyTypingTypeProvider.TYPE_ALIAS) val typeAliasExtQName = QualifiedName.fromDottedString(PyTypingTypeProvider.TYPE_ALIAS_EXT) + val typingSelf = QualifiedName.fromDottedString(PyTypingTypeProvider.SELF) + val typingExtSelf = QualifiedName.fromDottedString(PyTypingTypeProvider.SELF_EXT) val qNames = PyResolveUtil.resolveImportedElementQNameLocally(operand) var typingOnly = true @@ -604,6 +653,7 @@ class PyTypeHintsInspection : PyInspection() { literalQName, literalExtQName -> checkLiteralParameter(index) annotatedQName, annotatedExtQName -> checkAnnotatedParameter(index) typeAliasQName, typeAliasExtQName -> reportParameterizedTypeAlias(index) + typingSelf, typingExtSelf -> reportParameterizedSelf(index) callableQName -> { callableExists = true checkCallableParameters(index) @@ -626,6 +676,11 @@ class PyTypeHintsInspection : PyInspection() { } } + private fun reportParameterizedSelf(index: PyExpression) { + registerProblem(index, PyPsiBundle.message("INSP.type.hints.typing.self.cannot.be.parameterized"), + ProblemHighlightType.GENERIC_ERROR) + } + private fun checkLiteralParameter(index: PyExpression) { val subParameter = if (index is PySubscriptionExpression) index.operand else null if (subParameter is PyReferenceExpression && diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java index 63fbb5d6fced..9029ad69d601 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java @@ -1220,7 +1220,7 @@ public final class PyCallExpressionHelper { } allMappedParameters.putAll(mappedExplicitParameters); - return PyTypeChecker.unifyGenericCallWithParamSpecs(receiver, allMappedParameters, context) == null; + return PyTypeChecker.unifyGenericCall(receiver, allMappedParameters, context) == null; } public static class ArgumentMappingResults { diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java index dcc7e37acc40..2a57da2f0bdf 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java @@ -254,7 +254,7 @@ public class PyFunctionImpl extends PyBaseElementImpl implements @NotNull Map parameters, @NotNull TypeEvalContext context) { if (PyTypeChecker.hasGenerics(type, context)) { - final var substitutions = PyTypeChecker.unifyGenericCallWithParamSpecs(receiver, parameters, context); + final var substitutions = PyTypeChecker.unifyGenericCall(receiver, parameters, context); if (substitutions != null) { final var substitutionsWithUnresolvedReturnGenerics = PyTypeChecker.getSubstitutionsWithUnresolvedReturnGenerics(getParameters(context), type, substitutions, context); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java index 8b28ccaec83f..29b4e5b8fdd4 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java @@ -405,7 +405,7 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere boolean possiblyParameterizedQualifier = !(qualifierType instanceof PyModuleType || qualifierType instanceof PyImportedModuleType); if (possiblyParameterizedQualifier && PyTypeChecker.hasGenerics(type, context)) { final var substitutions = - PyTypeChecker.unifyGenericCallWithParamSpecs(qualifier, Collections.emptyMap(), context); + PyTypeChecker.unifyGenericCall(qualifier, Collections.emptyMap(), context); if (substitutions != null) { final PyType substituted = PyTypeChecker.substitute(type, substitutions, context); if (substituted != null) { diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyCallableTypeImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyCallableTypeImpl.java index 5d7d4f671ce7..f5683f559302 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyCallableTypeImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyCallableTypeImpl.java @@ -14,7 +14,9 @@ import com.jetbrains.python.psi.resolve.RatedResolveResult; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Collection; +import java.util.List; +import java.util.Map; /** * @author vlan @@ -67,7 +69,7 @@ public class PyCallableTypeImpl implements PyCallableType { @NotNull Collection allParameters, @Nullable PyExpression receiver, @NotNull TypeEvalContext context) { - final var substitutions = PyTypeChecker.unifyGenericCallWithParamSpecs(receiver, actualParameters, context); + final var substitutions = PyTypeChecker.unifyGenericCall(receiver, actualParameters, context); final var substitutionsWithUnresolvedReturnGenerics = PyTypeChecker.getSubstitutionsWithUnresolvedReturnGenerics(allParameters, type, substitutions, context); return PyTypeChecker.substitute(type, substitutionsWithUnresolvedReturnGenerics, context); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PySelfType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PySelfType.java new file mode 100644 index 000000000000..b411c6978a82 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PySelfType.java @@ -0,0 +1,74 @@ +package com.jetbrains.python.psi.types; + +import com.intellij.psi.PsiElement; +import com.intellij.util.ProcessingContext; +import com.jetbrains.python.psi.AccessDirection; +import com.jetbrains.python.psi.PyExpression; +import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.resolve.RatedResolveResult; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; + +public final class PySelfType implements PyType { + @NotNull private final PyClassLikeType myScopeClassType; + + public PySelfType(@NotNull PyClassLikeType scopeClassType) { + myScopeClassType = scopeClassType.toInstance(); + } + + @Override + public @Nullable List resolveMember(@NotNull String name, + @Nullable PyExpression location, + @NotNull AccessDirection direction, + @NotNull PyResolveContext resolveContext) { + return myScopeClassType.resolveMember(name, location, direction, resolveContext); + } + + @Override + public Object[] getCompletionVariants(String completionPrefix, + PsiElement location, + ProcessingContext context) { + return myScopeClassType.getCompletionVariants(completionPrefix, location, context); + } + + @Override + @NotNull + public String getName() { + return "Self"; + } + + @NotNull + public PyClassLikeType getScopeClassType() { + return myScopeClassType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PySelfType type = (PySelfType)o; + + if (!Objects.equals(myScopeClassType, type.myScopeClassType)) return false; + + return true; + } + + @Override + public int hashCode() { + return myScopeClassType.hashCode(); + } + + @Override + public boolean isBuiltin() { + return false; + } + + @Override + public void assertValid(String message) { + + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java index 0ff11f571ed0..865b287f91f2 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -75,7 +75,7 @@ public final class PyTypeChecker { @Nullable PyType actual, @NotNull TypeEvalContext context, @NotNull GenericSubstitutions substitutions) { - return match(expected, actual, new MatchContext(context, substitutions.typeVars, substitutions.paramSpecs, false)) + return match(expected, actual, new MatchContext(context, substitutions, false)) .orElse(true); } @@ -103,7 +103,7 @@ public final class PyTypeChecker { @NotNull private static Optional matchImpl(@Nullable PyType expected, @Nullable PyType actual, @NotNull MatchContext context) { for (PyTypeCheckerExtension extension : PyTypeCheckerExtension.EP_NAME.getExtensionList()) { - final Optional result = extension.match(expected, actual, context.context, context.genericSubstitutions); + final Optional result = extension.match(expected, actual, context.context, context.mySubstitutions.typeVars); if (result.isPresent()) { return result; } @@ -124,6 +124,14 @@ public final class PyTypeChecker { return Optional.of(match((PyGenericType)expected, actual, context)); } + if (expected instanceof PySelfType) { + return match(context.mySubstitutions.qualifierType, actual, context); + } + + if (actual instanceof PySelfType && context.reversedSubstitutions) { + return match(context.mySubstitutions.qualifierType, expected, context); + } + if (expected instanceof PyParamSpecType) { return Optional.of(match((PyParamSpecType)expected, actual, context)); } @@ -215,7 +223,7 @@ public final class PyTypeChecker { return false; } - final PyType substitution = context.genericSubstitutions.get(expected); + final PyType substitution = context.mySubstitutions.typeVars.get(expected); PyType bound = expected.getBound(); // Promote int in Type[TypeVar('T', int)] to Type[int] before checking that bounds match if (expected.isDefinition()) { @@ -242,10 +250,10 @@ public final class PyTypeChecker { } if (actual != null) { - context.genericSubstitutions.put(expected, actual); + context.mySubstitutions.typeVars.put(expected, actual); } else if (bound != null) { - context.genericSubstitutions.put(expected, PyUnionType.createWeakType(bound)); + context.mySubstitutions.typeVars.put(expected, PyUnionType.createWeakType(bound)); } return true; @@ -257,7 +265,7 @@ public final class PyTypeChecker { final var callableActual = (PyParamSpecType)actual; final var parameters = callableActual.getParameters(); if (parameters == null) return false; - context.paramSpecSubstitutions.put(expected, expected.withParameters(parameters, context.context)); + context.mySubstitutions.paramSpecs.put(expected, expected.withParameters(parameters, context.context)); return true; } @@ -349,8 +357,17 @@ public final class PyTypeChecker { .map(context::getType) .anyMatch( subclassElementType -> { - return match(protocolElementType, - dropSelfIfNeeded(actual, subclassElementType, context), matchContext).orElse(true); + var matched = match(protocolElementType, + dropSelfIfNeeded(actual, subclassElementType, context), matchContext).orElse(true); + if (!matched) return false; + if (!(protocolElementType instanceof PyFunctionType) || !(subclassElementType instanceof PyFunctionType)) return matched; + var protocolReturnType = ((PyFunctionType)protocolElementType).getReturnType(context); + if (protocolReturnType instanceof PySelfType) { + var subclassReturnType = ((PyFunctionType)subclassElementType).getReturnType(context); + if (subclassReturnType instanceof PySelfType) return true; + return match(actual, subclassReturnType, matchContext).orElse(true); + } + return matched; } ); @@ -509,7 +526,7 @@ public final class PyTypeChecker { // TODO: Check named and star params, not only positional ones if (expectedParamType instanceof PyParamSpecType && expectedParameters.size() == 1) { final var expectedParamSpecType = (PyParamSpecType)expectedParamType; - matchContext.paramSpecSubstitutions.put(expectedParamSpecType, expectedParamSpecType.withParameters(actualParameters, context)); + matchContext.mySubstitutions.paramSpecs.put(expectedParamSpecType, expectedParamSpecType.withParameters(actualParameters, context)); break; } else if (expectedParamType instanceof PyConcatenateType && expectedParameters.size() == 1) { @@ -539,7 +556,7 @@ public final class PyTypeChecker { final var expectedParamSpecType = expectedConcatenateType.getParamSpec(); final var restActualParameters = actualParameters.subList(actualParamRightBound, actualParameters.size()); final var parametersSubst = expectedParamSpecType.withParameters(restActualParameters, context); - matchContext.paramSpecSubstitutions.put(expectedParamSpecType, parametersSubst); + matchContext.mySubstitutions.paramSpecs.put(expectedParamSpecType, parametersSubst); } } @@ -779,6 +796,9 @@ public final class PyTypeChecker { if (type instanceof PyConcatenateType) { generics.concatenates.add((PyConcatenateType)type); } + if (type instanceof PySelfType) { + generics.self = (PySelfType)type; + } else if (type instanceof PyUnionType) { final PyUnionType union = (PyUnionType)type; for (PyType t : union.getMembers()) { @@ -819,7 +839,7 @@ public final class PyTypeChecker { @Nullable public static PyType substitute(@Nullable PyType type, @NotNull Map substitutions, @NotNull TypeEvalContext context) { - final var genericSubstitutions = new GenericSubstitutions(substitutions, new LinkedHashMap<>()); + final var genericSubstitutions = new GenericSubstitutions(substitutions, new LinkedHashMap<>(), null); return substitute(type, genericSubstitutions, context); } @@ -843,6 +863,13 @@ public final class PyTypeChecker { } return substitution; } + else if (type instanceof PySelfType) { + var qualifierType = substitutions.qualifierType; + var selfScopeClassType = ((PySelfType)type).getScopeClassType(); + return PyTypeUtil.toStream(qualifierType) + .filter(memberType -> match(selfScopeClassType, memberType, context)) + .collect(PyTypeUtil.toUnion()); + } else if (type instanceof PyUnionType) { return ((PyUnionType)type).map(member -> substitute(member, substitutions, context)); } @@ -919,23 +946,10 @@ public final class PyTypeChecker { return type; } - /** - * @deprecated use {@link PyTypeChecker#unifyGenericCallWithParamSpecs(PyExpression, Map, TypeEvalContext)} instead - */ - @Deprecated @Nullable - public static Map unifyGenericCall(@Nullable PyExpression receiver, - @NotNull Map arguments, - @NotNull TypeEvalContext context) { - final var result = unifyGenericCallWithParamSpecs(receiver, arguments, context); - if (result == null) return null; - return result.typeVars; - } - - @Nullable - public static GenericSubstitutions unifyGenericCallWithParamSpecs(@Nullable PyExpression receiver, - @NotNull Map arguments, - @NotNull TypeEvalContext context) { + public static GenericSubstitutions unifyGenericCall(@Nullable PyExpression receiver, + @NotNull Map arguments, + @NotNull TypeEvalContext context) { final var substitutions = unifyReceiverWithParamSpecs(receiver, context); for (Map.Entry entry : getRegularMappedParameters(arguments).entrySet()) { final PyCallableParameter paramWrapper = entry.getValue(); @@ -1001,6 +1015,10 @@ public final class PyTypeChecker { // Collect generic params of object type final var substitutions = new GenericSubstitutions(); final PyType qualifierType = receiver != null ? context.getType(receiver) : null; + substitutions.qualifierType = qualifierType; + if (qualifierType instanceof PyClassType) { + substitutions.qualifierType = ((PyClassType)qualifierType).toInstance(); + } final var generics = collectGenerics(qualifierType, context); for (PyGenericType t : generics.typeVars) { substitutions.typeVars.put(t, t); @@ -1222,7 +1240,7 @@ public final class PyTypeChecker { for (int i = 0; i < Math.min(formalTypeParams.size(), actualTypeParams.size()); i++) { substitutions.put(formalTypeParams.get(i), actualTypeParams.get(i)); } - return substitute(genericType, new GenericSubstitutions(substitutions, Collections.emptyMap()), context); + return substitute(genericType, new GenericSubstitutions(substitutions, Collections.emptyMap(), null), context); } // An already parameterized type, don't override existing values for type parameters else if (genericType instanceof PyCollectionType) { @@ -1247,18 +1265,25 @@ public final class PyTypeChecker { @NotNull private final Set concatenates; + @Nullable + private PySelfType self; + Generics() { - this(new LinkedHashSet<>(), new LinkedHashSet<>(), new LinkedHashSet<>()); + this(new LinkedHashSet<>(), new LinkedHashSet<>(), new LinkedHashSet<>(), null); } - Generics(@NotNull Set generics, @NotNull Set paramSpecs, @NotNull Set concatenates) { + Generics(@NotNull Set generics, + @NotNull Set paramSpecs, + @NotNull Set concatenates, + @Nullable PySelfType self) { this.typeVars = generics; this.paramSpecs = paramSpecs; this.concatenates = concatenates; + this.self = self; } boolean isEmpty() { - return typeVars.isEmpty() && paramSpecs.isEmpty() && concatenates.isEmpty(); + return typeVars.isEmpty() && paramSpecs.isEmpty() && concatenates.isEmpty() && self == null; } } @@ -1269,14 +1294,23 @@ public final class PyTypeChecker { @NotNull private final Map paramSpecs; + @Nullable + private PyType qualifierType; + GenericSubstitutions() { - this(new LinkedHashMap<>(), new LinkedHashMap<>()); + this(new LinkedHashMap<>(), new LinkedHashMap<>(), null); + } + + GenericSubstitutions(@NotNull Map typeVars) { + this(typeVars, new LinkedHashMap<>(), null); } GenericSubstitutions(@NotNull Map typeVars, - @NotNull Map paramSpecs) { + @NotNull Map paramSpecs, + @Nullable PyType qualifierType) { this.typeVars = typeVars; this.paramSpecs = paramSpecs; + this.qualifierType = qualifierType; } @NotNull @@ -1284,9 +1318,9 @@ public final class PyTypeChecker { return paramSpecs; } - @NotNull - public Map getTypeVars() { - return typeVars; + @Nullable + public PyType getQualifierType() { + return qualifierType; } } @@ -1296,37 +1330,24 @@ public final class PyTypeChecker { private final TypeEvalContext context; @NotNull - private final Map genericSubstitutions; // mutable - - @NotNull - private final Map paramSpecSubstitutions; // mutable + private final GenericSubstitutions mySubstitutions; private final boolean reversedSubstitutions; MatchContext(@NotNull TypeEvalContext context, - @NotNull Map genericSubstitutions) { - this(context, genericSubstitutions, false); + @NotNull Map typeVars) { + this(context, new GenericSubstitutions(typeVars), false); } - private MatchContext(@NotNull TypeEvalContext context, - @NotNull Map genericSubstitutions, - boolean reversedSubstitutions) { - this(context, genericSubstitutions, new HashMap<>(), reversedSubstitutions); - } - - private MatchContext(@NotNull TypeEvalContext context, - @NotNull Map genericSubstitutions, - @NotNull Map paramSpecSubstitutions, - boolean reversedSubstitutions) { + MatchContext(@NotNull TypeEvalContext context, @NotNull GenericSubstitutions substitutions, boolean reversedSubstitutions) { this.context = context; - this.genericSubstitutions = genericSubstitutions; - this.paramSpecSubstitutions = paramSpecSubstitutions; + this.mySubstitutions = substitutions; this.reversedSubstitutions = reversedSubstitutions; } @NotNull public MatchContext reverseSubstitutions() { - return new MatchContext(context, genericSubstitutions, !reversedSubstitutions); + return new MatchContext(context, mySubstitutions, !reversedSubstitutions); } } } diff --git a/python/testData/completion/onListSelf.py b/python/testData/completion/onListSelf.py new file mode 100644 index 000000000000..4a23ee1d938c --- /dev/null +++ b/python/testData/completion/onListSelf.py @@ -0,0 +1,9 @@ +from typing import Self + +class A: + def foo(self) -> list[Self]: + ... +class B(A) + def bar(self) -> Self: + ... +B().foo()[0]. \ No newline at end of file diff --git a/python/testData/completion/onListSelfInNestedClass.py b/python/testData/completion/onListSelfInNestedClass.py new file mode 100644 index 000000000000..a203f4ffeb29 --- /dev/null +++ b/python/testData/completion/onListSelfInNestedClass.py @@ -0,0 +1,10 @@ +from typing import Self + +class OuterClass: + class A: + def foo(self) -> list[Self]: + ... + class B(A) + def bar(self) -> Self: + ... +OuterClass.B().foo()[0]. \ No newline at end of file diff --git a/python/testData/completion/onSelf.py b/python/testData/completion/onSelf.py new file mode 100644 index 000000000000..ac420ec237ca --- /dev/null +++ b/python/testData/completion/onSelf.py @@ -0,0 +1,9 @@ +from typing import Self + +class A: + def foo(self) -> Self: + ... +class B(A) + def bar(self) -> Self: + ... +B().foo(). \ No newline at end of file diff --git a/python/testData/completion/onSelfInsideClassFunction.py b/python/testData/completion/onSelfInsideClassFunction.py new file mode 100644 index 000000000000..4ede34a0ac13 --- /dev/null +++ b/python/testData/completion/onSelfInsideClassFunction.py @@ -0,0 +1,8 @@ +from typing import Self + + +class HasNestedFunction: + x: int = 42 + + def foo(self, inner_self: Self) -> None: + print(inner_self.) diff --git a/python/testData/completion/onSelfInsideClassFunctionNestedFunction.py b/python/testData/completion/onSelfInsideClassFunctionNestedFunction.py new file mode 100644 index 000000000000..5750e7e092e1 --- /dev/null +++ b/python/testData/completion/onSelfInsideClassFunctionNestedFunction.py @@ -0,0 +1,12 @@ +from typing import Self + + +class HasNestedFunction: + x: int = 42 + + def foo(self) -> None: + def nested(z: int, inner_self: Self) -> Self: + print(inner_self.) + return inner_self + + nested(42, self) \ No newline at end of file diff --git a/python/testData/quickdoc/Self.html b/python/testData/quickdoc/Self.html new file mode 100644 index 000000000000..ddd8b632d358 --- /dev/null +++ b/python/testData/quickdoc/Self.html @@ -0,0 +1 @@ +
Self.A
def foo(self) -> Self
\ No newline at end of file diff --git a/python/testData/quickdoc/Self.py b/python/testData/quickdoc/Self.py new file mode 100644 index 000000000000..77809926bf61 --- /dev/null +++ b/python/testData/quickdoc/Self.py @@ -0,0 +1,9 @@ +from typing import Self + +class A: + def foo(self) -> Self: + ... +class B(A): + def bar(self) -> Self: + ... +B().foo() \ No newline at end of file diff --git a/python/testData/quickdoc/TypingExtensionsSelf.html b/python/testData/quickdoc/TypingExtensionsSelf.html new file mode 100644 index 000000000000..fd19345daf6a --- /dev/null +++ b/python/testData/quickdoc/TypingExtensionsSelf.html @@ -0,0 +1 @@ +
TypingExtensionsSelf.A
def foo(self) -> Self
\ No newline at end of file diff --git a/python/testData/quickdoc/TypingExtensionsSelf.py b/python/testData/quickdoc/TypingExtensionsSelf.py new file mode 100644 index 000000000000..3f432bcc4285 --- /dev/null +++ b/python/testData/quickdoc/TypingExtensionsSelf.py @@ -0,0 +1,10 @@ +from typing_extensions import Self + +class A: + def foo(self) -> Self: + ... +class B(A): + def bar(self) -> Self: + ... +b = B() +b.foo() \ No newline at end of file diff --git a/python/testData/types/NoUnstubInCalculateSelfTypeInFunctionDefinedInImportedFile/other.py b/python/testData/types/NoUnstubInCalculateSelfTypeInFunctionDefinedInImportedFile/other.py new file mode 100644 index 000000000000..97884167a028 --- /dev/null +++ b/python/testData/types/NoUnstubInCalculateSelfTypeInFunctionDefinedInImportedFile/other.py @@ -0,0 +1,5 @@ +from typing import Self + +class Clazz: + def foo(self) -> Self: + return self diff --git a/python/testSrc/com/jetbrains/python/Py3CompletionTest.java b/python/testSrc/com/jetbrains/python/Py3CompletionTest.java index 7388fe5e9de7..2de4c0e247a2 100644 --- a/python/testSrc/com/jetbrains/python/Py3CompletionTest.java +++ b/python/testSrc/com/jetbrains/python/Py3CompletionTest.java @@ -658,6 +658,39 @@ public class Py3CompletionTest extends PyTestCase { doTabTest(); } + // PY-53104 + public void testOnSelf() { + doTestVariants("foo", "bar"); + } + + // PY-53104 + public void testOnListSelf() { + doTestVariants("foo", "bar"); + } + + // PY-53104 + public void testOnListSelfInNestedClass() { + doTestVariants("foo", "bar"); + } + + // PY-53104 + public void testOnSelfInsideClassFunction() { + doTestVariants("x", "foo"); + } + + // PY-53104 + public void testOnSelfInsideClassFunctionNestedFunction() { + doTestVariants("x", "foo"); + } + + private void doTestVariants(String @NotNull ... expected) { + final String testName = getTestName(true); + myFixture.configureByFile(testName + ".py"); + myFixture.completeBasic(); + List variants = myFixture.getLookupElementStrings(); + assertContainsElements(variants, expected); + } + private @NotNull PyCodeStyleSettings getPythonCodeStyleSettings() { return getCodeStyleSettings().getCustomSettings(PyCodeStyleSettings.class); } diff --git a/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java b/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java index 01a2cf16fe9e..9c07fa92e69e 100644 --- a/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java +++ b/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java @@ -91,6 +91,16 @@ public class Py3QuickDocTest extends LightMarkedTestCase { checkHTMLOnly(); } + // PY-53104 + public void testSelf() { + checkHTMLOnly(); + } + + // PY-53104 + public void testTypingExtensionsSelf() { + checkHTMLOnly(); + } + @Override protected String getTestDataPath() { return super.getTestDataPath() + "/quickdoc/"; diff --git a/python/testSrc/com/jetbrains/python/PyTypingTest.java b/python/testSrc/com/jetbrains/python/PyTypingTest.java index c282a0a946e2..be61a1bf7ce6 100644 --- a/python/testSrc/com/jetbrains/python/PyTypingTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypingTest.java @@ -18,7 +18,9 @@ package com.jetbrains.python; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.psi.PsiLanguageInjectionHost; import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.python.fixtures.PyTestCase; @@ -2013,6 +2015,98 @@ public class PyTypingTest extends PyTestCase { expr = A.x"""); } + // PY-53104 + public void testMethodReturnSelf() { + doTest("B", + "from typing import Self\n" + + "\n" + + "class A:\n" + + " def foo(self) -> Self:\n" + + " ...\n" + + "class B(A):\n" + + " pass\n" + + "expr = B().foo()"); + } + + // PY-53104 + public void testMethodReturnListSelf() { + doTest("list[B]", + "from typing import Self\n" + + "\n" + + "class A:\n" + + " def foo(self) -> list[Self]:\n" + + " ...\n" + + "class B(A):\n" + + " pass:\n" + + " ...\n" + + "expr = B().foo()"); + } + + // PY-53104 + public void testClassMethodReturnSelf() { + doTest("Circle", + "from typing import Self\n" + + "\n" + + "\n" + + "class Shape:\n" + + " @classmethod\n" + + " def from_config(cls, config: dict[str, float]) -> Self:\n" + + " return cls(config[\"scale\"])\n" + + "\n" + + "\n" + + "class Circle(Shape):\n" + + " pass\n" + + "\n" + + "\n" + + "expr = Circle.from_config({})\n"); + } + + // PY-53104 + public void testClassMethodReturnSelfNestedClass() { + doTest("Circle", + "from typing import Self\n" + + "\n" + + "\n" + + "class OuterClass:\n" + + " class Shape:\n" + + " @classmethod\n" + + " def from_config(cls, config: dict[str, float]) -> Self:\n" + + " return cls(config[\"scale\"])\n" + + "\n" + + " class Circle(Shape):\n" + + " pass\n" + + "\n" + + "\n" + + "expr = OuterClass.Circle.from_config({})\n"); + } + + // PY-53104 + public void testNoUnstubInCalculateSelfTypeInFunctionDefinedInImportedFile() { + doMultiFileStubAwareTest("Clazz", + "from other import Clazz\n" + + "clz = Clazz()\n" + + "expr = clz.foo()\n"); + } + + // PY-53104 + public void testMatchSelfUnionType() { + doTest("C", + "from typing import Self\n" + + "\n" + + "\n" + + "class C:\n" + + " def method(self) -> Self:\n" + + " return self\n" + + "\n" + + "\n" + + "if bool():\n" + + " x = 42\n" + + "else:\n" + + " x = C()\n" + + "\n" + + "expr = x.method()"); + } + private void doTestNoInjectedText(@NotNull String text) { myFixture.configureByText(PythonFileType.INSTANCE, text); final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject()); diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java index 7d78af4efe02..e81e9ff2207f 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java @@ -962,4 +962,300 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase { public void testListLiteralPassedToIter() { doTestByText("iter([1, 2, 3])"); } + + // PY-53104 + public void testParameterSelf() { + doTestByText("from typing import Self, Callable\n" + + "\n" + + "class Shape:\n" + + " def difference(self, other: Self) -> float: ...\n" + + "\n" + + " def apply(self, f: Callable[[Self], None]) -> None: ...\n" + + "\n" + + "\n" + + "class Circle(Shape):\n" + + " pass\n" + + "\n" + + "\n" + + "def fCircle(c: Circle):\n" + + " pass\n" + + "\n" + + "\n" + + "def fShape(sh: Shape):\n" + + " pass\n" + + "\n" + + "\n" + + "sh = Shape()\n" + + "cir = Circle()\n" + + "\n" + + "sh.difference(cir)\n" + + "sh.difference(sh)\n" + + "cir.difference(cir)\n" + + "cir.difference(sh)\n" + + "\n" + + "cir.apply(fCircle)\n" + + "cir.apply( None' (matched generic type '(Self) -> None'), got '(sh: Shape) -> None' instead\">fShape)\n" + + "sh.apply(fCircle)\n" + + "sh.apply(fShape)"); + } + + // PY-53104 + public void testParameterTypeSelf() { + doTestByText("from typing import Self, Callable\n" + + "\n" + + "class MyClass:\n" + + " def foo(self, bar: Type[Self]) -> None: ...\n" + + "\n" + + "\n" + + "class SubClass(MyClass):\n" + + " pass\n" + + "\n" + + "\n" + + "myClass = MyClass()\n" + + "subClass = MySubClass()\n" + + "\n" + + "myClass.foo(myClass)\n" + + "myClass.foo(subClass)\n" + + "myClass.foo(MyClass)\n" + + "myClass.foo(SubClass)\n" + + "\n" + + "subClass.foo(myClass)\n" + + "subClass.foo(subClass)\n" + + "subClass.foo(MyClass)\n" + + "subClass.foo(SubClass)"); + } + + // PY-53104 + public void testParameterTypeSelfUnion() { + doTestByText("from typing import Self, Callable\n" + + "\n" + + "class MyClass:\n" + + " def foo(self, bar: Self | None | int) -> None: ...\n" + + "\n" + + "\n" + + "class SubClass(MyClass):\n" + + " pass\n" + + "\n" + + "\n" + + "myClass = MyClass()\n" + + "subClass = SubClass()\n" + + "\n" + + "myClass.foo(myClass)\n" + + "myClass.foo(subClass)\n" + + "myClass.foo(42)\n" + + "myClass.foo(None)\n" + + "myClass.foo(\"\")\n" + + "\n" + + "subClass.foo(myClass)\n" + + "subClass.foo(subClass)\n" + + "subClass.foo(42)\n" + + "subClass.foo(None)\n" + + "subClass.foo(\"\")"); + } + + // PY-53104 + public void testParameterTypeSelfReturnAsParameter() { + doTestByText("from typing import Self, Callable\n" + + "\n" + + "class MyClass:\n" + + " def foo(self, bar: Self) -> Self: ...\n" + + "\n" + + "\n" + + "class SubClass(MyClass):\n" + + " pass\n" + + "\n" + + "\n" + + "myClass = MyClass()\n" + + "subClass = SubClass()\n" + + "\n" + + "myClass.foo(myClass.foo(myClass))\n" + + "myClass.foo(subClass.foo(subClass))\n" + + "myClass.foo(myClass.foo(subClass))\n" + + "myClass.foo(subClass.foo(myClass))\n" + + "\n" + + "subClass.foo(myClass.foo(myClass))\n" + + "subClass.foo(subClass.foo(subClass))\n" + + "subClass.foo(myClass.foo(subClass))\n" + + "subClass.foo(subClass.foo(myClass))"); + } + + // PY-53104 + public void testProtocolSelfClass() { + doTestByText("from __future__ import annotations\n" + + "from typing import Self, Protocol\n" + + "\n" + + "\n" + + "class MyProtocol(Protocol):\n" + + " def foo(self, bar: float) -> Self: ...\n" + + "\n" + + "\n" + + "class MyClass:\n" + + " def foo(self, bar: float) -> MyClass:\n" + + " pass\n" + + "\n" + + "\n" + + "def accepts_protocol(obj: MyProtocol) -> None:\n" + + " print(obj)\n" + + "\n" + + "\n" + + "obj = MyClass()\n" + + "accepts_protocol(obj)\n"); + } + + // PY-53104 + public void testProtocolSelfSubclass() { + doTestByText("from __future__ import annotations\n" + + "from typing import Self, Protocol\n" + + "\n" + + "\n" + + "class MyProtocol(Protocol):\n" + + " def foo(self, bar: float) -> Self: ...\n" + + "\n" + + "\n" + + "class MyClass:\n" + + " def foo(self, bar: float) -> MySubClass:\n" + + " pass\n" + + "\n" + + "\n" + + "class MySubClass(MyClass):\n" + + " pass\n" + + "\n" + + "\n" + + "def accepts_protocol(obj: MyProtocol) -> None:\n" + + " print(obj)\n" + + "\n" + + "\n" + + "obj = MyClass()\n" + + "accepts_protocol(obj)\n"); + } + + // PY-53104 + public void testProtocolSelfOtherClass() { + doTestByText("from __future__ import annotations\n" + + "from typing import Self, Protocol\n" + + "\n" + + "\n" + + "class MyProtocol(Protocol):\n" + + " def foo(self, bar: float) -> Self: ...\n" + + "\n" + + "\n" + + "class MyClass:\n" + + " def foo(self, bar: float) -> int:\n" + + " pass\n" + + "\n" + + "\n" + + "def accepts_protocol(obj: MyProtocol) -> None:\n" + + " print(obj)\n" + + "\n" + + "\n" + + "obj = MyClass()\n" + + "accepts_protocol(obj)\n"); + } + + // PY-53104 + public void testProtocolSelfNotSubclass() { + doTestByText("from __future__ import annotations\n" + + "from typing import Self, Protocol\n" + + "\n" + + "\n" + + "class MyProtocol(Protocol):\n" + + " def foo(self, bar: float) -> Self: ...\n" + + "\n" + + "\n" + + "class MyClass:\n" + + " def foo(self, bar: float) -> MyClassNotSubclass:\n" + + " pass\n" + + "\n" + + "\n" + + "class MyClassNotSubclass:\n" + + " def foo(self, bar: float) -> int:\n" + + " pass\n" + + "\n" + + "\n" + + "def accepts_protocol(obj: MyProtocol) -> None:\n" + + " print(obj)\n" + + "\n" + + "\n" + + "obj = MyClass()\n" + + "accepts_protocol(obj)\n"); + } + + // PY-53104 + public void testProtocolSelfSelf() { + doTestByText("from __future__ import annotations\n" + + "from typing import Self, Protocol\n" + + "\n" + + "\n" + + "class MyProtocol(Protocol):\n" + + " def foo(self, bar: float) -> Self: ...\n" + + "\n" + + "\n" + + "class MyClass:\n" + + " def foo(self, bar: float) -> Self:\n" + + " pass\n" + + "\n" + + "\n" + + "def accepts_protocol(obj: MyProtocol) -> None:\n" + + " print(obj)\n" + + "\n" + + "\n" + + "obj = MyClass()\n" + + "accepts_protocol(obj)\n"); + } + + // PY-56785 + public void testTypingSelfNoInspectionReturnSelfMethod() { + doTestByText("from typing import Self\n" + + "\n" + + "\n" + + "class Builder:\n" + + " def foo(self) -> Self:\n" + + " result = self.bar()\n" + + " return result\n" + + "\n" + + " def bar(self) -> Self:\n" + + " pass\n"); + } + + // PY-56785 + public void testTypingSelfClassMethodReturnClsNoHighlighting() { + doTestByText("from typing import Self\n" + + "\n" + + "class Shape:\n" + + "\n" + + " def __init__(self, scale: float):\n" + + " self.scale = None\n" + + "\n" + + " @classmethod\n" + + " def from_config(cls, config: dict[str, float]) -> Self:\n" + + " return cls(config[\"scale\"])\n"); + } + + // PY-56785 + public void _testTypingSelfAndExplicitClassReturn() { + doTestByText("from __future__ import annotations\n" + + "from typing import Self\n" + + "\n" + + "class SomeClass():\n" + + " def foo(self, bar: Self) -> Self:\n" + + " return SomeClass()\n"); + } + + // PY-56785 + public void _testTypingSelfReturnSubClassMethod() { + doTestByText("from typing import Self\n" + + "\n" + + "\n" + + "class Builder:\n" + + " def foo(self) -> Self:\n" + + " result = SubBuilder().bar()\n" + + " return result\n" + + "\n" + + " def bar(self) -> Self:\n" + + " pass\n" + + "\n" + + "\n" + + "class SubBuilder(Builder):\n" + + " pass\n"); + } } diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java index f2bd447f4166..b95e921fb941 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -1202,6 +1202,101 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { ..."""); } + // PY-53104 + public void testInstanceAndClassChecksOnTypingSelf() { + doTestByText("from typing import Self\n" + + "\n" + + "\n" + + "class A:\n" + + " pass\n" + + "\n" + + "\n" + + "class B:\n" + + " def foo(self: Self):\n" + + " assert isinstance(A(), Self)\n" + + " assert issubclass(A, Self)\n"); + } + + // PY-53104 + public void testTypingSelfSubscription() { + doTestByText("from typing import Self, Generic, TypeVar\n" + + "\n" + + "T = TypeVar(\"T\")\n" + + "\n" + + "\n" + + "class A(Generic[T]):\n" + + " def foo(self):\n" + + " x: Self[int]\n"); + } + + // PY-53104 + public void testTypingSelfAnnotationOutsideClass() { + doTestByText("from typing import Self\n" + + "\n" + + "def foo() -> Self:\n" + + " pass\n"); + } + + // PY-53104 + public void testTypingSelfAnnotationForVariableOutsideClass() { + doTestByText("from typing import Self\n" + + "\n" + + "something: Self | None = None\n"); + } + + // PY-53104 + public void testTypingSelfInStaticMethod() { + doTestByText("from __future__ import annotations\n" + + "from typing import Self\n" + + "\n" + + "class SomeClass:\n" + + " @staticmethod\n" + + " def foo(bar: Self) -> Self:\n" + + " return bar\n"); + } + + // PY-53104 + public void testTypingSelfParameterHasDifferentAnnotation() { + doTestByText("from __future__ import annotations\n" + + "from typing import Self\n" + + "\n" + + "class SomeClass:\n" + + " def foo(self: SomeClass, bar: Self) -> Self:\n" + + " return self\n"); + } + + // PY-53104 + public void testTypingSelfClsParameterHasDifferentAnnotation() { + doTestByText("from __future__ import annotations\n" + + "from typing import Self\n" + + "\n" + + "class SomeClass:\n" + + " @classmethod\n" + + " def foo(cls: SomeClass, bar: Self) -> Self:\n" + + " return self\n"); + } + + // PY-53104 + public void testTypingSelfInStaticMethodBody() { + doTestByText("from typing import Self\n" + + "\n" + + "\n" + + "class C:\n" + + " @staticmethod\n" + + " def m():\n" + + " obj: Self = None"); + } + + // PY-53104 + public void testTypingSelfInFunctionBodySelfParameterHasDifferentAnnotation() { + doTestByText("from typing import Self\n" + + "\n" + + "\n" + + "class C:\n" + + " def m(self: C):\n" + + " obj: Self = None\n"); + } + @NotNull @Override protected Class getInspectionClass() {