Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEP 654 syntax #3016

Merged
merged 1 commit into from Apr 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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