From 2e617b803c84e9b2e4516846c70eb0efc5487e72 Mon Sep 17 00:00:00 2001 From: Lada Gagina Date: Thu, 4 Jun 2020 17:31:31 +0300 Subject: [PATCH] IDEA-CR-63687: PY-41305 Add PEP 614 support Allows to use any expression as a decorator GitOrigin-RevId: e92af1ebd2c4d7024971fd4542bfe52640faaa26 --- .../com/jetbrains/python/psi/PyDecorator.java | 4 + .../PyArgumentEqualDefaultInspection.java | 2 +- .../PyCallingNonCallableInspection.java | 2 + .../python/parsing/FunctionParsing.java | 12 +-- .../python/psi/impl/PyDecoratorImpl.java | 29 +++--- .../validation/CompatibilityVisitor.java | 13 +++ .../validation/HighlightingAnnotator.java | 1 + .../validation/PyDefinitionsAnnotator.java | 8 +- .../highlighting/expressionAsDecorator.py | 12 +++ .../expressionInDecorators.py | 9 ++ .../PyUnusedLocalInspection/unusedFunction.py | 2 +- python/testData/psi/BadDecoratorNotMethod.txt | 2 - python/testData/psi/ClassDecorators.txt | 4 - python/testData/psi/CommentAfterDecorator.txt | 2 - python/testData/psi/DecoratedAsyncDef.txt | 25 +++--- python/testData/psi/DecoratedFunction.txt | 23 +++-- .../testData/psi/ExpressionsInDecorators.py | 10 +++ .../testData/psi/ExpressionsInDecorators.txt | 89 +++++++++++++++++++ .../com/jetbrains/python/PyDecoratorTest.java | 2 +- .../python/PythonHighlightingTest.java | 5 ++ .../PyCompatibilityInspectionTest.java | 5 ++ .../python/parsing/PythonParsingTest.java | 5 ++ 22 files changed, 205 insertions(+), 61 deletions(-) create mode 100644 python/testData/highlighting/expressionAsDecorator.py create mode 100644 python/testData/inspections/PyCompatibilityInspection/expressionInDecorators.py create mode 100644 python/testData/psi/ExpressionsInDecorators.py create mode 100644 python/testData/psi/ExpressionsInDecorators.txt diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/PyDecorator.java b/python/python-psi-api/src/com/jetbrains/python/psi/PyDecorator.java index 03c409220708..387d767c430a 100644 --- a/python/python-psi-api/src/com/jetbrains/python/psi/PyDecorator.java +++ b/python/python-psi-api/src/com/jetbrains/python/psi/PyDecorator.java @@ -54,4 +54,8 @@ public interface PyDecorator extends PyCallExpression, StubBasedPsiElement callables = node.multiResolveCalleeFunction(getResolveContext()); diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyCallingNonCallableInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyCallingNonCallableInspection.java index 1375a73cf139..958a921a9ca9 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyCallingNonCallableInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyCallingNonCallableInspection.java @@ -66,6 +66,8 @@ public class PyCallingNonCallableInspection extends PyInspection { } private void checkCallable(@NotNull PyElement node, @Nullable PyExpression callee) { + if (node.getParent() instanceof PyDecorator) return; //we've already been here + if (callee != null && isCallable(callee, myTypeEvalContext) == Boolean.FALSE) { final PyType calleeType = myTypeEvalContext.getType(callee); String message = "Expression is not callable"; diff --git a/python/python-psi-impl/src/com/jetbrains/python/parsing/FunctionParsing.java b/python/python-psi-impl/src/com/jetbrains/python/parsing/FunctionParsing.java index cc4e8357453a..8c60af0c5e03 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/parsing/FunctionParsing.java +++ b/python/python-psi-impl/src/com/jetbrains/python/parsing/FunctionParsing.java @@ -16,7 +16,6 @@ package com.jetbrains.python.parsing; import com.intellij.lang.SyntaxTreeBuilder; -import com.intellij.lang.WhitespacesBinders; import com.intellij.psi.tree.IElementType; import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PyTokenTypes; @@ -83,14 +82,9 @@ public class FunctionParsing extends Parsing { while (myBuilder.getTokenType() == PyTokenTypes.AT) { SyntaxTreeBuilder.Marker decoratorMarker = myBuilder.mark(); myBuilder.advanceLexer(); - getStatementParser().parseDottedName(); - if (myBuilder.getTokenType() == PyTokenTypes.LPAR) { - getExpressionParser().parseArgumentList(); - } - else { // empty arglist node, so we always have it - SyntaxTreeBuilder.Marker argListMarker = myBuilder.mark(); - argListMarker.setCustomEdgeTokenBinders(WhitespacesBinders.GREEDY_LEFT_BINDER, null); - argListMarker.done(PyElementTypes.ARGUMENT_LIST); + + if (!getExpressionParser().parseSingleExpression(false)) { + myBuilder.error(message("PARSE.expected.expression")); } if (atToken(PyTokenTypes.STATEMENT_BREAK)) { decoratorMarker.done(PyElementTypes.DECORATOR_CALL); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyDecoratorImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyDecoratorImpl.java index 1fef662fcf4e..63ad6993b464 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyDecoratorImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyDecoratorImpl.java @@ -68,8 +68,13 @@ public class PyDecoratorImpl extends StubBasedPsiElementBase im @Override public boolean hasArgumentList() { - final ASTNode arglistNode = getNode().findChildByType(PyElementTypes.ARGUMENT_LIST); - return (arglistNode != null) && (arglistNode.findChildByType(PyTokenTypes.LPAR) != null); + return findChildByClass(PyCallExpression.class) != null; + } + + @Override + public PyArgumentList getArgumentList() { + final PyCallExpression callExpr = findChildByClass(PyCallExpression.class); + return callExpr != null ? callExpr.getArgumentList() : null; } @Override @@ -80,7 +85,8 @@ public class PyDecoratorImpl extends StubBasedPsiElementBase im return stub.getQualifiedName(); } else { - final PyReferenceExpression node = PsiTreeUtil.getChildOfType(this, PyReferenceExpression.class); + final PyCallExpression callExpr = findChildByClass(PyCallExpression.class); + final PyReferenceExpression node = PsiTreeUtil.getChildOfType(callExpr != null ? callExpr : this, PyReferenceExpression.class); if (node != null) { return node.asQualifiedName(); } @@ -91,15 +97,8 @@ public class PyDecoratorImpl extends StubBasedPsiElementBase im @Override @Nullable public PyExpression getCallee() { - try { - return (PyExpression)getFirstChild().getNextSibling(); // skip the @ before call - } - catch (NullPointerException npe) { // no sibling - return null; - } - catch (ClassCastException cce) { // error node instead - return null; - } + final PyExpression exprAfterAt = findChildByClass(PyExpression.class); + return exprAfterAt instanceof PyCallExpression ? ((PyCallExpression)exprAfterAt).getCallee() : exprAfterAt; } @NotNull @@ -151,4 +150,10 @@ public class PyDecoratorImpl extends StubBasedPsiElementBase im public PyType getType(@NotNull TypeEvalContext context, @NotNull TypeEvalContext.Key key) { return PyCallExpressionHelper.getCallType(this, context, key); } + + @Override + public boolean hasPlainReferenceCallee() { + final PyExpression callee = getCallee(); + return callee instanceof PyReferenceExpression && ((PyReferenceExpression)callee).asQualifiedName() != null; + } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java index f98d50cdbed5..29e7020f5f20 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java +++ b/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java @@ -8,6 +8,7 @@ import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiErrorElement; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; @@ -740,4 +741,16 @@ public abstract class CompatibilityVisitor extends PyAnnotator { node); } } + + @Override + public void visitPyDecoratorList(PyDecoratorList node) { + super.visitPyDecoratorList(node); + + for (PyDecorator decorator : node.getDecorators()) { + if (PsiTreeUtil.getChildOfType(decorator, PsiErrorElement.class) == null && !decorator.hasPlainReferenceCallee()) { + registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON39) && registerForLanguageLevel(level), + " not support arbitrary expressions as a decorator", decorator); + } + } + } } diff --git a/python/src/com/jetbrains/python/validation/HighlightingAnnotator.java b/python/src/com/jetbrains/python/validation/HighlightingAnnotator.java index f322db9e9faa..7ad054b0f9d2 100644 --- a/python/src/com/jetbrains/python/validation/HighlightingAnnotator.java +++ b/python/src/com/jetbrains/python/validation/HighlightingAnnotator.java @@ -83,6 +83,7 @@ public class HighlightingAnnotator extends PyAnnotator { @Override public void visitPyCallExpression(PyCallExpression node) { + if (node.getParent() instanceof PyDecorator) return; //if it's in decorator, then we've already highlighted it as a decorator final PyReferenceExpression callee = as(node.getCallee(), PyReferenceExpression.class); if (callee != null) { if (!callee.isQualified() && PyBuiltinCache.isInBuiltins(callee)) { diff --git a/python/src/com/jetbrains/python/validation/PyDefinitionsAnnotator.java b/python/src/com/jetbrains/python/validation/PyDefinitionsAnnotator.java index e84cd483e7e7..840ed9f9c35c 100644 --- a/python/src/com/jetbrains/python/validation/PyDefinitionsAnnotator.java +++ b/python/src/com/jetbrains/python/validation/PyDefinitionsAnnotator.java @@ -21,9 +21,10 @@ import com.intellij.psi.PsiElement; import com.jetbrains.python.PyNames; import com.jetbrains.python.highlighting.PyHighlighter; import com.jetbrains.python.psi.*; -import com.jetbrains.python.psi.impl.PyPsiUtils; import org.jetbrains.annotations.NotNull; +import java.util.Objects; + /** * Highlights class definitions, functrion definitions, and decorators. * User: dcheryasov @@ -79,9 +80,8 @@ public class PyDefinitionsAnnotator extends PyAnnotator { final PsiElement atSign = node.getFirstChild(); if (atSign != null) { addHighlightingAnnotation(atSign, PyHighlighter.PY_DECORATOR); - final PsiElement refExpression = PyPsiUtils.getNextNonWhitespaceSibling(atSign); - if (refExpression != null) { - addHighlightingAnnotation(refExpression, PyHighlighter.PY_DECORATOR); + if (node.hasPlainReferenceCallee()) { + addHighlightingAnnotation(Objects.requireNonNull(node.getCallee()), PyHighlighter.PY_DECORATOR); } } } diff --git a/python/testData/highlighting/expressionAsDecorator.py b/python/testData/highlighting/expressionAsDecorator.py new file mode 100644 index 000000000000..515af23ebcb9 --- /dev/null +++ b/python/testData/highlighting/expressionAsDecorator.py @@ -0,0 +1,12 @@ +@foo.bar +def f(): + pass + +@foo(1, 2, 3) +def f(): + pass + + +@foo[1].bar +def f(): + pass diff --git a/python/testData/inspections/PyCompatibilityInspection/expressionInDecorators.py b/python/testData/inspections/PyCompatibilityInspection/expressionInDecorators.py new file mode 100644 index 000000000000..f4622082a300 --- /dev/null +++ b/python/testData/inspections/PyCompatibilityInspection/expressionInDecorators.py @@ -0,0 +1,9 @@ +@x[0][1] +@my_decorator +def say_whee(): + print("Whee!") + +@foo[0].wrapper +@foo.bar() +def say_whee(): + print("Whee!") \ No newline at end of file diff --git a/python/testData/inspections/PyUnusedLocalInspection/unusedFunction.py b/python/testData/inspections/PyUnusedLocalInspection/unusedFunction.py index cc1e6c5245a3..9d820d43e7dd 100644 --- a/python/testData/inspections/PyUnusedLocalInspection/unusedFunction.py +++ b/python/testData/inspections/PyUnusedLocalInspection/unusedFunction.py @@ -40,6 +40,6 @@ def unused_inner_function_with_unknown_decorator(): pass def unused_inner_function_with_incomplete_decorator(): - @ + @ def func(): # pass pass diff --git a/python/testData/psi/BadDecoratorNotMethod.txt b/python/testData/psi/BadDecoratorNotMethod.txt index b2269d3ff516..c2e38d57ff8a 100644 --- a/python/testData/psi/BadDecoratorNotMethod.txt +++ b/python/testData/psi/BadDecoratorNotMethod.txt @@ -17,8 +17,6 @@ PyFile:BadDecoratorNotMethod.py PsiElement(Py:AT)('@') PyReferenceExpression: staticmethod PsiElement(Py:IDENTIFIER)('staticmethod') - PyArgumentList - PsiErrorElement:'@' or 'def' expected PsiWhiteSpace('\n ') diff --git a/python/testData/psi/ClassDecorators.txt b/python/testData/psi/ClassDecorators.txt index 671cffbc8fb9..ffdc26cba713 100644 --- a/python/testData/psi/ClassDecorators.txt +++ b/python/testData/psi/ClassDecorators.txt @@ -5,15 +5,11 @@ PyFile:ClassDecorators.py PsiElement(Py:AT)('@') PyReferenceExpression: foo PsiElement(Py:IDENTIFIER)('foo') - PyArgumentList - PsiWhiteSpace('\n') PyDecorator: @bar PsiElement(Py:AT)('@') PyReferenceExpression: bar PsiElement(Py:IDENTIFIER)('bar') - PyArgumentList - PsiWhiteSpace('\n') PsiElement(Py:CLASS_KEYWORD)('class') PsiWhiteSpace(' ') diff --git a/python/testData/psi/CommentAfterDecorator.txt b/python/testData/psi/CommentAfterDecorator.txt index 6e8214f79dda..6ee33924596a 100644 --- a/python/testData/psi/CommentAfterDecorator.txt +++ b/python/testData/psi/CommentAfterDecorator.txt @@ -5,8 +5,6 @@ PyFile:CommentAfterDecorator.py PsiElement(Py:AT)('@') PyReferenceExpression: uncallable_deco PsiElement(Py:IDENTIFIER)('uncallable_deco') - PyArgumentList - PsiWhiteSpace(' ') PsiComment(Py:END_OF_LINE_COMMENT)('# some comment') PsiWhiteSpace('\n') diff --git a/python/testData/psi/DecoratedAsyncDef.txt b/python/testData/psi/DecoratedAsyncDef.txt index c8b35f5b486e..5d9d2458f254 100644 --- a/python/testData/psi/DecoratedAsyncDef.txt +++ b/python/testData/psi/DecoratedAsyncDef.txt @@ -5,8 +5,6 @@ PyFile:DecoratedAsyncDef.py PsiElement(Py:AT)('@') PyReferenceExpression: foo PsiElement(Py:IDENTIFIER)('foo') - PyArgumentList - PsiWhiteSpace('\n') PsiElement(Py:ASYNC_KEYWORD)('async') PsiWhiteSpace(' ') @@ -36,17 +34,18 @@ PyFile:DecoratedAsyncDef.py PyDecoratorList PyDecorator: @baz PsiElement(Py:AT)('@') - PyReferenceExpression: baz - PsiElement(Py:IDENTIFIER)('baz') - PyArgumentList - PsiElement(Py:LPAR)('(') - PyReferenceExpression: x - PsiElement(Py:IDENTIFIER)('x') - PsiElement(Py:COMMA)(',') - PsiWhiteSpace(' ') - PyReferenceExpression: y - PsiElement(Py:IDENTIFIER)('y') - PsiElement(Py:RPAR)(')') + PyCallExpression: baz + PyReferenceExpression: baz + PsiElement(Py:IDENTIFIER)('baz') + PyArgumentList + PsiElement(Py:LPAR)('(') + PyReferenceExpression: x + PsiElement(Py:IDENTIFIER)('x') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyReferenceExpression: y + PsiElement(Py:IDENTIFIER)('y') + PsiElement(Py:RPAR)(')') PsiWhiteSpace('\n') PsiElement(Py:ASYNC_KEYWORD)('async') PsiWhiteSpace(' ') diff --git a/python/testData/psi/DecoratedFunction.txt b/python/testData/psi/DecoratedFunction.txt index 334e97d26453..4612e94c4f75 100644 --- a/python/testData/psi/DecoratedFunction.txt +++ b/python/testData/psi/DecoratedFunction.txt @@ -5,21 +5,20 @@ PyFile:DecoratedFunction.py PsiElement(Py:AT)('@') PyReferenceExpression: staticmethod PsiElement(Py:IDENTIFIER)('staticmethod') - PyArgumentList - PsiWhiteSpace('\n') PyDecorator: @xmlize PsiElement(Py:AT)('@') - PyReferenceExpression: xmlize - PsiElement(Py:IDENTIFIER)('xmlize') - PyArgumentList - PsiElement(Py:LPAR)('(') - PyKeywordArgumentImpl: node - PsiElement(Py:IDENTIFIER)('node') - PsiElement(Py:EQ)('=') - PyStringLiteralExpression: foo - PsiElement(Py:SINGLE_QUOTED_STRING)('"foo"') - PsiElement(Py:RPAR)(')') + PyCallExpression: xmlize + PyReferenceExpression: xmlize + PsiElement(Py:IDENTIFIER)('xmlize') + PyArgumentList + PsiElement(Py:LPAR)('(') + PyKeywordArgumentImpl: node + PsiElement(Py:IDENTIFIER)('node') + PsiElement(Py:EQ)('=') + PyStringLiteralExpression: foo + PsiElement(Py:SINGLE_QUOTED_STRING)('"foo"') + PsiElement(Py:RPAR)(')') PsiWhiteSpace('\n') PsiElement(Py:DEF_KEYWORD)('def') PsiWhiteSpace(' ') diff --git a/python/testData/psi/ExpressionsInDecorators.py b/python/testData/psi/ExpressionsInDecorators.py new file mode 100644 index 000000000000..b94aa8555b46 --- /dev/null +++ b/python/testData/psi/ExpressionsInDecorators.py @@ -0,0 +1,10 @@ +@x[0] +@y:=x[0] +@my_decorator +def say_whee(): + print("Whee!") + + +@y,x +def say_whee2(): + print("Whee!") \ No newline at end of file diff --git a/python/testData/psi/ExpressionsInDecorators.txt b/python/testData/psi/ExpressionsInDecorators.txt new file mode 100644 index 000000000000..749919b76e02 --- /dev/null +++ b/python/testData/psi/ExpressionsInDecorators.txt @@ -0,0 +1,89 @@ +PyFile:ExpressionsInDecorators.py + PyFunction('say_whee') + PyDecoratorList + PyDecorator: @x[0] + PsiElement(Py:AT)('@') + PySubscriptionExpression + PyReferenceExpression: x + PsiElement(Py:IDENTIFIER)('x') + PsiElement(Py:LBRACKET)('[') + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('0') + PsiElement(Py:RBRACKET)(']') + PsiWhiteSpace('\n') + PyDecorator: @y:=x[0] + PsiElement(Py:AT)('@') + PyAssignmentExpression + PyTargetExpression: y + PsiElement(Py:IDENTIFIER)('y') + PsiElement(Py:COLONEQ)(':=') + PySubscriptionExpression + PyReferenceExpression: x + PsiElement(Py:IDENTIFIER)('x') + PsiElement(Py:LBRACKET)('[') + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('0') + PsiElement(Py:RBRACKET)(']') + PsiWhiteSpace('\n') + PyDecorator: @my_decorator + PsiElement(Py:AT)('@') + PyReferenceExpression: my_decorator + PsiElement(Py:IDENTIFIER)('my_decorator') + PsiWhiteSpace('\n') + PsiElement(Py:DEF_KEYWORD)('def') + PsiWhiteSpace(' ') + PsiElement(Py:IDENTIFIER)('say_whee') + PyParameterList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyExpressionStatement + PyCallExpression: print + PyReferenceExpression: print + PsiElement(Py:IDENTIFIER)('print') + PyArgumentList + PsiElement(Py:LPAR)('(') + PyStringLiteralExpression: Whee! + PsiElement(Py:SINGLE_QUOTED_STRING)('"Whee!"') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace('\n\n\n') + PyFunction('null') + PyDecoratorList + PyDecorator: @y + PsiElement(Py:AT)('@') + PyReferenceExpression: y + PsiElement(Py:IDENTIFIER)('y') + PsiErrorElement:Statement break expected + + PyParameterList + + PyStatementList + + PsiElement(Py:COMMA)(',') + PsiErrorElement:Statement expected, found Py:COMMA + + PyExpressionStatement + PyReferenceExpression: x + PsiElement(Py:IDENTIFIER)('x') + PsiWhiteSpace('\n') + PyFunction('say_whee2') + PsiElement(Py:DEF_KEYWORD)('def') + PsiWhiteSpace(' ') + PsiElement(Py:IDENTIFIER)('say_whee2') + PyParameterList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyExpressionStatement + PyCallExpression: print + PyReferenceExpression: print + PsiElement(Py:IDENTIFIER)('print') + PyArgumentList + PsiElement(Py:LPAR)('(') + PyStringLiteralExpression: Whee! + PsiElement(Py:SINGLE_QUOTED_STRING)('"Whee!"') + PsiElement(Py:RPAR)(')') \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyDecoratorTest.java b/python/testSrc/com/jetbrains/python/PyDecoratorTest.java index 582fc812cfa4..b20b809d6656 100644 --- a/python/testSrc/com/jetbrains/python/PyDecoratorTest.java +++ b/python/testSrc/com/jetbrains/python/PyDecoratorTest.java @@ -45,7 +45,7 @@ public class PyDecoratorTest extends PyTestCase { } public void testDecoParamCall() { - PsiElement targetElement = find().getParent(); + PsiElement targetElement = find().getParent().getParent(); //first parent is PyCallExpression assertTrue(targetElement instanceof PyDecorator); PyDecorator deco = (PyDecorator)targetElement; PyFunction decofun = deco.getTarget(); diff --git a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java index bf9f53955a5c..ab22b0bc9a0d 100644 --- a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java +++ b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java @@ -369,6 +369,11 @@ public class PythonHighlightingTest extends PyTestCase { doTest(true, true); } + // PY-41305 + public void testExpressionAsDecorator() { + runWithLanguageLevel(LanguageLevel.getLatest(), this::doTest); + } + // PY-25381 public void testBuiltinDecorator() { doTest(true, true); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java index b8334a11d9d1..a93892b34a8e 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java @@ -257,6 +257,11 @@ public class PyCompatibilityInspectionTest extends PyInspectionTestCase { doTest(LanguageLevel.PYTHON38); } + // PY-41305 + public void testExpressionInDecorators() { + doTest(LanguageLevel.PYTHON39); + } + private void doTest(@NotNull LanguageLevel level) { runWithLanguageLevel(level, this::doTest); } diff --git a/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java b/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java index e256e6edaf4d..86a130cd9c88 100644 --- a/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java +++ b/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java @@ -936,6 +936,11 @@ public class PythonParsingTest extends ParsingTestCase { doTest(LanguageLevel.PYTHON36); } + // PY-41305 + public void testExpressionsInDecorators() { + doTest(LanguageLevel.getLatest()); + } + public void doTest() { doTest(LanguageLevel.PYTHON26); }