From 0f3cf1deb665fccbf390f3883258154434810897 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 20 Sep 2023 17:13:38 +0200 Subject: [PATCH 1/2] fix indentation of line breaks in long type hints by adding parentheses, and remove unnecessary parentheses --- src/black/linegen.py | 20 +- src/black/nodes.py | 2 + .../long_strings_flag_disabled.py | 4 +- .../preview/long_strings__type_annotations.py | 2 +- .../py_310/pep604_union_types_line_breaks.py | 190 ++++++++++++++++++ 5 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 tests/data/py_310/pep604_union_types_line_breaks.py diff --git a/src/black/linegen.py b/src/black/linegen.py index 507e860190f..17735c8d833 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -397,6 +397,23 @@ def visit_factor(self, node: Node) -> Iterator[Line]: node.insert_child(index, Node(syms.atom, [lpar, operand, rpar])) yield from self.visit_default(node) + def visit_tname(self, node: Node) -> Iterator[Line]: + """ + Add potential parentheses around types in function parameter lists to be made + into real parentheses in case the type hint is too long to fit on a line + Examples: + def foo(a: int, b: float = 7): ... + + -> + + def foo(a: (int), b: (float) = 7): ... + """ + assert len(node.children) == 3 + if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): + wrap_in_parentheses(node, node.children[2], visible=False) + + yield from self.visit_default(node) + def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if Preview.hex_codes_in_unicode_sequences in self.mode: normalize_unicode_escape_sequences(leaf) @@ -1368,7 +1385,7 @@ def maybe_make_parens_invisible_in_atom( Returns whether the node should itself be wrapped in invisible parentheses. """ if ( - node.type != syms.atom + node.type not in (syms.atom, syms.expr) or is_empty_tuple(node) or is_one_tuple(node) or (is_yield(node) and parent.type != syms.expr_stmt) @@ -1392,6 +1409,7 @@ def maybe_make_parens_invisible_in_atom( syms.except_clause, syms.funcdef, syms.with_stmt, + syms.tname, # these ones aren't useful to end users, but they do please fuzzers syms.for_stmt, syms.del_stmt, diff --git a/src/black/nodes.py b/src/black/nodes.py index edd201a21e9..06ef37829cb 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -121,6 +121,8 @@ ">>=", "**=", "//=", + # also handle annassign + ":", } IMPLICIT_TUPLE: Final = {syms.testlist, syms.testlist_star_expr, syms.exprlist} diff --git a/tests/data/miscellaneous/long_strings_flag_disabled.py b/tests/data/miscellaneous/long_strings_flag_disabled.py index db3954e3abd..f8792b3ef5a 100644 --- a/tests/data/miscellaneous/long_strings_flag_disabled.py +++ b/tests/data/miscellaneous/long_strings_flag_disabled.py @@ -254,7 +254,9 @@ + CONCATENATED + "using the '+' operator." ) -annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: ( + Final +) = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." annotated_variable: Literal[ "fakse_literal" ] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." diff --git a/tests/data/preview/long_strings__type_annotations.py b/tests/data/preview/long_strings__type_annotations.py index 41d7ee2b67b..45de882d02c 100644 --- a/tests/data/preview/long_strings__type_annotations.py +++ b/tests/data/preview/long_strings__type_annotations.py @@ -54,6 +54,6 @@ def func( def func( - argument: ("int |" "str"), + argument: "int |" "str", ) -> Set["int |" " str"]: pass diff --git a/tests/data/py_310/pep604_union_types_line_breaks.py b/tests/data/py_310/pep604_union_types_line_breaks.py new file mode 100644 index 00000000000..cdfbeb1d154 --- /dev/null +++ b/tests/data/py_310/pep604_union_types_line_breaks.py @@ -0,0 +1,190 @@ +# This has always worked +z= Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong + +# "AnnAssign"s now also work +z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong +z: (Short + | Short2 + | Short3 + | Short4) +z: (int) +z: ((int)) + + +z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7 +z: (Short + | Short2 + | Short3 + | Short4) = 8 +z: (int) = 2.3 +z: ((int)) = foo() + +# In case I go for not enforcing parantheses, this might get improved at the same time +x = ( + z + == 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999, + y + == 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999, +) + +x = ( + z == (9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999), + y == (9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999), +) + +# handle formatting of "tname"s in parameter list + +# remove unnecessary paren +def foo(i: (int)) -> None: ... + + +# this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so. +def foo(i: (int,)) -> None: ... + +def foo( + i: int, + x: Loooooooooooooooooooooooong + | Looooooooooooooooong + | Looooooooooooooooooooong + | Looooooong, + *, + s: str, +) -> None: + pass + + +@app.get("/path/") +async def foo( + q: str + | None = Query(None, title="Some long title", description="Some long description") +): + pass + + +def f( + max_jobs: int + | None = Option( + None, help="Maximum number of jobs to launch. And some additional text." + ), + another_option: bool = False + ): + ... + + +# output +# This has always worked +z = ( + Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong +) + +# "AnnAssign"s now also work +z: ( + Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong +) +z: Short | Short2 | Short3 | Short4 +z: int +z: int + + +z: ( + Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong + | Loooooooooooooooooooooooong +) = 7 +z: Short | Short2 | Short3 | Short4 = 8 +z: int = 2.3 +z: int = foo() + +# In case I go for not enforcing parantheses, this might get improved at the same time +x = ( + z + == 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999, + y + == 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999, +) + +x = ( + z + == ( + 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + | 9999999999999999999999999999999999999999 + ), + y + == ( + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + + 9999999999999999999999999999999999999999 + ), +) + +# handle formatting of "tname"s in parameter list + + +# remove unnecessary paren +def foo(i: int) -> None: + ... + + +# this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so. +def foo(i: (int,)) -> None: + ... + + +def foo( + i: int, + x: ( + Loooooooooooooooooooooooong + | Looooooooooooooooong + | Looooooooooooooooooooong + | Looooooong + ), + *, + s: str, +) -> None: + pass + + +@app.get("/path/") +async def foo( + q: str | None = Query( + None, title="Some long title", description="Some long description" + ) +): + pass + + +def f( + max_jobs: int | None = Option( + None, help="Maximum number of jobs to launch. And some additional text." + ), + another_option: bool = False, +): + ... From 391a41334100d1640859090a1b55862190680265 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 22 Sep 2023 16:24:15 +0200 Subject: [PATCH 2/2] add entry in CHANGES.md, make the style change only in preview mode --- CHANGES.md | 3 +++ src/black/linegen.py | 16 ++++++++++++---- src/black/mode.py | 1 + src/black/nodes.py | 2 -- .../miscellaneous/long_strings_flag_disabled.py | 4 +--- .../pep604_union_types_line_breaks.py | 9 +++------ 6 files changed, 20 insertions(+), 15 deletions(-) rename tests/data/{py_310 => preview_py_310}/pep604_union_types_line_breaks.py (98%) diff --git a/CHANGES.md b/CHANGES.md index a68106ad23f..a879ab3e8da 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,9 @@ ### Preview style +- Long type hints are now wrapped in parentheses and properly indented when split across + multiple lines (#3899) + ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 17735c8d833..9ddd4619f69 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -408,9 +408,10 @@ def foo(a: int, b: float = 7): ... def foo(a: (int), b: (float) = 7): ... """ - assert len(node.children) == 3 - if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): - wrap_in_parentheses(node, node.children[2], visible=False) + if Preview.parenthesize_long_type_hints in self.mode: + assert len(node.children) == 3 + if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): + wrap_in_parentheses(node, node.children[2], visible=False) yield from self.visit_default(node) @@ -515,7 +516,14 @@ def __post_init__(self) -> None: self.visit_except_clause = partial(v, keywords={"except"}, parens={"except"}) self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) - self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) + + # When this is moved out of preview, add ":" directly to ASSIGNMENTS in nodes.py + if Preview.parenthesize_long_type_hints in self.mode: + assignments = ASSIGNMENTS | {":"} + else: + assignments = ASSIGNMENTS + self.visit_expr_stmt = partial(v, keywords=Ø, parens=assignments) + self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"}) self.visit_import_from = partial(v, keywords=Ø, parens={"import"}) self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"}) diff --git a/src/black/mode.py b/src/black/mode.py index 8a855ac495a..f44a821bcd0 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -180,6 +180,7 @@ class Preview(Enum): # for https://github.com/psf/black/issues/3117 to be fixed. string_processing = auto() parenthesize_conditional_expressions = auto() + parenthesize_long_type_hints = auto() skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() wrap_multiple_context_managers_in_parens = auto() diff --git a/src/black/nodes.py b/src/black/nodes.py index 06ef37829cb..edd201a21e9 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -121,8 +121,6 @@ ">>=", "**=", "//=", - # also handle annassign - ":", } IMPLICIT_TUPLE: Final = {syms.testlist, syms.testlist_star_expr, syms.exprlist} diff --git a/tests/data/miscellaneous/long_strings_flag_disabled.py b/tests/data/miscellaneous/long_strings_flag_disabled.py index f8792b3ef5a..db3954e3abd 100644 --- a/tests/data/miscellaneous/long_strings_flag_disabled.py +++ b/tests/data/miscellaneous/long_strings_flag_disabled.py @@ -254,9 +254,7 @@ + CONCATENATED + "using the '+' operator." ) -annotated_variable: ( - Final -) = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." annotated_variable: Literal[ "fakse_literal" ] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." diff --git a/tests/data/py_310/pep604_union_types_line_breaks.py b/tests/data/preview_py_310/pep604_union_types_line_breaks.py similarity index 98% rename from tests/data/py_310/pep604_union_types_line_breaks.py rename to tests/data/preview_py_310/pep604_union_types_line_breaks.py index cdfbeb1d154..9c4ab870766 100644 --- a/tests/data/py_310/pep604_union_types_line_breaks.py +++ b/tests/data/preview_py_310/pep604_union_types_line_breaks.py @@ -149,13 +149,11 @@ def f( # remove unnecessary paren -def foo(i: int) -> None: - ... +def foo(i: int) -> None: ... # this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so. -def foo(i: (int,)) -> None: - ... +def foo(i: (int,)) -> None: ... def foo( @@ -186,5 +184,4 @@ def f( None, help="Maximum number of jobs to launch. And some additional text." ), another_option: bool = False, -): - ... +): ...