diff --git a/python/python-parser/src/com/jetbrains/python/parsing/StatementParsing.java b/python/python-parser/src/com/jetbrains/python/parsing/StatementParsing.java
index 08ced1267dff..62ceee0218a8 100644
--- a/python/python-parser/src/com/jetbrains/python/parsing/StatementParsing.java
+++ b/python/python-parser/src/com/jetbrains/python/parsing/StatementParsing.java
@@ -793,26 +793,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper {
if (myBuilder.getTokenType() == PyTokenTypes.EXCEPT_KEYWORD) {
haveExceptClause = true;
while (myBuilder.getTokenType() == PyTokenTypes.EXCEPT_KEYWORD) {
- final SyntaxTreeBuilder.Marker exceptBlock = myBuilder.mark();
- myBuilder.advanceLexer();
-
- boolean star = matchToken(PyTokenTypes.MULT);
- if (myBuilder.getTokenType() != PyTokenTypes.COLON) {
- if (!getExpressionParser().parseSingleExpression(false)) {
- myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
- }
- if (myBuilder.getTokenType() == PyTokenTypes.COMMA || myBuilder.getTokenType() == PyTokenTypes.AS_KEYWORD) {
- myBuilder.advanceLexer();
- if (!getExpressionParser().parseSingleExpression(true)) {
- myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
- }
- }
- }
- else if (star) {
- myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
- }
- parseColonAndSuite();
- exceptBlock.done(PyElementTypes.EXCEPT_PART);
+ parseExceptClause();
}
final SyntaxTreeBuilder.Marker elsePart = myBuilder.mark();
if (myBuilder.getTokenType() == PyTokenTypes.ELSE_KEYWORD) {
@@ -841,6 +822,45 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper {
statement.done(PyElementTypes.TRY_EXCEPT_STATEMENT);
}
+ private void parseExceptClause() {
+ final SyntaxTreeBuilder.Marker exceptBlock = myBuilder.mark();
+ myBuilder.advanceLexer();
+
+ boolean star = matchToken(PyTokenTypes.MULT);
+ if (myBuilder.getTokenType() != PyTokenTypes.COLON) {
+ if (myContext.getLanguageLevel().isAtLeast(LanguageLevel.PYTHON314)) {
+ // Python 3.14, support PEP-758 syntax: except ImportError, OtherError: ...
+ if (!getExpressionParser().parseExpressionOptional(false)) {
+ myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
+ }
+ if (myBuilder.getTokenType() == PyTokenTypes.AS_KEYWORD) {
+ myBuilder.advanceLexer();
+ if (!getExpressionParser().parseSingleExpression(true)) {
+ myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
+ }
+ }
+ }
+ else {
+ // Python 2, support syntax: except ImportError, targetE: ...
+ if (!getExpressionParser().parseSingleExpression(false)) {
+ myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
+ }
+ // support Py3K syntax with 'as' to show an error with a quickfix
+ if (myBuilder.getTokenType() == PyTokenTypes.COMMA || myBuilder.getTokenType() == PyTokenTypes.AS_KEYWORD) {
+ myBuilder.advanceLexer();
+ if (!getExpressionParser().parseSingleExpression(true)) {
+ myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
+ }
+ }
+ }
+ }
+ else if (star) {
+ myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
+ }
+ parseColonAndSuite();
+ exceptBlock.done(PyElementTypes.EXCEPT_PART);
+ }
+
private void parseColonAndSuite() {
if (expectColon()) {
parseSuite();
diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
index d8603dc30d8b..1dccfbf47525 100644
--- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties
+++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
@@ -957,6 +957,10 @@ INSP.NAME.bad.except.clauses.order=Wrong order of 'except' clauses
INSP.bad.except.exception.class.already.caught=Exception class ''{0}'' has already been caught
INSP.bad.except.superclass.of.exception.class.already.caught=''{0}'', superclass of the exception class ''{1}'', has already been caught
+# PyExceptClauseMissingParenthesesInspection
+INSP.except.clause.missing.parens=Parentheses are required when specifying a target for multiple exception classes
+QFIX.except.clause.missing.parens=Wrap exception tuple in parentheses
+
#PyGlobalUndefinedInspection
INSP.NAME.global.undefined=Global variable is not defined at the module level
INSP.global.variable.undefined=Global variable ''{0}'' is undefined at the module level
diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/WrapExceptTupleInParenthesesQuickFix.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/WrapExceptTupleInParenthesesQuickFix.kt
new file mode 100644
index 000000000000..871c4b20956c
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/WrapExceptTupleInParenthesesQuickFix.kt
@@ -0,0 +1,29 @@
+package com.jetbrains.python.inspections.quickfix
+
+import com.intellij.codeInsight.intention.IntentionAction
+import com.intellij.codeInspection.util.IntentionName
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiFile
+import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.psi.LanguageLevel
+import com.jetbrains.python.psi.PyElementGenerator
+import com.jetbrains.python.psi.PyTupleExpression
+
+class WrapExceptTupleInParenthesesQuickFix(val exceptPartTuple: PyTupleExpression) : IntentionAction {
+
+ override fun getFamilyName(): String = PyPsiBundle.message("QFIX.except.clause.missing.parens")
+
+ override fun getText(): @IntentionName String = PyPsiBundle.message("QFIX.except.clause.missing.parens")
+
+ override fun isAvailable(project: Project, editor: Editor?, psiFile: PsiFile?): Boolean = true
+
+ override fun startInWriteAction(): Boolean = true
+
+ override fun invoke(project: Project, editor: Editor?, psiFile: PsiFile?) {
+ val generator = PyElementGenerator.getInstance(project)
+ val level = LanguageLevel.forElement(exceptPartTuple)
+ val wrapped = generator.createExpressionFromText(level, "(${exceptPartTuple.text})")
+ exceptPartTuple.replace(wrapped)
+ }
+}
\ No newline at end of file
diff --git a/python/python-psi-impl/src/com/jetbrains/python/validation/PyTryExceptAnnotator.java b/python/python-psi-impl/src/com/jetbrains/python/validation/PyTryExceptAnnotator.java
index 7e675fdb5e1f..6d4e744cfc37 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/validation/PyTryExceptAnnotator.java
+++ b/python/python-psi-impl/src/com/jetbrains/python/validation/PyTryExceptAnnotator.java
@@ -6,6 +6,7 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.PyTokenTypes;
+import com.jetbrains.python.inspections.quickfix.WrapExceptTupleInParenthesesQuickFix;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
@@ -60,12 +61,26 @@ public final class PyTryExceptAnnotator extends PyAnnotator {
@Override
public void visitPyExceptBlock(@NotNull PyExceptPart node) {
- if (!node.isStar()) return;
+ if (node.isStar()) {
+ var exceptClass = node.getExceptClass();
+ var exceptionGroup = tryGetExceptionGroupInExpression(exceptClass);
+ if (exceptionGroup != null) {
+ getHolder().newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.exception.group.in.star.except")).range(exceptionGroup)
+ .create();
+ }
+ }
- var exceptClass = node.getExceptClass();
- var exceptionGroup = tryGetExceptionGroupInExpression(exceptClass);
- if (exceptionGroup != null) {
- getHolder().newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.exception.group.in.star.except")).range(exceptionGroup).create();
+ // Add PEP-758 Py314+ missing parentheses check: except Error1, Error2 as e:
+ var level = LanguageLevel.forElement(node);
+ if (!level.isPy3K()) return;
+ if (node.getTarget() == null) return;
+ var exceptExpr = node.getExceptClass();
+ if (exceptExpr instanceof PyParenthesizedExpression) return;
+ if (exceptExpr instanceof PyTupleExpression tuple && tuple.getElements().length > 1) {
+ getHolder().newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("INSP.except.clause.missing.parens"))
+ .range(tuple)
+ .withFix(new WrapExceptTupleInParenthesesQuickFix(tuple))
+ .create();
}
}
diff --git a/python/testData/highlighting/exceptClauseMissingParentheses.py b/python/testData/highlighting/exceptClauseMissingParentheses.py
new file mode 100644
index 000000000000..6895a4a0aba5
--- /dev/null
+++ b/python/testData/highlighting/exceptClauseMissingParentheses.py
@@ -0,0 +1,5 @@
+def f():
+ try:
+ pass
+ except ValueError, TypeError as e:
+ pass
\ No newline at end of file
diff --git a/python/testData/intentions/exceptPartAddMissingParentheses.py b/python/testData/intentions/exceptPartAddMissingParentheses.py
new file mode 100644
index 000000000000..abb9c58c739f
--- /dev/null
+++ b/python/testData/intentions/exceptPartAddMissingParentheses.py
@@ -0,0 +1,4 @@
+try:
+ pass
+except ImportError, AccessError as e:
+ pass
\ No newline at end of file
diff --git a/python/testData/intentions/exceptPartAddMissingParentheses_after.py b/python/testData/intentions/exceptPartAddMissingParentheses_after.py
new file mode 100644
index 000000000000..003b7e03f33d
--- /dev/null
+++ b/python/testData/intentions/exceptPartAddMissingParentheses_after.py
@@ -0,0 +1,4 @@
+try:
+ pass
+except (ImportError, AccessError) as e:
+ pass
\ No newline at end of file
diff --git a/python/testData/psi/parts/Try.py b/python/testData/psi/parts/Try.py
index 2831fcf4c3e7..ff2f41c2634a 100644
--- a/python/testData/psi/parts/Try.py
+++ b/python/testData/psi/parts/Try.py
@@ -1,9 +1,15 @@
# try and friends
try:
pass
-except ArithmeticError, e:
+except ArithmeticError:
pass
-except:
+except ArithmeticError, ImportError:
+ pass
+except ArithmeticError as e2:
+ pass
+except (ArithmeticError, ImportError) as e3:
+ pass
+except:
pass
else:
pass
diff --git a/python/testSrc/com/jetbrains/python/PyStatementPartsTest.java b/python/testSrc/com/jetbrains/python/PyStatementPartsTest.java
index dc345c78e610..10651b3f344e 100644
--- a/python/testSrc/com/jetbrains/python/PyStatementPartsTest.java
+++ b/python/testSrc/com/jetbrains/python/PyStatementPartsTest.java
@@ -144,7 +144,7 @@ public class PyStatementPartsTest extends LightMarkedTestCase {
public void testTry() {
Map marks = loadTest();
- Assert.assertEquals(6, marks.size());
+ Assert.assertEquals(9, marks.size());
PsiElement elt = marks.get("").getParent().getParent(); // keyword -> part -> stmt
Assert.assertTrue(elt instanceof PyTryExceptStatement);
@@ -155,13 +155,33 @@ public class PyStatementPartsTest extends LightMarkedTestCase {
Assert.assertNotNull(stmt_list);
Assert.assertEquals(marks.get("").getParent().getParent(), stmt_list); // keyword -> stmt -> stmt_list
- PyExceptPart exc_part = stmt.getExceptParts()[0];
- Assert.assertEquals("ArithmeticError", exc_part.getExceptClass().getText());
- Assert.assertEquals(marks.get("").getParent(), exc_part);
+ PyExceptPart exc_part0 = stmt.getExceptParts()[0];
+ Assert.assertEquals("ArithmeticError", exc_part0.getExceptClass().getText());
+ Assert.assertNull(exc_part0.getTarget());
+ Assert.assertEquals(marks.get("").getParent(), exc_part0);
- exc_part = (PyExceptPart)marks.get("").getParent(); // keyword -> part
- Assert.assertEquals(stmt.getExceptParts()[1], exc_part);
- Assert.assertNull(exc_part.getExceptClass());
+ PyExceptPart exc_part1 = stmt.getExceptParts()[1];
+ Assert.assertEquals("ArithmeticError, ImportError", exc_part1.getExceptClass().getText());
+ Assert.assertNull(exc_part1.getTarget());
+ Assert.assertEquals(marks.get("").getParent(), exc_part1);
+
+ PyExceptPart exc_part2 = stmt.getExceptParts()[2];
+ Assert.assertEquals("ArithmeticError", exc_part2.getExceptClass().getText());
+ Assert.assertNotNull(exc_part2.getTarget());
+ Assert.assertEquals("e2", exc_part2.getTarget().getText());
+ Assert.assertEquals(marks.get("").getParent(), exc_part2);
+
+ PyExceptPart exc_part3 = stmt.getExceptParts()[3];
+ Assert.assertEquals("(ArithmeticError, ImportError)", exc_part3.getExceptClass().getText());
+ Assert.assertNotNull(exc_part3.getTarget());
+ Assert.assertEquals("e3", exc_part3.getTarget().getText());
+ Assert.assertEquals(marks.get("").getParent(), exc_part3);
+
+ PyExceptPart exc_part4 = stmt.getExceptParts()[4];
+ Assert.assertNull(exc_part4.getExceptClass());
+ Assert.assertNull(exc_part4.getTarget());
+ Assert.assertEquals(marks.get("").getParent(), exc_part4);
+ Assert.assertNull(exc_part4.getExceptClass());
elt = marks.get("").getParent(); // keyword -> part
Assert.assertTrue(elt instanceof PyElsePart);
diff --git a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java
index 78caab9b8e41..b8aea21d8a27 100644
--- a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java
+++ b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java
@@ -301,6 +301,11 @@ public class PythonHighlightingTest extends PyTestCase {
doTest(LanguageLevel.getLatest(), false, false);
}
+ // PY-84077
+ public void testExceptClauseMissingParentheses() {
+ doTest(LanguageLevel.getLatest(), false, false);
+ }
+
// PY-52930
public void testContinueBreakReturnInExceptStar() {
doTest(LanguageLevel.getLatest(), false, false);
diff --git a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java
index db30a3a644df..8c3a944eabc8 100644
--- a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java
+++ b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java
@@ -84,7 +84,11 @@ public class PyIntentionTest extends PyTestCase {
}
public void testReplaceExceptPart() {
- doTest(PyPsiBundle.message("INTN.convert.except.to"));
+ runWithLanguageLevel(LanguageLevel.PYTHON35, () -> doTest(PyPsiBundle.message("INTN.convert.except.to")));
+ }
+
+ public void testExceptPartAddMissingParentheses() {
+ doTest(PyPsiBundle.message("QFIX.except.clause.missing.parens"));
}
public void testConvertBuiltins() {