Support 3.11 / PEP 654 syntax (#3016)

This commit is contained in:
Batuhan Taskaya 2022-04-15 19:25:07 +03:00 committed by GitHub
parent 712f8b37fb
commit 7f7673d941
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 223 additions and 1 deletions

View File

@ -53,6 +53,9 @@
### Parser ### Parser
- [PEP 654](https://peps.python.org/pep-0654/#except) syntax (for example,
`except *ExceptionGroup:`) is now supported (#3016)
<!-- Changes to the parser or to version autodetection --> <!-- Changes to the parser or to version autodetection -->
### Performance ### Performance

View File

@ -1296,6 +1296,13 @@ def get_features_used( # noqa: C901
): ):
features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) features.add(Feature.ANN_ASSIGN_EXTENDED_RHS)
elif (
n.type == syms.except_clause
and len(n.children) >= 2
and n.children[1].type == token.STAR
):
features.add(Feature.EXCEPT_STAR)
return features return features

View File

@ -915,6 +915,15 @@ def normalize_invisible_parens(
node.insert_child(index, Leaf(token.LPAR, "")) node.insert_child(index, Leaf(token.LPAR, ""))
node.append_child(Leaf(token.RPAR, "")) node.append_child(Leaf(token.RPAR, ""))
break break
elif (
index == 1
and child.type == token.STAR
and node.type == syms.except_clause
):
# In except* (PEP 654), the star is actually part of
# of the keyword. So we need to skip the insertion of
# invisible parentheses to work more precisely.
continue
elif not (isinstance(child, Leaf) and is_multiline_string(child)): elif not (isinstance(child, Leaf) and is_multiline_string(child)):
wrap_in_parentheses(node, child, visible=False) wrap_in_parentheses(node, child, visible=False)

View File

@ -30,6 +30,7 @@ class TargetVersion(Enum):
PY38 = 8 PY38 = 8
PY39 = 9 PY39 = 9
PY310 = 10 PY310 = 10
PY311 = 11
class Feature(Enum): class Feature(Enum):
@ -47,6 +48,7 @@ class Feature(Enum):
PATTERN_MATCHING = 11 PATTERN_MATCHING = 11
UNPACKING_ON_FLOW = 12 UNPACKING_ON_FLOW = 12
ANN_ASSIGN_EXTENDED_RHS = 13 ANN_ASSIGN_EXTENDED_RHS = 13
EXCEPT_STAR = 14
FORCE_OPTIONAL_PARENTHESES = 50 FORCE_OPTIONAL_PARENTHESES = 50
# __future__ flags # __future__ flags
@ -116,6 +118,21 @@ class Feature(Enum):
Feature.ANN_ASSIGN_EXTENDED_RHS, Feature.ANN_ASSIGN_EXTENDED_RHS,
Feature.PATTERN_MATCHING, Feature.PATTERN_MATCHING,
}, },
TargetVersion.PY311: {
Feature.F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.RELAXED_DECORATORS,
Feature.POS_ONLY_ARGUMENTS,
Feature.UNPACKING_ON_FLOW,
Feature.ANN_ASSIGN_EXTENDED_RHS,
Feature.PATTERN_MATCHING,
Feature.EXCEPT_STAR,
},
} }

View File

@ -401,6 +401,10 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901
elif p.type == syms.sliceop: elif p.type == syms.sliceop:
return NO return NO
elif p.type == syms.except_clause:
if t == token.STAR:
return NO
return SPACE return SPACE

View File

