Skip to content

Commit 0f8ded0

Browse files
authored
Added support for PEP 758, which allows Python 3.14 and newer to support multiple exceptions in an except clause without surrounding parentheses. This addresses #10546. (#10561)
1 parent 960fb52 commit 0f8ded0

File tree

7 files changed

+60
-12
lines changed

7 files changed

+60
-12
lines changed

packages/pyright-internal/src/localization/localize.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,8 @@ export namespace Localizer {
469469
new ParameterizedString<{ type: string }>(getRawString('Diagnostic.exceptionTypeNotClass'));
470470
export const exceptionTypeNotInstantiable = () =>
471471
new ParameterizedString<{ type: string }>(getRawString('Diagnostic.exceptionTypeNotInstantiable'));
472+
export const exceptRequiresParens = () => getRawString('Diagnostic.exceptRequiresParens');
473+
export const exceptWithAsRequiresParens = () => getRawString('Diagnostic.exceptWithAsRequiresParens');
472474
export const expectedAfterDecorator = () => getRawString('Diagnostic.expectedAfterDecorator');
473475
export const expectedArrow = () => getRawString('Diagnostic.expectedArrow');
474476
export const expectedAsAfterException = () => getRawString('Diagnostic.expectedAsAfterException');

packages/pyright-internal/src/localization/package.nls.en-us.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,8 @@
372372
},
373373
"exceptionTypeNotClass": "\"{type}\" is not a valid exception class",
374374
"exceptionTypeNotInstantiable": "Constructor for exception type \"{type}\" requires one or more arguments",
375+
"exceptWithAsRequiresParens": "Multiple exception types must be parenthesized when using \"as\"",
376+
"exceptRequiresParens": "Multiple exception types must be parenthesized prior to Python 3.14",
375377
"expectedAfterDecorator": "Expected function or class declaration after decorator",
376378
"expectedArrow": "Expected \"->\" followed by return type annotation",
377379
"expectedAsAfterException": {

packages/pyright-internal/src/parser/parser.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,23 +1889,37 @@ export class Parser {
18891889

18901890
let typeExpr: ExpressionNode | undefined;
18911891
let symbolName: IdentifierToken | undefined;
1892+
let isAsKeywordAllowed = true;
1893+
18921894
if (this._peekTokenType() !== TokenType.Colon) {
1893-
typeExpr = this._parseTestExpression(/* allowAssignmentExpression */ true);
1895+
const listResult = this._parseExpressionListGeneric(() =>
1896+
this._parseTestExpression(/* allowAssignmentExpression */ true)
1897+
);
1898+
if (listResult.parseError) {
1899+
typeExpr = listResult.parseError;
1900+
} else {
1901+
typeExpr = this._makeExpressionOrTuple(listResult, /* enclosedInParens */ false);
1902+
1903+
// Python 3.14 allows more than one exception type to be provided in
1904+
// an except clause.
1905+
if (listResult.list.length > 1) {
1906+
if (PythonVersion.isLessThan(this._getLanguageVersion(), pythonVersion3_14)) {
1907+
this._addSyntaxError(LocMessage.exceptRequiresParens(), typeExpr);
1908+
}
1909+
1910+
isAsKeywordAllowed = false;
1911+
}
1912+
}
18941913

18951914
if (this._consumeTokenIfKeyword(KeywordType.As)) {
1915+
if (!isAsKeywordAllowed) {
1916+
this._addSyntaxError(LocMessage.exceptWithAsRequiresParens(), typeExpr);
1917+
}
1918+
18961919
symbolName = this._getTokenIfIdentifier();
18971920
if (!symbolName) {
18981921
this._addSyntaxError(LocMessage.expectedNameAfterAs(), this._peekToken());
18991922
}
1900-
} else {
1901-
// Handle the python 2.x syntax in a graceful manner.
1902-
const peekToken = this._peekToken();
1903-
if (this._consumeTokenIfType(TokenType.Comma)) {
1904-
this._addSyntaxError(LocMessage.expectedAsAfterException(), peekToken);
1905-
1906-
// Parse the expression expected in python 2.x, but discard it.
1907-
this._parseTestExpression(/* allowAssignmentExpression */ false);
1908-
}
19091923
}
19101924
} else if (isExceptGroup) {
19111925
this._addSyntaxError(LocMessage.exceptGroupRequiresType(), this._peekToken());

packages/pyright-internal/src/tests/checker.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ test('ParamType1', () => {
414414
test('Python2', () => {
415415
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['python2.py']);
416416

417-
TestUtils.validateResults(analysisResults, 7);
417+
TestUtils.validateResults(analysisResults, 8);
418418
});
419419

420420
test('InconsistentSpaceTab1', () => {

packages/pyright-internal/src/tests/samples/python2.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
try:
1515
bar = 3
16-
# This should generate an error.
16+
# This should generate one error on Python 3.14 and newer
17+
# and two errors on older versions.
1718
except NameError, 'error caused':
1819
pass
1920

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# This sample tests that multi-exception lists are parsed correctly
2+
# based on PEP 758 in Python 3.14.
3+
4+
def func1():
5+
try:
6+
pass
7+
# This should generate an error for Python 3.13 or earlier.
8+
except ZeroDivisionError, TypeError:
9+
raise
10+
11+
def func2():
12+
try:
13+
pass
14+
# This should generate an error because an "as" clause always requires parens.
15+
except ZeroDivisionError, TypeError as e:
16+
raise

packages/pyright-internal/src/tests/typeEvaluator7.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
pythonVersion3_11,
1515
pythonVersion3_12,
1616
pythonVersion3_13,
17+
pythonVersion3_14,
1718
pythonVersion3_8,
1819
} from '../common/pythonVersion';
1920
import { Uri } from '../common/uri/uri';
@@ -1000,6 +1001,18 @@ test('TryExcept11', () => {
10001001
TestUtils.validateResults(analysisResults, 0);
10011002
});
10021003

1004+
test('TryExcept12', () => {
1005+
const configOptions = new ConfigOptions(Uri.empty());
1006+
1007+
configOptions.defaultPythonVersion = pythonVersion3_13;
1008+
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['tryExcept12.py'], configOptions);
1009+
TestUtils.validateResults(analysisResults1, 3);
1010+
1011+
configOptions.defaultPythonVersion = pythonVersion3_14;
1012+
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['tryExcept12.py'], configOptions);
1013+
TestUtils.validateResults(analysisResults2, 1);
1014+
});
1015+
10031016
test('exceptionGroup1', () => {
10041017
const configOptions = new ConfigOptions(Uri.empty());
10051018

0 commit comments

Comments
 (0)