Skip to content

Commit

Permalink
Support 3.11 / PEP 654 syntax (#3016)
Browse files Browse the repository at this point in the history
  • Loading branch information
isidentical committed Apr 15, 2022
1 parent 712f8b3 commit 7f7673d
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGES.md
Expand Up @@ -53,6 +53,9 @@

### 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 -->

### Performance
Expand Down
7 changes: 7 additions & 0 deletions src/black/__init__.py
Expand Up @@ -1296,6 +1296,13 @@ def get_features_used( # noqa: C901
):
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


Expand Down
9 changes: 9 additions & 0 deletions src/black/linegen.py
Expand Up @@ -915,6 +915,15 @@ def normalize_invisible_parens(
node.insert_child(index, Leaf(token.LPAR, ""))
node.append_child(Leaf(token.RPAR, ""))
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)):
wrap_in_parentheses(node, child, visible=False)
Expand Down
17 changes: 17 additions & 0 deletions src/black/mode.py
Expand Up @@ -30,6 +30,7 @@ class TargetVersion(Enum):
PY38 = 8
PY39 = 9
PY310 = 10
PY311 = 11


class Feature(Enum):
Expand All @@ -47,6 +48,7 @@ class Feature(Enum):
PATTERN_MATCHING = 11
UNPACKING_ON_FLOW = 12
ANN_ASSIGN_EXTENDED_RHS = 13
EXCEPT_STAR = 14
FORCE_OPTIONAL_PARENTHESES = 50

# __future__ flags
Expand Down Expand Up @@ -116,6 +118,21 @@ class Feature(Enum):
Feature.ANN_ASSIGN_EXTENDED_RHS,
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,
},
}


Expand Down
4 changes: 4 additions & 0 deletions src/black/nodes.py
Expand Up @@ -401,6 +401,10 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901
elif p.type == syms.sliceop:
return NO

elif p.type == syms.except_clause:
if t == token.STAR:
return NO

return SPACE


Expand Down
2 changes: 1 addition & 1 deletion src/blib2to3/Grammar.txt
Expand Up @@ -118,7 +118,7 @@ try_stmt: ('try' ':' suite
with_stmt: 'with' asexpr_test (',' asexpr_test)* ':' suite

# 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

# Backward compatibility cruft to support:
Expand Down
53 changes: 53 additions & 0 deletions tests/data/pep_654.py
@@ -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 changes: 111 additions & 0 deletions tests/data/pep_654_style.py
@@ -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
6 changes: 6 additions & 0 deletions tests/test_black.py
Expand Up @@ -794,6 +794,12 @@ def test_get_features_used(self) -> None:
self.assertEqual(
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:
for src, features in [
Expand Down
12 changes: 12 additions & 0 deletions tests/test_format.py
Expand Up @@ -72,6 +72,11 @@
"parenthesized_context_managers",
]

PY311_CASES: List[str] = [
"pep_654",
"pep_654_style",
]

PREVIEW_CASES: List[str] = [
# string processing
"cantfit",
Expand Down Expand Up @@ -227,6 +232,13 @@ def test_patma_invalid() -> None:
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:
with pytest.raises(black.parsing.InvalidInput) as exc_info:
assert_format("print 'daylily'", "print 'daylily'")
Expand Down

0 comments on commit 7f7673d

Please sign in to comment.