@ -118,7 +118,7 @@ try_stmt: ('try' ':' suite
with_stmt: 'with' asexpr_test (',' asexpr_test)* ':' suite with_stmt: 'with' asexpr_test (',' asexpr_test)* ':' suite
# NB compile.c makes sure that the default except clause is last # NB compile.c makes sure that the default except clause is last
except_clause: 'except' [test [(',' | 'as') test]] except_clause: 'except' ['*'] [test [(',' | 'as') test]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
# Backward compatibility cruft to support: # Backward compatibility cruft to support:

53
tests/data/pep_654.py Normal file
View File

@ -0,0 +1,53 @@
try:
raise OSError("blah")
except* ExceptionGroup as e:
pass
try:
async with trio.open_nursery() as nursery:
# Make two concurrent calls to child()
nursery.start_soon(child)
nursery.start_soon(child)
except* ValueError:
pass
try:
try:
raise ValueError(42)
except:
try:
raise TypeError(int)
except* Exception:
pass
1 / 0
except Exception as e:
exc = e
try:
try:
raise FalsyEG("eg", [TypeError(1), ValueError(2)])
except* TypeError as e:
tes = e
raise
except* ValueError as e:
ves = e
pass
except Exception as e:
exc = e
try:
try:
raise orig
except* (TypeError, ValueError) as e:
raise SyntaxError(3) from e
except BaseException as e:
exc = e
try:
try:
raise orig
except* OSError as e:
raise TypeError(3) from e
except ExceptionGroup as e:
exc = e

111
tests/data/pep_654_style.py Normal file
View File

@ -0,0 +1,111 @@
try:
raise OSError("blah")
except * ExceptionGroup as e:
pass
try:
async with trio.open_nursery() as nursery:
# Make two concurrent calls to child()
nursery.start_soon(child)
nursery.start_soon(child)
except *ValueError:
pass
try:
try:
raise ValueError(42)
except:
try:
raise TypeError(int)
except *(Exception):
pass
1 / 0
except Exception as e:
exc = e
try:
try:
raise FalsyEG("eg", [TypeError(1), ValueError(2)])
except \
*TypeError as e:
tes = e
raise
except * ValueError as e:
ves = e
pass
except Exception as e:
exc = e
try:
try:
raise orig
except *(TypeError, ValueError, *OTHER_EXCEPTIONS) as e:
raise SyntaxError(3) from e
except BaseException as e:
exc = e
try:
try:
raise orig
except\
* OSError as e:
raise TypeError(3) from e
except ExceptionGroup as e:
exc = e
# output
try:
raise OSError("blah")
except* ExceptionGroup as e:
pass
try:
async with trio.open_nursery() as nursery:
# Make two concurrent calls to child()
nursery.start_soon(child)
nursery.start_soon(child)
except* ValueError:
pass
try:
try:
raise ValueError(42)
except:
try:
raise TypeError(int)
except* (Exception):
pass
1 / 0
except Exception as e:
exc = e
try:
try:
raise FalsyEG("eg", [TypeError(1), ValueError(2)])
except* TypeError as e:
tes = e
raise
except* ValueError as e:
ves = e
pass
except Exception as e:
exc = e
try:
try:
raise orig
except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e:
raise SyntaxError(3) from e
except BaseException as e:
exc = e
try:
try:
raise orig
except* OSError as e:
raise TypeError(3) from e
except ExceptionGroup as e:
exc = e

View File

@ -794,6 +794,12 @@ def test_get_features_used(self) -> None:
self.assertEqual( self.assertEqual(
black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS} black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
) )
node = black.lib2to3_parse("try: pass\nexcept Something: pass")
self.assertEqual(black.get_features_used(node), set())
node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
self.assertEqual(black.get_features_used(node), set())
node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
def test_get_features_used_for_future_flags(self) -> None: def test_get_features_used_for_future_flags(self) -> None:
for src, features in [ for src, features in [

View File

@ -72,6 +72,11 @@
"parenthesized_context_managers", "parenthesized_context_managers",
] ]
PY311_CASES: List[str] = [
"pep_654",
"pep_654_style",
]
PREVIEW_CASES: List[str] = [ PREVIEW_CASES: List[str] = [
# string processing # string processing
"cantfit", "cantfit",
@ -227,6 +232,13 @@ def test_patma_invalid() -> None:
exc_info.match("Cannot parse: 10:11") exc_info.match("Cannot parse: 10:11")
@pytest.mark.parametrize("filename", PY311_CASES)
def test_python_311(filename: str) -> None:
source, expected = read_data(filename)
mode = black.Mode(target_versions={black.TargetVersion.PY311})
assert_format(source, expected, mode, minimum_version=(3, 11))
def test_python_2_hint() -> None: def test_python_2_hint() -> None:
with pytest.raises(black.parsing.InvalidInput) as exc_info: with pytest.raises(black.parsing.InvalidInput) as exc_info:
assert_format("print 'daylily'", "print 'daylily'") assert_format("print 'daylily'", "print 'daylily'")