From 732c694297ff01188257ee31da6a800bf776798d Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 15 Apr 2022 03:41:25 +0300 Subject: [PATCH] Support 3.11 / PEP 654 syntax --- CHANGES.md | 3 + src/black/__init__.py | 7 +++ src/black/linegen.py | 9 +++ src/black/mode.py | 17 ++++++ src/black/nodes.py | 4 ++ src/blib2to3/Grammar.txt | 2 +- tests/data/pep_654.py | 53 +++++++++++++++++ tests/data/pep_654_style.py | 111 ++++++++++++++++++++++++++++++++++++ tests/test_black.py | 6 ++ tests/test_format.py | 12 ++++ 10 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 tests/data/pep_654.py create mode 100644 tests/data/pep_654_style.py diff --git a/CHANGES.md b/CHANGES.md index b21c319d5e0..566077b1dbc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -53,6 +53,9 @@ ### Parser +- [PEP 654](https://peps.python.org/pep-0654/#except) syntax (for example, + `except *ExceptionGroup:`) is now supported (#3016) + ### Performance diff --git a/src/black/__init__.py b/src/black/__init__.py index 3a2d1cb8898..3a1ce24f059 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -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 diff --git a/src/black/linegen.py b/src/black/linegen.py index caffbab0cbc..91fdeef8f2f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -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) diff --git a/src/black/mode.py b/src/black/mode.py index 34905702a54..6bd4ce14421 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -30,6 +30,7 @@ class TargetVersion(Enum): PY38 = 8 PY39 = 9 PY310 = 10 + PY311 = 11 class Feature(Enum): @@ -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 @@ -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, + }, } diff --git a/src/black/nodes.py b/src/black/nodes.py index d18d4bde872..37b96a498d6 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -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 diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index 0ce6cf39111..1de54165513 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -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: diff --git a/tests/data/pep_654.py b/tests/data/pep_654.py new file mode 100644 index 00000000000..387c0816f4b --- /dev/null +++ b/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 diff --git a/tests/data/pep_654_style.py b/tests/data/pep_654_style.py new file mode 100644 index 00000000000..568e5e3efa4 --- /dev/null +++ b/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 diff --git a/tests/test_black.py b/tests/test_black.py index 20cc9f7379f..f6663fa5797 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -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 [ diff --git a/tests/test_format.py b/tests/test_format.py index fd5f596b6d5..1916146e84d 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -72,6 +72,11 @@ "parenthesized_context_managers", ] +PY311_CASES: List[str] = [ + "pep_654", + "pep_654_style", +] + PREVIEW_CASES: List[str] = [ # string processing "cantfit", @@ -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'")