Skip to content

Commit

Permalink
Add support for PEP 695 syntax (#3703)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra committed Jun 2, 2023
1 parent a538ab7 commit 3aad6e3
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Expand Up @@ -32,6 +32,8 @@

<!-- Changes to the parser or to version autodetection -->

- Add support for the new PEP 695 syntax in Python 3.12 (#3703)

### Performance

<!-- Changes that improve Black's performance. -->
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Expand Up @@ -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'''
]
3 changes: 3 additions & 0 deletions src/black/__init__.py
Expand Up @@ -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


Expand Down
12 changes: 12 additions & 0 deletions src/black/linegen.py
Expand Up @@ -215,6 +215,18 @@ def visit_stmt(

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):
Expand Down
21 changes: 21 additions & 0 deletions src/black/mode.py
Expand Up @@ -30,6 +30,7 @@ class TargetVersion(Enum):
PY39 = 9
PY310 = 10
PY311 = 11
PY312 = 12


class Feature(Enum):
Expand All @@ -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
Expand Down Expand Up @@ -143,6 +145,25 @@ class Feature(Enum):
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,
},
}


Expand Down
13 changes: 10 additions & 3 deletions src/blib2to3/Grammar.txt
Expand Up @@ -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:
Expand Down Expand Up @@ -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))*)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)* [',']

Expand Down
6 changes: 6 additions & 0 deletions src/blib2to3/pygram.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions 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))
57 changes: 57 additions & 0 deletions 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
30 changes: 26 additions & 4 deletions tests/test_black.py
Expand Up @@ -271,6 +271,15 @@ def test_pep_572_version_detection(self) -> None:
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))
Expand Down Expand Up @@ -1533,14 +1542,25 @@ 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]),
(">=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",
Expand All @@ -1552,6 +1572,7 @@ def test_infer_target_version(self) -> None:
TargetVersion.PY39,
TargetVersion.PY310,
TargetVersion.PY311,
TargetVersion.PY312,
],
),
(
Expand All @@ -1566,6 +1587,7 @@ def test_infer_target_version(self) -> None:
TargetVersion.PY39,
TargetVersion.PY310,
TargetVersion.PY311,
TargetVersion.PY312,
],
),
("==3.8.*", [TargetVersion.PY38]),
Expand Down
7 changes: 7 additions & 0 deletions tests/test_format.py
Expand Up @@ -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)
Expand Down

0 comments on commit 3aad6e3

Please sign in to comment.