PY-61651 Deprecation highlighting with PEP 702 @deprecated decorator

GitOrigin-RevId: 426e7001d20849d7029fea55431d3e2cfae3eb11
This commit is contained in:
Andrey Vokin
2024-06-10 09:29:06 +02:00
committed by intellij-monorepo-bot
parent c02d4c094d
commit 673383c3da
24 changed files with 253 additions and 52 deletions

View File

@@ -2,8 +2,8 @@ import sys
from _warnings import warn as warn, warn_explicit as warn_explicit
from collections.abc import Sequence
from types import ModuleType, TracebackType
from typing import Any, Generic, TextIO, TypeVar, overload
from typing_extensions import Literal, TypeAlias
from typing import Any, Generic, Literal, TextIO, TypeVar, overload
from typing_extensions import LiteralString, TypeAlias
__all__ = [
"warn",
@@ -16,6 +16,10 @@ __all__ = [
"catch_warnings",
]
if sys.version_info >= (3, 13):
__all__ += ["deprecated"]
_T = TypeVar("_T")
_W = TypeVar("_W", bound=list[WarningMessage] | None)
_ActionKind: TypeAlias = Literal["default", "error", "ignore", "always", "module", "once"]
@@ -110,3 +114,11 @@ class catch_warnings(Generic[_W]):
def __exit__(
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None: ...
if sys.version_info >= (3, 13):
class deprecated:
message: LiteralString
category: type[Warning] | None
stacklevel: int
def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
def __call__(self, arg: _T, /) -> _T: ...

View File

@@ -41,8 +41,7 @@ import java.util.List;
@ApiStatus.Experimental
public interface PyAstClass extends PsiNameIdentifierOwner, PyAstCompoundStatement, PyAstDocStringOwner, AstScopeOwner,
PyAstDecoratable, PyAstTypedElement, PyAstQualifiedNameOwner, PyAstStatementListContainer,
PyAstWithAncestors,
PyAstTypeParameterListOwner {
PyAstWithAncestors, PyAstTypeParameterListOwner {
PyAstClass[] EMPTY_ARRAY = new PyAstClass[0];
ArrayFactory<PyAstClass> ARRAY_FACTORY = count -> count == 0 ? EMPTY_ARRAY : new PyAstClass[count];

View File

@@ -3,7 +3,6 @@ package com.jetbrains.python.ast;
import com.intellij.lang.ASTNode;
import com.intellij.psi.*;
import com.intellij.psi.stubs.StubElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayFactory;
import com.intellij.util.ArrayUtil;
@@ -12,7 +11,6 @@ import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonDialectsTokenSetProvider;
import com.jetbrains.python.ast.impl.PyPsiUtilsCore;
import com.jetbrains.python.ast.impl.PyUtilCore;
import com.jetbrains.python.ast.controlFlow.AstScopeOwner;
import com.jetbrains.python.ast.docstring.DocStringUtilCore;
@@ -21,8 +19,6 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -32,7 +28,7 @@ import java.util.Optional;
@ApiStatus.Experimental
public interface PyAstFunction extends PsiNameIdentifierOwner, PyAstCompoundStatement,
PyAstDecoratable, PyAstCallable, PyAstStatementListContainer, PyAstPossibleClassMember,
AstScopeOwner, PyAstDocStringOwner, PyAstTypeCommentOwner, PyAstAnnotationOwner, PyAstTypeParameterListOwner {
AstScopeOwner, PyAstDocStringOwner, PyAstTypeCommentOwner, PyAstAnnotationOwner, PyAstTypeParameterListOwner{
PyAstFunction[] EMPTY_ARRAY = new PyAstFunction[0];
ArrayFactory<PyAstFunction> ARRAY_FACTORY = count -> count == 0 ? EMPTY_ARRAY : new PyAstFunction[count];
@@ -88,39 +84,6 @@ public interface PyAstFunction extends PsiNameIdentifierOwner, PyAstCompoundStat
}
}
/**
* If the function raises a DeprecationWarning or a PendingDeprecationWarning, returns the explanation text provided for the warning..
*
* @return the deprecation message or null if the function is not deprecated.
*/
@Nullable
default String getDeprecationMessage() {
return extractDeprecationMessage();
}
@Nullable
default String extractDeprecationMessage() {
PyAstStatementList statementList = getStatementList();
return extractDeprecationMessage(Arrays.asList(statementList.getStatements()));
}
static @Nullable String extractDeprecationMessage(List<? extends PyAstStatement> statements) {
for (PyAstStatement statement : statements) {
if (statement instanceof PyAstExpressionStatement expressionStatement) {
if (expressionStatement.getExpression() instanceof PyAstCallExpression callExpression) {
if (callExpression.isCalleeText(PyNames.WARN)) {
PyAstReferenceExpression warningClass = callExpression.getArgument(1, PyAstReferenceExpression.class);
if (warningClass != null && (PyNames.DEPRECATION_WARNING.equals(warningClass.getReferencedName()) ||
PyNames.PENDING_DEPRECATION_WARNING.equals(warningClass.getReferencedName()))) {
return PyPsiUtilsCore.strValue(callExpression.getArguments()[0]);
}
}
}
}
}
return null;
}
@Override
@Nullable
default String getDocStringValue() {

View File

@@ -0,0 +1,22 @@
package com.jetbrains.python.ast.impl
import com.jetbrains.python.ast.PyAstDecoratable
import com.jetbrains.python.ast.PyAstStringLiteralExpression
val deprecatedDecoratorContainers = arrayOf("typing_extensions.pyi", "warnings.pyi")
fun extractDeprecationMessageFromDecorator(element: PyAstDecoratable): String? {
val deprecatedDecorator = element.decoratorList?.decorators?.firstOrNull { it.name == "deprecated" } ?: return null
val annotationClass = deprecatedDecorator.callee?.reference?.resolve() ?: return null
if (annotationClass.containingFile?.name !in deprecatedDecoratorContainers) {
return null
}
val argumentList = deprecatedDecorator.argumentList ?: return null
if (argumentList.arguments.isEmpty()) {
return null
}
val argument = argumentList.arguments[0] as? PyAstStringLiteralExpression ?: return null
return argument.stringValue
}

View File

@@ -36,12 +36,14 @@ import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import static com.jetbrains.python.ast.impl.PyDeprecationUtilKt.extractDeprecationMessageFromDecorator;
/**
* Represents a class declaration in source.
*/
public interface PyClass extends PyAstClass, PsiNameIdentifierOwner, PyCompoundStatement, PyDocStringOwner, StubBasedPsiElement<PyClassStub>,
ScopeOwner, PyDecoratable, PyTypedElement, PyQualifiedNameOwner, PyStatementListContainer, PyWithAncestors,
PyTypeParameterListOwner {
PyTypeParameterListOwner, PyDeprecatable {
PyClass[] EMPTY_ARRAY = new PyClass[0];
ArrayFactory<PyClass> ARRAY_FACTORY = count -> count == 0 ? EMPTY_ARRAY : new PyClass[count];
@@ -367,6 +369,12 @@ public interface PyClass extends PyAstClass, PsiNameIdentifierOwner, PyCompoundS
@Nullable
PyClassLikeType getType(@NotNull TypeEvalContext context);
@Nullable
@Override
default String getDeprecationMessage() {
return extractDeprecationMessageFromDecorator(this);
}
@Override
@Nullable
default PyStringLiteralExpression getDocStringExpression() {

View File

@@ -0,0 +1,9 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.psi;
import org.jetbrains.annotations.Nullable;
public interface PyDeprecatable {
@Nullable
String getDeprecationMessage();
}

View File

@@ -4,7 +4,9 @@ package com.jetbrains.python.psi;
import com.intellij.psi.PsiNameIdentifierOwner;
import com.intellij.psi.StubBasedPsiElement;
import com.intellij.util.ArrayFactory;
import com.jetbrains.python.ast.PyAstFunction;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.ast.*;
import com.jetbrains.python.ast.impl.PyPsiUtilsCore;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.psi.stubs.PyFunctionStub;
import com.jetbrains.python.psi.types.PyType;
@@ -12,14 +14,18 @@ import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import static com.jetbrains.python.ast.impl.PyDeprecationUtilKt.extractDeprecationMessageFromDecorator;
/**
* Function declaration in source (the {@code def} and everything within).
*/
public interface PyFunction extends PyAstFunction, StubBasedPsiElement<PyFunctionStub>, PsiNameIdentifierOwner, PyCompoundStatement,
PyDecoratable, PyCallable, PyStatementListContainer, PyPossibleClassMember,
ScopeOwner, PyDocStringOwner, PyTypeCommentOwner, PyAnnotationOwner, PyTypeParameterListOwner {
ScopeOwner, PyDocStringOwner, PyTypeCommentOwner, PyAnnotationOwner, PyTypeParameterListOwner,
PyDeprecatable {
PyFunction[] EMPTY_ARRAY = new PyFunction[0];
ArrayFactory<PyFunction> ARRAY_FACTORY = count -> count == 0 ? EMPTY_ARRAY : new PyFunction[count];
@@ -66,6 +72,44 @@ public interface PyFunction extends PyAstFunction, StubBasedPsiElement<PyFunctio
return (PyStringLiteralExpression)PyAstFunction.super.getDocStringExpression();
}
/**
* If the function raises a DeprecationWarning or a PendingDeprecationWarning, returns the explanation text provided for the warning..
*
* @return the deprecation message or null if the function is not deprecated.
*/
@Nullable
@Override
default String getDeprecationMessage() {
return extractDeprecationMessage();
}
@Nullable
default String extractDeprecationMessage() {
String deprecationMessageFromDecorator = extractDeprecationMessageFromDecorator(this);
if (deprecationMessageFromDecorator != null) {
return deprecationMessageFromDecorator;
}
PyAstStatementList statementList = getStatementList();
return extractDeprecationMessage(Arrays.asList(statementList.getStatements()));
}
static @Nullable String extractDeprecationMessage(List<? extends PyAstStatement> statements) {
for (PyAstStatement statement : statements) {
if (statement instanceof PyAstExpressionStatement expressionStatement) {
if (expressionStatement.getExpression() instanceof PyAstCallExpression callExpression) {
if (callExpression.isCalleeText(PyNames.WARN)) {
PyAstReferenceExpression warningClass = callExpression.getArgument(1, PyAstReferenceExpression.class);
if (warningClass != null && (PyNames.DEPRECATION_WARNING.equals(warningClass.getReferencedName()) ||
PyNames.PENDING_DEPRECATION_WARNING.equals(warningClass.getReferencedName()))) {
return PyPsiUtilsCore.strValue(callExpression.getArguments()[0]);
}
}
}
}
}
return null;
}
@Override
@Nullable
default PyClass getContainingClass() {

View File

@@ -47,6 +47,9 @@ public interface PyClassStub extends NamedStub<PyClass> {
@Nullable
String getDocString();
@Nullable
String getDeprecationMessage();
/**
* @return literal text of expressions in the base classes list.
*/

View File

@@ -48,6 +48,17 @@ public final class PyDeprecationInspection extends PyInspection {
super(holder, context);
}
@Override
public void visitPyBinaryExpression(@NotNull PyBinaryExpression node) {
final PsiElement resolveResult = node.getReference(getResolveContext()).resolve();
if (resolveResult instanceof PyDeprecatable) {
@NlsSafe String deprecationMessage = ((PyDeprecatable)resolveResult).getDeprecationMessage();
if (deprecationMessage != null) {
registerProblem(node.getPsiOperator(), deprecationMessage, ProblemHighlightType.WARNING);
}
}
}
@Override
public void visitPyReferenceExpression(@NotNull PyReferenceExpression node) {
final PyExceptPart exceptPart = PsiTreeUtil.getParentOfType(node, PyExceptPart.class);
@@ -62,8 +73,8 @@ public final class PyDeprecationInspection extends PyInspection {
if (resolveResult != null && element != resolveResult.getContainingFile()) return;
}
@NlsSafe String deprecationMessage = null;
if (resolveResult instanceof PyFunction) {
deprecationMessage = ((PyFunction)resolveResult).getDeprecationMessage();
if (resolveResult instanceof PyDeprecatable deprecatable) {
deprecationMessage = deprecatable.getDeprecationMessage();
}
else if (resolveResult instanceof PyFile) {
deprecationMessage = ((PyFile)resolveResult).getDeprecationMessage();

View File

@@ -61,7 +61,7 @@ public class PyFileElementType extends IStubFileElementType<PyFileStub> {
@Override
public int getStubVersion() {
// Don't forget to update versions of indexes that use the updated stub-based elements
return 87;
return 88;
}
@Nullable

View File

@@ -1305,6 +1305,15 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
return PyClass.super.getDocStringValue();
}
@Override
public @Nullable String getDeprecationMessage() {
PyClassStub stub = getStub();
if (stub != null) {
return stub.getDeprecationMessage();
}
return PyClass.super.getDeprecationMessage();
}
@Nullable
@Override
public StructuredDocString getStructuredDocString() {

View File

@@ -28,7 +28,6 @@ import com.jetbrains.python.PyNames;
import com.jetbrains.python.PythonFileType;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.ast.PyAstElementVisitor;
import com.jetbrains.python.ast.PyAstFunction;
import com.jetbrains.python.ast.impl.PyUtilCore;
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
import com.jetbrains.python.documentation.docstrings.DocStringUtil;
@@ -701,7 +700,7 @@ public class PyFileImpl extends PsiFileBase implements PyFile, PyExpression {
public String extractDeprecationMessage() {
if (canHaveDeprecationMessage(getText())) {
return PyAstFunction.extractDeprecationMessage(getStatements());
return PyFunction.extractDeprecationMessage(getStatements());
}
else {
return null;

View File

@@ -7,7 +7,6 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.stubs.*;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyStubElementTypes;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyClassImpl;
@@ -54,6 +53,7 @@ public class PyClassElementType extends PyStubElementType<PyClassStub, PyClass>
PyPsiUtils.asQualifiedName(psi.getMetaClassExpression()),
psi.getOwnSlots(),
PyPsiUtils.strValue(psi.getDocStringExpression()),
psi.getDeprecationMessage(),
getStubElementType(),
createCustomStub(psi));
}
@@ -133,6 +133,7 @@ public class PyClassElementType extends PyStubElementType<PyClassStub, PyClass>
final String docString = pyClassStub.getDocString();
dataStream.writeUTFFast(docString != null ? docString : "");
dataStream.writeName(pyClassStub.getDeprecationMessage());
serializeCustomStub(pyClassStub.getCustomStub(PyCustomClassStub.class), dataStream);
}
@@ -162,9 +163,11 @@ public class PyClassElementType extends PyStubElementType<PyClassStub, PyClass>
final String docStringInStub = dataStream.readUTFFast();
final String docString = docStringInStub.length() > 0 ? docStringInStub : null;
final String deprecationMessage = dataStream.readNameString();
final PyCustomClassStub customStub = deserializeCustomStub(dataStream);
return new PyClassStubImpl(name, parentStub, superClasses, baseClassesText, metaClass, slots, docString,
return new PyClassStubImpl(name, parentStub, superClasses, baseClassesText, metaClass, slots, docString, deprecationMessage,
getStubElementType(), customStub);
}

View File

@@ -34,6 +34,9 @@ public class PyClassStubImpl extends StubBase<PyClass> implements PyClassStub {
@NotNull
private final List<String> mySuperClassesText;
@Nullable
private final String myDeprecationMessage;
@Nullable
private final PyCustomClassStub myCustomStub;
@@ -44,6 +47,7 @@ public class PyClassStubImpl extends StubBase<PyClass> implements PyClassStub {
@Nullable QualifiedName metaClass,
@Nullable List<String> slots,
@Nullable String docString,
@Nullable String deprecationMessage,
@NotNull IStubElementType stubElementType,
@Nullable PyCustomClassStub customStub) {
super(parentStub, stubElementType);
@@ -53,6 +57,7 @@ public class PyClassStubImpl extends StubBase<PyClass> implements PyClassStub {
myMetaClass = metaClass;
mySlots = slots;
myDocString = docString;
myDeprecationMessage = deprecationMessage;
myCustomStub = customStub;
}
@@ -86,6 +91,12 @@ public class PyClassStubImpl extends StubBase<PyClass> implements PyClassStub {
return myDocString;
}
@Nullable
@Override
public String getDeprecationMessage() {
return myDeprecationMessage;
}
@NotNull
@Override
public List<String> getSuperClassesText() {

View File

@@ -0,0 +1,21 @@
class deprecated():
def __init__(
self,
message: str,
/,
*,
category: type[Warning] | None = DeprecationWarning,
stacklevel: int = 1,
) -> None:
return None
def __call__(self, arg, /):
pass
@deprecated("deprecated")
def my_method():
pass
my_method()

View File

@@ -0,0 +1,9 @@
from warnings import deprecated
class Spam:
@deprecated("There is enough spam in the world")
def __add__(self, other: object) -> object:
pass
Spam() <warning descr="There is enough spam in the world">+</warning> 1

View File

@@ -0,0 +1,3 @@
from library import <warning descr="Use Spam instead">Ham</warning>
ham = <warning descr="Use Spam instead">Ham</warning>()

View File

@@ -0,0 +1,4 @@
from warnings import deprecated
@deprecated("Use Spam instead")
class Ham: ...

View File

@@ -0,0 +1,7 @@
from typing_extensions import deprecated
@deprecated("Deprecated class")
class MyClass:
pass
var = <warning descr="Deprecated class">MyClass</warning>()

View File

@@ -0,0 +1,7 @@
from warnings import deprecated
@deprecated("It is pining for the fiords")
def norwegian_blue(x: int) -> int:
pass
<warning descr="It is pining for the fiords">norwegian_blue</warning>(1)

View File

@@ -0,0 +1,7 @@
from warnings import deprecated
@deprecated("Deprecated class", category=DeprecationWarning, stacklevel=1)
class MyClass:
pass
var = <warning descr="Deprecated class">MyClass</warning>()

View File

@@ -0,0 +1,5 @@
from warnings import deprecated
@deprecated("Use Spam instead")
class Ham:
pass

View File

@@ -83,6 +83,43 @@ public class PyDeprecationTest extends PyTestCase {
myFixture.checkHighlighting(true, false, false);
}
public void testDeprecatedClass() {
myFixture.enableInspections(PyDeprecationInspection.class);
myFixture.copyDirectoryToProject("deprecation/deprecatedClass", "");
myFixture.configureByFile("deprecatedClass.py");
myFixture.checkHighlighting(true, false, false);
}
public void testDeprecatedMethod() {
myFixture.enableInspections(PyDeprecationInspection.class);
myFixture.configureByFile("deprecation/deprecatedMethod.py");
myFixture.checkHighlighting(true, false, false);
}
public void testDeprecatedAdd() {
myFixture.enableInspections(PyDeprecationInspection.class);
myFixture.configureByFile("deprecation/deprecatedAdd.py");
myFixture.checkHighlighting(true, false, false);
}
public void testDeprecatedFromTypingExtension() {
myFixture.enableInspections(PyDeprecationInspection.class);
myFixture.configureByFile("deprecation/deprecatedFromTypingExtension.py");
myFixture.checkHighlighting(true, false, false);
}
public void testDeprecatedWithSeveralArguments() {
myFixture.enableInspections(PyDeprecationInspection.class);
myFixture.configureByFile("deprecation/deprecatedWithSeveralArguments.py");
myFixture.checkHighlighting(true, false, false);
}
public void testCustomDeprecatedAnnotation() {
myFixture.enableInspections(PyDeprecationInspection.class);
myFixture.configureByFile("deprecation/customDeprecatedAnnotation.py");
myFixture.checkHighlighting(true, false, false);
}
public void testAbcDeprecatedAbstracts() {
myFixture.enableInspections(PyDeprecationInspection.class);
myFixture.configureByFile("deprecation/abcDeprecatedAbstracts.py");

View File

@@ -1107,6 +1107,14 @@ public class PyStubsTest extends PyTestCase {
doTestTypeParameterStub(cls, file);
}
// PY-61651 Deprecation highlighting with PEP 702 @deprecated decorator
public void testDeprecationMessageInClass() {
PyFile file = getTestFile();
PyClass cls = file.findTopLevelClass("Ham");
assertNotNull(cls);
assertEquals("Use Spam instead", cls.getDeprecationMessage());
}
// PY-62608
public void testTypeAliasStatement() {
PyFile file = getTestFile();