From 3aad6e385bfbd4348b2e13695cb6741806951160 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 1 Jun 2023 18:37:08 -0700 Subject: [PATCH] Add support for PEP 695 syntax (#3703) --- CHANGES.md | 2 ++ pyproject.toml | 2 ++ src/black/__init__.py | 3 ++ src/black/linegen.py | 12 +++++++ src/black/mode.py | 21 ++++++++++++ src/blib2to3/Grammar.txt | 13 +++++-- src/blib2to3/pygram.py | 6 ++++ tests/data/py_312/type_aliases.py | 13 +++++++ tests/data/py_312/type_params.py | 57 +++++++++++++++++++++++++++++++ tests/test_black.py | 30 +++++++++++++--- tests/test_format.py | 7 ++++ 11 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 tests/data/py_312/type_aliases.py create mode 100644 tests/data/py_312/type_params.py diff --git a/CHANGES.md b/CHANGES.md index 762a799..fb3dea8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,8 @@ +- Add support for the new PEP 695 syntax in Python 3.12 (#3703) + ### Performance diff --git a/pyproject.toml b/pyproject.toml index 435626a..6803a62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -214,4 +214,6 @@ filterwarnings = [ # aiohttp is using deprecated cgi modules - Safe to remove when fixed: # https://github.com/aio-libs/aiohttp/issues/6905 '''ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning''', + # Work around https://github.com/pytest-dev/pytest/issues/10977 for Python 3.12 + '''ignore:(Attribute s|Attribute n|ast.Str|ast.Bytes|ast.NameConstant|ast.Num) is deprecated and will be removed in Python 3.14:DeprecationWarning''' ] diff --git a/src/black/__init__.py b/src/black/__init__.py index 871e9a0..8a759aa 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1275,6 +1275,9 @@ def get_features_used( # noqa: C901 ): features.add(Feature.VARIADIC_GENERICS) + elif n.type in (syms.type_stmt, syms.typeparams): + features.add(Feature.TYPE_PARAMS) + return features diff --git a/src/black/linegen.py b/src/black/linegen.py index b6b83da..0091cbb 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -215,6 +215,18 @@ class LineGenerator(Visitor[Line]): yield from self.visit(child) + def visit_typeparams(self, node: Node) -> Iterator[Line]: + yield from self.visit_default(node) + node.children[0].prefix = "" + + def visit_typevartuple(self, node: Node) -> Iterator[Line]: + yield from self.visit_default(node) + node.children[1].prefix = "" + + def visit_paramspec(self, node: Node) -> Iterator[Line]: + yield from self.visit_default(node) + node.children[1].prefix = "" + def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: if Preview.wrap_long_dict_values_in_parens in self.mode: for i, child in enumerate(node.children): diff --git a/src/black/mode.py b/src/black/mode.py index a5841ed..1091494 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -30,6 +30,7 @@ class TargetVersion(Enum): PY39 = 9 PY310 = 10 PY311 = 11 + PY312 = 12 class Feature(Enum): @@ -51,6 +52,7 @@ class Feature(Enum): VARIADIC_GENERICS = 15 DEBUG_F_STRINGS = 16 PARENTHESIZED_CONTEXT_MANAGERS = 17 + TYPE_PARAMS = 18 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -143,6 +145,25 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { Feature.EXCEPT_STAR, Feature.VARIADIC_GENERICS, }, + TargetVersion.PY312: { + Feature.F_STRINGS, + Feature.DEBUG_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.PARENTHESIZED_CONTEXT_MANAGERS, + Feature.PATTERN_MATCHING, + Feature.EXCEPT_STAR, + Feature.VARIADIC_GENERICS, + Feature.TYPE_PARAMS, + }, } diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index bd8a452..e48e663 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -12,11 +12,17 @@ file_input: (NEWLINE | stmt)* ENDMARKER single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE eval_input: testlist NEWLINE* ENDMARKER +typevar: NAME [':' expr] +paramspec: '**' NAME +typevartuple: '*' NAME +typeparam: typevar | paramspec | typevartuple +typeparams: '[' typeparam (',' typeparam)* [','] ']' + decorator: '@' namedexpr_test NEWLINE decorators: decorator+ decorated: decorators (classdef | funcdef | async_funcdef) async_funcdef: ASYNC funcdef -funcdef: 'def' NAME parameters ['->' test] ':' suite +funcdef: 'def' NAME [typeparams] parameters ['->' test] ':' suite parameters: '(' [typedargslist] ')' # The following definition for typedarglist is equivalent to this set of rules: @@ -74,7 +80,7 @@ vfplist: vfpdef (',' vfpdef)* [','] stmt: simple_stmt | compound_stmt simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE -small_stmt: (expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt | +small_stmt: (type_stmt | expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt | import_stmt | global_stmt | exec_stmt | assert_stmt) expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) | ('=' (yield_expr|testlist_star_expr))*) @@ -105,6 +111,7 @@ dotted_name: NAME ('.' NAME)* global_stmt: ('global' | 'nonlocal') NAME (',' NAME)* exec_stmt: 'exec' expr ['in' test [',' test]] assert_stmt: 'assert' test [',' test] +type_stmt: "type" NAME [typeparams] '=' expr compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt | match_stmt async_stmt: ASYNC (funcdef | with_stmt | for_stmt) @@ -174,7 +181,7 @@ dictsetmaker: ( ((test ':' asexpr_test | '**' expr) ((test [':=' test] | star_expr) (comp_for | (',' (test [':=' test] | star_expr))* [','])) ) -classdef: 'class' NAME ['(' [arglist] ')'] ':' suite +classdef: 'class' NAME [typeparams] ['(' [arglist] ')'] ':' suite arglist: argument (',' argument)* [','] diff --git a/src/blib2to3/pygram.py b/src/blib2to3/pygram.py index 99012cd..15702e4 100644 --- a/src/blib2to3/pygram.py +++ b/src/blib2to3/pygram.py @@ -95,6 +95,7 @@ class _python_symbols(Symbols): old_test: int or_test: int parameters: int + paramspec: int pass_stmt: int pattern: int patterns: int @@ -126,7 +127,12 @@ class _python_symbols(Symbols): tname_star: int trailer: int try_stmt: int + type_stmt: int typedargslist: int + typeparam: int + typeparams: int + typevar: int + typevartuple: int varargslist: int vfpdef: int vfplist: int diff --git a/tests/data/py_312/type_aliases.py b/tests/data/py_312/type_aliases.py new file mode 100644 index 0000000..84e07e5 --- /dev/null +++ b/tests/data/py_312/type_aliases.py @@ -0,0 +1,13 @@ +type A=int +type Gen[T]=list[T] + +type = aliased +print(type(42)) + +# output + +type A = int +type Gen[T] = list[T] + +type = aliased +print(type(42)) diff --git a/tests/data/py_312/type_params.py b/tests/data/py_312/type_params.py new file mode 100644 index 0000000..5f8ec43 --- /dev/null +++ b/tests/data/py_312/type_params.py @@ -0,0 +1,57 @@ +def func [T ](): pass +async def func [ T ] (): pass +class C[ T ] : pass + +def all_in[T : int,U : (bytes, str),* Ts,**P](): pass + +def really_long[WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine](): pass + +def even_longer[WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine: WhatIfItHadABound](): pass + +def it_gets_worse[WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine, ItCouldBeGenericOverMultipleTypeVars](): pass + +def magic[Trailing, Comma,](): pass + +# output + + +def func[T](): + pass + + +async def func[T](): + pass + + +class C[T]: + pass + + +def all_in[T: int, U: (bytes, str), *Ts, **P](): + pass + + +def really_long[ + WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine +](): + pass + + +def even_longer[ + WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine: WhatIfItHadABound +](): + pass + + +def it_gets_worse[ + WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine, + ItCouldBeGenericOverMultipleTypeVars, +](): + pass + + +def magic[ + Trailing, + Comma, +](): + pass diff --git a/tests/test_black.py b/tests/test_black.py index 00de5b7..42b0161 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -271,6 +271,15 @@ class BlackTestCase(BlackBaseTestCase): versions = black.detect_target_versions(root) self.assertIn(black.TargetVersion.PY38, versions) + def test_pep_695_version_detection(self) -> None: + for file in ("type_aliases", "type_params"): + source, _ = read_data("py_312", file) + root = black.lib2to3_parse(source) + features = black.get_features_used(root) + self.assertIn(black.Feature.TYPE_PARAMS, features) + versions = black.detect_target_versions(root) + self.assertIn(black.TargetVersion.PY312, versions) + def test_expression_ff(self) -> None: source, expected = read_data("simple_cases", "expression.py") tmp_file = Path(black.dump_to_file(source)) @@ -1533,14 +1542,25 @@ class BlackTestCase(BlackBaseTestCase): for version, expected in [ ("3.6", [TargetVersion.PY36]), ("3.11.0rc1", [TargetVersion.PY311]), - (">=3.10", [TargetVersion.PY310, TargetVersion.PY311]), - (">=3.10.6", [TargetVersion.PY310, TargetVersion.PY311]), + (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]), + ( + ">=3.10.6", + [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312], + ), ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]), (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]), - (">3.7,!=3.8,!=3.9", [TargetVersion.PY310, TargetVersion.PY311]), + ( + ">3.7,!=3.8,!=3.9", + [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312], + ), ( "> 3.9.4, != 3.10.3", - [TargetVersion.PY39, TargetVersion.PY310, TargetVersion.PY311], + [ + TargetVersion.PY39, + TargetVersion.PY310, + TargetVersion.PY311, + TargetVersion.PY312, + ], ), ( "!=3.3,!=3.4", @@ -1552,6 +1572,7 @@ class BlackTestCase(BlackBaseTestCase): TargetVersion.PY39, TargetVersion.PY310, TargetVersion.PY311, + TargetVersion.PY312, ], ), ( @@ -1566,6 +1587,7 @@ class BlackTestCase(BlackBaseTestCase): TargetVersion.PY39, TargetVersion.PY310, TargetVersion.PY311, + TargetVersion.PY312, ], ), ("==3.8.*", [TargetVersion.PY38]), diff --git a/tests/test_format.py b/tests/test_format.py index 5a7b3bb..8e0ada9 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -134,6 +134,13 @@ def test_python_311(filename: str) -> None: assert_format(source, expected, mode, minimum_version=(3, 11)) +@pytest.mark.parametrize("filename", all_data_cases("py_312")) +def test_python_312(filename: str) -> None: + source, expected = read_data("py_312", filename) + mode = black.Mode(target_versions={black.TargetVersion.PY312}) + assert_format(source, expected, mode, minimum_version=(3, 12)) + + @pytest.mark.parametrize("filename", all_data_cases("fast")) def test_fast_cases(filename: str) -> None: source, expected = read_data("fast", filename) -- 2.39.2