From 3f0f8f1956646fd9f6fc47e133364c1352a478d1 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Apr 2024 22:08:37 -0700 Subject: [PATCH] Support PEP 696 (#4327) --- CHANGES.md | 3 ++ src/black/__init__.py | 6 +++ src/black/mode.py | 23 ++++++++++ src/black/resources/black.schema.json | 3 +- src/blib2to3/Grammar.txt | 6 +-- tests/data/cases/type_param_defaults.py | 61 +++++++++++++++++++++++++ tests/test_black.py | 42 +++++++++++++++-- 7 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 tests/data/cases/type_param_defaults.py diff --git a/CHANGES.md b/CHANGES.md index bfd433a8c07..fef2d7cc3da 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,9 @@ +- Add support for type parameter defaults, a new syntactic feature added to Python 3.13 + by PEP 696 (#4327) + ### Performance diff --git a/src/black/__init__.py b/src/black/__init__.py index 6ba49d5ef2d..942f3160751 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1436,6 +1436,12 @@ def get_features_used( # noqa: C901 elif n.type in (syms.type_stmt, syms.typeparams): features.add(Feature.TYPE_PARAMS) + elif ( + n.type in (syms.typevartuple, syms.paramspec, syms.typevar) + and n.children[-2].type == token.EQUAL + ): + features.add(Feature.TYPE_PARAM_DEFAULTS) + return features diff --git a/src/black/mode.py b/src/black/mode.py index e3b2d5dbadd..ca224fc8da1 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -24,6 +24,7 @@ class TargetVersion(Enum): PY310 = 10 PY311 = 11 PY312 = 12 + PY313 = 13 class Feature(Enum): @@ -47,6 +48,7 @@ class Feature(Enum): PARENTHESIZED_CONTEXT_MANAGERS = 17 TYPE_PARAMS = 18 FSTRING_PARSING = 19 + TYPE_PARAM_DEFAULTS = 20 FORCE_OPTIONAL_PARENTHESES = 50 # __future__ flags @@ -159,6 +161,27 @@ class Feature(Enum): Feature.TYPE_PARAMS, Feature.FSTRING_PARSING, }, + TargetVersion.PY313: { + 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, + Feature.FSTRING_PARSING, + Feature.TYPE_PARAM_DEFAULTS, + }, } diff --git a/src/black/resources/black.schema.json b/src/black/resources/black.schema.json index 5c800775d57..66f33c83902 100644 --- a/src/black/resources/black.schema.json +++ b/src/black/resources/black.schema.json @@ -27,7 +27,8 @@ "py39", "py310", "py311", - "py312" + "py312", + "py313" ] }, "description": "Python versions that should be supported by Black's output. You should include all versions that your code supports. By default, Black will infer target versions from the project metadata in pyproject.toml. If this does not yield conclusive results, Black will use per-file auto-detection." diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index 0c8ac99daba..c8800e21931 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -12,9 +12,9 @@ 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 +typevar: NAME [':' expr] ['=' expr] +paramspec: '**' NAME ['=' expr] +typevartuple: '*' NAME ['=' (expr|star_expr)] typeparam: typevar | paramspec | typevartuple typeparams: '[' typeparam (',' typeparam)* [','] ']' diff --git a/tests/data/cases/type_param_defaults.py b/tests/data/cases/type_param_defaults.py new file mode 100644 index 00000000000..cd844fe0746 --- /dev/null +++ b/tests/data/cases/type_param_defaults.py @@ -0,0 +1,61 @@ +# flags: --minimum-version=3.13 + +type A[T=int] = float +type B[*P=int] = float +type C[*Ts=int] = float +type D[*Ts=*int] = float +type D[something_that_is_very_very_very_long=something_that_is_very_very_very_long] = float +type D[*something_that_is_very_very_very_long=*something_that_is_very_very_very_long] = float +type something_that_is_long[something_that_is_long=something_that_is_long] = something_that_is_long + +def simple[T=something_that_is_long](short1: int, short2: str, short3: bytes) -> float: + pass + +def longer[something_that_is_long=something_that_is_long](something_that_is_long: something_that_is_long) -> something_that_is_long: + pass + +def trailing_comma1[T=int,](a: str): + pass + +def trailing_comma2[T=int](a: str,): + pass + +# output + +type A[T = int] = float +type B[*P = int] = float +type C[*Ts = int] = float +type D[*Ts = *int] = float +type D[ + something_that_is_very_very_very_long = something_that_is_very_very_very_long +] = float +type D[ + *something_that_is_very_very_very_long = *something_that_is_very_very_very_long +] = float +type something_that_is_long[ + something_that_is_long = something_that_is_long +] = something_that_is_long + + +def simple[ + T = something_that_is_long +](short1: int, short2: str, short3: bytes) -> float: + pass + + +def longer[ + something_that_is_long = something_that_is_long +](something_that_is_long: something_that_is_long) -> something_that_is_long: + pass + + +def trailing_comma1[ + T = int, +](a: str): + pass + + +def trailing_comma2[ + T = int +](a: str,): + pass diff --git a/tests/test_black.py b/tests/test_black.py index 498eb06fc39..6002bb1a5f6 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -263,6 +263,21 @@ def test_pep_695_version_detection(self) -> None: versions = black.detect_target_versions(root) self.assertIn(black.TargetVersion.PY312, versions) + def test_pep_696_version_detection(self) -> None: + source, _ = read_data("cases", "type_param_defaults") + samples = [ + source, + "type X[T=int] = float", + "type X[T:int=int]=int", + "type X[*Ts=int]=int", + "type X[*Ts=*int]=int", + "type X[**P=int]=int", + ] + for sample in samples: + root = black.lib2to3_parse(sample) + features = black.get_features_used(root) + self.assertIn(black.Feature.TYPE_PARAM_DEFAULTS, features) + def test_expression_ff(self) -> None: source, expected = read_data("cases", "expression.py") tmp_file = Path(black.dump_to_file(source)) @@ -1531,16 +1546,34 @@ def test_infer_target_version(self) -> None: for version, expected in [ ("3.6", [TargetVersion.PY36]), ("3.11.0rc1", [TargetVersion.PY311]), - (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]), + ( + ">=3.10", + [ + TargetVersion.PY310, + TargetVersion.PY311, + TargetVersion.PY312, + TargetVersion.PY313, + ], + ), ( ">=3.10.6", - [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312], + [ + TargetVersion.PY310, + TargetVersion.PY311, + TargetVersion.PY312, + TargetVersion.PY313, + ], ), ("<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, TargetVersion.PY312], + [ + TargetVersion.PY310, + TargetVersion.PY311, + TargetVersion.PY312, + TargetVersion.PY313, + ], ), ( "> 3.9.4, != 3.10.3", @@ -1549,6 +1582,7 @@ def test_infer_target_version(self) -> None: TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312, + TargetVersion.PY313, ], ), ( @@ -1562,6 +1596,7 @@ def test_infer_target_version(self) -> None: TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312, + TargetVersion.PY313, ], ), ( @@ -1577,6 +1612,7 @@ def test_infer_target_version(self) -> None: TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312, + TargetVersion.PY313, ], ), ("==3.8.*", [TargetVersion.PY38]),